diff --git a/internal/cli/daemon_unix.go b/internal/cli/daemon_unix.go index e0afa87..e0e14a3 100644 --- a/internal/cli/daemon_unix.go +++ b/internal/cli/daemon_unix.go @@ -3,6 +3,7 @@ package cli import ( + "os" "os/exec" "syscall" ) @@ -10,3 +11,14 @@ import ( func configureDaemonCommand(cmd *exec.Cmd) { cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} } + +func stopDaemonProcess(process *os.Process) error { + if process == nil { + return nil + } + err := syscall.Kill(-process.Pid, syscall.SIGTERM) + if err == syscall.ESRCH { + return nil + } + return err +} diff --git a/internal/cli/daemon_windows.go b/internal/cli/daemon_windows.go index 9483d0a..a3cd800 100644 --- a/internal/cli/daemon_windows.go +++ b/internal/cli/daemon_windows.go @@ -2,6 +2,16 @@ package cli -import "os/exec" +import ( + "os" + "os/exec" +) func configureDaemonCommand(_ *exec.Cmd) {} + +func stopDaemonProcess(process *os.Process) error { + if process == nil { + return nil + } + return process.Kill() +} diff --git a/internal/cli/webvnc.go b/internal/cli/webvnc.go index 1e568c0..5da016a 100644 --- a/internal/cli/webvnc.go +++ b/internal/cli/webvnc.go @@ -194,7 +194,7 @@ func (a App) startWebVNCDaemon(args []string, leaseID string) error { return exit(5, "release WebVNC daemon process: %v", err) } fmt.Fprintf(a.Stdout, "webvnc daemon: pid=%d log=%s\n", pid, logPath) - fmt.Fprintf(a.Stdout, "webvnc daemon: stop with kill $(cat %s)\n", pidPath) + fmt.Fprintln(a.Stdout, "webvnc daemon: stop with crabbox webvnc --id --stop") return nil } @@ -277,7 +277,7 @@ func (a App) stopWebVNCDaemon(leaseID string) error { if err != nil { return exit(5, "find WebVNC daemon pid %d: %v", pid, err) } - if err := process.Kill(); err != nil { + if err := stopDaemonProcess(process); err != nil { return exit(5, "stop WebVNC daemon pid %d: %v", pid, err) } _ = os.Remove(pidPath) diff --git a/worker/src/fleet.ts b/worker/src/fleet.ts index 9e82d2a..22195b3 100644 --- a/worker/src/fleet.ts +++ b/worker/src/fleet.ts @@ -1226,14 +1226,12 @@ export class FleetDurableObject implements DurableObject { ); } const existingViewer = this.webVNCViewers.get(lease.id); - if (existingViewer) { - closeSocket(existingViewer, 1012, "replaced by a newer WebVNC viewer"); - this.clearWebVNCViewer(lease.id, existingViewer); + if (existingViewer?.readyState === WebSocket.OPEN) { const command = webVNCBridgeCommand(lease); return json( { - error: "webvnc_bridge_reset", - message: `another viewer was active; restart or wait for the bridge to reconnect with: ${command}`, + error: "webvnc_viewer_active", + message: `another viewer is active; close stale WebVNC tabs or wait before reconnecting with: ${command}`, command, }, { status: 409 }, diff --git a/worker/src/portal.ts b/worker/src/portal.ts index d7647bb..99aae53 100644 --- a/worker/src/portal.ts +++ b/worker/src/portal.ts @@ -231,10 +231,10 @@ export function portalVNC(lease: LeaseRecord): Response { return; } if (state?.viewerConnected) { - setStatus("another viewer is connected; close stale tabs if this resets", "warn"); - } else { - setStatus(retryAttempt ? "bridge connected; opening viewer" : "connecting"); + scheduleRetry("another viewer is connected; close stale WebVNC tabs"); + return; } + setStatus(retryAttempt ? "bridge connected; opening viewer" : "connecting"); rfb = new RFB(screen, wsURL.toString(), options); rfb.scaleViewport = true; rfb.resizeSession = false; diff --git a/worker/test/fleet.test.ts b/worker/test/fleet.test.ts index 04367ab..cb06783 100644 --- a/worker/test/fleet.test.ts +++ b/worker/test/fleet.test.ts @@ -745,6 +745,7 @@ describe("fleet lease identity and idle", () => { expect(pageBody).toContain("/portal/leases/cbx_000000000001/vnc/status"); expect(pageBody).toContain("vnc-copy"); expect(pageBody).toContain("no bridge connected; run the bridge command below"); + expect(pageBody).toContain("another viewer is connected; close stale WebVNC tabs"); expect(pageBody).toContain('fragment.get("username")'); expect(pageBody).toContain('types.includes("username")'); expect(pageBody).not.toContain("cdn.jsdelivr.net");