forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhelperProcess.ts
More file actions
135 lines (123 loc) · 4.69 KB
/
Copy pathhelperProcess.ts
File metadata and controls
135 lines (123 loc) · 4.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
import { resolveHelperPath } from './resolveHelperPath'
import { encodeRequest, FrameDecoder, OP_OCR, OP_WINDOW } from './helperProtocol'
import type { OcrResult, WindowInfo } from '../../shared/types'
const REQUEST_TIMEOUT_MS = 5000
const MAX_BACKOFF_MS = 10000
type Pending = {
resolve: (json: string) => void
reject: (e: Error) => void
timer: NodeJS.Timeout
}
/**
* One supervised, long-running helper process shared by OCR + window-info.
* Lazy start; capped-backoff restart on crash; single-flight FIFO request queue
* (the helper processes one frame at a time, so we serialize). Per-request
* timeout recycles the process to avoid a wedged pipe blocking forever.
*/
class HelperProcess {
private child: ChildProcessWithoutNullStreams | null = null
private readonly queue: Pending[] = []
private backoff = 500
private starting = false
// Set once the helper binary is confirmed missing (spawn ENOENT). Without this,
// every OCR/window request re-spawns the missing exe, failing forever — flooding
// the log and stalling each caller on a doomed spawn. Once unavailable, fail fast.
private unavailable = false
private ensureStarted(): void {
if (this.child || this.starting || this.unavailable) return
this.starting = true
const exe = resolveHelperPath()
// windowsHide: the helper is a console-subsystem .NET exe (OutputType=Exe).
// Electron main is a GUI process with no console, so without CREATE_NO_WINDOW
// the child allocates a NEW visible console — a stray taskbar window. Its
// stdio is piped, so hiding the console loses nothing.
const child = spawn(exe, [], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true })
this.child = child
this.starting = false
const decoder = new FrameDecoder((json) => {
const pending = this.queue.shift()
if (!pending) return
clearTimeout(pending.timer)
pending.resolve(json)
})
child.stdout.on('data', (chunk: Buffer) => decoder.push(chunk))
child.stderr.on('data', (c: Buffer) => console.log('[win-ocr-helper]', c.toString().trim()))
child.on('exit', (code) => {
console.warn(`[win-ocr-helper] exited code=${code}`)
this.handleExit()
})
child.on('error', (e) => {
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
if (!this.unavailable) {
console.error(
'[win-ocr-helper] binary not found — OCR / screen-reading is DISABLED. ' +
'Build it once with: pnpm run build:ocr-helper (needs .NET SDK). ' +
`(${e.message})`
)
}
this.unavailable = true
} else {
console.error('[win-ocr-helper] spawn error:', e.message)
}
this.handleExit()
})
// Successful start — reset backoff after a short grace period.
setTimeout(() => {
if (this.child === child) this.backoff = 500
}, 2000)
}
private handleExit(): void {
this.child = null
// Fail every in-flight request; the helper restarts lazily on next request.
while (this.queue.length) {
const p = this.queue.shift()!
clearTimeout(p.timer)
p.reject(new Error('helper exited'))
}
this.backoff = Math.min(this.backoff * 2, MAX_BACKOFF_MS)
}
private recycle(): void {
if (this.child) {
try {
this.child.kill()
} catch {
/* already dead */
}
}
this.handleExit()
}
private request(opcode: number, payload: Buffer): Promise<string> {
if (this.unavailable) return Promise.reject(new Error('helper unavailable (binary missing)'))
this.ensureStarted()
const child = this.child
if (!child) return Promise.reject(new Error('helper not available'))
return new Promise<string>((resolve, reject) => {
const timer = setTimeout(() => {
// Drop the wedged request and recycle the process.
const idx = this.queue.findIndex((p) => p.timer === timer)
if (idx >= 0) this.queue.splice(idx, 1)
reject(new Error('helper request timed out'))
this.recycle()
}, REQUEST_TIMEOUT_MS)
this.queue.push({ resolve, reject, timer })
child.stdin.write(encodeRequest(opcode, payload))
})
}
async ocr(jpeg: Buffer): Promise<OcrResult> {
try {
const json = await this.request(OP_OCR, jpeg)
return JSON.parse(json) as OcrResult
} catch (e) {
return { ok: false, code: 'HELPER_ERROR', message: (e as Error).message }
}
}
async windowInfo(): Promise<WindowInfo> {
const json = await this.request(OP_WINDOW, Buffer.alloc(0))
return JSON.parse(json) as WindowInfo
}
dispose(): void {
this.recycle()
}
}
export const helperProcess = new HelperProcess()