@naanlang/naan 1.6.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 (50) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/index.js +2 -2
  4. package/dist/env_web.js +3 -31
  5. package/dist/naan.min.js +10 -10
  6. package/frameworks/browser/sworker.js +23 -23
  7. package/frameworks/browser/terminals.nlg +44 -122
  8. package/frameworks/browser/worker.nlg +64 -24
  9. package/frameworks/browser/workers.nlg +1 -1
  10. package/frameworks/client/apiclient.nlg +73 -43
  11. package/frameworks/client/psm_client.nlg +10 -14
  12. package/frameworks/common/common.nlg +2 -0
  13. package/frameworks/node/apiserver.nlg +0 -1
  14. package/frameworks/node/node.nlg +1 -0
  15. package/frameworks/node/pty.nlg +21 -21
  16. package/frameworks/node/shell.nlg +26 -3
  17. package/frameworks/node/worker.nlg +153 -79
  18. package/frameworks/project/build.nlg +55 -13
  19. package/frameworks/project/projects.nlg +1 -1
  20. package/frameworks/running/debugcom.nlg +20 -3
  21. package/frameworks/running/debugnub.nlg +16 -7
  22. package/frameworks/running/executors.nlg +14 -12
  23. package/frameworks/running/instances.nlg +1 -0
  24. package/frameworks/running/remote_req.nlg +4 -8
  25. package/frameworks/running/remote_res.nlg +1 -1
  26. package/frameworks/running/sourcecode.nlg +3 -2
  27. package/frameworks/running/taskexec.nlg +14 -57
  28. package/frameworks/service/imp.nlg +15 -0
  29. package/frameworks/service/nubnode.nlg +35 -4
  30. package/frameworks/service/nubweb.nlg +22 -4
  31. package/frameworks/storage/file_manager.nlg +3 -2
  32. package/lib/browser/env_web.js +10 -38
  33. package/lib/browser/env_webworker.js +1 -30
  34. package/lib/core/naanlib.js +10 -10
  35. package/lib/env_node.js +5 -22
  36. package/lib/env_nodeworker.js +8 -43
  37. package/lib/node_server_init.nlg +149 -0
  38. package/lib/node_worker.js +36 -0
  39. package/lib/node_worker_init.nlg +43 -0
  40. package/package.json +1 -1
  41. package/plugins/openSearch/openSearch.nlg +3 -2
  42. package/plugins/openSearch/os_dynamo.nlg +3 -2
  43. package/plugins/serviceAws/aws_cloudwatchlogs.nlg +44 -2
  44. package/plugins/serviceAws/aws_dynamo.nlg +144 -63
  45. package/plugins/serviceAws/aws_utils.nlg +6 -11
  46. package/plugins/serviceAws/serviceAws.nlg +3 -2
  47. package/test/test_01_core.nlg +2 -2
  48. package/test/test_02_context.nlg +8 -8
  49. package/test/test_03_jsinterop.nlg +3 -3
  50. package/test/test_07_lingo.nlg +1 -1
@@ -19,7 +19,7 @@ serviceMod;;
19
19
  /*
20
20
  * APIClient
21
21
  *
22
- * Make an API client object.
22
+ * Make an API client object. If you want websockets, and you do, call connect() on the object.
23
23
  *
24
24
  * Options:
25
25
  * {
@@ -46,18 +46,20 @@ closure APIClient(imp, options, local client, url, guid) {
46
46
  client.requester = clirRequester()
47
47
 
48
48
  //
49
- // wsopen
49
+ // connect
50
50
  //
51
51
 
52
- client.wsopen = function wsopen(error) {
53
- if !client.wesc {
52
+ client.connect = function connect(error) {
53
+ if !client.wesc
54
54
  client.wesc = WebSocketClient(options, imp)
55
- `(error) = client.wesc.connect()
56
- if error
57
- ErrorDebuglog("APIClient: cannot connect to", url)
55
+ `(error) = client.wesc.connect()
56
+ if error {
57
+ ErrorDebuglog("APIClient: cannot connect to", url, error)
58
+ return (list(error))
58
59
  }
60
+ client.serverID = client.wesc.serverID // serverID
61
+ list(false, { connected: true })
59
62
  }
60
- wsopen()
61
63
 
62
64
  // clientRequest() - append our guid to the headers
63
65
 
@@ -178,10 +180,12 @@ closure APIClient(imp, options, local client, url, guid) {
178
180
  // Close down the client and release all resources.
179
181
  //
180
182
  client.destroy = closure destroy() {
181
- debuglog("client destroy websocket")
183
+ debuglog("API client destroyed") // ### not pretty, but uncommon
182
184
  client.wesc.destroy()
183
185
  client.wesc = false
184
186
  }
187
+
188
+ // finis
185
189
 
186
190
  client
187
191
  };
@@ -197,25 +201,38 @@ closure APIClient(imp, options, local client, url, guid) {
197
201
  * {
198
202
  * url: <string> // hostname or URL of the remote server
199
203
  * guid: <string> // shared secret for access to remote server
200
- * instanceID: <string> // UUID for our client; can be persisted by caller
201
204
  * }
202
205
  *
203
206
  */
