refactor(app): require closable script input for shutdown
- commit
- e69541584a159b6695081d790472fc930572e558
- author
- brice · · 2026-09-23 16:18 UTC
- parent
- f192a7a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +6 -9
M internal/app/app.go +4 -7
| @@ -30,8 +30,9 @@ type Options struct { | |||
| 30 | 30 | Timeout time.Duration | |
| 31 | 31 | NoTerminal bool | |
| 32 | 32 | ScreenshotDir string | |
| 33 | - | Script io.Reader | |
| 34 | - | testScenes chan<- ports.Scene | |
| 33 | + | // Run closes Script on shutdown; the reader goroutine exits after Close. | |
| 34 | + | Script io.ReadCloser | |
| 35 | + | testScenes chan<- ports.Scene | |
| 35 | 36 | } | |
| 36 | 37 | ||
| 37 | 38 | func Run(ctx context.Context, opts Options) error { return run(ctx, opts, nil) } | |
| @@ -101,11 +102,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 101 | 102 | _ = headlessinput.Run(ctx, km, script, input, logging.For(ctx, "input")) | |
| 102 | 103 | }() | |
| 103 | 104 | if opts.Script != nil { | |
| 104 | - | // A reader without Close may remain blocked in Scan after shutdown. | |
| 105 | - | // It must not be joined with the application workers. | |
| 106 | - | if closer, ok := opts.Script.(io.Closer); ok { | |
| 107 | - | go func() { <-ctx.Done(); _ = closer.Close() }() | |
| 108 | - | } | |
| 105 | + | go func() { <-ctx.Done(); _ = opts.Script.Close() }() | |
| 109 | 106 | go func() { | |
| 110 | 107 | defer close(script) | |
| 111 | 108 | scanner := bufio.NewScanner(opts.Script) | |
| @@ -30,8 +30,9 @@ type Options struct { | |||||
| 30 | Timeout time.Duration | 30 | Timeout time.Duration | ||
| 31 | NoTerminal bool | 31 | NoTerminal bool | ||
| 32 | ScreenshotDir string | 32 | ScreenshotDir string | ||
| 33 | - | Script io.Reader | 33 | + | // Run closes Script on shutdown; the reader goroutine exits after Close. |
| 34 | - | testScenes chan<- ports.Scene | 34 | + | Script io.ReadCloser |
| 35 | + | testScenes chan<- ports.Scene | |||
| 35 | } | 36 | } | ||
| 36 | 37 | ||||
| 37 | func Run(ctx context.Context, opts Options) error { return run(ctx, opts, nil) } | 38 | func Run(ctx context.Context, opts Options) error { return run(ctx, opts, nil) } | ||
| @@ -101,11 +102,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 101 | _ = headlessinput.Run(ctx, km, script, input, logging.For(ctx, "input")) | 102 | _ = headlessinput.Run(ctx, km, script, input, logging.For(ctx, "input")) | ||
| 102 | }() | 103 | }() | ||
| 103 | if opts.Script != nil { | 104 | if opts.Script != nil { | ||
| 104 | - | // A reader without Close may remain blocked in Scan after shutdown. | 105 | + | go func() { <-ctx.Done(); _ = opts.Script.Close() }() |
| 105 | - | // It must not be joined with the application workers. | |||
| 106 | - | if closer, ok := opts.Script.(io.Closer); ok { | |||
| 107 | - | go func() { <-ctx.Done(); _ = closer.Close() }() | |||
| 108 | - | } | |||
| 109 | go func() { | 106 | go func() { | ||
| 110 | defer close(script) | 107 | defer close(script) | ||
| 111 | scanner := bufio.NewScanner(opts.Script) | 108 | scanner := bufio.NewScanner(opts.Script) | ||
M internal/app/app_test.go +2 -2
| @@ -167,7 +167,7 @@ func TestHeadlessTyping(t *testing.T) { | |||
| 167 | 167 | cfg := config.Defaults() | |
| 168 | 168 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | |
| 169 | 169 | dir := t.TempDir() | |
| 170 | - | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, ScreenshotDir: dir, Script: strings.NewReader("sleep 1.5s\ntype echo nefertty-ok\nkey Return\nsleep 1s\n"), Timeout: 6 * time.Second}) | |
| 170 | + | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, ScreenshotDir: dir, Script: io.NopCloser(strings.NewReader("sleep 1.5s\ntype echo nefertty-ok\nkey Return\nsleep 1s\n")), Timeout: 6 * time.Second}) | |
| 171 | 171 | if err != nil { | |
| 172 | 172 | if strings.Contains(err.Error(), "Vulkan") || strings.Contains(err.Error(), "vulkan") { | |
| 173 | 173 | t.Skipf("Vulkan unavailable: %v", err) | |
| @@ -219,7 +219,7 @@ func TestHeadlessPointerClickFocus(t *testing.T) { | |||
| 219 | 219 | cfg := config.Defaults() | |
| 220 | 220 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | |
| 221 | 221 | scenes := make(chan ports.Scene, 128) | |
| 222 | - | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, Script: strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\n"), Timeout: 5 * time.Second, testScenes: scenes}) | |
| 222 | + | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, Script: io.NopCloser(strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\n")), Timeout: 5 * time.Second, testScenes: scenes}) | |
| 223 | 223 | if err != nil { | |
| 224 | 224 | if strings.Contains(strings.ToLower(err.Error()), "vulkan") { | |
| 225 | 225 | t.Skipf("Vulkan unavailable: %v", err) | |
| @@ -167,7 +167,7 @@ func TestHeadlessTyping(t *testing.T) { | |||||
| 167 | cfg := config.Defaults() | 167 | cfg := config.Defaults() | ||
| 168 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | 168 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | ||
| 169 | dir := t.TempDir() | 169 | dir := t.TempDir() | ||
| 170 | - | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, ScreenshotDir: dir, Script: strings.NewReader("sleep 1.5s\ntype echo nefertty-ok\nkey Return\nsleep 1s\n"), Timeout: 6 * time.Second}) | 170 | + | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, ScreenshotDir: dir, Script: io.NopCloser(strings.NewReader("sleep 1.5s\ntype echo nefertty-ok\nkey Return\nsleep 1s\n")), Timeout: 6 * time.Second}) |
| 171 | if err != nil { | 171 | if err != nil { | ||
| 172 | if strings.Contains(err.Error(), "Vulkan") || strings.Contains(err.Error(), "vulkan") { | 172 | if strings.Contains(err.Error(), "Vulkan") || strings.Contains(err.Error(), "vulkan") { | ||
| 173 | t.Skipf("Vulkan unavailable: %v", err) | 173 | t.Skipf("Vulkan unavailable: %v", err) | ||
| @@ -219,7 +219,7 @@ func TestHeadlessPointerClickFocus(t *testing.T) { | |||||
| 219 | cfg := config.Defaults() | 219 | cfg := config.Defaults() | ||
| 220 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | 220 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | ||
| 221 | scenes := make(chan ports.Scene, 128) | 221 | scenes := make(chan ports.Scene, 128) | ||
| 222 | - | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, Script: strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\n"), Timeout: 5 * time.Second, testScenes: scenes}) | 222 | + | err := Run(context.Background(), Options{Backend: "headless", Config: cfg, Script: io.NopCloser(strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\n")), Timeout: 5 * time.Second, testScenes: scenes}) |
| 223 | if err != nil { | 223 | if err != nil { | ||
| 224 | if strings.Contains(strings.ToLower(err.Error()), "vulkan") { | 224 | if strings.Contains(strings.ToLower(err.Error()), "vulkan") { | ||
| 225 | t.Skipf("Vulkan unavailable: %v", err) | 225 | t.Skipf("Vulkan unavailable: %v", err) | ||