-
-
Notifications
You must be signed in to change notification settings - Fork 204
Expand file tree
/
Copy pathexecute.ts
More file actions
145 lines (129 loc) · 4.48 KB
/
Copy pathexecute.ts
File metadata and controls
145 lines (129 loc) · 4.48 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
136
137
138
139
140
141
142
143
144
145
import { ChildProcess, spawn } from 'child_process'
import { Transform, TransformCallback, Writable } from 'stream'
import { pgnToActisenseSerialFormat } from '@canboat/canboatjs'
import type { PGN } from '@canboat/ts-pgns'
import type { CreateDebug, DebugLogger } from './types'
interface ExecuteOptions {
command: string
app: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(event: string, cb: (...args: any[]) => void): void
emit(event: string, ...args: unknown[]): void
setProviderStatus(id: string, msg: string): void
setProviderError(id: string, msg: string): void
}
providerId: string
toChildProcess?: string | false
restartOnClose?: boolean
restartThrottleTime?: number
createDebug?: CreateDebug
debug?: DebugLogger
[key: string]: unknown
}
export default class Execute extends Transform {
private readonly options: ExecuteOptions
private readonly debug: DebugLogger
childProcess!: ChildProcess
private pipeTo: Writable | null = null
private lastStartupTime = 0
private stopped = false
constructor(options: ExecuteOptions) {
super({})
this.options = options
const createDebug = options.createDebug ?? require('debug')
this.debug = options.debug ?? createDebug('signalk:streams:execute')
}
_transform(
chunk: Buffer,
encoding: BufferEncoding,
done: TransformCallback
): void {
this.childProcess.stdin?.write(chunk.toString())
done()
}
pipe<T extends NodeJS.WritableStream>(pipeTo: T): T {
this.pipeTo = pipeTo as unknown as Writable
this.startProcess(this.options.command)
// `false` disables stdin wiring entirely, for gateways that cannot
// transmit (the YDWG-02's receive-only UDP mode). Leaving the
// option undefined subscribes to the default event instead, so
// absence alone would still write to the child's stdin.
if (this.options.toChildProcess === false) {
this.debug('Receive-only provider: not wiring the child process stdin')
return super.pipe(pipeTo)
}
const stdOutEvent = this.options.toChildProcess ?? 'toChildProcess'
this.debug(
'Using event ' + stdOutEvent + " for output to child process's stdin"
)
this.options.app.on(stdOutEvent, (d: string) => {
try {
this.childProcess.stdin?.write(d + '\n')
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err)
console.log('execute:' + message)
}
})
if (stdOutEvent === 'nmea2000out') {
this.options.app.on('nmea2000JsonOut', (pgn: PGN) => {
this.childProcess.stdin?.write(pgnToActisenseSerialFormat(pgn) + '\r\n')
})
this.options.app.emit('nmea2000OutAvailable')
}
super.pipe(pipeTo)
return pipeTo
}
end(): this {
this.debug('end, killing child process')
this.stopped = true
this.childProcess.kill()
if (this.pipeTo) {
this.pipeTo.end()
}
return this
}
private startProcess(command: string): void {
this.debug(`starting |${command}|`)
if (process.platform === 'win32') {
this.childProcess = spawn('cmd', ['/c', command])
} else {
this.childProcess = spawn('sh', ['-c', command])
}
this.lastStartupTime = Date.now()
this.options.app.setProviderStatus(this.options.providerId, 'Started')
this.childProcess.stderr?.on('data', (data: Buffer) => {
const msg = data.toString()
this.options.app.setProviderError(this.options.providerId, msg)
console.error(msg)
})
this.childProcess.stdout?.on('data', (data: Buffer) => {
if (this.debug.enabled) {
this.debug(data.toString())
}
this.push(data)
})
this.childProcess.on('close', (code: number | null) => {
const msg = `|${command}| exited with ${code}`
console.error(msg)
if (this.stopped) return
if (
this.options.restartOnClose === undefined ||
this.options.restartOnClose
) {
const throttleTime = (this.options.restartThrottleTime ?? 60) * 1000
const sinceLast = Date.now() - this.lastStartupTime
if (sinceLast > throttleTime) {
this.startProcess(command)
} else {
const nextStart = throttleTime - sinceLast
const waitMsg = `Waiting ${nextStart / 1000} seconds to restart`
this.options.app.setProviderStatus(this.options.providerId, waitMsg)
this.debug(waitMsg)
setTimeout(() => {
this.startProcess(command)
}, nextStart)
}
}
})
}
}