204
207
 
205
- closure WebSocketClient(options, imp, local wesc, ws) {
208
+ closure WebSocketClient(options, imp, local wesc, ws, pending) {
206
209
  global(js)
207
210
  wesc = new(object, this)
208
211
 
212
+ // reconnect
213
+ //
214
+ // Attempt to reconnect if needed, returning false on success or an error tuple otherwise.
215
+ //
216
+ function reconnect() {
217
+ if !ws || wesc.fatal || pending {
218
+ connect() // try to connect
219
+ if wesc.fatal
220
+ return (list(wesc.fatal))
221
+ }
222
+ false
223
+ }
224
+
209
225
  // connect
210
226
  //
211
227
  // Establish a websocket connection with the host.
212
228
  //
213
- wesc.connect = closure connect(local url, pending) {
214
- if ws && !wesc.fatal
215
- return(list(false, { connected: true }))
216
- if wesc.fatal
217
- debuglog("WebSocketClient reopen")
218
- wesc.fatal = false
229
+ wesc.connect = closure connect(local url, result) {
230
+ if ws {
231
+ if pending
232
+ return (pending.wait()) // connect in process
233
+ if !wesc.fatal
234
+ return(list(false, { connected: true })) // already open
235
+ }
219
236
  try {
220
237
  url = xnew(js.w.URL, options.url)
221
238
  } catch {
@@ -226,27 +243,35 @@ closure WebSocketClient(options, imp, local wesc, ws) {
226
243
  else
227
244
  url.protocol = "ws:"
228
245
  ws = xnew(js.w.WebSocket, url.href)
246
+ if wesc.fatal
247
+ debuglog("WebSocketClient reopen") // ### not pretty to see this
248
+ wesc.fatal = false
229
249
  pending = new(nonce)
230
-
250
+
231
251
  // onopen
232
252
  // The WebSocket connection is now open.
233
- ws.onopen = function onopen(e, local error) {
253
+ ws.onopen = function onopen(e) {
254
+ if wesc.impconn {
255
+ result = wesc.impconn.reopen()
256
+ pending.signal(result)
257
+ return
258
+ }
234
259
  wesc.impconn = imp.requester(function(data) {
235
- if wesc.fatal // raw send function
236
- return (list(wesc.fatal))
260
+ if !pending && reconnect() // !pending means not opening/reopening
261
+ return // failed to reconnect
237
262
  ws.send(js.w.JSON.stringify(data))
238
263
  list(false, { sent: true })
239
264
  }, {
240
265
  secret: options.guid
241
266
  label: "api-server"
242
- wesc: wesc
243
267
  })
244
- if error
245
- pending.signal(list(error))
246
- else
268
+ wesc.impconn.addEventListener("active", function (event, info) {
269
+ wesc.serverID = info.destID // serverID we connected to
247
270
  pending.signal(list(false, { ok: true }))
271
+ pending = false
272
+ })
248
273
  }
249
-
274
+
250
275
  // onmessage
251
276
  // A message was received via WebSockets.
252
277
  ws.onmessage = function onmessage(e, local error, message) {
@@ -256,9 +281,11 @@ closure WebSocketClient(options, imp, local wesc, ws) {
256
281
  if wesc.impconn.dispatch(message)
257
282
  return
258
283
  }
259
- debuglog("socket message:", e.data)
284
+ pending.signal(list(Error("incompatible responder")))
285
+ pending = false
286
+ debuglog("socket message:", e.data) // unexpected incoming data
260
287
  }
261
-
288
+
262
289
  // onerror
263
290
  // An error occurred on the WebSocket connection.
264
291
  // This can happen in normal operation when the connection times out.
@@ -266,35 +293,34 @@ closure WebSocketClient(options, imp, local wesc, ws) {
266
293
  if !wesc.fatal
267
294
  wesc.fatal = Error("websocket failed", e)
268
295
  pending.signal(list(wesc.fatal))
296
+ pending = false
269
297
  }
270
298
 
271
299
  // onclose
272
300
  // The WebSocket connection was closed.
273
301
  ws.onclose = function onclose(e) {
274
302
  // if e.code != 1000 && e.code != 1001 && e.code != 1005 && e.code != 1006
275
- debuglog("WebSocketClient: closed:", e.code, e.reason)
303
+ debuglog("WebSocketClient: closed:", e.code, e.reason) // ### not pretty to see this
276
304
  ws = false
277
305
  if !wesc.fatal
278
306
  wesc.fatal = Error("websocket closed", e.code, e.reason)
279
307
  pending.signal(list(wesc.fatal))
280
- wesc.impconn.close({
281
- code: e.code
282
- reason: e.reason
283
- })
284
- wesc.impconn = false
308
+ pending = false
285
309
  }
286
310
 
287
- pending.wait()
311
+ result = pending.wait(10000, list(Error("IMP connection timeout")))
312
+ pending = false
313
+ result
288
314
  }
289
-
315
+
290
316
  // close
291
317
  //
292
318
  // Close the connection, but can be opened again with connect().
293
319
  //
294
320
  wesc.close = function close() {
295
- if !ws
296
- return
297
- ws.close(1000) // 1000: normal close
321
+ if ws
322
+ ws.close(1000) // 1000: normal close
323
+ ws = false
298
324
  }
299
325
 
300
326
  // destroy
@@ -303,23 +329,27 @@ closure WebSocketClient(options, imp, local wesc, ws) {
303
329
  //
304
330
  wesc.destroy = function destroy() {
305
331
  close()
332
+ wesc.impconn.destroy()
333
+ wesc.impconn = false
306
334
  }
307
335
 
308
336
  // send
309
337
  //
310
- // Send a message.
338
+ // Send a message, returning result tuple.
311
339
  //
312
340
  wesc.send = closure send(data) {
313
- if wesc.fatal
314
- return (wesc.fatal)
341
+ if reconnect()
342
+ return // failed to reconnect
315
343
  wesc.impconn.send(data)
316
344
  }
317
345
 
318
346
  // sendRaw
319
347
  //
320
- // Send a message with reconnect but no routing.
348
+ // Send a message with reconnect but no routing. Returns a result tuple.
321
349
  //
322
350
  wesc.sendRaw = closure sendRaw(data) {
351
+ if reconnect()
352
+ return // failed to reconnect
323
353
  ws.send(data)
324
354
  }
325
355
 
@@ -634,18 +634,14 @@ closure psmcFsConnector(psm, api, local connClassID, connector, watch) {
634
634
  else {
635
635
  api = connector.apis[creds.urlName]
636
636
  if !api { // create API to remote server
637
- connector.apis[creds.urlName] = api = APIClient({
637
+ api = APIClient(App.imp, {
638
638
  url: creds.urlName
639
639
  guid: creds.authSecret
640
- instanceID: creds.instanceID
641
640
  })
642
- if creds.instanceID != api.instanceID { // update our client instanceID
643
- creds.instanceID = api.instanceID
644
- `(error) = connector.vault.updateResource(resID, creds)
645
- }
646
- }
647
- }
648
- }
641
+ `(error) = api.connect()
642
+ if !error
643
+ connector.apis[creds.urlName] = api
644
+ } } }
649
645
  if error
650
646
  list(error)
651
647
  else if service == "NideTerminal" {
@@ -653,11 +649,11 @@ closure psmcFsConnector(psm, api, local connClassID, connector, watch) {
653
649
  return (list(Error("NideTerminal not supported in this environment")))
654
650
  list(false, App.nide.track.spawn(App.nide.registerRemote(api),
655
651
  args.0, // terminal display name
656
- args.1)) // options:
657
- // {
658
- // workerID: "NideServer" -- for main thread; other strings for worker threads
659
- // type: "My Remotes" -- display name of terminal group; e.g. URL
660
- // }
652
+ merge(args.1, {
653
+ serverID: api.serverID // server we found
654
+ // workerID: "NideServer" -- for main thread; other strings for worker threads
655
+ // type: "My Remotes" -- display name of terminal group; e.g. URL
656
+ }))) // options:
661
657
  }
662
658
  else if service == "NideHostAPI"
663
659
  list(false, api) // connect to the Host API
@@ -247,6 +247,8 @@ macro LoadComponentOnDemand(functionKeys, componentPath, local modobj, key) {
247
247
  macro LiveExport() {
248
248
  global()
249
249
  closure (exporterMod, local importList) {
250
+ if !exporterMod
251
+ return (false) // no module (e.g. build time)
250
252
  importList = []
251
253
  //
252
254
  // LiveImport
@@ -126,7 +126,6 @@ closure APIServer(options, local server, chroot) {
126
126
  return (list(error))
127
127
  }
128
128
  server.hostfs = hostfs
129
- server.workco = workerMod.WorkerController()
130
129
  server.Register(server.workco.serverID, server.workco)
131
130
 
132
131
  list(false, port)
@@ -124,6 +124,7 @@ function nodrInit(local manifest) {
124
124
  throw("module frameworks/node cannot be used from a browser")
125
125
  Naan.module.build(module.id, "node", function(modobj, compobj) {
126
126
  require("../common/").LiveImport()
127
+ require("../common/events.nlg")
127
128
  commonWatching = require("../common/watching.nlg")
128
129
  compobj.manifest = manifest
129
130
  function nodrReload() {
@@ -102,8 +102,9 @@ closure ptyManagerMain(local ptym, pty) {
102
102
  //
103
103
  // Close the pty manager, killing any open spawned processes.
104
104
  //
105
- ptym.close = closure close() {
106
- // ### kill any open spawned processes
105
+ ptym.close = closure close(local worker) {
106
+ for worker in ptym.workers
107
+ worker.kill()
107
108
  }
108
109
 
109
110
  // spawn
@@ -111,14 +112,16 @@ closure ptyManagerMain(local ptym, pty) {
111
112
  // Spawn a new pty process, returning a result tuple. If executable is omitted then a shell is
112
113
  // spawned.
113
114
  //
114
- // options:
115
+ // options: (all optional)
115
116
  // {
116
- // workerGUID: <string> // if specified, kills pty process when worker terminates
117
+ // cwd: <string> // current working directory
118
+ // env: <string> // environment
119
+ // raw: <boolean> // enable raw mode with ioctls
117
120
  // }
118
121
  //
119
122
  //
120
123
  ptym.spawn = closure spawn(executable, args, notify, options,
121
- local ptySpawned, ptypro) {
124
+ local error, ptySpawned, ptypro) {
122
125
 
123
126
  if !executable {
124
127
  if os.platform() == "win32"
@@ -136,6 +139,10 @@ closure ptyManagerMain(local ptym, pty) {
136
139
  env: js.g.process.env
137
140
  }, options)
138
141
 
142
+ `(error) = open() // ensure we're open
143
+ if error
144
+ return (list(error))
145
+
139
146
  ptySpawned = pty.spawn(executable, args, { // spawn the pty process
140
147
  name: options.name
141
148
  cols: options.cols
@@ -147,7 +154,6 @@ closure ptyManagerMain(local ptym, pty) {
147
154
  ptypro = ptyProcess(ptySpawned, notify)
148
155
  ptym.workers.push({ // track all pty processes
149
156
  pid: ptySpawned.pid
150
- workerGUID: options.workerGUID || undefined
151
157
  ptySpawned: ptySpawned
152
158
  })
153
159
 
@@ -160,19 +166,6 @@ closure ptyManagerMain(local ptym, pty) {
160
166
  ptypro.killed(event)
161
167
  })
162
168
 
163
- if options.workerGUID
164
- WorkerController().watcher.watch(
165
- function(action, worker, local ptydex) { // worker-owner of pty process died
166
- if action == "terminated" {
167
- ptydex = ptym.workers.findIndex(function(item) {
168
- options.workerGUID == item.workerGUID
169
- })
170
- if ptydex >= 0 {
171
- ptySpawned.kill()
172
- ptym.workers.splice(ptydex)
173
- }
174
- }
175
- })
176
169
  list(false, ptypro)
177
170
  }
178
171
 
@@ -238,7 +231,14 @@ closure ptyProcess(ptySpawned, notify, local ptyp) {
238
231
  // Kill the spawned process.
239
232
  //
240
233
  ptyp.kill = closure kill(signame) {
241
- ptySpawned.kill(signame)
234
+ if signame
235
+ ptySpawned.kill(signame) // use caller signal
236
+ else {
237
+ ptySpawned.kill("SIGTERM") // SIGTERM/SIGKILL sequence
238
+ sleep(1000)
239
+ if ptySpawned
240
+ ptySpawned.kill("SIGKILL")
241
+ }
242
242
  }
243
243
 
244
244
  // finis
@@ -268,7 +268,7 @@ closure ptyManagerWorker(local ptyw, mainContext, mainPty) {
268
268
  }
269
269
 
270
270
  if !mainContext {
271
- `(error, main) = require("naanlib:frameworks/running/mainthread.nlg").MainThread()
271
+ `(error, main) = require("naanlib:frameworks/running/remote_req.nlg").MainThread()
272
272
  if !error
273
273
  `(error, mainContext) = main.context()
274
274
  if error
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2024-2025 by Richard C. Zulch
9
+ * Copyright (c) 2024-2026 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -28,7 +28,7 @@
28
28
  */
29
29
 
30
30
  closure ShellConnect(options, local shob, error, fs, xparser, shelly) {
31
- global(js, compress)
31
+ global(js, compress, App)
32
32
  shob = new(object, this)
33
33
 
34
34
  // readline
@@ -78,6 +78,26 @@ closure ShellConnect(options, local shob, error, fs, xparser, shelly) {
78
78
  pending.wait()
79
79
  }
80
80
 
81
+ // pty
82
+ //
83
+ // Execute a shell command in a PTY terminal with optional current-working-directory.
84
+ //
85
+ shob.pty = closure pty(cmd, args, cwd, local exec, error, options, ptyp) {
86
+ exec = App.exec
87
+ if !exec
88
+ `(error, exec) = App.workerNub.execProxy()
89
+ if !error {
90
+ options = { input:"keyboard", output:"display", raw:true }
91
+ if cwd
92
+ options.spawn = { cwd: cwd }
93
+ `(error, ptyp) = exec.subprocess(cmd, args, options)
94
+ }
95
+ if ptyp
96
+ list(false, ptyp)
97
+ else
98
+ exec(cmd, args, cwd)
99
+ }
100
+
81
101
  // shellparser
82
102
  // intercept command-line parsing and look for a !shell escape
83
103
  function shellparser(source, grammar, operators, local token, text) {
@@ -89,7 +109,10 @@ closure ShellConnect(options, local shob, error, fs, xparser, shelly) {
89
109
  atom: `;;
90
110
  loc: tuple(0, text.length)
91
111
  })
92
- exec("bash", ["-c", text], js.d) // default directory: js.d
112
+ if text == "\n"
113
+ pty()
114
+ else
115
+ exec("bash", ["-c", text], js.d) // default directory: js.d
93
116
  }
94
117
  else {
95
118
  source.tokenpush(token)