fix(daemon): cancel floating PTY opens during teardown
- commit
- 037ef43febdf4f76b546fb1d3e43aefb143e1ce0
- author
- brice · · 2026-07-12 12:20 UTC
- parent
- 75a1afd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
18 files · +204 -146
M internal/adapters/pty/pty_linux.go +6 -2
| @@ -8,6 +8,7 @@ package pty | |||
| 8 | 8 | // x/sys/unix plus the standard library. | |
| 9 | 9 | ||
| 10 | 10 | import ( | |
| 11 | + | "context" | |
| 11 | 12 | "errors" | |
| 12 | 13 | "fmt" | |
| 13 | 14 | "io" | |
| @@ -38,7 +39,10 @@ func NewFactory() *Factory { return &Factory{} } | |||
| 38 | 39 | // verbatim (nil means inherit the current process environment); the caller | |
| 39 | 40 | // decides TERM and friends. sz sets the terminal window size before the child | |
| 40 | 41 | // starts, so the child observes the correct dimensions on its first query. | |
| 41 | - | func (Factory) Open(command string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | |
| 42 | + | func (Factory) Open(ctx context.Context, command string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | |
| 43 | + | if err := ctx.Err(); err != nil { | |
| 44 | + | return nil, err | |
| 45 | + | } | |
| 42 | 46 | masterFd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY|unix.O_CLOEXEC, 0) | |
| 43 | 47 | if err != nil { | |
| 44 | 48 | return nil, fmt.Errorf("pty: open /dev/ptmx: %w", err) | |
| @@ -79,7 +83,7 @@ func (Factory) Open(command string, args []string, env []string, dir string, sz | |||
| 79 | 83 | } | |
| 80 | 84 | } | |
| 81 | 85 | ||
| 82 | - | cmd := exec.Command(command, args...) | |
| 86 | + | cmd := exec.CommandContext(ctx, command, args...) | |
| 83 | 87 | cmd.Env = env | |
| 84 | 88 | cmd.Dir = platform.DirOrHome(dir) | |
| 85 | 89 | cmd.Stdin = slave | |
| @@ -8,6 +8,7 @@ package pty | |||||
| 8 | // x/sys/unix plus the standard library. | 8 | // x/sys/unix plus the standard library. | ||
| 9 | 9 | ||||
| 10 | import ( | 10 | import ( | ||
| 11 | + | "context" | |||
| 11 | "errors" | 12 | "errors" | ||
| 12 | "fmt" | 13 | "fmt" | ||
| 13 | "io" | 14 | "io" | ||
| @@ -38,7 +39,10 @@ func NewFactory() *Factory { return &Factory{} } | |||||
| 38 | // verbatim (nil means inherit the current process environment); the caller | 39 | // verbatim (nil means inherit the current process environment); the caller | ||
| 39 | // decides TERM and friends. sz sets the terminal window size before the child | 40 | // decides TERM and friends. sz sets the terminal window size before the child | ||
| 40 | // starts, so the child observes the correct dimensions on its first query. | 41 | // starts, so the child observes the correct dimensions on its first query. | ||
| 41 | - | func (Factory) Open(command string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | 42 | + | func (Factory) Open(ctx context.Context, command string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { |
| 43 | + | if err := ctx.Err(); err != nil { | |||
| 44 | + | return nil, err | |||
| 45 | + | } | |||
| 42 | masterFd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY|unix.O_CLOEXEC, 0) | 46 | masterFd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY|unix.O_CLOEXEC, 0) | ||
| 43 | if err != nil { | 47 | if err != nil { | ||
| 44 | return nil, fmt.Errorf("pty: open /dev/ptmx: %w", err) | 48 | return nil, fmt.Errorf("pty: open /dev/ptmx: %w", err) | ||
| @@ -79,7 +83,7 @@ func (Factory) Open(command string, args []string, env []string, dir string, sz | |||||
| 79 | } | 83 | } | ||
| 80 | } | 84 | } | ||
| 81 | 85 | ||||
| 82 | - | cmd := exec.Command(command, args...) | 86 | + | cmd := exec.CommandContext(ctx, command, args...) |
| 83 | cmd.Env = env | 87 | cmd.Env = env | ||
| 84 | cmd.Dir = platform.DirOrHome(dir) | 88 | cmd.Dir = platform.DirOrHome(dir) | ||
| 85 | cmd.Stdin = slave | 89 | cmd.Stdin = slave | ||
M internal/adapters/pty/pty_linux_test.go +8 -7
| @@ -4,6 +4,7 @@ package pty_test | |||
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "bytes" | |
| 7 | + | "context" | |
| 7 | 8 | "errors" | |
| 8 | 9 | "io" | |
| 9 | 10 | "os" | |
| @@ -53,7 +54,7 @@ func TestOpen_ChildOutputToEOF(t *testing.T) { | |||
| 53 | 54 | t.Skip("skipping pty integration test in -short mode") | |
| 54 | 55 | } | |
| 55 | 56 | f := newFactory() | |
| 56 | - | p, err := f.Open("sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 57 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 57 | 58 | require.NoError(t, err) | |
| 58 | 59 | t.Cleanup(func() { _ = p.Close() }) | |
| 59 | 60 | ||
| @@ -70,7 +71,7 @@ func TestOpen_EchoRoundtrip(t *testing.T) { | |||
| 70 | 71 | t.Skip("skipping pty integration test in -short mode") | |
| 71 | 72 | } | |
| 72 | 73 | f := newFactory() | |
| 73 | - | p, err := f.Open("cat", nil, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 74 | + | p, err := f.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 74 | 75 | require.NoError(t, err) | |
| 75 | 76 | t.Cleanup(func() { _ = p.Close() }) | |
| 76 | 77 | ||
| @@ -103,7 +104,7 @@ func TestForegroundPgid(t *testing.T) { | |||
| 103 | 104 | t.Skip("skipping pty integration test in -short mode") | |
| 104 | 105 | } | |
| 105 | 106 | f := newFactory() | |
| 106 | - | p, err := f.Open("sh", []string{"-c", "sleep 2"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 107 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 2"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 107 | 108 | require.NoError(t, err) | |
| 108 | 109 | t.Cleanup(func() { _ = p.Close() }) | |
| 109 | 110 | ||
| @@ -141,7 +142,7 @@ func TestResize_SttySize(t *testing.T) { | |||
| 141 | 142 | for _, tt := range tests { | |
| 142 | 143 | t.Run(tt.name, func(t *testing.T) { | |
| 143 | 144 | f := newFactory() | |
| 144 | - | p, err := f.Open("sh", []string{"-c", tt.script}, os.Environ(), "", tt.initial) | |
| 145 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", tt.script}, os.Environ(), "", tt.initial) | |
| 145 | 146 | require.NoError(t, err) | |
| 146 | 147 | t.Cleanup(func() { _ = p.Close() }) | |
| 147 | 148 | ||
| @@ -160,7 +161,7 @@ func TestClose_ReapsChildNoZombie(t *testing.T) { | |||
| 160 | 161 | t.Skip("skipping pty integration test in -short mode") | |
| 161 | 162 | } | |
| 162 | 163 | f := newFactory() | |
| 163 | - | p, err := f.Open("sh", []string{"-c", "sleep 30"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 164 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 30"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 164 | 165 | require.NoError(t, err) | |
| 165 | 166 | ||
| 166 | 167 | pid := p.Pid() | |
| @@ -191,7 +192,7 @@ func TestClose_TerminatesLongRunningChild(t *testing.T) { | |||
| 191 | 192 | t.Skip("skipping pty integration test in -short mode") | |
| 192 | 193 | } | |
| 193 | 194 | f := newFactory() | |
| 194 | - | p, err := f.Open("sh", []string{"-c", "sleep 60"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 195 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 60"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 195 | 196 | require.NoError(t, err) | |
| 196 | 197 | pid := p.Pid() | |
| 197 | 198 | ||
| @@ -212,7 +213,7 @@ func TestResize_DeliversSIGWINCH(t *testing.T) { | |||
| 212 | 213 | // deliver SIGWINCH to the foreground process group. | |
| 213 | 214 | script := `trap 'echo GOTWINCH; exit 0' WINCH; echo READY; i=0; while [ $i -lt 200 ]; do sleep 0.05; i=$((i+1)); done` | |
| 214 | 215 | f := newFactory() | |
| 215 | - | p, err := f.Open("sh", []string{"-c", script}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 216 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", script}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | |
| 216 | 217 | require.NoError(t, err) | |
| 217 | 218 | t.Cleanup(func() { _ = p.Close() }) | |
| 218 | 219 | ||
| @@ -4,6 +4,7 @@ package pty_test | |||||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "bytes" | 6 | "bytes" | ||
| 7 | + | "context" | |||
| 7 | "errors" | 8 | "errors" | ||
| 8 | "io" | 9 | "io" | ||
| 9 | "os" | 10 | "os" | ||
| @@ -53,7 +54,7 @@ func TestOpen_ChildOutputToEOF(t *testing.T) { | |||||
| 53 | t.Skip("skipping pty integration test in -short mode") | 54 | t.Skip("skipping pty integration test in -short mode") | ||
| 54 | } | 55 | } | ||
| 55 | f := newFactory() | 56 | f := newFactory() | ||
| 56 | - | p, err := f.Open("sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 57 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "printf hello"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) |
| 57 | require.NoError(t, err) | 58 | require.NoError(t, err) | ||
| 58 | t.Cleanup(func() { _ = p.Close() }) | 59 | t.Cleanup(func() { _ = p.Close() }) | ||
| 59 | 60 | ||||
| @@ -70,7 +71,7 @@ func TestOpen_EchoRoundtrip(t *testing.T) { | |||||
| 70 | t.Skip("skipping pty integration test in -short mode") | 71 | t.Skip("skipping pty integration test in -short mode") | ||
| 71 | } | 72 | } | ||
| 72 | f := newFactory() | 73 | f := newFactory() | ||
| 73 | - | p, err := f.Open("cat", nil, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 74 | + | p, err := f.Open(context.Background(), "cat", nil, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) |
| 74 | require.NoError(t, err) | 75 | require.NoError(t, err) | ||
| 75 | t.Cleanup(func() { _ = p.Close() }) | 76 | t.Cleanup(func() { _ = p.Close() }) | ||
| 76 | 77 | ||||
| @@ -103,7 +104,7 @@ func TestForegroundPgid(t *testing.T) { | |||||
| 103 | t.Skip("skipping pty integration test in -short mode") | 104 | t.Skip("skipping pty integration test in -short mode") | ||
| 104 | } | 105 | } | ||
| 105 | f := newFactory() | 106 | f := newFactory() | ||
| 106 | - | p, err := f.Open("sh", []string{"-c", "sleep 2"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 107 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 2"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) |
| 107 | require.NoError(t, err) | 108 | require.NoError(t, err) | ||
| 108 | t.Cleanup(func() { _ = p.Close() }) | 109 | t.Cleanup(func() { _ = p.Close() }) | ||
| 109 | 110 | ||||
| @@ -141,7 +142,7 @@ func TestResize_SttySize(t *testing.T) { | |||||
| 141 | for _, tt := range tests { | 142 | for _, tt := range tests { | ||
| 142 | t.Run(tt.name, func(t *testing.T) { | 143 | t.Run(tt.name, func(t *testing.T) { | ||
| 143 | f := newFactory() | 144 | f := newFactory() | ||
| 144 | - | p, err := f.Open("sh", []string{"-c", tt.script}, os.Environ(), "", tt.initial) | 145 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", tt.script}, os.Environ(), "", tt.initial) |
| 145 | require.NoError(t, err) | 146 | require.NoError(t, err) | ||
| 146 | t.Cleanup(func() { _ = p.Close() }) | 147 | t.Cleanup(func() { _ = p.Close() }) | ||
| 147 | 148 | ||||
| @@ -160,7 +161,7 @@ func TestClose_ReapsChildNoZombie(t *testing.T) { | |||||
| 160 | t.Skip("skipping pty integration test in -short mode") | 161 | t.Skip("skipping pty integration test in -short mode") | ||
| 161 | } | 162 | } | ||
| 162 | f := newFactory() | 163 | f := newFactory() | ||
| 163 | - | p, err := f.Open("sh", []string{"-c", "sleep 30"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 164 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 30"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) |
| 164 | require.NoError(t, err) | 165 | require.NoError(t, err) | ||
| 165 | 166 | ||||
| 166 | pid := p.Pid() | 167 | pid := p.Pid() | ||
| @@ -191,7 +192,7 @@ func TestClose_TerminatesLongRunningChild(t *testing.T) { | |||||
| 191 | t.Skip("skipping pty integration test in -short mode") | 192 | t.Skip("skipping pty integration test in -short mode") | ||
| 192 | } | 193 | } | ||
| 193 | f := newFactory() | 194 | f := newFactory() | ||
| 194 | - | p, err := f.Open("sh", []string{"-c", "sleep 60"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 195 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", "sleep 60"}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) |
| 195 | require.NoError(t, err) | 196 | require.NoError(t, err) | ||
| 196 | pid := p.Pid() | 197 | pid := p.Pid() | ||
| 197 | 198 | ||||
| @@ -212,7 +213,7 @@ func TestResize_DeliversSIGWINCH(t *testing.T) { | |||||
| 212 | // deliver SIGWINCH to the foreground process group. | 213 | // deliver SIGWINCH to the foreground process group. | ||
| 213 | script := `trap 'echo GOTWINCH; exit 0' WINCH; echo READY; i=0; while [ $i -lt 200 ]; do sleep 0.05; i=$((i+1)); done` | 214 | script := `trap 'echo GOTWINCH; exit 0' WINCH; echo READY; i=0; while [ $i -lt 200 ]; do sleep 0.05; i=$((i+1)); done` | ||
| 214 | f := newFactory() | 215 | f := newFactory() | ||
| 215 | - | p, err := f.Open("sh", []string{"-c", script}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) | 216 | + | p, err := f.Open(context.Background(), "sh", []string{"-c", script}, os.Environ(), "", domain.Size{Cols: 80, Rows: 24}) |
| 216 | require.NoError(t, err) | 217 | require.NoError(t, err) | ||
| 217 | t.Cleanup(func() { _ = p.Close() }) | 218 | t.Cleanup(func() { _ = p.Close() }) | ||
| 218 | 219 | ||||
M internal/ports/mocks/mocks.go +26 -20
| @@ -499,8 +499,8 @@ func (_m *MockPTYFactory) EXPECT() *MockPTYFactory_Expecter { | |||
| 499 | 499 | } | |
| 500 | 500 | ||
| 501 | 501 | // Open provides a mock function for the type MockPTYFactory | |
| 502 | - | func (_mock *MockPTYFactory) Open(cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | |
| 503 | - | ret := _mock.Called(cmd, args, env, dir, sz) | |
| 502 | + | func (_mock *MockPTYFactory) Open(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | |
| 503 | + | ret := _mock.Called(ctx, cmd, args, env, dir, sz) | |
| 504 | 504 | ||
| 505 | 505 | if len(ret) == 0 { | |
| 506 | 506 | panic("no return value specified for Open") | |
| @@ -508,18 +508,18 @@ func (_mock *MockPTYFactory) Open(cmd string, args []string, env []string, dir s | |||
| 508 | 508 | ||
| 509 | 509 | var r0 ports.PTY | |
| 510 | 510 | var r1 error | |
| 511 | - | if returnFunc, ok := ret.Get(0).(func(string, []string, []string, string, domain.Size) (ports.PTY, error)); ok { | |
| 512 | - | return returnFunc(cmd, args, env, dir, sz) | |
| 511 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Size) (ports.PTY, error)); ok { | |
| 512 | + | return returnFunc(ctx, cmd, args, env, dir, sz) | |
| 513 | 513 | } | |
| 514 | - | if returnFunc, ok := ret.Get(0).(func(string, []string, []string, string, domain.Size) ports.PTY); ok { | |
| 515 | - | r0 = returnFunc(cmd, args, env, dir, sz) | |
| 514 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Size) ports.PTY); ok { | |
| 515 | + | r0 = returnFunc(ctx, cmd, args, env, dir, sz) | |
| 516 | 516 | } else { | |
| 517 | 517 | if ret.Get(0) != nil { | |
| 518 | 518 | r0 = ret.Get(0).(ports.PTY) | |
| 519 | 519 | } | |
| 520 | 520 | } | |
| 521 | - | if returnFunc, ok := ret.Get(1).(func(string, []string, []string, string, domain.Size) error); ok { | |
| 522 | - | r1 = returnFunc(cmd, args, env, dir, sz) | |
| 521 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string, []string, []string, string, domain.Size) error); ok { | |
| 522 | + | r1 = returnFunc(ctx, cmd, args, env, dir, sz) | |
| 523 | 523 | } else { | |
| 524 | 524 | r1 = ret.Error(1) | |
| 525 | 525 | } | |
| @@ -532,36 +532,41 @@ type MockPTYFactory_Open_Call struct { | |||
| 532 | 532 | } | |
| 533 | 533 | ||
| 534 | 534 | // Open is a helper method to define mock.On call | |
| 535 | + | // - ctx context.Context | |
| 535 | 536 | // - cmd string | |
| 536 | 537 | // - args []string | |
| 537 | 538 | // - env []string | |
| 538 | 539 | // - dir string | |
| 539 | 540 | // - sz domain.Size | |
| 540 | - | func (_e *MockPTYFactory_Expecter) Open(cmd any, args any, env any, dir any, sz any) *MockPTYFactory_Open_Call { | |
| 541 | - | return &MockPTYFactory_Open_Call{Call: _e.mock.On("Open", cmd, args, env, dir, sz)} | |
| 541 | + | func (_e *MockPTYFactory_Expecter) Open(ctx any, cmd any, args any, env any, dir any, sz any) *MockPTYFactory_Open_Call { | |
| 542 | + | return &MockPTYFactory_Open_Call{Call: _e.mock.On("Open", ctx, cmd, args, env, dir, sz)} | |
| 542 | 543 | } | |
| 543 | 544 | ||
| 544 | - | func (_c *MockPTYFactory_Open_Call) Run(run func(cmd string, args []string, env []string, dir string, sz domain.Size)) *MockPTYFactory_Open_Call { | |
| 545 | + | func (_c *MockPTYFactory_Open_Call) Run(run func(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size)) *MockPTYFactory_Open_Call { | |
| 545 | 546 | _c.Call.Run(func(args mock.Arguments) { | |
| 546 | - | var arg0 string | |
| 547 | + | var arg0 context.Context | |
| 547 | 548 | if args[0] != nil { | |
| 548 | - | arg0 = args[0].(string) | |
| 549 | + | arg0 = args[0].(context.Context) | |
| 549 | 550 | } | |
| 550 | - | var arg1 []string | |
| 551 | + | var arg1 string | |
| 551 | 552 | if args[1] != nil { | |
| 552 | - | arg1 = args[1].([]string) | |
| 553 | + | arg1 = args[1].(string) | |
| 553 | 554 | } | |
| 554 | 555 | var arg2 []string | |
| 555 | 556 | if args[2] != nil { | |
| 556 | 557 | arg2 = args[2].([]string) | |
| 557 | 558 | } | |
| 558 | - | var arg3 string | |
| 559 | + | var arg3 []string | |
| 559 | 560 | if args[3] != nil { | |
| 560 | - | arg3 = args[3].(string) | |
| 561 | + | arg3 = args[3].([]string) | |
| 561 | 562 | } | |
| 562 | - | var arg4 domain.Size | |
| 563 | + | var arg4 string | |
| 563 | 564 | if args[4] != nil { | |
| 564 | - | arg4 = args[4].(domain.Size) | |
| 565 | + | arg4 = args[4].(string) | |
| 566 | + | } | |
| 567 | + | var arg5 domain.Size | |
| 568 | + | if args[5] != nil { | |
| 569 | + | arg5 = args[5].(domain.Size) | |
| 565 | 570 | } | |
| 566 | 571 | run( | |
| 567 | 572 | arg0, | |
| @@ -569,6 +574,7 @@ func (_c *MockPTYFactory_Open_Call) Run(run func(cmd string, args []string, env | |||
| 569 | 574 | arg2, | |
| 570 | 575 | arg3, | |
| 571 | 576 | arg4, | |
| 577 | + | arg5, | |
| 572 | 578 | ) | |
| 573 | 579 | }) | |
| 574 | 580 | return _c | |
| @@ -579,7 +585,7 @@ func (_c *MockPTYFactory_Open_Call) Return(pTY ports.PTY, err error) *MockPTYFac | |||
| 579 | 585 | return _c | |
| 580 | 586 | } | |
| 581 | 587 | ||
| 582 | - | func (_c *MockPTYFactory_Open_Call) RunAndReturn(run func(cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error)) *MockPTYFactory_Open_Call { | |
| 588 | + | func (_c *MockPTYFactory_Open_Call) RunAndReturn(run func(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error)) *MockPTYFactory_Open_Call { | |
| 583 | 589 | _c.Call.Return(run) | |
| 584 | 590 | return _c | |
| 585 | 591 | } | |
| @@ -499,8 +499,8 @@ func (_m *MockPTYFactory) EXPECT() *MockPTYFactory_Expecter { | |||||
| 499 | } | 499 | } | ||
| 500 | 500 | ||||
| 501 | // Open provides a mock function for the type MockPTYFactory | 501 | // Open provides a mock function for the type MockPTYFactory | ||
| 502 | - | func (_mock *MockPTYFactory) Open(cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | 502 | + | func (_mock *MockPTYFactory) Open(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { |
| 503 | - | ret := _mock.Called(cmd, args, env, dir, sz) | 503 | + | ret := _mock.Called(ctx, cmd, args, env, dir, sz) |
| 504 | 504 | ||||
| 505 | if len(ret) == 0 { | 505 | if len(ret) == 0 { | ||
| 506 | panic("no return value specified for Open") | 506 | panic("no return value specified for Open") | ||
| @@ -508,18 +508,18 @@ func (_mock *MockPTYFactory) Open(cmd string, args []string, env []string, dir s | |||||
| 508 | 508 | ||||
| 509 | var r0 ports.PTY | 509 | var r0 ports.PTY | ||
| 510 | var r1 error | 510 | var r1 error | ||
| 511 | - | if returnFunc, ok := ret.Get(0).(func(string, []string, []string, string, domain.Size) (ports.PTY, error)); ok { | 511 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Size) (ports.PTY, error)); ok { |
| 512 | - | return returnFunc(cmd, args, env, dir, sz) | 512 | + | return returnFunc(ctx, cmd, args, env, dir, sz) |
| 513 | } | 513 | } | ||
| 514 | - | if returnFunc, ok := ret.Get(0).(func(string, []string, []string, string, domain.Size) ports.PTY); ok { | 514 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, []string, string, domain.Size) ports.PTY); ok { |
| 515 | - | r0 = returnFunc(cmd, args, env, dir, sz) | 515 | + | r0 = returnFunc(ctx, cmd, args, env, dir, sz) |
| 516 | } else { | 516 | } else { | ||
| 517 | if ret.Get(0) != nil { | 517 | if ret.Get(0) != nil { | ||
| 518 | r0 = ret.Get(0).(ports.PTY) | 518 | r0 = ret.Get(0).(ports.PTY) | ||
| 519 | } | 519 | } | ||
| 520 | } | 520 | } | ||
| 521 | - | if returnFunc, ok := ret.Get(1).(func(string, []string, []string, string, domain.Size) error); ok { | 521 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string, []string, []string, string, domain.Size) error); ok { |
| 522 | - | r1 = returnFunc(cmd, args, env, dir, sz) | 522 | + | r1 = returnFunc(ctx, cmd, args, env, dir, sz) |
| 523 | } else { | 523 | } else { | ||
| 524 | r1 = ret.Error(1) | 524 | r1 = ret.Error(1) | ||
| 525 | } | 525 | } | ||
| @@ -532,36 +532,41 @@ type MockPTYFactory_Open_Call struct { | |||||
| 532 | } | 532 | } | ||
| 533 | 533 | ||||
| 534 | // Open is a helper method to define mock.On call | 534 | // Open is a helper method to define mock.On call | ||
| 535 | + | // - ctx context.Context | |||
| 535 | // - cmd string | 536 | // - cmd string | ||
| 536 | // - args []string | 537 | // - args []string | ||
| 537 | // - env []string | 538 | // - env []string | ||
| 538 | // - dir string | 539 | // - dir string | ||
| 539 | // - sz domain.Size | 540 | // - sz domain.Size | ||
| 540 | - | func (_e *MockPTYFactory_Expecter) Open(cmd any, args any, env any, dir any, sz any) *MockPTYFactory_Open_Call { | 541 | + | func (_e *MockPTYFactory_Expecter) Open(ctx any, cmd any, args any, env any, dir any, sz any) *MockPTYFactory_Open_Call { |
| 541 | - | return &MockPTYFactory_Open_Call{Call: _e.mock.On("Open", cmd, args, env, dir, sz)} | 542 | + | return &MockPTYFactory_Open_Call{Call: _e.mock.On("Open", ctx, cmd, args, env, dir, sz)} |
| 542 | } | 543 | } | ||
| 543 | 544 | ||||
| 544 | - | func (_c *MockPTYFactory_Open_Call) Run(run func(cmd string, args []string, env []string, dir string, sz domain.Size)) *MockPTYFactory_Open_Call { | 545 | + | func (_c *MockPTYFactory_Open_Call) Run(run func(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size)) *MockPTYFactory_Open_Call { |
| 545 | _c.Call.Run(func(args mock.Arguments) { | 546 | _c.Call.Run(func(args mock.Arguments) { | ||
| 546 | - | var arg0 string | 547 | + | var arg0 context.Context |
| 547 | if args[0] != nil { | 548 | if args[0] != nil { | ||
| 548 | - | arg0 = args[0].(string) | 549 | + | arg0 = args[0].(context.Context) |
| 549 | } | 550 | } | ||
| 550 | - | var arg1 []string | 551 | + | var arg1 string |
| 551 | if args[1] != nil { | 552 | if args[1] != nil { | ||
| 552 | - | arg1 = args[1].([]string) | 553 | + | arg1 = args[1].(string) |
| 553 | } | 554 | } | ||
| 554 | var arg2 []string | 555 | var arg2 []string | ||
| 555 | if args[2] != nil { | 556 | if args[2] != nil { | ||
| 556 | arg2 = args[2].([]string) | 557 | arg2 = args[2].([]string) | ||
| 557 | } | 558 | } | ||
| 558 | - | var arg3 string | 559 | + | var arg3 []string |
| 559 | if args[3] != nil { | 560 | if args[3] != nil { | ||
| 560 | - | arg3 = args[3].(string) | 561 | + | arg3 = args[3].([]string) |
| 561 | } | 562 | } | ||
| 562 | - | var arg4 domain.Size | 563 | + | var arg4 string |
| 563 | if args[4] != nil { | 564 | if args[4] != nil { | ||
| 564 | - | arg4 = args[4].(domain.Size) | 565 | + | arg4 = args[4].(string) |
| 566 | + | } | |||
| 567 | + | var arg5 domain.Size | |||
| 568 | + | if args[5] != nil { | |||
| 569 | + | arg5 = args[5].(domain.Size) | |||
| 565 | } | 570 | } | ||
| 566 | run( | 571 | run( | ||
| 567 | arg0, | 572 | arg0, | ||
| @@ -569,6 +574,7 @@ func (_c *MockPTYFactory_Open_Call) Run(run func(cmd string, args []string, env | |||||
| 569 | arg2, | 574 | arg2, | ||
| 570 | arg3, | 575 | arg3, | ||
| 571 | arg4, | 576 | arg4, | ||
| 577 | + | arg5, | |||
| 572 | ) | 578 | ) | ||
| 573 | }) | 579 | }) | ||
| 574 | return _c | 580 | return _c | ||
| @@ -579,7 +585,7 @@ func (_c *MockPTYFactory_Open_Call) Return(pTY ports.PTY, err error) *MockPTYFac | |||||
| 579 | return _c | 585 | return _c | ||
| 580 | } | 586 | } | ||
| 581 | 587 | ||||
| 582 | - | func (_c *MockPTYFactory_Open_Call) RunAndReturn(run func(cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error)) *MockPTYFactory_Open_Call { | 588 | + | func (_c *MockPTYFactory_Open_Call) RunAndReturn(run func(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (ports.PTY, error)) *MockPTYFactory_Open_Call { |
| 583 | _c.Call.Return(run) | 589 | _c.Call.Return(run) | ||
| 584 | return _c | 590 | return _c | ||
| 585 | } | 591 | } | ||
M internal/ports/ports.go +3 -1
| @@ -24,7 +24,9 @@ type PTY interface { | |||
| 24 | 24 | ||
| 25 | 25 | // PTYFactory creates PTYs by spawning a command attached to a new pseudo-terminal. | |
| 26 | 26 | type PTYFactory interface { | |
| 27 | - | Open(cmd string, args []string, env []string, dir string, sz domain.Size) (PTY, error) | |
| 27 | + | // Open creates a PTY while honoring ctx cancellation. Implementations must | |
| 28 | + | // return promptly once ctx is cancelled and must not leave a child running. | |
| 29 | + | Open(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (PTY, error) | |
| 28 | 30 | } | |
| 29 | 31 | ||
| 30 | 32 | // Terminal is the CLIENT-side controlling terminal. | |
| @@ -24,7 +24,9 @@ type PTY interface { | |||||
| 24 | 24 | ||||
| 25 | // PTYFactory creates PTYs by spawning a command attached to a new pseudo-terminal. | 25 | // PTYFactory creates PTYs by spawning a command attached to a new pseudo-terminal. | ||
| 26 | type PTYFactory interface { | 26 | type PTYFactory interface { | ||
| 27 | - | Open(cmd string, args []string, env []string, dir string, sz domain.Size) (PTY, error) | 27 | + | // Open creates a PTY while honoring ctx cancellation. Implementations must |
| 28 | + | // return promptly once ctx is cancelled and must not leave a child running. | |||
| 29 | + | Open(ctx context.Context, cmd string, args []string, env []string, dir string, sz domain.Size) (PTY, error) | |||
| 28 | } | 30 | } | ||
| 29 | 31 | ||||
| 30 | // Terminal is the CLIENT-side controlling terminal. | 32 | // Terminal is the CLIENT-side controlling terminal. | ||
M internal/usecase/daemon/daemon.go +0 -10
| @@ -22,7 +22,6 @@ import ( | |||
| 22 | 22 | // sustained floods step toward the maximum to reduce frame/syscall pressure. | |
| 23 | 23 | const ( | |
| 24 | 24 | minDebounceInterval = 2 * time.Millisecond | |
| 25 | - | maxDebounceInterval = 16 * time.Millisecond | |
| 26 | 25 | maxSyncUpdateDuration = 500 * time.Millisecond | |
| 27 | 26 | ) | |
| 28 | 27 | ||
| @@ -74,10 +73,6 @@ type Daemon struct { | |||
| 74 | 73 | // under the same mutex — so a Hello racing shutdown can never insert a new | |
| 75 | 74 | // session that nobody would tear down. | |
| 76 | 75 | closing bool | |
| 77 | - | // floatingLaunchGate serializes the irreversible shutdown decision with the | |
| 78 | - | // final transition into PTYFactory.Open. It is intentionally not d.mu: no | |
| 79 | - | // daemon, session, or tab lock is held while an external Open can block. | |
| 80 | - | floatingLaunchGate sync.Mutex | |
| 81 | 76 | // notifies holds one completion channel per in-flight async Detached | |
| 82 | 77 | // notification (guarded by mu, pruned on insert). Channels rather than a | |
| 83 | 78 | // WaitGroup: notifications are spawned from arbitrary goroutines while | |
| @@ -423,10 +418,6 @@ func (d *Daemon) Serve(ctx context.Context, l ports.Listener) error { | |||
| 423 | 418 | // inserted after the snapshot: route rejects once closing is set, and both run | |
| 424 | 419 | // under d.mu. killSession (which relocks) runs after the lock is released. | |
| 425 | 420 | func (d *Daemon) shutdownAll(reason uint8) { | |
| 426 | - | // Take the launch gate before publishing closing. A launch either entered | |
| 427 | - | // Open before this point (and is subsequently reaped), or observes closing | |
| 428 | - | // and never starts an external child. | |
| 429 | - | d.floatingLaunchGate.Lock() | |
| 430 | 421 | d.mu.Lock() | |
| 431 | 422 | d.closing = true | |
| 432 | 423 | for token, parked := range d.parked { | |
| @@ -435,7 +426,6 @@ func (d *Daemon) shutdownAll(reason uint8) { | |||
| 435 | 426 | snapshot := d.sessionsSnapshotLocked() | |
| 436 | 427 | empty := len(snapshot) == 0 | |
| 437 | 428 | d.mu.Unlock() | |
| 438 | - | d.floatingLaunchGate.Unlock() | |
| 439 | 429 | d.log.Info("graceful shutdown begin", "reason", reason, "sessions", len(snapshot)) | |
| 440 | 430 | if empty { | |
| 441 | 431 | d.doneOnce.Do(func() { close(d.done) }) | |
| @@ -22,7 +22,6 @@ import ( | |||||
| 22 | // sustained floods step toward the maximum to reduce frame/syscall pressure. | 22 | // sustained floods step toward the maximum to reduce frame/syscall pressure. | ||
| 23 | const ( | 23 | const ( | ||
| 24 | minDebounceInterval = 2 * time.Millisecond | 24 | minDebounceInterval = 2 * time.Millisecond | ||
| 25 | - | maxDebounceInterval = 16 * time.Millisecond | |||
| 26 | maxSyncUpdateDuration = 500 * time.Millisecond | 25 | maxSyncUpdateDuration = 500 * time.Millisecond | ||
| 27 | ) | 26 | ) | ||
| 28 | 27 | ||||
| @@ -74,10 +73,6 @@ type Daemon struct { | |||||
| 74 | // under the same mutex — so a Hello racing shutdown can never insert a new | 73 | // under the same mutex — so a Hello racing shutdown can never insert a new | ||
| 75 | // session that nobody would tear down. | 74 | // session that nobody would tear down. | ||
| 76 | closing bool | 75 | closing bool | ||
| 77 | - | // floatingLaunchGate serializes the irreversible shutdown decision with the | |||
| 78 | - | // final transition into PTYFactory.Open. It is intentionally not d.mu: no | |||
| 79 | - | // daemon, session, or tab lock is held while an external Open can block. | |||
| 80 | - | floatingLaunchGate sync.Mutex | |||
| 81 | // notifies holds one completion channel per in-flight async Detached | 76 | // notifies holds one completion channel per in-flight async Detached | ||
| 82 | // notification (guarded by mu, pruned on insert). Channels rather than a | 77 | // notification (guarded by mu, pruned on insert). Channels rather than a | ||
| 83 | // WaitGroup: notifications are spawned from arbitrary goroutines while | 78 | // WaitGroup: notifications are spawned from arbitrary goroutines while | ||
| @@ -423,10 +418,6 @@ func (d *Daemon) Serve(ctx context.Context, l ports.Listener) error { | |||||
| 423 | // inserted after the snapshot: route rejects once closing is set, and both run | 418 | // inserted after the snapshot: route rejects once closing is set, and both run | ||
| 424 | // under d.mu. killSession (which relocks) runs after the lock is released. | 419 | // under d.mu. killSession (which relocks) runs after the lock is released. | ||
| 425 | func (d *Daemon) shutdownAll(reason uint8) { | 420 | func (d *Daemon) shutdownAll(reason uint8) { | ||
| 426 | - | // Take the launch gate before publishing closing. A launch either entered | |||
| 427 | - | // Open before this point (and is subsequently reaped), or observes closing | |||
| 428 | - | // and never starts an external child. | |||
| 429 | - | d.floatingLaunchGate.Lock() | |||
| 430 | d.mu.Lock() | 421 | d.mu.Lock() | ||
| 431 | d.closing = true | 422 | d.closing = true | ||
| 432 | for token, parked := range d.parked { | 423 | for token, parked := range d.parked { | ||
| @@ -435,7 +426,6 @@ func (d *Daemon) shutdownAll(reason uint8) { | |||||
| 435 | snapshot := d.sessionsSnapshotLocked() | 426 | snapshot := d.sessionsSnapshotLocked() | ||
| 436 | empty := len(snapshot) == 0 | 427 | empty := len(snapshot) == 0 | ||
| 437 | d.mu.Unlock() | 428 | d.mu.Unlock() | ||
| 438 | - | d.floatingLaunchGate.Unlock() | |||
| 439 | d.log.Info("graceful shutdown begin", "reason", reason, "sessions", len(snapshot)) | 429 | d.log.Info("graceful shutdown begin", "reason", reason, "sessions", len(snapshot)) | ||
| 440 | if empty { | 430 | if empty { | ||
| 441 | d.doneOnce.Do(func() { close(d.done) }) | 431 | d.doneOnce.Do(func() { close(d.done) }) | ||
M internal/usecase/daemon/daemon_test.go +5 -5
| @@ -142,7 +142,7 @@ func newBlockingOpenFactory(t *testing.T, d *Daemon) *blockingOpenFactory { | |||
| 142 | 142 | return f | |
| 143 | 143 | } | |
| 144 | 144 | ||
| 145 | - | func (f *blockingOpenFactory) Open(string, []string, []string, string, domain.Size) (ports.PTY, error) { | |
| 145 | + | func (f *blockingOpenFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 146 | 146 | <-f.release | |
| 147 | 147 | return nil, io.ErrClosedPipe | |
| 148 | 148 | } | |
| @@ -154,8 +154,8 @@ func newFactory(t *testing.T, p ports.PTY) *portsmocks.MockPTYFactory { | |||
| 154 | 154 | t.Helper() | |
| 155 | 155 | f := portsmocks.NewMockPTYFactory(t) | |
| 156 | 156 | var normal domain.Size | |
| 157 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 158 | - | func(_ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | |
| 157 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 158 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | |
| 159 | 159 | if !normal.Valid() { | |
| 160 | 160 | normal = sz | |
| 161 | 161 | } | |
| @@ -174,8 +174,8 @@ func newFactorySeq(t *testing.T, ptys ...ports.PTY) *portsmocks.MockPTYFactory { | |||
| 174 | 174 | var mu sync.Mutex | |
| 175 | 175 | var normal domain.Size | |
| 176 | 176 | next := 0 | |
| 177 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 178 | - | func(_ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | |
| 177 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 178 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | |
| 179 | 179 | mu.Lock() | |
| 180 | 180 | defer mu.Unlock() | |
| 181 | 181 | if !normal.Valid() { | |
| @@ -142,7 +142,7 @@ func newBlockingOpenFactory(t *testing.T, d *Daemon) *blockingOpenFactory { | |||||
| 142 | return f | 142 | return f | ||
| 143 | } | 143 | } | ||
| 144 | 144 | ||||
| 145 | - | func (f *blockingOpenFactory) Open(string, []string, []string, string, domain.Size) (ports.PTY, error) { | 145 | + | func (f *blockingOpenFactory) Open(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { |
| 146 | <-f.release | 146 | <-f.release | ||
| 147 | return nil, io.ErrClosedPipe | 147 | return nil, io.ErrClosedPipe | ||
| 148 | } | 148 | } | ||
| @@ -154,8 +154,8 @@ func newFactory(t *testing.T, p ports.PTY) *portsmocks.MockPTYFactory { | |||||
| 154 | t.Helper() | 154 | t.Helper() | ||
| 155 | f := portsmocks.NewMockPTYFactory(t) | 155 | f := portsmocks.NewMockPTYFactory(t) | ||
| 156 | var normal domain.Size | 156 | var normal domain.Size | ||
| 157 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 157 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( |
| 158 | - | func(_ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | 158 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { |
| 159 | if !normal.Valid() { | 159 | if !normal.Valid() { | ||
| 160 | normal = sz | 160 | normal = sz | ||
| 161 | } | 161 | } | ||
| @@ -174,8 +174,8 @@ func newFactorySeq(t *testing.T, ptys ...ports.PTY) *portsmocks.MockPTYFactory { | |||||
| 174 | var mu sync.Mutex | 174 | var mu sync.Mutex | ||
| 175 | var normal domain.Size | 175 | var normal domain.Size | ||
| 176 | next := 0 | 176 | next := 0 | ||
| 177 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 177 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( |
| 178 | - | func(_ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { | 178 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, sz domain.Size) (ports.PTY, error) { |
| 179 | mu.Lock() | 179 | mu.Lock() | ||
| 180 | defer mu.Unlock() | 180 | defer mu.Unlock() | ||
| 181 | if !normal.Valid() { | 181 | if !normal.Valid() { | ||
M internal/usecase/daemon/floating.go +12 -44
| @@ -7,7 +7,6 @@ import ( | |||
| 7 | 7 | "strings" | |
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/vev/internal/domain" | |
| 10 | - | "github.com/bnema/vev/internal/ports" | |
| 11 | 10 | "github.com/bnema/vev/internal/usecase/layout" | |
| 12 | 11 | ) | |
| 13 | 12 | ||
| @@ -246,18 +245,6 @@ func (sess *session) registerFloatingLaunch() (*floatingLaunch, bool) { | |||
| 246 | 245 | return launch, true | |
| 247 | 246 | } | |
| 248 | 247 | ||
| 249 | - | // openFloating runs fn while only the dedicated launch-ownership lock is | |
| 250 | - | // held. It deliberately does not hold sess.mu, tb.mu, or d.mu across Open. | |
| 251 | - | func (sess *session) openFloating(fn func() (ports.PTY, error)) (ports.PTY, error, bool) { | |
| 252 | - | sess.floatingLaunchMu.Lock() | |
| 253 | - | defer sess.floatingLaunchMu.Unlock() | |
| 254 | - | if sess.floatingLaunchStopping { | |
| 255 | - | return nil, nil, false | |
| 256 | - | } | |
| 257 | - | pty, err := fn() | |
| 258 | - | return pty, err, true | |
| 259 | - | } | |
| 260 | - | ||
| 261 | 248 | func (sess *session) finishFloatingLaunch(launch *floatingLaunch) { | |
| 262 | 249 | sess.floatingLaunchMu.Lock() | |
| 263 | 250 | delete(sess.floatingLaunches, launch) | |
| @@ -265,15 +252,10 @@ func (sess *session) finishFloatingLaunch(launch *floatingLaunch) { | |||
| 265 | 252 | sess.floatingLaunchMu.Unlock() | |
| 266 | 253 | } | |
| 267 | 254 | ||
| 268 | - | func (sess *session) stopFloatingLaunches() []<-chan struct{} { | |
| 255 | + | func (sess *session) stopFloatingLaunches() { | |
| 269 | 256 | sess.floatingLaunchMu.Lock() | |
| 270 | 257 | sess.floatingLaunchStopping = true | |
| 271 | - | done := make([]<-chan struct{}, 0, len(sess.floatingLaunches)) | |
| 272 | - | for launch := range sess.floatingLaunches { | |
| 273 | - | done = append(done, launch.done) | |
| 274 | - | } | |
| 275 | 258 | sess.floatingLaunchMu.Unlock() | |
| 276 | - | return done | |
| 277 | 259 | } | |
| 278 | 260 | ||
| 279 | 261 | type floatingLaunchSpec struct { | |
| @@ -366,33 +348,19 @@ func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.Floati | |||
| 366 | 348 | // openAndInstallFloating runs entirely in the launch worker. No PTY operation | |
| 367 | 349 | // occurs under tb.mu; the pane is fully initialized before publication. | |
| 368 | 350 | func (d *Daemon) openAndInstallFloating(sess *session, tb *tab, spec floatingLaunchSpec, generation uint64, launch *floatingLaunch) { | |
| 369 | - | // The gate makes the final ownership check and external Open one shutdown | |
| 370 | - | // epoch: shutdown cannot become irreversible between them. Neither d.mu, | |
| 371 | - | // sess.mu, nor tb.mu is held while the factory can block. | |
| 372 | - | d.floatingLaunchGate.Lock() | |
| 373 | - | select { | |
| 374 | - | case <-spec.parentCtx.Done(): | |
| 375 | - | d.floatingLaunchGate.Unlock() | |
| 376 | - | return | |
| 377 | - | default: | |
| 378 | - | } | |
| 379 | - | d.mu.Lock() | |
| 380 | - | // Unit-scoped launch paths may use a session without registering it in the | |
| 381 | - | // daemon map. Production sessions have an ID and must still be owned by | |
| 382 | - | // the map until teardown removes them. | |
| 383 | - | live := !d.closing && (sess.id == "" || d.sessions[sess.id] == sess) | |
| 384 | - | d.mu.Unlock() | |
| 385 | - | if !live { | |
| 386 | - | d.floatingLaunchGate.Unlock() | |
| 387 | - | return | |
| 388 | - | } | |
| 389 | - | pty, err, opened := sess.openFloating(func() (ports.PTY, error) { | |
| 390 | - | return d.ptys.Open(spec.command, spec.args, spec.env, spec.cwd, spec.size) | |
| 391 | - | }) | |
| 392 | - | d.floatingLaunchGate.Unlock() | |
| 393 | - | if !opened { | |
| 351 | + | // Open is context-aware and is intentionally called without any daemon, | |
| 352 | + | // session, tab, or launch-ownership lock. Cancellation makes teardown | |
| 353 | + | // bounded even if the adapter is waiting to create the child. | |
| 354 | + | if err := spec.parentCtx.Err(); err != nil { | |
| 394 | 355 | return | |
| 395 | 356 | } | |
| 357 | + | openCtx, cancelOpen := context.WithCancel(spec.parentCtx) | |
| 358 | + | stopSessionCancel := context.AfterFunc(sess.ctx, cancelOpen) | |
| 359 | + | defer func() { | |
| 360 | + | stopSessionCancel() | |
| 361 | + | cancelOpen() | |
| 362 | + | }() | |
| 363 | + | pty, err := d.ptys.Open(openCtx, spec.command, spec.args, spec.env, spec.cwd, spec.size) | |
| 396 | 364 | if err != nil { | |
| 397 | 365 | d.failFloatingLaunch(tb, generation, spec.userOpen, spec.sessionName, err) | |
| 398 | 366 | return | |
| @@ -7,7 +7,6 @@ import ( | |||||
| 7 | "strings" | 7 | "strings" | ||
| 8 | 8 | ||||
| 9 | "github.com/bnema/vev/internal/domain" | 9 | "github.com/bnema/vev/internal/domain" | ||
| 10 | - | "github.com/bnema/vev/internal/ports" | |||
| 11 | "github.com/bnema/vev/internal/usecase/layout" | 10 | "github.com/bnema/vev/internal/usecase/layout" | ||
| 12 | ) | 11 | ) | ||
| 13 | 12 | ||||
| @@ -246,18 +245,6 @@ func (sess *session) registerFloatingLaunch() (*floatingLaunch, bool) { | |||||
| 246 | return launch, true | 245 | return launch, true | ||
| 247 | } | 246 | } | ||
| 248 | 247 | ||||
| 249 | - | // openFloating runs fn while only the dedicated launch-ownership lock is | |||
| 250 | - | // held. It deliberately does not hold sess.mu, tb.mu, or d.mu across Open. | |||
| 251 | - | func (sess *session) openFloating(fn func() (ports.PTY, error)) (ports.PTY, error, bool) { | |||
| 252 | - | sess.floatingLaunchMu.Lock() | |||
| 253 | - | defer sess.floatingLaunchMu.Unlock() | |||
| 254 | - | if sess.floatingLaunchStopping { | |||
| 255 | - | return nil, nil, false | |||
| 256 | - | } | |||
| 257 | - | pty, err := fn() | |||
| 258 | - | return pty, err, true | |||
| 259 | - | } | |||
| 260 | - | ||||
| 261 | func (sess *session) finishFloatingLaunch(launch *floatingLaunch) { | 248 | func (sess *session) finishFloatingLaunch(launch *floatingLaunch) { | ||
| 262 | sess.floatingLaunchMu.Lock() | 249 | sess.floatingLaunchMu.Lock() | ||
| 263 | delete(sess.floatingLaunches, launch) | 250 | delete(sess.floatingLaunches, launch) | ||
| @@ -265,15 +252,10 @@ func (sess *session) finishFloatingLaunch(launch *floatingLaunch) { | |||||
| 265 | sess.floatingLaunchMu.Unlock() | 252 | sess.floatingLaunchMu.Unlock() | ||
| 266 | } | 253 | } | ||
| 267 | 254 | ||||
| 268 | - | func (sess *session) stopFloatingLaunches() []<-chan struct{} { | 255 | + | func (sess *session) stopFloatingLaunches() { |
| 269 | sess.floatingLaunchMu.Lock() | 256 | sess.floatingLaunchMu.Lock() | ||
| 270 | sess.floatingLaunchStopping = true | 257 | sess.floatingLaunchStopping = true | ||
| 271 | - | done := make([]<-chan struct{}, 0, len(sess.floatingLaunches)) | |||
| 272 | - | for launch := range sess.floatingLaunches { | |||
| 273 | - | done = append(done, launch.done) | |||
| 274 | - | } | |||
| 275 | sess.floatingLaunchMu.Unlock() | 258 | sess.floatingLaunchMu.Unlock() | ||
| 276 | - | return done | |||
| 277 | } | 259 | } | ||
| 278 | 260 | ||||
| 279 | type floatingLaunchSpec struct { | 261 | type floatingLaunchSpec struct { | ||
| @@ -366,33 +348,19 @@ func (d *Daemon) newFloatingLaunchSpec(sess *session, tb *tab, cfg domain.Floati | |||||
| 366 | // openAndInstallFloating runs entirely in the launch worker. No PTY operation | 348 | // openAndInstallFloating runs entirely in the launch worker. No PTY operation | ||
| 367 | // occurs under tb.mu; the pane is fully initialized before publication. | 349 | // occurs under tb.mu; the pane is fully initialized before publication. | ||
| 368 | func (d *Daemon) openAndInstallFloating(sess *session, tb *tab, spec floatingLaunchSpec, generation uint64, launch *floatingLaunch) { | 350 | func (d *Daemon) openAndInstallFloating(sess *session, tb *tab, spec floatingLaunchSpec, generation uint64, launch *floatingLaunch) { | ||
| 369 | - | // The gate makes the final ownership check and external Open one shutdown | 351 | + | // Open is context-aware and is intentionally called without any daemon, |
| 370 | - | // epoch: shutdown cannot become irreversible between them. Neither d.mu, | 352 | + | // session, tab, or launch-ownership lock. Cancellation makes teardown |
| 371 | - | // sess.mu, nor tb.mu is held while the factory can block. | 353 | + | // bounded even if the adapter is waiting to create the child. |
| 372 | - | d.floatingLaunchGate.Lock() | 354 | + | if err := spec.parentCtx.Err(); err != nil { |
| 373 | - | select { | |||
| 374 | - | case <-spec.parentCtx.Done(): | |||
| 375 | - | d.floatingLaunchGate.Unlock() | |||
| 376 | - | return | |||
| 377 | - | default: | |||
| 378 | - | } | |||
| 379 | - | d.mu.Lock() | |||
| 380 | - | // Unit-scoped launch paths may use a session without registering it in the | |||
| 381 | - | // daemon map. Production sessions have an ID and must still be owned by | |||
| 382 | - | // the map until teardown removes them. | |||
| 383 | - | live := !d.closing && (sess.id == "" || d.sessions[sess.id] == sess) | |||
| 384 | - | d.mu.Unlock() | |||
| 385 | - | if !live { | |||
| 386 | - | d.floatingLaunchGate.Unlock() | |||
| 387 | - | return | |||
| 388 | - | } | |||
| 389 | - | pty, err, opened := sess.openFloating(func() (ports.PTY, error) { | |||
| 390 | - | return d.ptys.Open(spec.command, spec.args, spec.env, spec.cwd, spec.size) | |||
| 391 | - | }) | |||
| 392 | - | d.floatingLaunchGate.Unlock() | |||
| 393 | - | if !opened { | |||
| 394 | return | 355 | return | ||
| 395 | } | 356 | } | ||
| 357 | + | openCtx, cancelOpen := context.WithCancel(spec.parentCtx) | |||
| 358 | + | stopSessionCancel := context.AfterFunc(sess.ctx, cancelOpen) | |||
| 359 | + | defer func() { | |||
| 360 | + | stopSessionCancel() | |||
| 361 | + | cancelOpen() | |||
| 362 | + | }() | |||
| 363 | + | pty, err := d.ptys.Open(openCtx, spec.command, spec.args, spec.env, spec.cwd, spec.size) | |||
| 396 | if err != nil { | 364 | if err != nil { | ||
| 397 | d.failFloatingLaunch(tb, generation, spec.userOpen, spec.sessionName, err) | 365 | d.failFloatingLaunch(tb, generation, spec.userOpen, spec.sessionName, err) | ||
| 398 | return | 366 | return | ||
A internal/usecase/daemon/floating_open_test.go +96 -0
| @@ -0,0 +1,96 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "sync" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/stretchr/testify/mock" | |
| 10 | + | "github.com/stretchr/testify/require" | |
| 11 | + | ||
| 12 | + | "github.com/bnema/vev/internal/domain" | |
| 13 | + | "github.com/bnema/vev/internal/ports" | |
| 14 | + | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | func TestFloatingOpenCancellationBoundsSessionTeardownAndClosesLatePTY(t *testing.T) { | |
| 18 | + | factory := portsmocks.NewMockPTYFactory(t) | |
| 19 | + | opened := make(chan struct{}) | |
| 20 | + | latePTY := portsmocks.NewMockPTY(t) | |
| 21 | + | closed := make(chan struct{}) | |
| 22 | + | latePTY.EXPECT().Close().RunAndReturn(func() error { close(closed); return nil }).Once() | |
| 23 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 24 | + | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 25 | + | close(opened) | |
| 26 | + | <-ctx.Done() | |
| 27 | + | return latePTY, nil | |
| 28 | + | }).Once() | |
| 29 | + | d := newTestDaemon(t, factory, stubClock{}) | |
| 30 | + | tb := newFloatingTestTab(t) | |
| 31 | + | ctx, cancel := context.WithCancel(t.Context()) | |
| 32 | + | sess := &session{id: "blocked-open", name: "work", tabs: []*tab{tb}, ctx: ctx, cancel: cancel} | |
| 33 | + | d.mu.Lock() | |
| 34 | + | d.sessions[sess.id] = sess | |
| 35 | + | d.mu.Unlock() | |
| 36 | + | d.startFloating(sess, tb, true) | |
| 37 | + | select { | |
| 38 | + | case <-opened: | |
| 39 | + | case <-time.After(time.Second): | |
| 40 | + | t.Fatal("floating Open did not begin") | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | killed := make(chan error, 1) | |
| 44 | + | go func() { killed <- d.killSession(sess, ports.ReasonSessionKilled, false) }() | |
| 45 | + | select { | |
| 46 | + | case err := <-killed: | |
| 47 | + | require.NoError(t, err) | |
| 48 | + | case <-time.After(time.Second): | |
| 49 | + | t.Fatal("session teardown waited for blocked Open") | |
| 50 | + | } | |
| 51 | + | select { | |
| 52 | + | case <-closed: | |
| 53 | + | case <-time.After(time.Second): | |
| 54 | + | t.Fatal("late Open PTY was not closed") | |
| 55 | + | } | |
| 56 | + | tb.mu.Lock() | |
| 57 | + | require.Nil(t, tb.floating.pane, "late PTY must not be installed") | |
| 58 | + | tb.mu.Unlock() | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | func TestFloatingOpensFromUnrelatedSessionsAreConcurrent(t *testing.T) { | |
| 62 | + | factory := portsmocks.NewMockPTYFactory(t) | |
| 63 | + | entered := make(chan struct{}, 2) | |
| 64 | + | release := make(chan struct{}) | |
| 65 | + | var once sync.Once | |
| 66 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 67 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 68 | + | entered <- struct{}{} | |
| 69 | + | <-release | |
| 70 | + | return newQuietPTY(), nil | |
| 71 | + | }).Twice() | |
| 72 | + | d := newTestDaemon(t, factory, stubClock{}) | |
| 73 | + | tabs := make([]*tab, 0, 2) | |
| 74 | + | cancels := make([]context.CancelFunc, 0, 2) | |
| 75 | + | for _, id := range []domain.SessionID{"one", "two"} { | |
| 76 | + | tb := newFloatingTestTab(t) | |
| 77 | + | ctx, cancel := context.WithCancel(t.Context()) | |
| 78 | + | sess := &session{id: id, name: string(id), tabs: []*tab{tb}, ctx: ctx, cancel: cancel} | |
| 79 | + | tabs = append(tabs, tb) | |
| 80 | + | cancels = append(cancels, cancel) | |
| 81 | + | d.startFloating(sess, tb, false) | |
| 82 | + | } | |
| 83 | + | for range 2 { | |
| 84 | + | select { | |
| 85 | + | case <-entered: | |
| 86 | + | case <-time.After(time.Second): | |
| 87 | + | t.Fatal("unrelated floating launch was serialized") | |
| 88 | + | } | |
| 89 | + | } | |
| 90 | + | once.Do(func() { close(release) }) | |
| 91 | + | for i, cancel := range cancels { | |
| 92 | + | cancel() | |
| 93 | + | d.teardownFloating(tabs[i], nil) | |
| 94 | + | } | |
| 95 | + | d.sessWg.Wait() | |
| 96 | + | } | |
| @@ -0,0 +1,96 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "sync" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/stretchr/testify/mock" | |||
| 10 | + | "github.com/stretchr/testify/require" | |||
| 11 | + | ||||
| 12 | + | "github.com/bnema/vev/internal/domain" | |||
| 13 | + | "github.com/bnema/vev/internal/ports" | |||
| 14 | + | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | func TestFloatingOpenCancellationBoundsSessionTeardownAndClosesLatePTY(t *testing.T) { | |||
| 18 | + | factory := portsmocks.NewMockPTYFactory(t) | |||
| 19 | + | opened := make(chan struct{}) | |||
| 20 | + | latePTY := portsmocks.NewMockPTY(t) | |||
| 21 | + | closed := make(chan struct{}) | |||
| 22 | + | latePTY.EXPECT().Close().RunAndReturn(func() error { close(closed); return nil }).Once() | |||
| 23 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |||
| 24 | + | func(ctx context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |||
| 25 | + | close(opened) | |||
| 26 | + | <-ctx.Done() | |||
| 27 | + | return latePTY, nil | |||
| 28 | + | }).Once() | |||
| 29 | + | d := newTestDaemon(t, factory, stubClock{}) | |||
| 30 | + | tb := newFloatingTestTab(t) | |||
| 31 | + | ctx, cancel := context.WithCancel(t.Context()) | |||
| 32 | + | sess := &session{id: "blocked-open", name: "work", tabs: []*tab{tb}, ctx: ctx, cancel: cancel} | |||
| 33 | + | d.mu.Lock() | |||
| 34 | + | d.sessions[sess.id] = sess | |||
| 35 | + | d.mu.Unlock() | |||
| 36 | + | d.startFloating(sess, tb, true) | |||
| 37 | + | select { | |||
| 38 | + | case <-opened: | |||
| 39 | + | case <-time.After(time.Second): | |||
| 40 | + | t.Fatal("floating Open did not begin") | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | killed := make(chan error, 1) | |||
| 44 | + | go func() { killed <- d.killSession(sess, ports.ReasonSessionKilled, false) }() | |||
| 45 | + | select { | |||
| 46 | + | case err := <-killed: | |||
| 47 | + | require.NoError(t, err) | |||
| 48 | + | case <-time.After(time.Second): | |||
| 49 | + | t.Fatal("session teardown waited for blocked Open") | |||
| 50 | + | } | |||
| 51 | + | select { | |||
| 52 | + | case <-closed: | |||
| 53 | + | case <-time.After(time.Second): | |||
| 54 | + | t.Fatal("late Open PTY was not closed") | |||
| 55 | + | } | |||
| 56 | + | tb.mu.Lock() | |||
| 57 | + | require.Nil(t, tb.floating.pane, "late PTY must not be installed") | |||
| 58 | + | tb.mu.Unlock() | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | func TestFloatingOpensFromUnrelatedSessionsAreConcurrent(t *testing.T) { | |||
| 62 | + | factory := portsmocks.NewMockPTYFactory(t) | |||
| 63 | + | entered := make(chan struct{}, 2) | |||
| 64 | + | release := make(chan struct{}) | |||
| 65 | + | var once sync.Once | |||
| 66 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |||
| 67 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, _ domain.Size) (ports.PTY, error) { | |||
| 68 | + | entered <- struct{}{} | |||
| 69 | + | <-release | |||
| 70 | + | return newQuietPTY(), nil | |||
| 71 | + | }).Twice() | |||
| 72 | + | d := newTestDaemon(t, factory, stubClock{}) | |||
| 73 | + | tabs := make([]*tab, 0, 2) | |||
| 74 | + | cancels := make([]context.CancelFunc, 0, 2) | |||
| 75 | + | for _, id := range []domain.SessionID{"one", "two"} { | |||
| 76 | + | tb := newFloatingTestTab(t) | |||
| 77 | + | ctx, cancel := context.WithCancel(t.Context()) | |||
| 78 | + | sess := &session{id: id, name: string(id), tabs: []*tab{tb}, ctx: ctx, cancel: cancel} | |||
| 79 | + | tabs = append(tabs, tb) | |||
| 80 | + | cancels = append(cancels, cancel) | |||
| 81 | + | d.startFloating(sess, tb, false) | |||
| 82 | + | } | |||
| 83 | + | for range 2 { | |||
| 84 | + | select { | |||
| 85 | + | case <-entered: | |||
| 86 | + | case <-time.After(time.Second): | |||
| 87 | + | t.Fatal("unrelated floating launch was serialized") | |||
| 88 | + | } | |||
| 89 | + | } | |||
| 90 | + | once.Do(func() { close(release) }) | |||
| 91 | + | for i, cancel := range cancels { | |||
| 92 | + | cancel() | |||
| 93 | + | d.teardownFloating(tabs[i], nil) | |||
| 94 | + | } | |||
| 95 | + | d.sessWg.Wait() | |||
| 96 | + | } | |||
M internal/usecase/daemon/floating_test.go +4 -4
| @@ -36,8 +36,8 @@ func newGatedOpenFactory(t *testing.T, result ports.PTY, openErr error) (*portsm | |||
| 36 | 36 | var releaseOnce sync.Once | |
| 37 | 37 | releaseOpen := func() { releaseOnce.Do(func() { close(release) }) } | |
| 38 | 38 | t.Cleanup(releaseOpen) | |
| 39 | - | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 40 | - | func(command string, args, _ []string, dir string, size domain.Size) (ports.PTY, error) { | |
| 39 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 40 | + | func(_ context.Context, command string, args, _ []string, dir string, size domain.Size) (ports.PTY, error) { | |
| 41 | 41 | opened <- gatedFloatingOpen{command: command, args: append([]string(nil), args...), dir: dir, size: size} | |
| 42 | 42 | <-release | |
| 43 | 43 | return result, openErr | |
| @@ -467,8 +467,8 @@ func TestFloatingLaunchUsesLiveOrValidatedSessionCwd(t *testing.T) { | |||
| 467 | 467 | t.Run(tt.name, func(t *testing.T) { | |
| 468 | 468 | factory := portsmocks.NewMockPTYFactory(t) | |
| 469 | 469 | opened := make(chan string, 1) | |
| 470 | - | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 471 | - | func(_ string, _ []string, _ []string, cwd string, _ domain.Size) (ports.PTY, error) { | |
| 470 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 471 | + | func(_ context.Context, _ string, _ []string, _ []string, cwd string, _ domain.Size) (ports.PTY, error) { | |
| 472 | 472 | opened <- cwd | |
| 473 | 473 | return nil, io.ErrUnexpectedEOF | |
| 474 | 474 | }).Once() | |
| @@ -36,8 +36,8 @@ func newGatedOpenFactory(t *testing.T, result ports.PTY, openErr error) (*portsm | |||||
| 36 | var releaseOnce sync.Once | 36 | var releaseOnce sync.Once | ||
| 37 | releaseOpen := func() { releaseOnce.Do(func() { close(release) }) } | 37 | releaseOpen := func() { releaseOnce.Do(func() { close(release) }) } | ||
| 38 | t.Cleanup(releaseOpen) | 38 | t.Cleanup(releaseOpen) | ||
| 39 | - | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 39 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( |
| 40 | - | func(command string, args, _ []string, dir string, size domain.Size) (ports.PTY, error) { | 40 | + | func(_ context.Context, command string, args, _ []string, dir string, size domain.Size) (ports.PTY, error) { |
| 41 | opened <- gatedFloatingOpen{command: command, args: append([]string(nil), args...), dir: dir, size: size} | 41 | opened <- gatedFloatingOpen{command: command, args: append([]string(nil), args...), dir: dir, size: size} | ||
| 42 | <-release | 42 | <-release | ||
| 43 | return result, openErr | 43 | return result, openErr | ||
| @@ -467,8 +467,8 @@ func TestFloatingLaunchUsesLiveOrValidatedSessionCwd(t *testing.T) { | |||||
| 467 | t.Run(tt.name, func(t *testing.T) { | 467 | t.Run(tt.name, func(t *testing.T) { | ||
| 468 | factory := portsmocks.NewMockPTYFactory(t) | 468 | factory := portsmocks.NewMockPTYFactory(t) | ||
| 469 | opened := make(chan string, 1) | 469 | opened := make(chan string, 1) | ||
| 470 | - | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 470 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( |
| 471 | - | func(_ string, _ []string, _ []string, cwd string, _ domain.Size) (ports.PTY, error) { | 471 | + | func(_ context.Context, _ string, _ []string, _ []string, cwd string, _ domain.Size) (ports.PTY, error) { |
| 472 | opened <- cwd | 472 | opened <- cwd | ||
| 473 | return nil, io.ErrUnexpectedEOF | 473 | return nil, io.ErrUnexpectedEOF | ||
| 474 | }).Once() | 474 | }).Once() | ||
M internal/usecase/daemon/palette_test.go +1 -1
| @@ -371,7 +371,7 @@ func TestPaletteCreateTabErrorRepaintsAfterClose(t *testing.T) { | |||
| 371 | 371 | d, sess, ac, sends, releases := newManualTabSession(t, 1) | |
| 372 | 372 | defer releases[0]() | |
| 373 | 373 | ptys := portsmocks.NewMockPTYFactory(t) | |
| 374 | - | ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("open failed")) | |
| 374 | + | ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("open failed")) | |
| 375 | 375 | d.ptys = ptys | |
| 376 | 376 | ||
| 377 | 377 | d.handleInput(sess, ac, []byte("\x1b ")) | |
| @@ -371,7 +371,7 @@ func TestPaletteCreateTabErrorRepaintsAfterClose(t *testing.T) { | |||||
| 371 | d, sess, ac, sends, releases := newManualTabSession(t, 1) | 371 | d, sess, ac, sends, releases := newManualTabSession(t, 1) | ||
| 372 | defer releases[0]() | 372 | defer releases[0]() | ||
| 373 | ptys := portsmocks.NewMockPTYFactory(t) | 373 | ptys := portsmocks.NewMockPTYFactory(t) | ||
| 374 | - | ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("open failed")) | 374 | + | ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("open failed")) |
| 375 | d.ptys = ptys | 375 | d.ptys = ptys | ||
| 376 | 376 | ||||
| 377 | d.handleInput(sess, ac, []byte("\x1b ")) | 377 | d.handleInput(sess, ac, []byte("\x1b ")) | ||
M internal/usecase/daemon/pane_actions.go +1 -1
| @@ -69,7 +69,7 @@ func (d *Daemon) spawnPaneOp( | |||
| 69 | 69 | tb.mu.Unlock() | |
| 70 | 70 | return fmt.Errorf("daemon: generating pane identity: %w", err) | |
| 71 | 71 | } | |
| 72 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, rectSize(newRect)) | |
| 72 | + | pty, err := d.ptys.Open(sess.ctx, d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, rectSize(newRect)) | |
| 73 | 73 | if err != nil { | |
| 74 | 74 | d.log.Warn("pty spawn failed", "err", err, "session", name, "pane", newID, "kind", "pane") | |
| 75 | 75 | tb.mu.Lock() | |
| @@ -69,7 +69,7 @@ func (d *Daemon) spawnPaneOp( | |||||
| 69 | tb.mu.Unlock() | 69 | tb.mu.Unlock() | ||
| 70 | return fmt.Errorf("daemon: generating pane identity: %w", err) | 70 | return fmt.Errorf("daemon: generating pane identity: %w", err) | ||
| 71 | } | 71 | } | ||
| 72 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, rectSize(newRect)) | 72 | + | pty, err := d.ptys.Open(sess.ctx, d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, rectSize(newRect)) |
| 73 | if err != nil { | 73 | if err != nil { | ||
| 74 | d.log.Warn("pty spawn failed", "err", err, "session", name, "pane", newID, "kind", "pane") | 74 | d.log.Warn("pty spawn failed", "err", err, "session", name, "pane", newID, "kind", "pane") | ||
| 75 | tb.mu.Lock() | 75 | tb.mu.Lock() | ||
M internal/usecase/daemon/pane_actions_test.go +9 -9
| @@ -39,7 +39,7 @@ func TestSplitPaneCreatesFocusedShellInRequestedPosition(t *testing.T) { | |||
| 39 | 39 | newPTY := portsmocks.NewMockPTY(t) | |
| 40 | 40 | oldPTY.EXPECT().Resize(rectSize(tt.wantOld)).Return(nil).Once() | |
| 41 | 41 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 42 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", rectSize(tt.wantNew)).Return(newPTY, nil).Once() | |
| 42 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", rectSize(tt.wantNew)).Return(newPTY, nil).Once() | |
| 43 | 43 | ||
| 44 | 44 | require.NoError(t, d.splitPane(sess, nil, tt.dir)) | |
| 45 | 45 | ||
| @@ -68,7 +68,7 @@ func TestSplitPaneRightFromStackSplitsWholeStack(t *testing.T) { | |||
| 68 | 68 | newPTY := portsmocks.NewMockPTY(t) | |
| 69 | 69 | stackPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 2}).Return(nil).Once() | |
| 70 | 70 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 71 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 4}).Return(newPTY, nil).Once() | |
| 71 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 4}).Return(newPTY, nil).Once() | |
| 72 | 72 | ||
| 73 | 73 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | |
| 74 | 74 | ||
| @@ -84,7 +84,7 @@ func TestSplitPaneRightFromStackSplitsWholeStack(t *testing.T) { | |||
| 84 | 84 | ||
| 85 | 85 | func TestSplitPaneOpenErrorRollsBackTreeAndPaneMap(t *testing.T) { | |
| 86 | 86 | d, sess, _, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | |
| 87 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(nil, errors.New("open failed")).Once() | |
| 87 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(nil, errors.New("open failed")).Once() | |
| 88 | 88 | ||
| 89 | 89 | tb := sess.activeTab() | |
| 90 | 90 | tb.mu.Lock() | |
| @@ -231,7 +231,7 @@ func TestStackPaneCreatesStackAndToggleRestoresSplit(t *testing.T) { | |||
| 231 | 231 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 5}) | |
| 232 | 232 | newPTY := portsmocks.NewMockPTY(t) | |
| 233 | 233 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 234 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 3}).Return(newPTY, nil).Once() | |
| 234 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 3}).Return(newPTY, nil).Once() | |
| 235 | 235 | ||
| 236 | 236 | require.NoError(t, d.stackPane(sess, nil)) | |
| 237 | 237 | tb := sess.activeTab() | |
| @@ -251,7 +251,7 @@ func TestStackFocusWalkExpandsAndOverflowRefuses(t *testing.T) { | |||
| 251 | 251 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 3}) | |
| 252 | 252 | newPTY := portsmocks.NewMockPTY(t) | |
| 253 | 253 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 254 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 1}).Return(newPTY, nil).Once() | |
| 254 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 1}).Return(newPTY, nil).Once() | |
| 255 | 255 | require.NoError(t, d.stackPane(sess, nil)) | |
| 256 | 256 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 1}).Return(nil).Once() | |
| 257 | 257 | ||
| @@ -261,7 +261,7 @@ func TestStackFocusWalkExpandsAndOverflowRefuses(t *testing.T) { | |||
| 261 | 261 | require.Equal(t, layout.PaneID("pane-1"), tb.tree.Focus) | |
| 262 | 262 | require.Equal(t, layout.PaneID("pane-1"), tb.tree.Root.Expanded) | |
| 263 | 263 | thirdPTY := portsmocks.NewMockPTY(t) | |
| 264 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", mock.Anything).Return(thirdPTY, nil).Maybe() | |
| 264 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", mock.Anything).Return(thirdPTY, nil).Maybe() | |
| 265 | 265 | require.ErrorIs(t, d.stackPane(sess, nil), layout.ErrTooSmall) | |
| 266 | 266 | require.Len(t, tb.panes, 2) | |
| 267 | 267 | } | |
| @@ -291,7 +291,7 @@ func TestCloseOriginalPaneLeavesSurvivorFunctional(t *testing.T) { | |||
| 291 | 291 | defer releaseNew() | |
| 292 | 292 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | |
| 293 | 293 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 294 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() | |
| 294 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() | |
| 295 | 295 | ||
| 296 | 296 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | |
| 297 | 297 | tb := sess.activeTab() | |
| @@ -350,8 +350,8 @@ func TestSplitPaneInheritsTerminalEnv(t *testing.T) { | |||
| 350 | 350 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | |
| 351 | 351 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | |
| 352 | 352 | var gotEnv []string | |
| 353 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).RunAndReturn( | |
| 354 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 353 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).RunAndReturn( | |
| 354 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 355 | 355 | gotEnv = append([]string(nil), env...) | |
| 356 | 356 | return newPTY, nil | |
| 357 | 357 | }, | |
| @@ -39,7 +39,7 @@ func TestSplitPaneCreatesFocusedShellInRequestedPosition(t *testing.T) { | |||||
| 39 | newPTY := portsmocks.NewMockPTY(t) | 39 | newPTY := portsmocks.NewMockPTY(t) | ||
| 40 | oldPTY.EXPECT().Resize(rectSize(tt.wantOld)).Return(nil).Once() | 40 | oldPTY.EXPECT().Resize(rectSize(tt.wantOld)).Return(nil).Once() | ||
| 41 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 41 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 42 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", rectSize(tt.wantNew)).Return(newPTY, nil).Once() | 42 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", rectSize(tt.wantNew)).Return(newPTY, nil).Once() |
| 43 | 43 | ||||
| 44 | require.NoError(t, d.splitPane(sess, nil, tt.dir)) | 44 | require.NoError(t, d.splitPane(sess, nil, tt.dir)) | ||
| 45 | 45 | ||||
| @@ -68,7 +68,7 @@ func TestSplitPaneRightFromStackSplitsWholeStack(t *testing.T) { | |||||
| 68 | newPTY := portsmocks.NewMockPTY(t) | 68 | newPTY := portsmocks.NewMockPTY(t) | ||
| 69 | stackPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 2}).Return(nil).Once() | 69 | stackPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 2}).Return(nil).Once() | ||
| 70 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 70 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 71 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 4}).Return(newPTY, nil).Once() | 71 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 4}).Return(newPTY, nil).Once() |
| 72 | 72 | ||||
| 73 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | 73 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | ||
| 74 | 74 | ||||
| @@ -84,7 +84,7 @@ func TestSplitPaneRightFromStackSplitsWholeStack(t *testing.T) { | |||||
| 84 | 84 | ||||
| 85 | func TestSplitPaneOpenErrorRollsBackTreeAndPaneMap(t *testing.T) { | 85 | func TestSplitPaneOpenErrorRollsBackTreeAndPaneMap(t *testing.T) { | ||
| 86 | d, sess, _, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | 86 | d, sess, _, factory := newSplitTestDaemon(t, domain.Size{Cols: 41, Rows: 10}) | ||
| 87 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(nil, errors.New("open failed")).Once() | 87 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(nil, errors.New("open failed")).Once() |
| 88 | 88 | ||||
| 89 | tb := sess.activeTab() | 89 | tb := sess.activeTab() | ||
| 90 | tb.mu.Lock() | 90 | tb.mu.Lock() | ||
| @@ -231,7 +231,7 @@ func TestStackPaneCreatesStackAndToggleRestoresSplit(t *testing.T) { | |||||
| 231 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 5}) | 231 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 5}) | ||
| 232 | newPTY := portsmocks.NewMockPTY(t) | 232 | newPTY := portsmocks.NewMockPTY(t) | ||
| 233 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 233 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 234 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 3}).Return(newPTY, nil).Once() | 234 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 3}).Return(newPTY, nil).Once() |
| 235 | 235 | ||||
| 236 | require.NoError(t, d.stackPane(sess, nil)) | 236 | require.NoError(t, d.stackPane(sess, nil)) | ||
| 237 | tb := sess.activeTab() | 237 | tb := sess.activeTab() | ||
| @@ -251,7 +251,7 @@ func TestStackFocusWalkExpandsAndOverflowRefuses(t *testing.T) { | |||||
| 251 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 3}) | 251 | d, sess, oldPTY, factory := newSplitTestDaemon(t, domain.Size{Cols: 20, Rows: 3}) | ||
| 252 | newPTY := portsmocks.NewMockPTY(t) | 252 | newPTY := portsmocks.NewMockPTY(t) | ||
| 253 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 253 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 254 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 1}).Return(newPTY, nil).Once() | 254 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 1}).Return(newPTY, nil).Once() |
| 255 | require.NoError(t, d.stackPane(sess, nil)) | 255 | require.NoError(t, d.stackPane(sess, nil)) | ||
| 256 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 1}).Return(nil).Once() | 256 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 1}).Return(nil).Once() | ||
| 257 | 257 | ||||
| @@ -261,7 +261,7 @@ func TestStackFocusWalkExpandsAndOverflowRefuses(t *testing.T) { | |||||
| 261 | require.Equal(t, layout.PaneID("pane-1"), tb.tree.Focus) | 261 | require.Equal(t, layout.PaneID("pane-1"), tb.tree.Focus) | ||
| 262 | require.Equal(t, layout.PaneID("pane-1"), tb.tree.Root.Expanded) | 262 | require.Equal(t, layout.PaneID("pane-1"), tb.tree.Root.Expanded) | ||
| 263 | thirdPTY := portsmocks.NewMockPTY(t) | 263 | thirdPTY := portsmocks.NewMockPTY(t) | ||
| 264 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", mock.Anything).Return(thirdPTY, nil).Maybe() | 264 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", mock.Anything).Return(thirdPTY, nil).Maybe() |
| 265 | require.ErrorIs(t, d.stackPane(sess, nil), layout.ErrTooSmall) | 265 | require.ErrorIs(t, d.stackPane(sess, nil), layout.ErrTooSmall) | ||
| 266 | require.Len(t, tb.panes, 2) | 266 | require.Len(t, tb.panes, 2) | ||
| 267 | } | 267 | } | ||
| @@ -291,7 +291,7 @@ func TestCloseOriginalPaneLeavesSurvivorFunctional(t *testing.T) { | |||||
| 291 | defer releaseNew() | 291 | defer releaseNew() | ||
| 292 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | 292 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | ||
| 293 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 293 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 294 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() | 294 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() |
| 295 | 295 | ||||
| 296 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | 296 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | ||
| 297 | tb := sess.activeTab() | 297 | tb := sess.activeTab() | ||
| @@ -350,8 +350,8 @@ func TestSplitPaneInheritsTerminalEnv(t *testing.T) { | |||||
| 350 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | 350 | oldPTY.EXPECT().Resize(domain.Size{Cols: 20, Rows: 10}).Return(nil).Once() | ||
| 351 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | 351 | newPTY.EXPECT().Read(mock.Anything).RunAndReturn(blockingRead(t)).Maybe() | ||
| 352 | var gotEnv []string | 352 | var gotEnv []string | ||
| 353 | - | factory.EXPECT().Open("/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).RunAndReturn( | 353 | + | factory.EXPECT().Open(mock.Anything, "/bin/sh", []string(nil), mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).RunAndReturn( |
| 354 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 354 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { |
| 355 | gotEnv = append([]string(nil), env...) | 355 | gotEnv = append([]string(nil), env...) | ||
| 356 | return newPTY, nil | 356 | return newPTY, nil | ||
| 357 | }, | 357 | }, | ||
M internal/usecase/daemon/picker_test.go +5 -5
| @@ -399,8 +399,8 @@ func TestPickerCrossSessionSwitchCopiesTerminalEnvForFutureTabs(t *testing.T) { | |||
| 399 | 399 | defer releasePTY3() | |
| 400 | 400 | var openedEnv []string | |
| 401 | 401 | f := portsmocks.NewMockPTYFactory(t) | |
| 402 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 403 | - | func(_ string, _ []string, env []string, _ string, sz domain.Size) (ports.PTY, error) { | |
| 402 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 403 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, sz domain.Size) (ports.PTY, error) { | |
| 404 | 404 | if sz != (domain.Size{Cols: 80, Rows: 22}) { | |
| 405 | 405 | return newQuietPTY(), nil | |
| 406 | 406 | } | |
| @@ -514,8 +514,8 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 514 | 514 | var opens [][]string | |
| 515 | 515 | f := portsmocks.NewMockPTYFactory(t) | |
| 516 | 516 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 517 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 518 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 517 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 518 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 519 | 519 | opens = append(opens, append([]string(nil), env...)) | |
| 520 | 520 | if len(opens) == 1 { | |
| 521 | 521 | return p1, nil | |
| @@ -524,7 +524,7 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 524 | 524 | }, | |
| 525 | 525 | ).Twice() | |
| 526 | 526 | floating := newQuietPTY() | |
| 527 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 527 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 528 | 528 | return got != normalSize && got.Valid() | |
| 529 | 529 | })).Return(floating, nil).Once() | |
| 530 | 530 | d := newTestDaemon(t, f, stubClock{}) | |
| @@ -399,8 +399,8 @@ func TestPickerCrossSessionSwitchCopiesTerminalEnvForFutureTabs(t *testing.T) { | |||||
| 399 | defer releasePTY3() | 399 | defer releasePTY3() | ||
| 400 | var openedEnv []string | 400 | var openedEnv []string | ||
| 401 | f := portsmocks.NewMockPTYFactory(t) | 401 | f := portsmocks.NewMockPTYFactory(t) | ||
| 402 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 402 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( |
| 403 | - | func(_ string, _ []string, env []string, _ string, sz domain.Size) (ports.PTY, error) { | 403 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, sz domain.Size) (ports.PTY, error) { |
| 404 | if sz != (domain.Size{Cols: 80, Rows: 22}) { | 404 | if sz != (domain.Size{Cols: 80, Rows: 22}) { | ||
| 405 | return newQuietPTY(), nil | 405 | return newQuietPTY(), nil | ||
| 406 | } | 406 | } | ||
| @@ -514,8 +514,8 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 514 | var opens [][]string | 514 | var opens [][]string | ||
| 515 | f := portsmocks.NewMockPTYFactory(t) | 515 | f := portsmocks.NewMockPTYFactory(t) | ||
| 516 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | 516 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | ||
| 517 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 517 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( |
| 518 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 518 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { |
| 519 | opens = append(opens, append([]string(nil), env...)) | 519 | opens = append(opens, append([]string(nil), env...)) | ||
| 520 | if len(opens) == 1 { | 520 | if len(opens) == 1 { | ||
| 521 | return p1, nil | 521 | return p1, nil | ||
| @@ -524,7 +524,7 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 524 | }, | 524 | }, | ||
| 525 | ).Twice() | 525 | ).Twice() | ||
| 526 | floating := newQuietPTY() | 526 | floating := newQuietPTY() | ||
| 527 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | 527 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { |
| 528 | return got != normalSize && got.Valid() | 528 | return got != normalSize && got.Valid() | ||
| 529 | })).Return(floating, nil).Once() | 529 | })).Return(floating, nil).Once() | ||
| 530 | d := newTestDaemon(t, f, stubClock{}) | 530 | d := newTestDaemon(t, f, stubClock{}) | ||
M internal/usecase/daemon/session.go +5 -14
| @@ -131,7 +131,7 @@ func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz | |||
| 131 | 131 | closeTabs(tabs) | |
| 132 | 132 | return nil, err | |
| 133 | 133 | } | |
| 134 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) | |
| 134 | + | pty, err := d.ptys.Open(d.serveCtx, d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) | |
| 135 | 135 | if err != nil { | |
| 136 | 136 | closeTabs(tabs) | |
| 137 | 137 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | |
| @@ -272,7 +272,7 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||
| 272 | 272 | if err != nil { | |
| 273 | 273 | return err | |
| 274 | 274 | } | |
| 275 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) | |
| 275 | + | pty, err := d.ptys.Open(d.serveCtx, d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) | |
| 276 | 276 | if err != nil { | |
| 277 | 277 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "tab") | |
| 278 | 278 | return fmt.Errorf("daemon: spawning tab for session %q: %w", name, err) | |
| @@ -674,14 +674,9 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||
| 674 | 674 | d.captureSession(sess) | |
| 675 | 675 | } | |
| 676 | 676 | } | |
| 677 | - | // Serialize removal of the final session with the last transition into a | |
| 678 | - | // floating Open. This establishes whether an Open is pre-shutdown work or | |
| 679 | - | // is rejected, without holding d.mu across the external call. | |
| 680 | - | d.floatingLaunchGate.Lock() | |
| 681 | 677 | d.mu.Lock() | |
| 682 | 678 | if _, ok := d.sessions[sess.id]; !ok { | |
| 683 | 679 | d.mu.Unlock() | |
| 684 | - | d.floatingLaunchGate.Unlock() | |
| 685 | 680 | return nil | |
| 686 | 681 | } | |
| 687 | 682 | delete(d.sessions, sess.id) | |
| @@ -706,7 +701,6 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||
| 706 | 701 | d.closing = true | |
| 707 | 702 | } | |
| 708 | 703 | d.mu.Unlock() | |
| 709 | - | d.floatingLaunchGate.Unlock() | |
| 710 | 704 | d.log.Info("session closed", "session", stoppedName, "id", sess.id, "ephemeral", ephemeral, "purge", purge, "reason", reason) | |
| 711 | 705 | ||
| 712 | 706 | purgeErr := snapshotDeleteErr | |
| @@ -743,9 +737,9 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||
| 743 | 737 | } | |
| 744 | 738 | ||
| 745 | 739 | // Prevent queued launches from entering Open, then cancel the parent | |
| 746 | - | // context. In-flight Opens are joined below so a late result is always | |
| 747 | - | // closed before teardown returns. | |
| 748 | - | floatingLaunches := sess.stopFloatingLaunches() | |
| 740 | + | // context. The launch worker owns any late PTY result and closes it rather | |
| 741 | + | // than publishing it into the torn-down tab. | |
| 742 | + | sess.stopFloatingLaunches() | |
| 749 | 743 | sess.cancel() | |
| 750 | 744 | sess.mu.Lock() | |
| 751 | 745 | tabs := append([]*tab(nil), sess.tabs...) | |
| @@ -757,9 +751,6 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||
| 757 | 751 | d.teardownFloating(tb, ac) | |
| 758 | 752 | tb.closeAllPanes() | |
| 759 | 753 | } | |
| 760 | - | for _, launchDone := range floatingLaunches { | |
| 761 | - | <-launchDone | |
| 762 | - | } | |
| 763 | 754 | for _, path := range clipFiles { | |
| 764 | 755 | if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { | |
| 765 | 756 | d.log.Warn("removing clipboard temp file failed", "err", err, "path", path) | |
| @@ -131,7 +131,7 @@ func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz | |||||
| 131 | closeTabs(tabs) | 131 | closeTabs(tabs) | ||
| 132 | return nil, err | 132 | return nil, err | ||
| 133 | } | 133 | } | ||
| 134 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) | 134 | + | pty, err := d.ptys.Open(d.serveCtx, d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) |
| 135 | if err != nil { | 135 | if err != nil { | ||
| 136 | closeTabs(tabs) | 136 | closeTabs(tabs) | ||
| 137 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | 137 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | ||
| @@ -272,7 +272,7 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||||
| 272 | if err != nil { | 272 | if err != nil { | ||
| 273 | return err | 273 | return err | ||
| 274 | } | 274 | } | ||
| 275 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) | 275 | + | pty, err := d.ptys.Open(d.serveCtx, d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) |
| 276 | if err != nil { | 276 | if err != nil { | ||
| 277 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "tab") | 277 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "tab") | ||
| 278 | return fmt.Errorf("daemon: spawning tab for session %q: %w", name, err) | 278 | return fmt.Errorf("daemon: spawning tab for session %q: %w", name, err) | ||
| @@ -674,14 +674,9 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||||
| 674 | d.captureSession(sess) | 674 | d.captureSession(sess) | ||
| 675 | } | 675 | } | ||
| 676 | } | 676 | } | ||
| 677 | - | // Serialize removal of the final session with the last transition into a | |||
| 678 | - | // floating Open. This establishes whether an Open is pre-shutdown work or | |||
| 679 | - | // is rejected, without holding d.mu across the external call. | |||
| 680 | - | d.floatingLaunchGate.Lock() | |||
| 681 | d.mu.Lock() | 677 | d.mu.Lock() | ||
| 682 | if _, ok := d.sessions[sess.id]; !ok { | 678 | if _, ok := d.sessions[sess.id]; !ok { | ||
| 683 | d.mu.Unlock() | 679 | d.mu.Unlock() | ||
| 684 | - | d.floatingLaunchGate.Unlock() | |||
| 685 | return nil | 680 | return nil | ||
| 686 | } | 681 | } | ||
| 687 | delete(d.sessions, sess.id) | 682 | delete(d.sessions, sess.id) | ||
| @@ -706,7 +701,6 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||||
| 706 | d.closing = true | 701 | d.closing = true | ||
| 707 | } | 702 | } | ||
| 708 | d.mu.Unlock() | 703 | d.mu.Unlock() | ||
| 709 | - | d.floatingLaunchGate.Unlock() | |||
| 710 | d.log.Info("session closed", "session", stoppedName, "id", sess.id, "ephemeral", ephemeral, "purge", purge, "reason", reason) | 704 | d.log.Info("session closed", "session", stoppedName, "id", sess.id, "ephemeral", ephemeral, "purge", purge, "reason", reason) | ||
| 711 | 705 | ||||
| 712 | purgeErr := snapshotDeleteErr | 706 | purgeErr := snapshotDeleteErr | ||
| @@ -743,9 +737,9 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||||
| 743 | } | 737 | } | ||
| 744 | 738 | ||||
| 745 | // Prevent queued launches from entering Open, then cancel the parent | 739 | // Prevent queued launches from entering Open, then cancel the parent | ||
| 746 | - | // context. In-flight Opens are joined below so a late result is always | 740 | + | // context. The launch worker owns any late PTY result and closes it rather |
| 747 | - | // closed before teardown returns. | 741 | + | // than publishing it into the torn-down tab. |
| 748 | - | floatingLaunches := sess.stopFloatingLaunches() | 742 | + | sess.stopFloatingLaunches() |
| 749 | sess.cancel() | 743 | sess.cancel() | ||
| 750 | sess.mu.Lock() | 744 | sess.mu.Lock() | ||
| 751 | tabs := append([]*tab(nil), sess.tabs...) | 745 | tabs := append([]*tab(nil), sess.tabs...) | ||
| @@ -757,9 +751,6 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||||
| 757 | d.teardownFloating(tb, ac) | 751 | d.teardownFloating(tb, ac) | ||
| 758 | tb.closeAllPanes() | 752 | tb.closeAllPanes() | ||
| 759 | } | 753 | } | ||
| 760 | - | for _, launchDone := range floatingLaunches { | |||
| 761 | - | <-launchDone | |||
| 762 | - | } | |||
| 763 | for _, path := range clipFiles { | 754 | for _, path := range clipFiles { | ||
| 764 | if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { | 755 | if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { | ||
| 765 | d.log.Warn("removing clipboard temp file failed", "err", err, "path", path) | 756 | d.log.Warn("removing clipboard temp file failed", "err", err, "path", path) | ||
M internal/usecase/daemon/session_test.go +20 -20
| @@ -34,8 +34,8 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) { | |||
| 34 | 34 | f := portsmocks.NewMockPTYFactory(t) | |
| 35 | 35 | var dirs []string | |
| 36 | 36 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 37 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 38 | - | func(_ string, _ []string, _ []string, dir string, _ domain.Size) (ports.PTY, error) { | |
| 37 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 38 | + | func(_ context.Context, _ string, _ []string, _ []string, dir string, _ domain.Size) (ports.PTY, error) { | |
| 39 | 39 | dirs = append(dirs, dir) | |
| 40 | 40 | if len(dirs) == 1 { | |
| 41 | 41 | return first, nil | |
| @@ -44,7 +44,7 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) { | |||
| 44 | 44 | }, | |
| 45 | 45 | ).Twice() | |
| 46 | 46 | floating := newQuietPTY() | |
| 47 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 47 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 48 | 48 | return got != normalSize && got.Valid() | |
| 49 | 49 | })).Return(floating, nil).Maybe() | |
| 50 | 50 | ||
| @@ -210,13 +210,13 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) { | |||
| 210 | 210 | ||
| 211 | 211 | f := portsmocks.NewMockPTYFactory(t) | |
| 212 | 212 | normalSize := domain.Size{Cols: 80, Rows: 22} | |
| 213 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).Return(p1, nil).Once() | |
| 213 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).Return(p1, nil).Once() | |
| 214 | 214 | floating := newQuietPTY() | |
| 215 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 215 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 216 | 216 | return got != normalSize && got.Valid() | |
| 217 | 217 | })).Return(floating, nil).Maybe() | |
| 218 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 219 | - | func(string, []string, []string, string, domain.Size) (ports.PTY, error) { | |
| 218 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 219 | + | func(context.Context, string, []string, []string, string, domain.Size) (ports.PTY, error) { | |
| 220 | 220 | close(opened) | |
| 221 | 221 | <-releaseOpen | |
| 222 | 222 | return p2, nil | |
| @@ -516,8 +516,8 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) { | |||
| 516 | 516 | preShutdownFloatingOpen := make(chan struct{}, 1) | |
| 517 | 517 | var opensAfterShutdown atomic.Int32 | |
| 518 | 518 | d := newTestDaemon(t, f, stubClock{}) | |
| 519 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 520 | - | func(_ string, _ []string, _ []string, _ string, size domain.Size) (ports.PTY, error) { | |
| 519 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 520 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, size domain.Size) (ports.PTY, error) { | |
| 521 | 521 | select { | |
| 522 | 522 | case <-d.done: | |
| 523 | 523 | opensAfterShutdown.Add(1) | |
| @@ -1113,8 +1113,8 @@ func TestNewSessionAssignsStableIDsAndChildEnv(t *testing.T) { | |||
| 1113 | 1113 | ||
| 1114 | 1114 | var gotEnv []string | |
| 1115 | 1115 | f := portsmocks.NewMockPTYFactory(t) | |
| 1116 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 1117 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1116 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | |
| 1117 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1118 | 1118 | gotEnv = append([]string(nil), env...) | |
| 1119 | 1119 | return p, nil | |
| 1120 | 1120 | }, | |
| @@ -1266,8 +1266,8 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1266 | 1266 | var opens [][]string | |
| 1267 | 1267 | f := portsmocks.NewMockPTYFactory(t) | |
| 1268 | 1268 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 1269 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1270 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1269 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1270 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1271 | 1271 | opens = append(opens, append([]string(nil), env...)) | |
| 1272 | 1272 | if len(opens) == 1 { | |
| 1273 | 1273 | return p1, nil | |
| @@ -1276,7 +1276,7 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1276 | 1276 | }, | |
| 1277 | 1277 | ).Twice() | |
| 1278 | 1278 | floating := newQuietPTY() | |
| 1279 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 1279 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 1280 | 1280 | return got != normalSize && got.Valid() | |
| 1281 | 1281 | })).Return(floating, nil).Maybe() | |
| 1282 | 1282 | ||
| @@ -1311,8 +1311,8 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1311 | 1311 | var opens [][]string | |
| 1312 | 1312 | f := portsmocks.NewMockPTYFactory(t) | |
| 1313 | 1313 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 1314 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1315 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1314 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1315 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1316 | 1316 | opens = append(opens, append([]string(nil), env...)) | |
| 1317 | 1317 | if len(opens) == 1 { | |
| 1318 | 1318 | return p1, nil | |
| @@ -1321,7 +1321,7 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||
| 1321 | 1321 | }, | |
| 1322 | 1322 | ).Twice() | |
| 1323 | 1323 | floating := newQuietPTY() | |
| 1324 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 1324 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 1325 | 1325 | return got != normalSize && got.Valid() | |
| 1326 | 1326 | })).Return(floating, nil).Maybe() | |
| 1327 | 1327 | ||
| @@ -1358,8 +1358,8 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 1358 | 1358 | var opens [][]string | |
| 1359 | 1359 | f := portsmocks.NewMockPTYFactory(t) | |
| 1360 | 1360 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | |
| 1361 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1362 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1361 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | |
| 1362 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | |
| 1363 | 1363 | opens = append(opens, append([]string(nil), env...)) | |
| 1364 | 1364 | if len(opens) == 1 { | |
| 1365 | 1365 | return p1, nil | |
| @@ -1368,7 +1368,7 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||
| 1368 | 1368 | }, | |
| 1369 | 1369 | ).Twice() | |
| 1370 | 1370 | floating := newQuietPTY() | |
| 1371 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 1371 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | |
| 1372 | 1372 | return got != normalSize && got.Valid() | |
| 1373 | 1373 | })).Return(floating, nil).Maybe() | |
| 1374 | 1374 | d := newTestDaemon(t, f, stubClock{}) | |
| @@ -34,8 +34,8 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) { | |||||
| 34 | f := portsmocks.NewMockPTYFactory(t) | 34 | f := portsmocks.NewMockPTYFactory(t) | ||
| 35 | var dirs []string | 35 | var dirs []string | ||
| 36 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | 36 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | ||
| 37 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 37 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( |
| 38 | - | func(_ string, _ []string, _ []string, dir string, _ domain.Size) (ports.PTY, error) { | 38 | + | func(_ context.Context, _ string, _ []string, _ []string, dir string, _ domain.Size) (ports.PTY, error) { |
| 39 | dirs = append(dirs, dir) | 39 | dirs = append(dirs, dir) | ||
| 40 | if len(dirs) == 1 { | 40 | if len(dirs) == 1 { | ||
| 41 | return first, nil | 41 | return first, nil | ||
| @@ -44,7 +44,7 @@ func TestRoutePropagatesHelloCwdAndTabsInheritIt(t *testing.T) { | |||||
| 44 | }, | 44 | }, | ||
| 45 | ).Twice() | 45 | ).Twice() | ||
| 46 | floating := newQuietPTY() | 46 | floating := newQuietPTY() | ||
| 47 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | 47 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { |
| 48 | return got != normalSize && got.Valid() | 48 | return got != normalSize && got.Valid() | ||
| 49 | })).Return(floating, nil).Maybe() | 49 | })).Return(floating, nil).Maybe() | ||
| 50 | 50 | ||||
| @@ -210,13 +210,13 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) { | |||||
| 210 | 210 | ||||
| 211 | f := portsmocks.NewMockPTYFactory(t) | 211 | f := portsmocks.NewMockPTYFactory(t) | ||
| 212 | normalSize := domain.Size{Cols: 80, Rows: 22} | 212 | normalSize := domain.Size{Cols: 80, Rows: 22} | ||
| 213 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).Return(p1, nil).Once() | 213 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).Return(p1, nil).Once() |
| 214 | floating := newQuietPTY() | 214 | floating := newQuietPTY() | ||
| 215 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | 215 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { |
| 216 | return got != normalSize && got.Valid() | 216 | return got != normalSize && got.Valid() | ||
| 217 | })).Return(floating, nil).Maybe() | 217 | })).Return(floating, nil).Maybe() | ||
| 218 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 218 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( |
| 219 | - | func(string, []string, []string, string, domain.Size) (ports.PTY, error) { | 219 | + | func(context.Context, string, []string, []string, string, domain.Size) (ports.PTY, error) { |
| 220 | close(opened) | 220 | close(opened) | ||
| 221 | <-releaseOpen | 221 | <-releaseOpen | ||
| 222 | return p2, nil | 222 | return p2, nil | ||
| @@ -516,8 +516,8 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) { | |||||
| 516 | preShutdownFloatingOpen := make(chan struct{}, 1) | 516 | preShutdownFloatingOpen := make(chan struct{}, 1) | ||
| 517 | var opensAfterShutdown atomic.Int32 | 517 | var opensAfterShutdown atomic.Int32 | ||
| 518 | d := newTestDaemon(t, f, stubClock{}) | 518 | d := newTestDaemon(t, f, stubClock{}) | ||
| 519 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 519 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( |
| 520 | - | func(_ string, _ []string, _ []string, _ string, size domain.Size) (ports.PTY, error) { | 520 | + | func(_ context.Context, _ string, _ []string, _ []string, _ string, size domain.Size) (ports.PTY, error) { |
| 521 | select { | 521 | select { | ||
| 522 | case <-d.done: | 522 | case <-d.done: | ||
| 523 | opensAfterShutdown.Add(1) | 523 | opensAfterShutdown.Add(1) | ||
| @@ -1113,8 +1113,8 @@ func TestNewSessionAssignsStableIDsAndChildEnv(t *testing.T) { | |||||
| 1113 | 1113 | ||||
| 1114 | var gotEnv []string | 1114 | var gotEnv []string | ||
| 1115 | f := portsmocks.NewMockPTYFactory(t) | 1115 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1116 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( | 1116 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).RunAndReturn( |
| 1117 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1117 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { |
| 1118 | gotEnv = append([]string(nil), env...) | 1118 | gotEnv = append([]string(nil), env...) | ||
| 1119 | return p, nil | 1119 | return p, nil | ||
| 1120 | }, | 1120 | }, | ||
| @@ -1266,8 +1266,8 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1266 | var opens [][]string | 1266 | var opens [][]string | ||
| 1267 | f := portsmocks.NewMockPTYFactory(t) | 1267 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1268 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | 1268 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | ||
| 1269 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 1269 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( |
| 1270 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1270 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { |
| 1271 | opens = append(opens, append([]string(nil), env...)) | 1271 | opens = append(opens, append([]string(nil), env...)) | ||
| 1272 | if len(opens) == 1 { | 1272 | if len(opens) == 1 { | ||
| 1273 | return p1, nil | 1273 | return p1, nil | ||
| @@ -1276,7 +1276,7 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1276 | }, | 1276 | }, | ||
| 1277 | ).Twice() | 1277 | ).Twice() | ||
| 1278 | floating := newQuietPTY() | 1278 | floating := newQuietPTY() | ||
| 1279 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | 1279 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { |
| 1280 | return got != normalSize && got.Valid() | 1280 | return got != normalSize && got.Valid() | ||
| 1281 | })).Return(floating, nil).Maybe() | 1281 | })).Return(floating, nil).Maybe() | ||
| 1282 | 1282 | ||||
| @@ -1311,8 +1311,8 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1311 | var opens [][]string | 1311 | var opens [][]string | ||
| 1312 | f := portsmocks.NewMockPTYFactory(t) | 1312 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1313 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | 1313 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | ||
| 1314 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 1314 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( |
| 1315 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1315 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { |
| 1316 | opens = append(opens, append([]string(nil), env...)) | 1316 | opens = append(opens, append([]string(nil), env...)) | ||
| 1317 | if len(opens) == 1 { | 1317 | if len(opens) == 1 { | ||
| 1318 | return p1, nil | 1318 | return p1, nil | ||
| @@ -1321,7 +1321,7 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) { | |||||
| 1321 | }, | 1321 | }, | ||
| 1322 | ).Twice() | 1322 | ).Twice() | ||
| 1323 | floating := newQuietPTY() | 1323 | floating := newQuietPTY() | ||
| 1324 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | 1324 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { |
| 1325 | return got != normalSize && got.Valid() | 1325 | return got != normalSize && got.Valid() | ||
| 1326 | })).Return(floating, nil).Maybe() | 1326 | })).Return(floating, nil).Maybe() | ||
| 1327 | 1327 | ||||
| @@ -1358,8 +1358,8 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 1358 | var opens [][]string | 1358 | var opens [][]string | ||
| 1359 | f := portsmocks.NewMockPTYFactory(t) | 1359 | f := portsmocks.NewMockPTYFactory(t) | ||
| 1360 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | 1360 | normalSize := domain.Size{Cols: sz.Cols, Rows: sz.Rows - 2} | ||
| 1361 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( | 1361 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, normalSize).RunAndReturn( |
| 1362 | - | func(_ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { | 1362 | + | func(_ context.Context, _ string, _ []string, env []string, _ string, _ domain.Size) (ports.PTY, error) { |
| 1363 | opens = append(opens, append([]string(nil), env...)) | 1363 | opens = append(opens, append([]string(nil), env...)) | ||
| 1364 | if len(opens) == 1 { | 1364 | if len(opens) == 1 { | ||
| 1365 | return p1, nil | 1365 | return p1, nil | ||
| @@ -1368,7 +1368,7 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) { | |||||
| 1368 | }, | 1368 | }, | ||
| 1369 | ).Twice() | 1369 | ).Twice() | ||
| 1370 | floating := newQuietPTY() | 1370 | floating := newQuietPTY() | ||
| 1371 | - | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { | 1371 | + | f.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(got domain.Size) bool { |
| 1372 | return got != normalSize && got.Valid() | 1372 | return got != normalSize && got.Valid() | ||
| 1373 | })).Return(floating, nil).Maybe() | 1373 | })).Return(floating, nil).Maybe() | ||
| 1374 | d := newTestDaemon(t, f, stubClock{}) | 1374 | d := newTestDaemon(t, f, stubClock{}) | ||
M internal/usecase/daemon/snapshot.go +1 -1
| @@ -141,7 +141,7 @@ func (d *Daemon) restoreSession(ctx context.Context, snap snapcodec.Session) err | |||
| 141 | 141 | return fmt.Errorf("snapshot: generating pane identity: %w", err) | |
| 142 | 142 | } | |
| 143 | 143 | } | |
| 144 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(snap.Name, tabStableID, paneStableID, restoreTerm), paneSnap.Cwd, contentSize) | |
| 144 | + | pty, err := d.ptys.Open(d.serveCtx, d.shell, d.shellArgs, d.childEnv(snap.Name, tabStableID, paneStableID, restoreTerm), paneSnap.Cwd, contentSize) | |
| 145 | 145 | if err != nil { | |
| 146 | 146 | closeOpened() | |
| 147 | 147 | return err | |
| @@ -141,7 +141,7 @@ func (d *Daemon) restoreSession(ctx context.Context, snap snapcodec.Session) err | |||||
| 141 | return fmt.Errorf("snapshot: generating pane identity: %w", err) | 141 | return fmt.Errorf("snapshot: generating pane identity: %w", err) | ||
| 142 | } | 142 | } | ||
| 143 | } | 143 | } | ||
| 144 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(snap.Name, tabStableID, paneStableID, restoreTerm), paneSnap.Cwd, contentSize) | 144 | + | pty, err := d.ptys.Open(d.serveCtx, d.shell, d.shellArgs, d.childEnv(snap.Name, tabStableID, paneStableID, restoreTerm), paneSnap.Cwd, contentSize) |
| 145 | if err != nil { | 145 | if err != nil { | ||
| 146 | closeOpened() | 146 | closeOpened() | ||
| 147 | return err | 147 | return err | ||
M internal/usecase/daemon/snapshot_restore_test.go +1 -1
| @@ -444,7 +444,7 @@ type restorePTYOpen struct { | |||
| 444 | 444 | pty *restorePTY | |
| 445 | 445 | } | |
| 446 | 446 | ||
| 447 | - | func (f *restorePTYFactory) Open(_ string, _ []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | |
| 447 | + | func (f *restorePTYFactory) Open(_ context.Context, _ string, _ []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | |
| 448 | 448 | f.mu.Lock() | |
| 449 | 449 | defer f.mu.Unlock() | |
| 450 | 450 | pty := newRestorePTY() | |
| @@ -444,7 +444,7 @@ type restorePTYOpen struct { | |||||
| 444 | pty *restorePTY | 444 | pty *restorePTY | ||
| 445 | } | 445 | } | ||
| 446 | 446 | ||||
| 447 | - | func (f *restorePTYFactory) Open(_ string, _ []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { | 447 | + | func (f *restorePTYFactory) Open(_ context.Context, _ string, _ []string, env []string, dir string, sz domain.Size) (ports.PTY, error) { |
| 448 | f.mu.Lock() | 448 | f.mu.Lock() | ||
| 449 | defer f.mu.Unlock() | 449 | defer f.mu.Unlock() | ||
| 450 | pty := newRestorePTY() | 450 | pty := newRestorePTY() | ||
M internal/usecase/daemon/snapshot_test.go +1 -1
| @@ -385,7 +385,7 @@ func TestSnapshotSaverCapturesLayoutOnlyDirtySave(t *testing.T) { | |||
| 385 | 385 | newPTY.EXPECT().Write(mock.Anything).Return(0, errors.New("unused")).Maybe() | |
| 386 | 386 | newPTY.EXPECT().Resize(mock.Anything).Return(nil).Maybe() | |
| 387 | 387 | newPTY.EXPECT().ForegroundPgid().Return(0, nil).Maybe() | |
| 388 | - | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() | |
| 388 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() | |
| 389 | 389 | ||
| 390 | 390 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | |
| 391 | 391 | require.True(t, sess.snapDirty.Load()) | |
| @@ -385,7 +385,7 @@ func TestSnapshotSaverCapturesLayoutOnlyDirtySave(t *testing.T) { | |||||
| 385 | newPTY.EXPECT().Write(mock.Anything).Return(0, errors.New("unused")).Maybe() | 385 | newPTY.EXPECT().Write(mock.Anything).Return(0, errors.New("unused")).Maybe() | ||
| 386 | newPTY.EXPECT().Resize(mock.Anything).Return(nil).Maybe() | 386 | newPTY.EXPECT().Resize(mock.Anything).Return(nil).Maybe() | ||
| 387 | newPTY.EXPECT().ForegroundPgid().Return(0, nil).Maybe() | 387 | newPTY.EXPECT().ForegroundPgid().Return(0, nil).Maybe() | ||
| 388 | - | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() | 388 | + | factory.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, "/work", domain.Size{Cols: 20, Rows: 10}).Return(newPTY, nil).Once() |
| 389 | 389 | ||||
| 390 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | 390 | require.NoError(t, d.splitPane(sess, nil, layout.Right)) | ||
| 391 | require.True(t, sess.snapDirty.Load()) | 391 | require.True(t, sess.snapDirty.Load()) | ||