@naanlang/naan 1.5.0 → 1.7.0

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.
Files changed (62) hide show
  1. package/LICENSE.md +2 -2
  2. package/README.md +2 -2
  3. package/bin/index.js +7 -3
  4. package/dist/env_web.js +162 -48
  5. package/dist/naan.min.js +11 -11
  6. package/frameworks/browser/https_request.nlg +2 -2
  7. package/frameworks/browser/sworker.js +28 -26
  8. package/frameworks/browser/terminals.nlg +295 -737
  9. package/frameworks/browser/worker.nlg +295 -0
  10. package/frameworks/browser/workers.nlg +6 -6
  11. package/frameworks/browser/ws_client.nlg +15 -10
  12. package/frameworks/client/apiclient.nlg +211 -9
  13. package/frameworks/client/psm_client.nlg +10 -14
  14. package/frameworks/client/relaycon.nlg +87 -161
  15. package/frameworks/common/common.nlg +2 -0
  16. package/frameworks/common/events.nlg +101 -0
  17. package/frameworks/common/repltools.nlg +80 -1
  18. package/frameworks/node/apiserver.nlg +38 -246
  19. package/frameworks/node/filesystem.nlg +31 -6
  20. package/frameworks/node/http_request.nlg +34 -13
  21. package/frameworks/node/https_request.nlg +8 -3
  22. package/frameworks/node/node.nlg +3 -0
  23. package/frameworks/node/pty.nlg +346 -0
  24. package/frameworks/node/shell.nlg +26 -3
  25. package/frameworks/node/worker.nlg +504 -560
  26. package/frameworks/node/ws_client.nlg +2 -2
  27. package/frameworks/project/build.nlg +56 -14
  28. package/frameworks/project/projects.nlg +1 -1
  29. package/frameworks/running/debugcom.nlg +20 -3
  30. package/frameworks/running/debugnub.nlg +33 -11
  31. package/frameworks/running/executors.nlg +40 -285
  32. package/frameworks/running/instances.nlg +394 -0
  33. package/frameworks/running/remote_req.nlg +139 -0
  34. package/frameworks/running/remote_res.nlg +89 -0
  35. package/frameworks/running/sourcecode.nlg +3 -2
  36. package/frameworks/running/taskexec.nlg +16 -59
  37. package/frameworks/service/imp.nlg +751 -0
  38. package/frameworks/service/model.nlg +71 -0
  39. package/frameworks/service/nubnode.nlg +136 -0
  40. package/frameworks/service/nubweb.nlg +124 -0
  41. package/frameworks/service/service.nlg +28 -0
  42. package/frameworks/storage/file_manager.nlg +3 -2
  43. package/lib/browser/env_web.js +169 -55
  44. package/lib/browser/env_webworker.js +89 -31
  45. package/lib/core/naanlib.js +11 -11
  46. package/lib/env_node.js +186 -27
  47. package/lib/env_nodeworker.js +82 -44
  48. package/lib/node_server_init.nlg +149 -0
  49. package/lib/node_worker.js +36 -0
  50. package/lib/node_worker_init.nlg +43 -0
  51. package/package.json +1 -1
  52. package/plugins/openSearch/openSearch.nlg +3 -2
  53. package/plugins/openSearch/os_dynamo.nlg +3 -2
  54. package/plugins/serviceAws/aws_cloudwatchlogs.nlg +44 -2
  55. package/plugins/serviceAws/aws_dynamo.nlg +144 -63
  56. package/plugins/serviceAws/aws_utils.nlg +6 -11
  57. package/plugins/serviceAws/serviceAws.nlg +3 -2
  58. package/plugins/serviceNexara/speechrec.nlg +10 -6
  59. package/test/test_01_core.nlg +2 -2
  60. package/test/test_02_context.nlg +32 -9
  61. package/test/test_03_jsinterop.nlg +3 -3
  62. package/test/test_07_lingo.nlg +1 -1
@@ -6,711 +6,656 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2019-2024 by Richard C. Zulch
9
+ * Copyright (c) 2019-2026 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
13
- /*
14
- * globals
15
- *
16
- */
17
-
18
- gWorkers;
19
-
20
13
 
21
14
  /*
22
- * workCurrent
15
+ * CurBot
23
16
  *
24
- * Attach a remote terminal interface to the current NodeJS thread.
17
+ * Attach a remote terminal interface to the current NodeJS thread.
18
+ *
19
+ * Options:
20
+ * {
21
+ * name: <string> // target name, e.g. "Play"
22
+ * type: <string> // target category, e.g. "Local"
23
+ * }
25
24
  *
26
25
  */
27
26
 
