@gotcos/glasses-server 6.18.6 → 6.18.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 6.18.7
2
+
3
+ - **Phone Restart no longer leaves the server Stopped.** Control LaunchAgent
4
+ KeepAlive is `SuccessfulExit: false`, so the old SIGTERM→exit(0) path never
5
+ came back — phone showed "Restart requested; reconnect is taking longer than
6
+ expected" while Control sat at Stopped. Restart now `launchctl kickstart -k`
7
+ (fallback `exit(1)` so KeepAlive still fires).
8
+
1
9
  ## 6.18.6
2
10
 
3
11
  - **Phone Restart works on COS Control managed installs.** Recovery status /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.18.6",
3
+ "version": "6.18.7",
4
4
  "description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, or Cursor Agent CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,47 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { userInfo } from 'node:os'
3
+
4
+ /** LaunchAgent label Control installs for the managed glasses server. */
5
+ export const DEFAULT_LAUNCHD_LABEL = 'com.cos.glasses-server'
6
+
7
+ /**
8
+ * COS Control's LaunchAgent uses KeepAlive SuccessfulExit:false — a clean
9
+ * SIGTERM/exit(0) leaves the service STOPPED. Phone Restart must either
10
+ * kickstart -k (kill + start) or exit non-zero so KeepAlive brings it back.
11
+ */
12
+ export function scheduleManagedServerRestart(options?: {
13
+ delayMs?: number
14
+ label?: string
15
+ kickstart?: (args: string[]) => void
16
+ exitProcess?: (code: number) => void
17
+ onError?: (error: unknown) => void
18
+ }): void {
19
+ const delayMs = options?.delayMs ?? 350
20
+ const label = (options?.label ?? process.env.COS_LAUNCHD_LABEL?.trim()) || DEFAULT_LAUNCHD_LABEL
21
+ const kickstart = options?.kickstart ?? ((args: string[]) => {
22
+ const child = spawn('launchctl', args, { detached: true, stdio: 'ignore' })
23
+ child.unref()
24
+ })
25
+ const exitProcess = options?.exitProcess ?? ((code: number) => { process.exit(code) })
26
+ const onError = options?.onError ?? ((error: unknown) => {
27
+ console.error('[recovery] Failed to schedule managed server restart:', error)
28
+ })
29
+
30
+ const timer = setTimeout(() => {
31
+ if (process.env.COS_DISABLE_SELF_RESTART === '1') return
32
+ try {
33
+ const uid = userInfo().uid
34
+ kickstart(['kickstart', '-k', `gui/${uid}/${label}`])
35
+ // If kickstart did not kill us quickly (misconfigured label), force a
36
+ // non-zero exit so KeepAlive SuccessfulExit:false still restarts.
37
+ setTimeout(() => {
38
+ if (process.env.COS_DISABLE_SELF_RESTART === '1') return
39
+ try { exitProcess(1) } catch (error) { onError(error) }
40
+ }, 2_000).unref?.()
41
+ } catch (error) {
42
+ onError(error)
43
+ try { exitProcess(1) } catch (exitError) { onError(exitError) }
44
+ }
45
+ }, delayMs)
46
+ timer.unref?.()
47
+ }
@@ -2,6 +2,7 @@ import { Router } from 'express'
2
2
  import { readFileSync } from 'node:fs'
3
3
  import { resolve } from 'node:path'
4
4
  import { atomicWriteFileSync } from '../lib/atomic-fs.js'
5
+ import { scheduleManagedServerRestart } from '../lib/managed-restart.js'
5
6
  import { isManagedRuntime } from '../lib/managed-runtime.js'
6
7
  import { acquireMaintenance, getRecoveryActivityStatus } from '../lib/recovery-activity.js'
7
8
  import { getWhisperHealth, restartWhisperServer } from '../lib/whisper-local.js'
@@ -65,15 +66,15 @@ recoveryRouter.post('/recovery/server/restart', (_req, res) => {
65
66
  res.status(202).json({ accepted: true, oldBootId: serverMetrics.bootId })
66
67
  // Schedule independently of the response socket. The phone may change
67
68
  // network/close the sheet immediately after receiving 202; that must not
68
- // cancel an accepted restart or strand maintenance forever.
69
- const timer = setTimeout(() => {
70
- if (process.env.COS_DISABLE_SELF_RESTART === '1') { gate.release(); return }
71
- try {
72
- process.kill(process.pid, 'SIGTERM')
73
- } catch (error) {
69
+ // cancel an accepted restart. Maintenance is in-process only — it dies with us.
70
+ //
71
+ // Do NOT SIGTERM→exit(0): Control's LaunchAgent KeepAlive is
72
+ // SuccessfulExit:false, so a clean exit leaves the service STOPPED and the
73
+ // phone shows "Restart requested; reconnect is taking longer than expected."
74
+ scheduleManagedServerRestart({
75
+ onError: (error) => {
74
76
  gate.release()
75
- console.error('[recovery] Failed to signal managed server restart:', error)
76
- }
77
- }, 350)
78
- timer.unref?.()
77
+ console.error('[recovery] Failed to schedule managed server restart:', error)
78
+ },
79
+ })
79
80
  })