fix: improve agent shutdown handling and error reporting

Kim committed May 22, 2026 at 17:41 UTC fe2dcc8df9c2662a39d407cbf178cfb4974511fb
2 files changed +35 -9
cmd/portal-tunnel/agent.go
+16 -7
@@ -242,17 +242,26 @@ func runAgentStopCommand(args []string) error {
242 if stateDir != "" {
243 cfg.Agent.StateDir = stateDir
244 }
245 - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
245 + ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
246 defer cancel()
247
248 shutdownErr := agent.Shutdown(ctx, cfg.Agent.StateDir)
249 - if err := service.StopDisable(ctx, cfg.Agent.ServiceName); err != nil {
250 - if shutdownErr == nil {
251 - fmt.Fprintf(os.Stderr, "Warning: agent stopped, but service manager cleanup failed: %v\n", err)
252 - fmt.Fprintln(os.Stdout, "Portal agent stopped.")
253 - return nil
249 + serviceErr := service.StopDisable(ctx, cfg.Agent.ServiceName)
250 + if errors.Is(shutdownErr, agent.ErrNotRunning) {
251 + if serviceErr != nil {
252 + fmt.Fprintf(os.Stderr, "Warning: agent is not running, but service manager cleanup failed: %v\n", serviceErr)
253 }
255 - return fmt.Errorf("stop portal agent service: %w", err)
254 + fmt.Fprintln(os.Stdout, "Portal agent is not running.")
255 + return nil
256 + }
257 + if shutdownErr != nil {
258 + return fmt.Errorf("stop portal agent: %w", errors.Join(shutdownErr, serviceErr))
259 + }
260 + if err := waitAgentStopped(ctx, cfg.Agent.StateDir); err != nil {
261 + return fmt.Errorf("stop portal agent: %w", errors.Join(err, serviceErr))
262 + }
263 + if serviceErr != nil {
264 + fmt.Fprintf(os.Stderr, "Warning: agent stopped, but service manager cleanup failed: %v\n", serviceErr)
265 }
266 fmt.Fprintln(os.Stdout, "Portal agent stopped.")
267 return nil
cmd/portal-tunnel/agent/control.go
+19 -2
@@ -3,8 +3,10 @@ package agent
3 import (
4 "context"
5 "errors"
6 + "net"
7 "net/http"
8 "net/url"
9 + "os"
10 "path/filepath"
11 "strings"
12 "time"
@@ -20,7 +22,10 @@ const (
22 agentCookieName = "portal_agent"
23 )
24
23 -var controlHTTPClient = utils.NewHTTPClient(utils.WithHTTPTimeout(5 * time.Second))
25 +var (
26 + ErrNotRunning = errors.New("portal agent is not running")
27 + controlHTTPClient = utils.NewHTTPClient(utils.WithHTTPTimeout(5 * time.Second))
28 +)
29
30 type endpoint struct {
31 ControlAddr string `json:"control_addr"`
@@ -330,6 +335,9 @@ func controlRequest(ctx context.Context, stateDir, method, path string, payload
335 }
336 var endpoint endpoint
337 if err := utils.ReadJSONFile(filepath.Join(stateDir, endpointFilename), &endpoint); err != nil {
338 + if os.IsNotExist(err) {
339 + return ErrNotRunning
340 + }
341 return err
342 }
343 if strings.TrimSpace(endpoint.ControlAddr) == "" || strings.TrimSpace(endpoint.Token) == "" {
@@ -340,5 +348,14 @@ func controlRequest(ctx context.Context, stateDir, method, path string, payload
348 return err
349 }
350 headers := http.Header{"Authorization": []string{"Bearer " + endpoint.Token}}
343 - return utils.HTTPDoAPIPath(ctx, controlHTTPClient, baseURL, method, path, payload, headers, out)
351 + err = utils.HTTPDoAPIPath(ctx, controlHTTPClient, baseURL, method, path, payload, headers, out)
352 + if controlDialError(err) {
353 + return ErrNotRunning
354 + }
355 + return err
356 +}
357 +
358 +func controlDialError(err error) bool {
359 + var opErr *net.OpError
360 + return errors.As(err, &opErr) && opErr.Op == "dial"
361 }