28
- closure workCurrent(workco, workerID, startup, local current) {
29
- global(js)
30
- current = require("../running/executors.nlg").ExecutorBase(workco, workerID, startup.name)
31
- current.workerID = workerID
32
- current.name = startup.name
33
- current.links = []
34
- current.termif = xnew({
27
+ closure CurBot(options, local curbot) {
28
+ global(App, js)
29
+ curbot = new(object, this)
30
+ curbot.topsubs = []
31
+ options = merge({
32
+ type: "Host"
33
+ }, options)
34
+ curbot.type = options.type
35
+ curbot.name = options.name
36
+
37
+ curbot.termif = xnew({
35
38
  textout: ONtextOut.proc
36
39
  debugtext: ONdebugText.proc
37
40
  status: ONstatus.proc
38
41
  debugout: ONdebugOut.proc
39
- messageout: ONmessageOut.proc
42
+ ioctl: ONioctl.proc
40
43
  })
41
- current.guid = 1
42
- current.host = js.t // our magic Naan controller reference
43
-
44
- // replyHook
44
+ curbot.guid = 1
45
+ curbot.host = js.t // our magic Naan controller reference
46
+
47
+ curbot.instanceID = UUID()
48
+ curbot.sender = App.imp.register(curbot, {
49
+ name: curbot.name
50
+ type: curbot.type
51
+ instanceID: curbot.instanceID
52
+ services: ["naanide-server-main"]
53
+ })
54
+
55
+ // subscribe
45
56
  //
46
- // Intercept the ReplyMessage function to see if we should handle it.
57
+ // Add an InTop to our subscribers if not already known.
47
58
  //
48
- function replyHook(data) {
49
- if data._guid
50
- workco.uworkers[data._guid].msgport.postMessage({
51
- id: "targetreceive"
52
- data: data
59
+ function subscribe(destID) {
60
+ if !curbot.topsubs.find(function(item) { item == destID }) {
61
+ curbot.topsubs.push(destID)
62
+ curbot.sender(destID, {
63
+ id: "workerDestID"
64
+ workerDestID: App.imp.us.instanceID // our remote responder
53
65
  })
66
+ }
67
+ }
68
+
69
+ // unsubscribe
70
+ //
71
+ // Remove an InTop from our subscribers if present.
72
+ //
73
+ function unsubscribe(destID) {
74
+ curbot.topsubs = curbot.topsubs.filter(function(item) { item != destID })
75
+ }
76
+
77
+ // notifyAll
78
+ //
79
+ // Send the message to all our subscribers.
80
+ //
81
+ curbot.notifyAll = function notifyAll(message, exceptID, local topDestID) {
82
+ for topDestID in curbot.topsubs
83
+ if topDestID != exceptID
84
+ curbot.sender(topDestID, message)
85
+ }
86
+
87
+ // textEntry
88
+ //
89
+ // Simulate text entry for subprocess support.
90
+ //
91
+ curbot.textEntry = function textEntry(text) {
92
+ curbot.host.DispatchMessage({
93
+ id: "keyline"
94
+ text: text
95
+ })
96
+ }
97
+
98
+ // rawStart
99
+ //
100
+ // Configure ioctls for raw terminal operation using the spawned PTY process.
101
+ //
102
+ curbot.rawStart = closure rawStart(ptySpawned, local dokey, doresize, doattach) {
103
+ dokey = xnew(function(msg, arg) { ptySpawned.write(arg.key) })
104
+ js.t.ioctl_add_listener("raw-key", dokey)
105
+ doresize = xnew(function(msg, dim) { ptySpawned.resize(dim.cols, dim.rows) })
106
+ js.t.ioctl_add_listener("term-resize", doresize)
107
+ js.t.ioctl("raw", true)
108
+ doattach = xnew(function(msg, arg) { js.t.ioctl("raw", true) }) // restore raw mode when we are attached
109
+ js.t.ioctl_add_listener("term-attach", doattach)
110
+
111
+ curbot.rawStop = function rawStop() {
112
+ js.t.ioctl("raw")
113
+ js.t.ioctl_remove_listener("raw-key", dokey)
114
+ js.t.ioctl_remove_listener("term-resize", doresize)
115
+ js.t.ioctl_remove_listener("term-attach", doattach)
116
+ dokey = doresize = doattach = false
117
+ curbot.rawStop = undefined
118
+ }
119
+ }
120
+
121
+ // impReceive
122
+ //
123
+ // Received a payload.
124
+ //
125
+ curbot.impReceive = function impReceive(message, sourceID) {
126
+ // js.g.console.log("CurBot.impReceive(${sourceID}):\n", Dialect.print(new(message)))
127
+ if message.id == "destroy"
128
+ { }
129
+ else if message.id == "subscribe"
130
+ subscribe(sourceID)
131
+ else if message.id == "unsubscribe"
132
+ unsubscribe(sourceID)
133
+ else {
134
+ subscribe(sourceID) // auto-subscribe
135
+ if message.id == "typed"
136
+ notifyAll(message, sourceID)
137
+ else if message.id == "keyline" && curbot.keyin
138
+ curbot.keyin(message.text) // terminal keyboard -> pty subprocess
139
+ else
140
+ curbot.host.DispatchMessage(message)
141
+ }
142
+ }
143
+
144
+ // impUnknown
145
+ //
146
+ // Received an unknown destination notice.
147
+ //
148
+ curbot.impUnknown = function impUnknown(unknownID, sourceID, gateway) {
149
+ if curbot.topsubs.indexOf(unknownID) >= 0 { // did not have courtesy to unsubscribe!
150
+ unsubscribe(unknownID)
151
+ return
152
+ }
153
+ if !gateway
154
+ debuglog("CurBot.impUnknownID:", unknownID, "sourceID:", sourceID)
54
155
  }
55
- current.host.OnReplyHook(replyHook.proc)
56
156
 
57
157
  // ONtextOut
158
+ //
58
159
  // Process console text output by Naan.
59
-
160
+ //
60
161
  function ONtextOut(text) {
61
- sendAll({
162
+ if curbot.naanin {
163
+ curbot.naanin(text) // naan output -> pty subprocess
164
+ return
165
+ }
166
+ notifyAll({
62
167
  id: "textout",
63
168
  text: text,
64
169
  })
65
170
  }
66
-
171
+
67
172
  // ONdebugText
173
+ //
68
174
  // Process console debug text output by Naan.
69
-
175
+ //
70
176
  function ONdebugText(text, level) {
71
- sendAll({
177
+ notifyAll({
72
178
  id: "debugtext",
73
179
  text: text,
74
180
  level: level
75
181
  })
76
182
  }
77
-
183
+
78
184
  // ONstatus
185
+ //
79
186
  // Process status update output by Naan.
80
-
187
+ //
81
188
  function ONstatus(status) {
82
- sendAll({
189
+ notifyAll({
83
190
  id: "status",
84
191
  status: status
85
192
  })
86
193
  }
87
-
194
+
88
195
  // ONdebugOut
196
+ //
89
197
  // Process debugger output.
90
-
198
+ //
91
199
  function ONdebugOut(data) {
92
- sendAll({
200
+ notifyAll({
93
201
  id: "debugout",
94
202
  data: data
95
203
  })
96
204
  }
97
-
98
- // ONmessageOut
99
- // Process message output.
100
-
101
- function ONmessageOut(data) {
102
- if data._guid
103
- return // already replied
104
- current.execReceived(data)
105
- sendAll({
106
- id: "targetout",
107
- data: data
108
- })
109
- }
110
205
 
111
- // removeLink
112
- // Remove a link.
113
-
114
- function removeLink(link) {
115
- current.links = current.links.filter(function(item) {
116
- !(item.link eq link)
117
- })
118
- }
119
-
120
- // exitNotify
121
- // Notify all links that we have terminated.
122
-
123
- function exitNotify(exitCode, local link) {
124
- workco.uworkers[current.guid] = undefined
125
- for link in current.links
126
- link.exitWorker(exitCode)
127
- }
128
-
129
- // sendAll
130
- // Send a message to all links.
131
-
132
- function sendAll(msg, local link) {
133
- for link in current.links
134
- link.responder(msg)
135
- }
136
-
137
- //
138
- // attach
206
+ // ONioctl
139
207
  //
140
- // Attach a link.
208
+ // Process ioctl.
141
209
  //
142
-
143
- current.attach = function attach(link, responder) {
144
- removeLink(link)
145
- current.links.push({
146
- link: link // a link to us
147
- responder: responder // link's responder
210
+ function ONioctl(op, arg) {
211
+ notifyAll({
212
+ id: "ioctl",
213
+ op: op,
214
+ arg: arg
148
215
  })
149
- if current.links.length == 1
150
- current.host.Attach(current.termif) // attached while one link is active
151
- }
152
-
153
- //
154
- // detach
155
- //
156
- // Detach a link.
157
- //
158
-
159
- current.detach = function detach(link) {
160
- removeLink(link)
161
- if current.links.length == 0
162
- current.host.Detach(current.termif) // attached while one link is active
163
- }
164
-
165
- //
166
- // target.PostMessage
167
- //
168
- // Post a message to the main thread.
169
- //
170
- current.PostMessage = function PostMessage(data) {
171
- js.t.DispatchMessage(data)
172
- list(false, { posted: true })
173
- }
174
-
175
- //
176
- // msgin
177
- //
178
- // Send a message to the host interface.
179
-
180
- current.msgin = function msgin(msg) {
181
- current.host.DispatchMessage(msg)
182
216
  }
183
217
 
184
- //
185
- // echo
186
- //
187
- // Echo a message to the *other* links.
188
-
189
- current.echo = function echo(msg, thislink, local linkref) {
190
- for linkref in current.links
191
- if !(linkref.link eq thislink)
192
- linkref.responder(msg)
193
- }
194
-
195
- //
196
- // vitalUpdate
197
- //
198
- // Worker thread birth/death/enumeration.
218
+ curbot.host.Attach(curbot.termif)
199
219
 
200
- current.vitalUpdate = function vitalUpdate(msg) {
201
- sendAll(msg)
202
- }
220
+ curbot.subprocess = MakeSubProcessor(curbot) // Enable PTY subprocesses on our terminal
203
221
 
204
222
  // finis
205
- current
223
+
224
+ curbot
206
225
  };
207
226
 
208
227
 
209
228
  /*
210
- * WorkerConnect
229
+ * NowBot
211
230
  *
212
- * Return the existing web worker for a workerGUID, or false if it does not exist.
213
- *
214
- */
215
-
216
- function WorkerConnect(workerGUID) {
217
- gWorkers[workerGUID]
218
- }
219
-
220
-
221
- /*
222
- * workMainProxy
231
+ * Spawn a Naan worker in its own thread.
223
232
  *
224
- * Make a nodejs proxy representing the main thread. This runs inside the worker and allows it
225
- * to do remote execution with Main.
233
+ * Options:
234
+ * {
235
+ * name: <string> // target name, e.g. "Play"
236
+ * type: <string> // target category, e.g. "Local"
237
+ * startup: <dictionary> // startup options
238
+ * topOriginID: <string> // so top can route to us through IMP
239
+ * rootPath: <string> // root path of worker's folder, must have lib/node_worker_init.nlg
240
+ * node: {
241
+ * // see NodeJS docs
242
+ * workerData: { // passed to node_worker.js
243
+ * }
244
+ * }
245
+ * }
226
246
  *
227
247
  */
228
248
 
229
- closure workMainProxy(track, name, options, local target) {
230
- global(js)
231
- target = require("../running/executors.nlg").ExecutorBase(track, "mainProxy", name)
232
- target.name = name
249
+ closure NowBot(options, local nowbot, nodeops, subChannel, ptySpawned) {
250
+ global(App, js, threads, JSpath)
251
+ nowbot = new(object, this)
252
+ nowbot.topsubs = []
253
+ options = merge({
254
+ type: "node-worker"
255
+ }, options)
256
+
257
+ nodeops = merge({
258
+ // execArgv: ["--inspect-brk"] // unfortunately doesn't work; use VS Code instead
259
+ }, options.node)
260
+ if options.rootPath
261
+ nodeops.workerData = merge({
262
+ rootPath: options.rootPath
263
+ }, nodeops.workerData)
264
+ nowbot.thread = xnew(threads.Worker, JSpath.resolve(js.d, "lib/node_worker.js"), nodeops)
265
+ subChannel = xnew(threads.MessageChannel)
266
+ nowbot.thread.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1])
267
+ nowbot.msgport = subChannel.port2
268
+
269
+ nowbot.instanceID = UUID()
270
+ nowbot.sender = App.imp.register(nowbot, {
271
+ name: options.name
272
+ type: options.type
273
+ instanceID: nowbot.instanceID
274
+ services: ["node-worker"]
275
+ })
233
276
 
277
+ // If this NowBot is created remotely then we need to send a message from our new instanceID
278
+ // to the InTop origin so that it can send messages to us. The intermediate gateways save the
279
+ // route to allow replies.
280
+ if options.topOriginID && options.topOriginID != App.imp.us.instanceID
281
+ nowbot.sender(options.topOriginID) // create route top->bottom
282
+
283
+ // subscribe
284
+ //
285
+ // Add an InTop to our subscribers if not already known.
286
+ //
287
+ function subscribe(destID) {
288
+ if !nowbot.topsubs.find(function(item) { item == destID }) {
289
+ nowbot.topsubs.push(destID)
290
+ if nowbot.workerDestID {
291
+ App.imp.inform(nowbot.workerDestID, destID) // provide path to workerID
292
+ nowbot.sender(destID, { // send to new subscriber
293
+ id: "workerDestID"
294
+ workerDestID: nowbot.workerDestID
295
+ })
296
+ }
297
+ }
298
+ }
299
+
300
+ // unsubscribe
234
301
  //
235
- // target.PostMessage
236
- //
237
- // Post a message to the main thread.
302
+ // Remove an InTop from our subscribers if present.
238
303
  //
239
- target.PostMessage = function PostMessage(data) {
240
- js.t.DispatchMessage(data)
241
- list(false, { posted: true })
304
+ function unsubscribe(destID) {
305
+ nowbot.topsubs = nowbot.topsubs.filter(function(item) { item != destID })
242
306
  }
243
307
 
308
+ // notifyAll
244
309
  //
245
- // onreceive
246
- //
247
- // Process a received message from the main thread, typically in response.
310
+ // Send the message to all our subscribers.
248
311
  //
249
- function onreceive(data) {
250
- target.execReceived(data)
312
+ nowbot.notifyAll = function notifyAll(message, exceptID, local topDestID) {
313
+ for topDestID in nowbot.topsubs
314
+ if topDestID != exceptID
315
+ nowbot.sender(topDestID, message)
251
316
  }
252
-
253
- // destroy
317
+
318
+ // textEntry
254
319
  //
255
- // Destroy the execution instance.
320
+ // Simulate text entry for subprocess support.
256
321
  //
257
- target.destroy = function destroy() {
258
- target.execClosed(Error("instance destroyed"))
259
- track.delete(target)
322
+ nowbot.textEntry = function textEntry(text) {
323
+ nowbot.msgport.postMessage({
324
+ id: "keyline"
325
+ text: text
326
+ })
260
327
  }
261
328
 
262
- js.t.OnReceive(onreceive.proc)
263
- track.add(target)
264
-
265
- // finis
266
-
267
- list(false, target)
268
- };
269
-
270
-
271
- /*
272
- * WorkerToMain
273
- *
274
- * Connect our worker to the main thread, returning a result tuple with an executor representing
275
- * Main. Use this to create a context object for Main, allowing remote evaluation.
276
- *
277
- */
278
-
279
- closure WorkerToMain(local nideRunning, track) {
280
- // mainProxy -- link tracker to main thread proxy
281
- function mainProxy(track, name, options) {
282
- workMainProxy(track, name, options)
329
+ // ioctl
330
+ //
331
+ // Send an ioctl to the terminal.
332
+ //
333
+ nowbot.ioctl = function ioctl(op, arg) {
334
+ notifyAll({
335
+ id: "ioctl"
336
+ op: op
337
+ arg: arg
338
+ })
283
339
  }
284
340
 
285
- if !js.g {
286
- error = Error("WorkerToMain only available in node workers")
287
- return (list(error))
341
+ // rawMode
342
+ //
343
+ // Configure ioctls for raw terminal operation using the spawned PTY process.
344
+ //
345
+ nowbot.rawStart = function rawStart(ptys) {
346
+ ptySpawned = ptys
347
+ ioctl("raw", true)
348
+ nowbot.rawStop = function rawStop() {
349
+ ioctl("raw", false)
350
+ nowbot.rawStop = undefined
351
+ ptySpawned = false
352
+ }
288
353
  }
289
- nideRunning = require("naanlib:frameworks/running/executors.nlg")
290
- track = nideRunning.ExecutorTracker()
291
- track.register(mainProxy, "proxy")
292
- track.spawn("proxy") // returns result tuple
293
- };
294
354
 
295
-
296
- /*
297
- * workThread
298
- *
299
- * Spawn a Naan worker in its own thread.
300
- *
301
- */
302
-
303
- closure workThread(workco, workerID, startup, guid
304
- local worker, options, subChannel, deathWatcher) {
305
- global(js, threads, JSpath)
306
- worker = require("../running/executors.nlg").ExecutorBase(workco, workerID, startup.name)
307
- worker.name = startup.name
308
- worker.workerID = workerID
309
- worker.links = []
310
- worker.guid = guid || UUID()
311
-
312
- // create our worker thread
313
-
314
- // options = { execArgv: ["--inspect-brk"] } // uncomment for NodeJS debugging
315
- worker.thread = xnew(threads.Worker, JSpath.resolve(js.d, "lib/node_worker.js"), options)
316
- subChannel = xnew(threads.MessageChannel)
317
- worker.thread.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1])
318
- worker.msgport = subChannel.port2
319
-
320
- // removeLink
321
- // Remove a link.
322
-
323
- function removeLink(link) {
324
- worker.links = worker.links.filter(function(item) {
325
- !(item.link eq link)
326
- })
327
- }
328
-
329
- // exitNotify
330
- // Notify all links that we have terminated.
331
-
332
- function exitNotify(exitCode, local link) {
333
- workco.uworkers[worker.guid] = undefined
334
- for link in worker.links
335
- link.exitWorker(exitCode)
336
- worker.links = []
337
- deathWatcher(workerID)
355
+ // impReceive
356
+ //
357
+ // Received a payload.
358
+ //
359
+ nowbot.impReceive = function impReceive(message, sourceID) {
360
+ if message.id == "destroy"
361
+ terminate()
362
+ else if message.id == "subscribe"
363
+ subscribe(sourceID)
364
+ else if message.id == "unsubscribe"
365
+ unsubscribe(sourceID)
366
+ else {
367
+ subscribe(sourceID) // auto-subscribe
368
+ if message.id == "typed" {
369
+ notifyAll(message, sourceID)
370
+ return
371
+ }
372
+ if ptySpawned { // raw mode: kludgey, but works
373
+ if message.id == "ioctl-event" {
374
+ if message.msg == "raw-key"
375
+ ptySpawned.write(message.arg.key)
376
+ else if message.msg == "term-resize"
377
+ ptySpawned.resize(message.arg.cols, message.arg.rows)
378
+ }
379
+ if (message.id == "attached")
380
+ ioctl("raw", true) // restore raw mode after terminal attach
381
+ }
382
+ if message.id == "keyline" && nowbot.keyin
383
+ nowbot.keyin(message.text) // terminal keyboard -> pty subprocess
384
+ else
385
+ nowbot.msgport.postMessage(message)
386
+ }
338
387
  }
339
-
340
- // sendAll
341
- // Send a message to all links.
342
-
343
- function sendAll(msg, local link) {
344
- for link in worker.links
345
- link.responder(msg)
388
+
389
+ // impUnknown
390
+ //
391
+ // Received an unknown destination notice.
392
+ //
393
+ nowbot.impUnknown = function impUnknown(unknownID, sourceID, gateway) {
394
+ if nowbot.topsubs.indexOf(unknownID) >= 0 { // did not have courtesy to unsubscribe!
395
+ unsubscribe(unknownID)
396
+ return
397
+ }
398
+ if !gateway
399
+ debuglog("NowBot.impUnknownID:", unknownID, "sourceID:", sourceID)
346
400
  }
347
401
 
402
+ // nowbot
348
403
  //
349
- // onerror
350
- //
351
- // An exception occurred on the worker thread. Since Naan catches all exceptions this should
404
+ // An exception occurred on the worker thread. Since Naan catches all exceptions this should
352
405
  // not occur normally.
353
-
354
- worker.thread.on("error", function onerror(error) {
355
- debuglog("worker terminated with error", workerID, error)
356
- exitNotify(-1)
357
- })
358
-
359
406
  //
407
+ nowbot.thread.on("error", function onerror(error) {
408
+ debuglog("worker terminated with error", options.type, options.name, error)
409
+ notifyAll({
410
+ id: "terminated"
411
+ error: error
412
+ })
413
+ nowbot.dispatchEvent("terminated", nowbot.instanceID, { error: error })
414
+ nowbot.thread = false
415
+ destroy()
416
+ })
417
+
360
418
  // onexit
361
419
  //
362
- // Note that the worker thread has terminated.
363
-
364
- worker.thread.on("exit", function onexit(exitCode) {
365
- exitNotify(exitCode)
420
+ // Note that the worker thread has terminated.
421
+ //
422
+ nowbot.thread.on("exit", function onexit(exitCode) {
423
+ notifyAll({
424
+ id: "terminated"
425
+ exitCode: exitCode
426
+ })
427
+ nowbot.dispatchEvent("terminated", nowbot.instanceID, { exitCode: exitCode })
428
+ nowbot.thread = false
429
+ destroy()
366
430
  })
367
431
 
432
+ // destroy
368
433
  //
369
- // naanmsg
434
+ // Ensure the worker is destroyed.
370
435
  //
371
- // A message has been received from Naan running on the worker thread.
372
-
373
- worker.msgport.on("message", function naanmsg(msg, local startmsg) {
374
- if msg.id == "loaded" {
436
+ nowbot.destroy = function destroy() {
437
+ terminate()
438
+ nowbot.thread = false
439
+ nowbot.impconn.destroy()
440
+ nowbot.impconn = false
441
+ App.imp.unregister(nowbot.instanceID)
442
+ }
443
+
444
+ // NaaN message
445
+ //
446
+ // A message has been received from NaaN running on the worker thread.
447
+ //
448
+ nowbot.msgport.on("message", function (msg, local startmsg) {
449
+ if msg.id == "naan-imp-node" {
450
+ nowbot.impconn.dispatch(new(msg.data))
451
+ return
452
+ } else if msg.id == "loaded" {
375
453
  startmsg = {
376
454
  id: "start"
377
455
  state: false // resume state, if we have any
378
- altcmd: false
456
+ altcmd: options.startup.initcmds
379
457
  dirpath: js.d
380
458
  workerID: workerID
381
- workerGUID: worker.guid
459
+ metadata: {
460
+ mainInstanceID: App.imp.us.instanceID
461
+ name: options.name
462
+ maintext: options.startup.maintext.concat("\n")
463
+ }
382
464
  }
383
- if startup.initcmds
384
- startmsg.altcmd = startup.initcmds.concat("\n")
385
- if startup.dirpath
386
- startmsg.dirpath = startup.dirpath
387
- worker.msgport.postMessage(startmsg)
388
- worker.links[0].responder({ // only send this to first link
389
- id: "started" // don't want multiple initializers
390
- }) }
391
- else if msg.id == "targetsend" {
392
- msg.data._guid = worker.guid
393
- js.t.DispatchMessage({
394
- id: "targetin"
395
- data: msg.data
396
- }) }
397
- else {
398
- if msg.id == "status"
399
- worker.lastStatus = milliseconds()
400
- else if msg.id == "targetout"
401
- worker.execReceived(msg.data)
402
- sendAll(msg) } // default is just return message to the client
465
+ if options.startup.dirpath
466
+ startmsg.dirpath = options.startup.dirpath
467
+ nowbot.msgport.postMessage(startmsg)
468
+ notifyAll({
469
+ id: "started"
470
+ })
471
+ nowbot.dispatchEvent("started", nowbot.instanceID)
472
+ } else if msg.id == "textout" && nowbot.naanin
473
+ nowbot.naanin(msg.text) // naan output -> pty subprocess
474
+ else
475
+ notifyAll(msg)
403
476
  })
404
-
405
- //
406
- // terminate
407
- //
408
- // Terminate this worker asap.
409
-
410
- worker.terminate = function terminate(local error, exitCode) {
411
- `(error, exitCode) = await(worker.thread.terminate())
412
- exitNotify(exitCode)
413
- }
414
477
 
478
+ // terminate
415
479
  //
416
- // attach
417
- //
418
- // Attach a connection to our worker.
419
- //
420
-
421
- worker.attach = function attach(link, responder) {
422
- removeLink(link)
423
- worker.links.push({
424
- link: link // a link to us
425
- responder: responder // link's responder
426
- })
427
- }
428
-
429
- //
430
- // detach
431
- //
432
- // Detach a link from our worker.
480
+ // Terminate this worker asap.
433
481
  //
434
-
435
- worker.detach = function detach(link) {
436
- removeLink(link)
482
+ nowbot.terminate = function terminate(local error, exitCode) {
483
+ if nowbot.thread
484
+ `(error, exitCode) = await(nowbot.thread.terminate()) // notified by on-error and on-exit
485
+ else
486
+ list(false, { terminated: true }) // already terminated
437
487
  }
438
488
 
439
- //
440
489
  // PostMessage
441
490
  //
442
- // Post a message to the main thread.
491
+ // Post a message to the worker thread.
443
492
  //
444
- worker.PostMessage = function PostMessage(data) {
445
- worker.msgport.postMessage({ // send data to worker
493
+ nowbot.PostMessage = function PostMessage(data) {
494
+ nowbot.msgport.postMessage({ // send data to worker
446
495
  id: "targetin",
447
496
  data: data
448
497
  })
449
498
  list(false, { posted: true })
450
499
  }
451
500
 
452
- //
453
- // msgin
454
- //
455
- // Send a message to the worker thread.
456
-
457
- worker.msgin = function msgin(msg) {
458
- worker.msgport.postMessage(msg)
459
- }
460
-
461
- //
462
- // echo
463
- //
464
- // Echo a message to the *other* links.
465
-
466
- worker.echo = function echo(msg, thislink, local linkref) {
467
- for linkref in worker.links
468
- if !(linkref.link eq thislink)
469
- linkref.responder(msg)
470
- }
471
-
472
- //
473
- // vitalWatch
474
- //
475
- // Report our termination to the specified watcher.
476
-
477
- worker.vitalWatch = function vitalWatch(watcher) {
478
- deathWatcher = watcher
479
- }
501
+ nowbot.subprocess = MakeSubProcessor(nowbot) // Enable PTY subprocesses on our terminal
502
+ Events(nowbot)
480
503
 
481
504
  // finis
482
- worker
505
+
506
+ nowbot
483
507
  };
484
508
 
485
509
 
486
510
  /*
487
- * workLink
511
+ * NobBot
488
512
  *
489
- * An object representing a specific connection and worker pair. Both workers and connections may
490
- * have multiple links. This allows a worker to communicate with multiple connections and allows
491
- * clients to create a new connection to talk with an existing worker.
513
+ * Create a remote interface like NowBot, but also provide messaging support for NodeWorkerNub inside
514
+ * the worker.
492
515
  *
493
516
  */
494
517
 
495
- closure workLink(workco, conn, worker, sender, local link) {
496
- global()
497
- link = new(object, this)
518
+ closure NobBot(options, local nobbot)
519
+ {
520
+ global(App)
521
+ nobbot = NowBot(options)
498
522
 
523
+ // informAll
499
524
  //
500
- // closeConn
501
- //
502
- // Note that the connection has closed.
503
- //
504
-
505
- link.closeConn = function closeConn() {
506
- worker.detach(link) // stop receiving updates from worker
507
- }
508
-
509
- //
510
- // exitWorker
511
- //
512
- // Note that the worker has exited.
525
+ // Inform sourceID path to all our subscribers.
513
526
  //
514
-
515
- link.exitWorker = function exitWorker(exitCode) {
516
- sender("terminated", [false, {
517
- workerID: worker.workerID // notify client they've lost a worker
518
- exitCode: exitCode
519
- }])
520
- workco.closeLink(link, conn, worker.workerID)
521
- worker.detach(link)
527
+ function informAll(sourceID, local topDestID) {
528
+ for topDestID in nobbot.topsubs
529
+ App.imp.inform(sourceID, topDestID)
522
530
  }
523
531
 
524
- // initialize
532
+ App.imp.addEventListener("active", function(event, info) {
533
+ if info.ourID == nobbot.instanceID {
534
+ nobbot.workerDestID = info.destID // handshake completed
535
+ informAll(nobbot.workerDestID) // provide path to workerID
536
+ nobbot.notifyAll({ // send to subscribers
537
+ id: "workerDestID"
538
+ workerDestID: nobbot.workerDestID
539
+ })
540
+ }
541
+ })
525
542
 
526
- worker.attach(link, function msgout(data) {
527
- sender("msgout", data)
543
+ nobbot.impconn = App.imp.responder(function(data) {
544
+ nobbot.msgport.postMessage({ // send to worker
545
+ id: "naan-imp-node" // scoped for NodeWorkerNub
546
+ data: data
547
+ })
548
+ list(false, { sent: true })
549
+ }, {
550
+ ourID: nobbot.instanceID
528
551
  })
529
- sender("linked", [false, {
530
- workerID: worker.workerID // notify client they've gained a worker
531
- }])
532
552
 
533
553
  // finis
534
554
 
535
- link
555
+ nobbot
536
556
  };
537
557
 
538
558
 
539
559
  /*
540
- * workController
560
+ * MakeSubProcessor
541
561
  *
542
- * Controller for workers. This knows about all the workers and links. If a connection dies then
543
- * this tears down all links from the connection. If a worker dies then this tears down all links to
544
- * that worker.
562
+ * Method PTY subprocess capability to CurBot and NowBot. They also need these:
563
+ *
564
+ * function impReceive(message, sourceID) { ...
565
+ * ...
566
+ * if message.id == "keyline" && nowbot.keyin
567
+ * execbot.keyin(message.text) // terminal keyboard -> pty subprocess
568
+ *
569
+ * function naanmsg(msg, local startmsg) { ...
570
+ * ...
571
+ * else if msg.id == "textout" && nowbot.naanin
572
+ * execbot.naanin(msg.text) // naan output -> pty subprocess
573
+ *
574
+ * subprocess
575
+ *
576
+ * Create a PTY subprocess and connect with the worker and terminal. You specify the path of
577
+ * the executable and the arguments. If no executable is specified then a shell is created. The
578
+ * return value is a result tuple with an object that can be used to kill or monitor the
579
+ * subprocess.
580
+ *
581
+ * exec.subprocess(executable, args, options)
582
+ *
583
+ * The subprocess can be connected to the terminal raw, or in a variety of ways:
584
+ *
585
+ * term-keyboard | term-display | worker-in | worker-out
586
+ * -------------------------------------|---------------|------------|-----------
587
+ * subprocess input keyboard | | | pipe
588
+ * subprocess output | display | tee | pipe | tee |
589
+ *
590
+ * Options:
591
+ * {
592
+ * input: false | "keyboard" | "pipe"
593
+ * output: false | "display" | "pipe" | "tee"
594
+ * raw: <boolean>
595
+ * spawn: <PtyManager.spawn options>
596
+ * }
545
597
  *
546
598
  */
547
599
 
548
- closure workController(api, local workco) {
549
- global(gWorkers)
550
- workco = new(object, this)
551
- workco.serverID = "Workers" // our serverID, used for communications
552
- workco.workers = { } // dictionary of workers by workerID
553
- workco.uworkers = { } // dictionary of workers by GUID
554
- workco.links = new(weakmap)
555
- api.Register(workco.serverID, workco)
556
-
557
- // addLinkForConnection
558
- // Add a link for a connection under its workerID.
559
-
560
- function addLinkForConnection(conn, workerID, link, local iddict) {
561
- iddict = workco.links[conn]
562
- if !iddict
563
- workco.links[conn] = iddict = { }
564
- iddict[workerID] = link
565
- }
566
-
567
- // deleteLinkForConnection
568
- // Remove a link for a connection under its workerID, or false.
569
-
570
- function deleteLinkForConnection(conn, workerID) {
571
- if workco.links[conn][workerID]
572
- workco.links[conn][workerID] = undefined
573
- }
574
-
575
- // makeSender
576
- // Sender factory for specified connection and message.
577
-
578
- closure makeSender(conn, message) {
579
- function sender(respOp, payload) {
580
- conn.send({
581
- clientID: message.clientID
582
- serverID: message.serverID
583
- workerID: message.workerID
584
- respOp: respOp
585
- payload: payload
586
- })
600
+ closure MakeSubProcessor(execbot) {
601
+ global()
602
+ closure subprocess(
603
+ executable, args, options, local error, pty) {
604
+
605
+ // ptyData - text has been returned from the subprocess
606
+ function ptyData(text) {
607
+ if options.output == "display" || options.output == "tee"
608
+ execbot.notifyAll({ // send to display
609
+ id: "textout",
610
+ text: text
611
+ })
612
+ if options.output == "pipe" || options.output == "tee"
613
+ execbot.textEntry(text) // send to NaaN worker
587
614
  }
588
- }
589
-
590
- // makeLink
591
- // Link the specified message, connection and worker.
592
-
593
- function makeLink(message, conn, local worker, link) {
594
- worker = workco.workers[message.workerID]
595
- link = workLink(workco, conn, worker, makeSender(conn, message))
596
- addLinkForConnection(conn, message.workerID, link)
597
- }
598
-
599
- // termination
600
- // Note that a worker has terminated.
601
-
602
- function termination(workerID) {
603
- workco.current.vitalUpdate({
604
- id: "termination", // note a thread has gone away
605
- workerID: workerID
606
- })
607
- workco.workers[workerID] = undefined // it's like this worker has never been
608
- }
609
615
 
610
- // spawn
611
- //
612
- // Spawn a new worker with the specified configuration dictionary and optional GUID.
613
- //
614
- workco.spawn = closure spawn(workerID, config, guid, local worker) {
615
- if !dictionary(gWorkers)
616
- gWorkers = {}
617
- worker = workco.workers[workerID]
618
- if !worker { // create worker if needed
619
- if workerID == "NideServer"
620
- worker = workco.current = workCurrent(workco, workerID, config)
621
- else
622
- worker = workThread(workco, workerID, config, guid)
623
- workco.uworkers[worker.guid] = worker
624
- workco.workers[workerID] = worker
625
- gWorkers[worker.guid] = worker }
626
- if workerID == "NideServer"
627
- workco.current.vitalUpdate({
628
- id: "workerlist", // send the initial list of workers
629
- workers: let(workers, workerID, worker) {
630
- workers = { }
631
- for `(workerID, worker) in workco.workers
632
- workers[workerID] = {
633
- name: worker.name
634
- }
635
- workers
636
- }()
637
- })
638
- else {
639
- worker.vitalWatch(termination)
640
- workco.current.vitalUpdate({
641
- id: "spawned", // note a new worker thread
642
- name: config.name
643
- workerID: workerID
644
- }) }
645
- list(false, worker)
646
- }
616
+ // ptyKilled - the subprocess has terminated
617
+ function ptyKilled(event) {
618
+ if execbot.keyin === keyin
619
+ execbot.keyin = false
620
+ if execbot.naanin === naanin
621
+ execbot.naanin = false
622
+ if options.raw {
623
+ execbot.rawStop()
624
+ execbot.textEntry("\x15mu();;\n") // send to NaaN worker
625
+ }
626
+ }
647
627
 
648
- // wsReceive
649
- //
650
- // Process a message from the websocket API.
651
- //
652
- workco.wsReceive = function wsReceive(message, conn, local worker, link, error) {
653
- if !message.workerID
654
- error = Error("workerID required", message.workerOp)
655
- else
656
- {
657
- worker = workco.workers[message.workerID]
658
- link = workco.links[conn][message.workerID]
659
- if message.workerOp == "msgin" {
660
- if worker && message.payload {
661
- if !link
662
- makeLink(message, conn) // make sure we have a link for responses
663
- worker.msgin(message.payload) } // send message to worker
664
- return }
665
- else if message.workerOp == "echo" {
666
- if worker && message.payload {
667
- if !link
668
- makeLink(message, conn) // make sure we have a link for responses
669
- worker.echo(message.payload, link) } // echo message to other links
670
- return }
671
- else if message.workerOp == "spawn" {
672
- spawn(message.workerID, message.payload, message.workerGUID)
673
- if !link
674
- makeLink(message, conn) // make sure we have a link for responses
675
- return }
676
- else if message.workerOp == "terminate" {
677
- if worker {
678
- if !link
679
- makeLink(message, conn) // make sure we have a link for responses
680
- worker.terminate() } // echo message to other links
681
- return }
682
- error = Error("invalid parameters", message.workerOp, message.workerID)
628
+ // keyin - the terminal has generated text for the subprocess
629
+ function keyin(text) {
630
+ pty.write(text)
683
631
  }
684
- ErrorDebuglog("workController.wsReceive error:", error)
685
- }
686
-
687
- //
688
- // wsClose
689
- //
690
- // Note that the connection with the client was closed.
691
- //
692
-
693
- workco.wsClose = function wsClose(conn, local workerID, link) {
694
- for workerID in workco.workers {
695
- link = workco.links[conn][workerID]
696
- if link
697
- link.closeConn()
698
- deleteLinkForConnection(conn, workerID)
632
+
633
+ // naanin - NaaN worker has generated text for the subprocess
634
+ function naanin(text) {
635
+ pty.write(text)
699
636
  }
700
- }
701
-
702
- //
703
- // closeLink
704
- //
705
- // Note that a link with the worker has closed.
706
- //
707
637
 
708
- workco.closeLink = function closeLink(link, conn, workerID) {
709
- deleteLinkForConnection(conn, workerID)
710
- }
638
+ require("./pty.nlg") // load on demand
639
+ `(error, pty) = PtyManager().spawn(executable, args, {
640
+ ptyData: ptyData
641
+ ptyKilled: ptyKilled
642
+ }, options.spawn)
643
+ if error
644
+ return (list(error))
645
+ if options.input == "keyboard"
646
+ execbot.keyin = keyin // replace existing
647
+ else if options.input == "pipe"
648
+ execbot.naanin = naanin // replace existing
649
+ if options.raw
650
+ execbot.rawStart(pty)
651
+
652
+ // our worker has terminated
653
+ execbot.addEventListener("terminated", function() {
654
+ pty.kill()
655
+ })
711
656
 
712
- // finis
713
- workco
657
+ list(false, pty)
658
+ }
714
659
  };
715
660
 
716
661
 
@@ -722,13 +667,12 @@ closure workController(api, local workco) {
722
667
  */
723
668
 
724
669
  function workInit(local manifest) {
725
- manifest = `(workCurrent, WorkerConnect, workMainProxy, WorkerToMain, workThread, workLink,
726
- workController, workInit)
670
+ manifest = `(CurBot, NowBot, NobBot, MakeSubProcessor, workInit)
727
671
  Naan.module.build(module.id, "worker", function(modobj, compobj) {
728
672
  require("./node.nlg")
729
673
  compobj.manifest = manifest
730
- modobj.exports.WorkerController = workController
731
- modobj.exports.WorkerConnect = WorkerConnect
732
- modobj.exports.WorkerToMain = WorkerToMain
674
+ modobj.exports.CurBot = CurBot
675
+ modobj.exports.NowBot = NowBot
676
+ modobj.exports.NobBot = NobBot
733
677
  })
734
678
  }();