@3fv/electron-utility-process-manager 1.0.6 → 1.0.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/README.md +52 -2
- package/lib/cjs/index.js +17 -7
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/main/UPMMainService.js +24 -12
- package/lib/cjs/main/UPMMainService.js.map +1 -1
- package/lib/cjs/main/UPMMainServiceManager.js +17 -7
- package/lib/cjs/main/UPMMainServiceManager.js.map +1 -1
- package/lib/cjs/node/UPMNodeProcess.js +17 -7
- package/lib/cjs/node/UPMNodeProcess.js.map +1 -1
- package/lib/cjs/package.json +1 -0
- package/lib/cjs/renderer/UPMRendererClientFactory.js +17 -7
- package/lib/cjs/renderer/UPMRendererClientFactory.js.map +1 -1
- package/lib/mjs/main/UPMMainService.js +5 -3
- package/lib/mjs/main/UPMMainService.js.map +1 -1
- package/lib/mjs/package.json +1 -0
- package/package.json +79 -41
- package/src/common/clients.ts +276 -0
- package/src/common/guards.ts +11 -0
- package/src/common/index.ts +3 -0
- package/src/common/types.ts +106 -0
- package/src/index.ts +8 -0
- package/src/main/UPMMainService.ts +454 -0
- package/src/main/UPMMainServiceManager.ts +178 -0
- package/src/main/index.ts +2 -0
- package/src/node/UPMNodeProcess.ts +164 -0
- package/src/node/index.ts +3 -0
- package/src/renderer/UPMRendererClientFactory.ts +57 -0
- package/src/renderer/index.ts +4 -0
- package/.prettierrc.js +0 -14
- package/examples/lib-examples/complex/complex-main.d.ts +0 -1
- package/examples/lib-examples/complex/complex-main.js +0 -108
- package/examples/lib-examples/complex/complex-main.js.map +0 -1
- package/examples/lib-examples/complex/complex-node.d.ts +0 -1
- package/examples/lib-examples/complex/complex-node.js +0 -17
- package/examples/lib-examples/complex/complex-node.js.map +0 -1
- package/examples/lib-examples/complex/complex-renderer.d.ts +0 -1
- package/examples/lib-examples/complex/complex-renderer.js +0 -18
- package/examples/lib-examples/complex/complex-renderer.js.map +0 -1
- package/examples/lib-examples/complex/complex-types.d.ts +0 -3
- package/examples/lib-examples/complex/complex-types.js +0 -3
- package/examples/lib-examples/complex/complex-types.js.map +0 -1
- package/examples/lib-examples/simple/simple-main.d.ts +0 -1
- package/examples/lib-examples/simple/simple-main.js +0 -113
- package/examples/lib-examples/simple/simple-main.js.map +0 -1
- package/examples/lib-examples/simple/simple-node.d.ts +0 -1
- package/examples/lib-examples/simple/simple-node.js +0 -17
- package/examples/lib-examples/simple/simple-node.js.map +0 -1
- package/examples/lib-examples/simple/simple-renderer.d.ts +0 -1
- package/examples/lib-examples/simple/simple-renderer.js +0 -17
- package/examples/lib-examples/simple/simple-renderer.js.map +0 -1
- package/jest.config.js +0 -20
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import {
|
|
2
|
+
app, MessageChannelMain, MessagePortMain, utilityProcess, UtilityProcess
|
|
3
|
+
} from "electron"
|
|
4
|
+
import { assert, getValue, isNumber, isString } from "@3fv/guard"
|
|
5
|
+
import Fs from "fs"
|
|
6
|
+
import { Deferred } from "@3fv/deferred"
|
|
7
|
+
import * as UPM from "../common/index.js"
|
|
8
|
+
import defaults from "lodash/defaults.js"
|
|
9
|
+
import isEmpty from "lodash/isEmpty.js"
|
|
10
|
+
import negate from "lodash/negate.js"
|
|
11
|
+
import Tracer from "tracer"
|
|
12
|
+
import { asOption } from "@3fv/prelude-ts"
|
|
13
|
+
import { match } from "ts-pattern"
|
|
14
|
+
|
|
15
|
+
const log = Tracer.colorConsole()
|
|
16
|
+
const isNotEmpty = negate(isEmpty)
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Contains & Manages a `Utility/Process`
|
|
20
|
+
* providing simplified messaging & event
|
|
21
|
+
* propagation.
|
|
22
|
+
*/
|
|
23
|
+
export class UPMMainService<ReqMap extends UPM.MessageRequestMap = any, MType extends UPM.MessageRequestNames<ReqMap> = UPM.MessageRequestNames<ReqMap>> implements UPM.IMessageClient<ReqMap, MType> {
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* As this container/wrapper implements
|
|
27
|
+
* `IMessageClient`, it needs to provide
|
|
28
|
+
* a `clientId`, which is constant in the
|
|
29
|
+
* main process
|
|
30
|
+
*/
|
|
31
|
+
readonly clientId:string = "main"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Send an event as `fire-and-forget`
|
|
35
|
+
*
|
|
36
|
+
* @param data
|
|
37
|
+
* @param port
|
|
38
|
+
*/
|
|
39
|
+
sendEvent(data:any, port:UPM.Port = this.childProcess_):void {
|
|
40
|
+
const messageId = this.generateMessageId()
|
|
41
|
+
const payload:UPM.NodeEnvelope = {
|
|
42
|
+
channel: UPM.IPCChannel.UPMServiceMessage,
|
|
43
|
+
payload: { messageId, eventData: data, kind: UPM.MessageKind.Event }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
port.postMessage(payload)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Send request
|
|
51
|
+
*
|
|
52
|
+
* @param type
|
|
53
|
+
* @param args
|
|
54
|
+
*/
|
|
55
|
+
async executeRequest<Type extends MType, R extends UPM.MessageRequestReturnType<ReqMap, Type> = UPM.MessageRequestReturnType<ReqMap, Type>>(
|
|
56
|
+
type:Type,
|
|
57
|
+
...args:UPM.MessageRequestParams<ReqMap, Type>
|
|
58
|
+
):Promise<R> {
|
|
59
|
+
assert(!!this.childProcess_, "The process is not running")
|
|
60
|
+
const messageId = this.generateMessageId(),
|
|
61
|
+
pending:UPM.PendingRequestMessage<ReqMap, Type, R> = {
|
|
62
|
+
deferred: new Deferred<R>(),
|
|
63
|
+
messageId,
|
|
64
|
+
timeoutId: setTimeout(
|
|
65
|
+
() => this.removePendingMessage(messageId),
|
|
66
|
+
UPM.Defaults.RequestTimeout
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.pendingMessages_.set(messageId, pending)
|
|
71
|
+
const payload:UPM.NodeMessage<ReqMap, Type> = {
|
|
72
|
+
channel: UPM.IPCChannel.UPMServiceMessage,
|
|
73
|
+
payload: { type, messageId, args, kind: UPM.MessageKind.Request }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.childProcess_.postMessage(payload)
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await pending.deferred.promise
|
|
80
|
+
log.debug(`Completed utility request (messageId=${messageId})`)
|
|
81
|
+
return result
|
|
82
|
+
} catch (err) {
|
|
83
|
+
log.error(`Failed utility request (messageId=${messageId})`, err)
|
|
84
|
+
throw err
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
close():void {
|
|
89
|
+
log.warn("Close has no effect when called via the main process")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolves when ready
|
|
94
|
+
*
|
|
95
|
+
* @returns {any}
|
|
96
|
+
*/
|
|
97
|
+
whenReady():Promise<UPM.IMessageClient<ReqMap, MType>> {
|
|
98
|
+
return this.init()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Return a simple proxy facade wrapping
|
|
103
|
+
* `executeRequest` into individual methods
|
|
104
|
+
*/
|
|
105
|
+
getServiceClient():UPM.IServiceClient<ReqMap, MType> {
|
|
106
|
+
return new Proxy<UPM.IServiceClient<ReqMap, MType>>({} as any, {
|
|
107
|
+
get: (_target, prop) => {
|
|
108
|
+
if (prop === "then") {
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
return (...args:any[]) => this.whenReady()
|
|
112
|
+
.then(client => client.executeRequest(
|
|
113
|
+
prop as MType,
|
|
114
|
+
...args as any
|
|
115
|
+
))
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private readonly config_:UPM.CreateServiceConfig
|
|
122
|
+
|
|
123
|
+
private readyDeferred_:Deferred<this> = null
|
|
124
|
+
|
|
125
|
+
private killDeferred_:Deferred<number> = null
|
|
126
|
+
|
|
127
|
+
private messageIdSeq_ = 0
|
|
128
|
+
|
|
129
|
+
private childProcess_:UtilityProcess = null
|
|
130
|
+
|
|
131
|
+
private pendingMessages_ = new Map<number, UPM.PendingRequestMessage>()
|
|
132
|
+
|
|
133
|
+
private messageChannels_ = new Map<string, MessageChannelMain>()
|
|
134
|
+
|
|
135
|
+
private onServiceExit(code:number) {
|
|
136
|
+
log.warn(`Service Exit (code=${code})`)
|
|
137
|
+
let deferred = this.killDeferred_
|
|
138
|
+
if (deferred) {
|
|
139
|
+
if (!deferred.isSettled()) {
|
|
140
|
+
deferred.resolve(code)
|
|
141
|
+
} else {
|
|
142
|
+
log.warn(`kill deferred is already settled (code=${code})`)
|
|
143
|
+
}
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
this.killDeferred_ = Deferred.resolve(code)
|
|
147
|
+
|
|
148
|
+
this.cancelAllPendingMessages()
|
|
149
|
+
this.removeAllMessageChannels()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Returned client side port (for Renderer)
|
|
154
|
+
* @param clientId
|
|
155
|
+
*/
|
|
156
|
+
createMessageChannel(clientId:string):MessagePortMain {
|
|
157
|
+
assert(
|
|
158
|
+
this.readyDeferred_?.isFulfilled() ?? false,
|
|
159
|
+
`Service (${this.serviceName}) is not fulfilled or has an error`
|
|
160
|
+
)
|
|
161
|
+
const channel = new MessageChannelMain(), { port1, port2 } = channel
|
|
162
|
+
|
|
163
|
+
this.messageChannels_.set(clientId, channel)
|
|
164
|
+
try {
|
|
165
|
+
this.childProcess_.postMessage({
|
|
166
|
+
message: {
|
|
167
|
+
channel: UPM.IPCChannel.UPMServiceNewClient, payload: { clientId }
|
|
168
|
+
}
|
|
169
|
+
}, [port1])
|
|
170
|
+
} catch (err) {
|
|
171
|
+
log.error(`Unable to create channel id=${clientId}`, err)
|
|
172
|
+
this.closeMessageChannel(clientId, channel)
|
|
173
|
+
throw err
|
|
174
|
+
}
|
|
175
|
+
return port2
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create a port client for the main process
|
|
180
|
+
* (useful to avoid congestion on default child_process channel)
|
|
181
|
+
*
|
|
182
|
+
* @param {string} clientId
|
|
183
|
+
* @returns {UPM.MessagePortClient}
|
|
184
|
+
*/
|
|
185
|
+
createMainClient(clientId:string):UPM.MessagePortClient<ReqMap, MType> {
|
|
186
|
+
return new UPM.MessagePortClient(
|
|
187
|
+
clientId,
|
|
188
|
+
this.createMessageChannel(clientId)
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Service is still in a healthy/running/runnable state
|
|
194
|
+
*/
|
|
195
|
+
get isRunning() {
|
|
196
|
+
return !this.killDeferred_
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Stop the service (kill)
|
|
201
|
+
*/
|
|
202
|
+
async stop():Promise<void> {
|
|
203
|
+
await this.kill()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resource management & cleanup
|
|
208
|
+
*/
|
|
209
|
+
async [Symbol.asyncDispose]() {
|
|
210
|
+
await this.stop()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Initialize the service
|
|
215
|
+
*
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
private async init():Promise<this> {
|
|
219
|
+
let deferred = this.readyDeferred_
|
|
220
|
+
if (deferred) {
|
|
221
|
+
return deferred.promise
|
|
222
|
+
}
|
|
223
|
+
deferred = this.readyDeferred_ = new Deferred()
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const exitListener = (code:number) => {
|
|
227
|
+
if (code !== 0) {
|
|
228
|
+
deferred.reject(new Error(`process exited with code ${code} upon starting`))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const spawnListener = async () => {
|
|
232
|
+
this.childProcess_.off("exit", exitListener)
|
|
233
|
+
this.childProcess_.on("exit", this.onServiceExit.bind(this))
|
|
234
|
+
this.childProcess_.on("message", this.onMessage.bind(this))
|
|
235
|
+
deferred.resolve(this)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// BUILD ADDITIONAL FORK OPTIONS
|
|
239
|
+
const childProcessOptions = asOption(this.config_.inspect)
|
|
240
|
+
.filter(it => it !== false)
|
|
241
|
+
.map(it => (
|
|
242
|
+
{
|
|
243
|
+
execArgv: [
|
|
244
|
+
match(it as UPM.ServiceInspectConfig)
|
|
245
|
+
.when(isNumber, port => `--inspect=${port}`)
|
|
246
|
+
.otherwise(({ port, break: shouldBreak }) => {
|
|
247
|
+
const ext = shouldBreak ? "-brk" : ""
|
|
248
|
+
return `--inspect${ext}=${port}`
|
|
249
|
+
})
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
))
|
|
253
|
+
.getOrElse({} as any)
|
|
254
|
+
|
|
255
|
+
// FORK
|
|
256
|
+
this.childProcess_ = utilityProcess
|
|
257
|
+
.fork(this.entryFile, [], {
|
|
258
|
+
...childProcessOptions, serviceName: this.serviceName, env: {
|
|
259
|
+
...process.env, IN_CHILD_PROCESS: "true", APP_NAME: app.name
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
.on("spawn", spawnListener)
|
|
263
|
+
.on("exit", exitListener)
|
|
264
|
+
|
|
265
|
+
await deferred.promise
|
|
266
|
+
} catch (err) {
|
|
267
|
+
deferred.reject(err)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return deferred.promise
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* OnMessage really only handles `results` from
|
|
275
|
+
* `executeRequest(...)`; this may be expanded in
|
|
276
|
+
* the future
|
|
277
|
+
*
|
|
278
|
+
* @param payload
|
|
279
|
+
* @private
|
|
280
|
+
*/
|
|
281
|
+
private onMessage<Type extends MType = any>(payload:UPM.Message<ReqMap, Type>) {
|
|
282
|
+
const { messageId, result, error } = payload
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const pending = this.pendingMessages_.get(messageId)
|
|
286
|
+
if (!pending) {
|
|
287
|
+
log.info(`Unable to find pending record ${messageId}`)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (pending.deferred.isSettled()) {
|
|
292
|
+
log.error(`Already settled record ${messageId}`)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.removePendingMessage(messageId)
|
|
297
|
+
if (!!error) {
|
|
298
|
+
log.error(error)
|
|
299
|
+
pending.deferred.reject(new Error(error))
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
pending.deferred.resolve(result)
|
|
303
|
+
} catch (err) {
|
|
304
|
+
log.error(`Failed to handle message`, err)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* This handler is currently unused, but
|
|
310
|
+
* could be attached to a `MessagePort.on('close')`
|
|
311
|
+
* event
|
|
312
|
+
*
|
|
313
|
+
* @param id
|
|
314
|
+
* @param channel
|
|
315
|
+
* @private
|
|
316
|
+
*/
|
|
317
|
+
// noinspection JSUnusedLocalSymbols
|
|
318
|
+
private onPortClose(id:string, channel:MessageChannelMain) {
|
|
319
|
+
log.info(`main side of message channel(id=${id}) closed`)
|
|
320
|
+
this.closeMessageChannel(id, channel)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Close a message channel
|
|
325
|
+
*
|
|
326
|
+
* @param id
|
|
327
|
+
* @param channel
|
|
328
|
+
* @private
|
|
329
|
+
*/
|
|
330
|
+
private closeMessageChannel(id:string, channel:MessageChannelMain) {
|
|
331
|
+
if (this.messageChannels_.has(id)) {
|
|
332
|
+
this.messageChannels_.delete(id)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
Array(channel.port1, channel.port2)
|
|
336
|
+
.forEach(p => {
|
|
337
|
+
getValue(() => p.close())
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Kill the service
|
|
343
|
+
*
|
|
344
|
+
* @private
|
|
345
|
+
*/
|
|
346
|
+
private async kill():Promise<number> {
|
|
347
|
+
if (!this.readyDeferred_ ||
|
|
348
|
+
!this.readyDeferred_.isSettled() ||
|
|
349
|
+
!this.childProcess_ ||
|
|
350
|
+
this.killDeferred_) {
|
|
351
|
+
log.error("Service is not ready OR the process is not running")
|
|
352
|
+
// noinspection ES6MissingAwait
|
|
353
|
+
return this.killDeferred_?.promise ?? Promise.resolve<number>(0)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let killDeferred = this.killDeferred_ = new Deferred<number>()
|
|
357
|
+
|
|
358
|
+
// GET THE PROCESS
|
|
359
|
+
const process = this.childProcess_
|
|
360
|
+
|
|
361
|
+
// CANCEL ALL PENDING MESSAGES & REMOVE ALL CHANNELS
|
|
362
|
+
this.cancelAllPendingMessages()
|
|
363
|
+
this.removeAllMessageChannels()
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
process.kill()
|
|
367
|
+
} catch (err) {
|
|
368
|
+
log.error("Unable to kill utility process", err)
|
|
369
|
+
killDeferred.reject(err)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return await killDeferred.promise
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Close all message channels
|
|
377
|
+
*
|
|
378
|
+
* @private
|
|
379
|
+
*/
|
|
380
|
+
private removeAllMessageChannels() {
|
|
381
|
+
for (const [id, channel] of [...this.messageChannels_.entries()]) {
|
|
382
|
+
this.closeMessageChannel(id, channel)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Remove all pending messages
|
|
388
|
+
*
|
|
389
|
+
* @private
|
|
390
|
+
*/
|
|
391
|
+
private cancelAllPendingMessages():void {
|
|
392
|
+
for (const key of [...this.pendingMessages_.keys()]) {
|
|
393
|
+
this.removePendingMessage(key)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Remove a pending message from the queue,
|
|
399
|
+
* whether settled or not
|
|
400
|
+
*
|
|
401
|
+
* @param messageId
|
|
402
|
+
* @private
|
|
403
|
+
*/
|
|
404
|
+
private removePendingMessage(messageId:number):void {
|
|
405
|
+
const pending = this.pendingMessages_.get(messageId)
|
|
406
|
+
if (!pending) {
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
clearTimeout(pending.timeoutId)
|
|
410
|
+
if (!pending.deferred.isSettled()) {
|
|
411
|
+
pending.deferred.reject(
|
|
412
|
+
"The pending message is being removed before promise is settled")
|
|
413
|
+
}
|
|
414
|
+
this.pendingMessages_.delete(messageId)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Generate next message id
|
|
419
|
+
*
|
|
420
|
+
* @private
|
|
421
|
+
*/
|
|
422
|
+
private generateMessageId():number {
|
|
423
|
+
return this.messageIdSeq_++
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Construct a new service
|
|
428
|
+
*
|
|
429
|
+
* @param serviceName
|
|
430
|
+
* @param entryFile
|
|
431
|
+
* @param options
|
|
432
|
+
*/
|
|
433
|
+
constructor(
|
|
434
|
+
readonly serviceName:string,
|
|
435
|
+
readonly entryFile:string,
|
|
436
|
+
options:UPM.CreateServiceOptions = {}
|
|
437
|
+
) {
|
|
438
|
+
assert(isString(entryFile) &&
|
|
439
|
+
isNotEmpty(entryFile) &&
|
|
440
|
+
Fs.existsSync(entryFile), `entry file must be provided (${entryFile})`)
|
|
441
|
+
|
|
442
|
+
this.config_ = defaults({
|
|
443
|
+
...options
|
|
444
|
+
}, {
|
|
445
|
+
serviceName, inspect: false
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
this.init().catch(err => {
|
|
449
|
+
log.error(`Failed to init utility process`, err)
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export default UPMMainService
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { ipcMain } from "electron"
|
|
2
|
+
import { assert, isString } from "@3fv/guard"
|
|
3
|
+
import { Future } from "@3fv/prelude-ts"
|
|
4
|
+
import * as UPM from "../common/index.js"
|
|
5
|
+
import UPMMainService from "./UPMMainService.js"
|
|
6
|
+
|
|
7
|
+
import Tracer from "tracer"
|
|
8
|
+
|
|
9
|
+
const log = Tracer.colorConsole()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export class UPMMainServiceManager {
|
|
14
|
+
private services_ = new Map<string, UPMMainService<any,any>>()
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* On new client request from renderer
|
|
18
|
+
*
|
|
19
|
+
* @param ev
|
|
20
|
+
* @param serviceName
|
|
21
|
+
* @param clientId
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
private onNewClient(ev: Electron.IpcMainEvent, serviceName: string, clientId: string) {
|
|
25
|
+
const sender = ev.sender
|
|
26
|
+
if (!sender) {
|
|
27
|
+
log.error(`sender is invalid on new client ipc message`, ev)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const proc = this.getService(serviceName)
|
|
32
|
+
if (!proc) {
|
|
33
|
+
log.error(`no proc found for ${serviceName}`, ev)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const clientPort = proc.createMessageChannel(clientId)
|
|
38
|
+
log.info(`Received client port for proc (id=${serviceName},clientId=${clientId})`)
|
|
39
|
+
sender.postMessage(
|
|
40
|
+
UPM.IPCChannel.UPMServiceNewClient,
|
|
41
|
+
{serviceName, clientId},
|
|
42
|
+
[clientPort]
|
|
43
|
+
)
|
|
44
|
+
} catch (err) {
|
|
45
|
+
log.error(`failed to register new client`, err)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
createMainChannel
|
|
50
|
+
<
|
|
51
|
+
Args extends UPM.MessageRequestMap = any,
|
|
52
|
+
MType extends UPM.MessageRequestNames<Args> = UPM.MessageRequestNames<Args>,
|
|
53
|
+
>
|
|
54
|
+
(serviceOrName: string | UPMMainService<Args, MType>, clientId: string): UPM.Port {
|
|
55
|
+
try {
|
|
56
|
+
const service = isString(serviceOrName) ? this.getService<Args,MType>(serviceOrName) : serviceOrName
|
|
57
|
+
|
|
58
|
+
const clientPort = service.createMessageChannel(clientId)
|
|
59
|
+
log.info(`Received client port for proc (id=${service.serviceName},clientId=${clientId})`)
|
|
60
|
+
return clientPort
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log.error(`failed to register new client`, err)
|
|
63
|
+
throw err
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
createMainClient<
|
|
68
|
+
ReqMap extends UPM.MessageRequestMap = any,
|
|
69
|
+
MType extends UPM.MessageRequestNames<ReqMap> = UPM.MessageRequestNames<ReqMap>,
|
|
70
|
+
>(serviceName: string, clientId: string) {
|
|
71
|
+
const service = this.getService<ReqMap,MType>(serviceName)
|
|
72
|
+
assert(!!service,`no proc found for ${serviceName}`)
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
return service.createMainClient(clientId)
|
|
76
|
+
} catch (err) {
|
|
77
|
+
log.error(`failed to register new client`, err)
|
|
78
|
+
throw err
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Destroy all services
|
|
84
|
+
*
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
private async destroy() {
|
|
88
|
+
for (const [id] of [...this.services_.keys()]) {
|
|
89
|
+
await this.destroyService(id)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the service for a given name
|
|
95
|
+
*
|
|
96
|
+
* @param serviceName
|
|
97
|
+
*/
|
|
98
|
+
getService<
|
|
99
|
+
ReqMap extends UPM.MessageRequestMap,
|
|
100
|
+
Type extends UPM.MessageRequestNames<ReqMap> = UPM.MessageRequestNames<ReqMap>
|
|
101
|
+
>(serviceName: string): UPMMainService<ReqMap, Type> {
|
|
102
|
+
return this.services_.has(serviceName) ? this.services_.get(serviceName) : null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a utility process/service
|
|
107
|
+
*
|
|
108
|
+
* @param serviceName
|
|
109
|
+
* @param entryFile
|
|
110
|
+
* @param options
|
|
111
|
+
*/
|
|
112
|
+
async createService<
|
|
113
|
+
ReqMap extends UPM.MessageRequestMap,
|
|
114
|
+
Type extends UPM.MessageRequestNames<ReqMap> = UPM.MessageRequestNames<ReqMap>
|
|
115
|
+
>(serviceName: string, entryFile: string, options: UPM.CreateServiceOptions = {}): Promise<UPMMainService<ReqMap,Type>> {
|
|
116
|
+
assert(!this.services_.has(serviceName), `utility process with id (${serviceName}) is already registered`)
|
|
117
|
+
const proc = new UPMMainService(serviceName, entryFile, {
|
|
118
|
+
...options,
|
|
119
|
+
serviceName: options.serviceName ?? serviceName,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
this.services_.set(serviceName, proc)
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await proc.whenReady()
|
|
126
|
+
return proc
|
|
127
|
+
} catch (err) {
|
|
128
|
+
await this.destroyService(serviceName)
|
|
129
|
+
throw err
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Destroy a specific service
|
|
135
|
+
*
|
|
136
|
+
* @param serviceName
|
|
137
|
+
*/
|
|
138
|
+
async destroyService(serviceName: string) {
|
|
139
|
+
if (!this.services_.has(serviceName)) return
|
|
140
|
+
|
|
141
|
+
const proc = this.services_.get(serviceName)
|
|
142
|
+
this.services_.delete(serviceName)
|
|
143
|
+
await proc.stop()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Async dispose symbol
|
|
148
|
+
*/
|
|
149
|
+
async [Symbol.asyncDispose]() {
|
|
150
|
+
await this.destroy()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private constructor() {
|
|
154
|
+
ipcMain.on(UPM.IPCChannel.UPMServiceNewClient, this.onNewClient.bind(this))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Singleton instance
|
|
159
|
+
*
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
private static sInstance_: UPMMainServiceManager = null
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get singleton instance
|
|
166
|
+
*/
|
|
167
|
+
static get(): UPMMainServiceManager {
|
|
168
|
+
if (!this.sInstance_) {
|
|
169
|
+
this.sInstance_ = new UPMMainServiceManager()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return this.sInstance_
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const upmMainServiceManager = UPMMainServiceManager.get()
|
|
177
|
+
|
|
178
|
+
export default upmMainServiceManager
|