fix(copy): bound clipboard send latency
- commit
- f38c91b7234a35f91109131bc9660d5908597f21
- author
- brice · · 2026-07-03 04:59 UTC
- parent
- 3d26bab
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +15 -10
M internal/usecase/daemon/daemon.go +15 -10
| @@ -226,18 +226,23 @@ func (ac *attachedClient) send(f ports.Frame) error { | |||
| 226 | 226 | // write. Detach/kill/shutdown paths use this so they are never gated on a | |
| 227 | 227 | // client that has stopped draining its socket. | |
| 228 | 228 | func (d *Daemon) boundedSend(ac *attachedClient, f ports.Frame) { | |
| 229 | + | _ = d.boundedSendErr(ac, f) | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | func (d *Daemon) boundedSendErr(ac *attachedClient, f ports.Frame) error { | |
| 229 | 233 | timer := d.clock.NewTimer(detachNotifyTimeout) | |
| 230 | - | sent := make(chan struct{}) | |
| 234 | + | result := make(chan error, 1) | |
| 231 | 235 | go func() { | |
| 232 | - | select { | |
| 233 | - | case <-timer.C(): | |
| 234 | - | _ = ac.tr.Close() | |
| 235 | - | case <-sent: | |
| 236 | - | } | |
| 236 | + | result <- ac.send(f) | |
| 237 | 237 | }() | |
| 238 | - | _ = ac.send(f) | |
| 239 | - | timer.Stop() | |
| 240 | - | close(sent) | |
| 238 | + | select { | |
| 239 | + | case err := <-result: | |
| 240 | + | timer.Stop() | |
| 241 | + | return err | |
| 242 | + | case <-timer.C(): | |
| 243 | + | _ = ac.tr.Close() | |
| 244 | + | return errors.New("send timed out") | |
| 245 | + | } | |
| 241 | 246 | } | |
| 242 | 247 | ||
| 243 | 248 | // notifyDetachedAsync delivers a best-effort Detached notice off the caller's | |
| @@ -949,7 +954,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 949 | 954 | if copyOut && text != "" { | |
| 950 | 955 | chunks := scopy.OSC52(text) | |
| 951 | 956 | for _, chunk := range chunks { | |
| 952 | - | if err := ac.send(frameOutput(chunk)); err != nil { | |
| 957 | + | if err := d.boundedSendErr(ac, frameOutput(chunk)); err != nil { | |
| 953 | 958 | d.detachOnSendError(sess, ac) | |
| 954 | 959 | return | |
| 955 | 960 | } | |
| @@ -226,18 +226,23 @@ func (ac *attachedClient) send(f ports.Frame) error { | |||||
| 226 | // write. Detach/kill/shutdown paths use this so they are never gated on a | 226 | // write. Detach/kill/shutdown paths use this so they are never gated on a | ||
| 227 | // client that has stopped draining its socket. | 227 | // client that has stopped draining its socket. | ||
| 228 | func (d *Daemon) boundedSend(ac *attachedClient, f ports.Frame) { | 228 | func (d *Daemon) boundedSend(ac *attachedClient, f ports.Frame) { | ||
| 229 | + | _ = d.boundedSendErr(ac, f) | |||
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | func (d *Daemon) boundedSendErr(ac *attachedClient, f ports.Frame) error { | |||
| 229 | timer := d.clock.NewTimer(detachNotifyTimeout) | 233 | timer := d.clock.NewTimer(detachNotifyTimeout) | ||
| 230 | - | sent := make(chan struct{}) | 234 | + | result := make(chan error, 1) |
| 231 | go func() { | 235 | go func() { | ||
| 232 | - | select { | 236 | + | result <- ac.send(f) |
| 233 | - | case <-timer.C(): | |||
| 234 | - | _ = ac.tr.Close() | |||
| 235 | - | case <-sent: | |||
| 236 | - | } | |||
| 237 | }() | 237 | }() | ||
| 238 | - | _ = ac.send(f) | 238 | + | select { |
| 239 | - | timer.Stop() | 239 | + | case err := <-result: |
| 240 | - | close(sent) | 240 | + | timer.Stop() |
| 241 | + | return err | |||
| 242 | + | case <-timer.C(): | |||
| 243 | + | _ = ac.tr.Close() | |||
| 244 | + | return errors.New("send timed out") | |||
| 245 | + | } | |||
| 241 | } | 246 | } | ||
| 242 | 247 | ||||
| 243 | // notifyDetachedAsync delivers a best-effort Detached notice off the caller's | 248 | // notifyDetachedAsync delivers a best-effort Detached notice off the caller's | ||
| @@ -949,7 +954,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 949 | if copyOut && text != "" { | 954 | if copyOut && text != "" { | ||
| 950 | chunks := scopy.OSC52(text) | 955 | chunks := scopy.OSC52(text) | ||
| 951 | for _, chunk := range chunks { | 956 | for _, chunk := range chunks { | ||
| 952 | - | if err := ac.send(frameOutput(chunk)); err != nil { | 957 | + | if err := d.boundedSendErr(ac, frameOutput(chunk)); err != nil { |
| 953 | d.detachOnSendError(sess, ac) | 958 | d.detachOnSendError(sess, ac) | ||
| 954 | return | 959 | return | ||
| 955 | } | 960 | } | ||