nterminal 1.2.15 → 1.2.17

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.
@@ -68,6 +68,7 @@ load_env_file
68
68
 
69
69
  DEFAULT_STATE_PATH="$DEFAULT_RUNTIME_DIR/state.json"
70
70
  DEFAULT_PID_PATH="$DEFAULT_RUNTIME_DIR/nterminal.pid"
71
+ DEFAULT_MONITOR_PID_PATH="$DEFAULT_RUNTIME_DIR/nterminal.monitor.pid"
71
72
  DEFAULT_LOG_PATH="$DEFAULT_RUNTIME_DIR/nterminal.log"
72
73
 
73
74
  HOST="${NTERMINAL_HOST:-127.0.0.1}"
@@ -75,6 +76,7 @@ PORT="${NTERMINAL_PORT:-3107}"
75
76
  NODE_BIN="${NTERMINAL_NODE_BIN:-node}"
76
77
  NPM_BIN="${NTERMINAL_NPM_BIN:-npm}"
77
78
  PID_PATH="$(resolve_path "${NTERMINAL_PID_PATH:-${NTERMINAL_RUNTIME_PID_PATH:-$DEFAULT_PID_PATH}}")"
79
+ MONITOR_PID_PATH="$(resolve_path "${NTERMINAL_MONITOR_PID_PATH:-${NTERMINAL_RUNTIME_MONITOR_PID_PATH:-$DEFAULT_MONITOR_PID_PATH}}")"
78
80
  LOG_PATH="$(resolve_path "${NTERMINAL_LOG_PATH:-${NTERMINAL_RUNTIME_LOG_PATH:-$DEFAULT_LOG_PATH}}")"
79
81
  STATE_PATH="$(resolve_path "${NTERMINAL_STATE_PATH:-${NTERMINAL_RUNTIME_STATE_PATH:-$DEFAULT_STATE_PATH}}")"
80
82
  HEALTH_TIMEOUT_SECONDS="${NTERMINAL_HEALTH_TIMEOUT_SECONDS:-15}"
@@ -100,6 +102,7 @@ export NTERMINAL_APP_DIR="$APP_DIR"
100
102
  export NTERMINAL_ENV_FILE="$ENV_FILE"
101
103
  export NTERMINAL_STATE_PATH="$STATE_PATH"
102
104
  export NTERMINAL_PID_PATH="$PID_PATH"
105
+ export NTERMINAL_MONITOR_PID_PATH="$MONITOR_PID_PATH"
103
106
  export NTERMINAL_LOG_PATH="$LOG_PATH"
104
107
 
105
108
  health_url() {
@@ -130,7 +133,7 @@ ensure_runtime() {
130
133
  }
131
134
 
132
135
  ensure_directories() {
133
- mkdir -p "$(dirname "$PID_PATH")" "$(dirname "$LOG_PATH")"
136
+ mkdir -p "$(dirname "$PID_PATH")" "$(dirname "$MONITOR_PID_PATH")" "$(dirname "$LOG_PATH")"
134
137
  : >> "$LOG_PATH"
135
138
  chmod 600 "$LOG_PATH" 2>/dev/null || true
136
139
  }
@@ -152,6 +155,11 @@ read_pid() {
152
155
  read_pid_file "$PID_PATH"
153
156
  }
154
157
 
158
+ read_monitor_pid() {
159
+ [[ -f "$MONITOR_PID_PATH" ]] || return 1
160
+ read_pid_file "$MONITOR_PID_PATH"
161
+ }
162
+
155
163
  read_pid_file() {
156
164
  local pid_path="$1"
157
165
  [[ -f "$pid_path" ]] || return 1
@@ -166,6 +174,19 @@ is_running() {
166
174
  kill -0 "$pid" 2>/dev/null
167
175
  }
168
176
 
177
+ parent_pid() {
178
+ local pid="$1"
179
+ if [[ -r "/proc/$pid/status" ]]; then
180
+ awk '/^PPid:/ { print $2; exit }' "/proc/$pid/status" 2>/dev/null
181
+ return
182
+ fi
183
+ if command -v ps >/dev/null 2>&1; then
184
+ ps -o ppid= -p "$pid" 2>/dev/null | tr -d '[:space:]'
185
+ return
186
+ fi
187
+ return 1
188
+ }
189
+
169
190
  pid_controls_app() {
170
191
  local pid="$1"
171
192
  is_running "$pid" || return 1
@@ -176,7 +197,6 @@ pid_controls_app() {
176
197
  local cwd=""
177
198
  cwd="$(readlink "/proc/$pid/cwd" 2>/dev/null || true)"
178
199
  if [[ "$cmdline" == *"$RUNTIME_ENTRY"* ]]; then
179
- [[ -z "$cwd" || "$cwd" == "$APP_DIR" ]] || return 1
180
200
  return 0
181
201
  fi
182
202
  if [[ "$cwd" == "$APP_DIR" && "$cmdline" == *"dist/server/index.js"* ]]; then
@@ -194,6 +214,26 @@ pid_controls_app() {
194
214
  return 1
195
215
  }
196
216
 
217
+ monitor_controls_app() {
218
+ local pid="$1"
219
+ is_running "$pid" || return 1
220
+
221
+ local cmdline=""
222
+ if [[ -r "/proc/$pid/cmdline" ]]; then
223
+ cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)"
224
+ [[ "$cmdline" == *"$SCRIPT_DIR/nterminalctl"* && "$cmdline" == *" supervise"* ]]
225
+ return
226
+ fi
227
+
228
+ if command -v ps >/dev/null 2>&1; then
229
+ cmdline="$(ps -p "$pid" -o command= 2>/dev/null || true)"
230
+ [[ "$cmdline" == *"$SCRIPT_DIR/nterminalctl"* && "$cmdline" == *" supervise"* ]]
231
+ return
232
+ fi
233
+
234
+ return 1
235
+ }
236
+
197
237
  current_pid() {
198
238
  local pid
199
239
  pid="$(read_pid || true)"
@@ -201,6 +241,35 @@ current_pid() {
201
241
  printf '%s' "$pid"
202
242
  return 0
203
243
  fi
244
+
245
+ # npm can replace the package directory while the old Node process is still
246
+ # listening. If the pid file is missing, recover by recognizing the port owner
247
+ # as this package runtime before deciding the app is stopped.
248
+ pid="$(port_owner_pid || true)"
249
+ if [[ -n "$pid" && "$(pid_controls_app "$pid" && echo yes || echo no)" == yes ]]; then
250
+ printf '%s' "$pid"
251
+ return 0
252
+ fi
253
+ return 1
254
+ }
255
+
256
+ current_monitor_pid() {
257
+ local pid
258
+ pid="$(read_monitor_pid || true)"
259
+ if [[ -n "$pid" && "$(monitor_controls_app "$pid" && echo yes || echo no)" == yes ]]; then
260
+ printf '%s' "$pid"
261
+ return 0
262
+ fi
263
+
264
+ local child_pid parent
265
+ child_pid="$(current_pid || true)"
266
+ if [[ -n "$child_pid" ]]; then
267
+ parent="$(parent_pid "$child_pid" || true)"
268
+ if [[ -n "$parent" && "$(monitor_controls_app "$parent" && echo yes || echo no)" == yes ]]; then
269
+ printf '%s' "$parent"
270
+ return 0
271
+ fi
272
+ fi
204
273
  return 1
205
274
  }
206
275
 
@@ -259,6 +328,14 @@ remove_stale_pid() {
259
328
  fi
260
329
  }
261
330
 
331
+ remove_stale_monitor_pid() {
332
+ local pid
333
+ pid="$(read_monitor_pid || true)"
334
+ if [[ -n "$pid" && ! "$(monitor_controls_app "$pid" && echo yes || echo no)" == yes ]]; then
335
+ rm -f "$MONITOR_PID_PATH"
336
+ fi
337
+ }
338
+
262
339
  node_health_check() {
263
340
  "$NODE_BIN" -e '
264
341
  const http = require("node:http");
@@ -273,15 +350,90 @@ request.on("error", () => process.exit(1));
273
350
  }
274
351
 
275
352
  start_daemon() {
276
- cd "$APP_DIR"
277
353
  if command -v setsid >/dev/null 2>&1; then
278
- nohup setsid "$NODE_BIN" "$RUNTIME_ENTRY" >> "$LOG_PATH" 2>&1 &
354
+ nohup setsid bash "$SCRIPT_DIR/nterminalctl" supervise >> "$LOG_PATH" 2>&1 &
279
355
  else
280
- nohup "$NODE_BIN" "$RUNTIME_ENTRY" >> "$LOG_PATH" 2>&1 &
356
+ nohup bash "$SCRIPT_DIR/nterminalctl" supervise >> "$LOG_PATH" 2>&1 &
281
357
  fi
282
- local pid="$!"
283
- disown "$pid" 2>/dev/null || true
284
- echo "$pid" > "$PID_PATH"
358
+ local monitor_pid="$!"
359
+ disown "$monitor_pid" 2>/dev/null || true
360
+ echo "$monitor_pid" > "$MONITOR_PID_PATH"
361
+ chmod 600 "$MONITOR_PID_PATH" 2>/dev/null || true
362
+
363
+ local pid=""
364
+ local deadline=$((SECONDS + HEALTH_TIMEOUT_SECONDS))
365
+ while (( SECONDS <= deadline )); do
366
+ if ! is_running "$monitor_pid"; then
367
+ return 1
368
+ fi
369
+ pid="$(current_pid || true)"
370
+ [[ -n "$pid" ]] && return 0
371
+ sleep 0.25
372
+ done
373
+ return 1
374
+ }
375
+
376
+ stop_supervised_child() {
377
+ local pid="$1"
378
+ [[ -n "$pid" ]] || return 0
379
+ if ! is_running "$pid"; then
380
+ rm -f "$PID_PATH"
381
+ return 0
382
+ fi
383
+ kill -TERM "$pid" 2>/dev/null || true
384
+ local deadline=$((SECONDS + STOP_TIMEOUT_SECONDS))
385
+ while (( SECONDS <= deadline )); do
386
+ if ! is_running "$pid"; then
387
+ rm -f "$PID_PATH"
388
+ return 0
389
+ fi
390
+ sleep 0.25
391
+ done
392
+ kill -KILL "$pid" 2>/dev/null || true
393
+ rm -f "$PID_PATH"
394
+ }
395
+
396
+ cmd_supervise() {
397
+ ensure_app_dir
398
+ ensure_env_file
399
+ ensure_runtime
400
+ validate_required_env || die "generate real secrets before starting"
401
+ ensure_directories
402
+ echo "$$" > "$MONITOR_PID_PATH"
403
+ chmod 600 "$MONITOR_PID_PATH" 2>/dev/null || true
404
+
405
+ local child_pid=""
406
+ local stop_requested=0
407
+ supervisor_shutdown() {
408
+ stop_requested=1
409
+ trap - TERM INT
410
+ stop_supervised_child "$child_pid"
411
+ rm -f "$MONITOR_PID_PATH"
412
+ exit 0
413
+ }
414
+ trap supervisor_shutdown TERM INT
415
+
416
+ while true; do
417
+ cd "$APP_DIR"
418
+ "$NODE_BIN" "$RUNTIME_ENTRY" >> "$LOG_PATH" 2>&1 &
419
+ child_pid="$!"
420
+ echo "$child_pid" > "$PID_PATH"
421
+ chmod 600 "$PID_PATH" 2>/dev/null || true
422
+
423
+ set +e
424
+ wait "$child_pid"
425
+ local status="$?"
426
+ set -e
427
+
428
+ rm -f "$PID_PATH"
429
+ if (( stop_requested == 1 )); then
430
+ rm -f "$MONITOR_PID_PATH"
431
+ exit 0
432
+ fi
433
+
434
+ printf '[%s] NTerminal process exited status=%s; restarting in 2s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$status" >> "$LOG_PATH"
435
+ sleep 2
436
+ done
285
437
  }
286
438
 
287
439
  start_launchd() {
@@ -321,8 +473,15 @@ wait_for_health() {
321
473
  }
322
474
 
323
475
  port_owner_pid() {
324
- command -v lsof >/dev/null 2>&1 || return 1
325
- lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t 2>/dev/null | head -n 1
476
+ if command -v lsof >/dev/null 2>&1; then
477
+ lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t 2>/dev/null | head -n 1
478
+ return
479
+ fi
480
+ if command -v ss >/dev/null 2>&1; then
481
+ ss -ltnp "sport = :$PORT" 2>/dev/null | sed -n 's/.*pid=\([0-9]\+\).*/\1/p' | head -n 1
482
+ return
483
+ fi
484
+ return 1
326
485
  }
327
486
 
328
487
  assert_port_available() {
@@ -337,12 +496,19 @@ assert_port_available() {
337
496
 
338
497
  cmd_status() {
339
498
  remove_stale_pid
499
+ remove_stale_monitor_pid
340
500
  local pid
341
501
  pid="$(current_pid || true)"
342
502
  if [[ -n "$pid" ]]; then
343
503
  info "NTerminal running pid=$pid url=$(public_url) log=$LOG_PATH"
344
504
  return 0
345
505
  fi
506
+ local monitor_pid
507
+ monitor_pid="$(current_monitor_pid || true)"
508
+ if [[ -n "$monitor_pid" ]]; then
509
+ info "NTerminal supervisor running pid=$monitor_pid; server is restarting url=$(public_url) log=$LOG_PATH"
510
+ return 4
511
+ fi
346
512
  info "NTerminal stopped pid_file=$PID_PATH"
347
513
  return 3
348
514
  }
@@ -354,6 +520,7 @@ cmd_start() {
354
520
  validate_required_env || die "generate real secrets before starting"
355
521
  ensure_directories
356
522
  remove_stale_pid
523
+ remove_stale_monitor_pid
357
524
 
358
525
  local pid
359
526
  pid="$(current_pid || true)"
@@ -361,6 +528,12 @@ cmd_start() {
361
528
  info "NTerminal already running pid=$pid url=$(public_url)"
362
529
  return 0
363
530
  fi
531
+ local monitor_pid
532
+ monitor_pid="$(current_monitor_pid || true)"
533
+ if [[ -n "$monitor_pid" ]]; then
534
+ info "NTerminal supervisor already running pid=$monitor_pid url=$(public_url)"
535
+ return 0
536
+ fi
364
537
 
365
538
  if launchd_controls_app; then
366
539
  start_launchd
@@ -368,12 +541,28 @@ cmd_start() {
368
541
  fi
369
542
 
370
543
  assert_port_available
371
- start_daemon
544
+ if ! start_daemon; then
545
+ rm -f "$MONITOR_PID_PATH"
546
+ echo "NTerminal failed to start supervisor. Last log lines:" >&2
547
+ tail -n 40 "$LOG_PATH" >&2 || true
548
+ return 1
549
+ fi
372
550
  chmod 600 "$PID_PATH" 2>/dev/null || true
373
- pid="$(read_pid)"
551
+ pid="$(current_pid || true)"
552
+ if [[ -z "$pid" ]]; then
553
+ rm -f "$MONITOR_PID_PATH"
554
+ echo "NTerminal failed to start supervisor. Last log lines:" >&2
555
+ tail -n 40 "$LOG_PATH" >&2 || true
556
+ return 1
557
+ fi
374
558
 
375
559
  if wait_for_health "$pid"; then
376
- info "NTerminal started pid=$pid url=$(public_url) log=$LOG_PATH"
560
+ monitor_pid="$(current_monitor_pid || true)"
561
+ if [[ -n "$monitor_pid" ]]; then
562
+ info "NTerminal started pid=$pid supervisor=$monitor_pid url=$(public_url) log=$LOG_PATH"
563
+ else
564
+ info "NTerminal started pid=$pid url=$(public_url) log=$LOG_PATH"
565
+ fi
377
566
  return 0
378
567
  fi
379
568
 
@@ -381,7 +570,11 @@ cmd_start() {
381
570
  if ! is_running "$pid"; then
382
571
  exit_hint="process exited during startup"
383
572
  fi
384
- rm -f "$PID_PATH"
573
+ monitor_pid="$(current_monitor_pid || true)"
574
+ if [[ -n "$monitor_pid" ]]; then
575
+ kill -TERM "$monitor_pid" 2>/dev/null || true
576
+ fi
577
+ rm -f "$PID_PATH" "$MONITOR_PID_PATH"
385
578
  echo "NTerminal $exit_hint. Last log lines:" >&2
386
579
  tail -n 40 "$LOG_PATH" >&2 || true
387
580
  return 1
@@ -389,13 +582,44 @@ cmd_start() {
389
582
 
390
583
  cmd_stop() {
391
584
  remove_stale_pid
585
+ remove_stale_monitor_pid
392
586
  local pid
393
587
  pid="$(current_pid || true)"
394
- if [[ -z "$pid" ]]; then
588
+ local monitor_pid
589
+ monitor_pid="$(current_monitor_pid || true)"
590
+ if [[ -z "$pid" && -z "$monitor_pid" ]]; then
395
591
  info "NTerminal already stopped"
396
592
  return 0
397
593
  fi
398
594
 
595
+ if [[ -n "$monitor_pid" ]]; then
596
+ kill -TERM "$monitor_pid" 2>/dev/null || true
597
+ local monitor_deadline=$((SECONDS + STOP_TIMEOUT_SECONDS))
598
+ while (( SECONDS <= monitor_deadline )); do
599
+ if ! is_running "$monitor_pid"; then
600
+ rm -f "$MONITOR_PID_PATH"
601
+ break
602
+ fi
603
+ sleep 0.25
604
+ done
605
+ if is_running "$monitor_pid"; then
606
+ kill -KILL "$monitor_pid" 2>/dev/null || true
607
+ rm -f "$MONITOR_PID_PATH"
608
+ fi
609
+
610
+ if [[ -n "$pid" && "$(is_running "$pid" && echo yes || echo no)" == yes ]]; then
611
+ stop_supervised_child "$pid"
612
+ fi
613
+
614
+ rm -f "$PID_PATH" "$MONITOR_PID_PATH"
615
+ if [[ -n "$pid" ]]; then
616
+ info "NTerminal stopped pid=$pid supervisor=$monitor_pid"
617
+ else
618
+ info "NTerminal stopped supervisor=$monitor_pid"
619
+ fi
620
+ return 0
621
+ fi
622
+
399
623
  kill -TERM "$pid" 2>/dev/null || true
400
624
  local deadline=$((SECONDS + STOP_TIMEOUT_SECONDS))
401
625
  while (( SECONDS <= deadline )); do
@@ -453,43 +677,17 @@ cmd_logs() {
453
677
  fi
454
678
  }
455
679
 
456
- stop_pid_file() {
457
- local pid_path="$1"
458
- local pid
459
- pid="$(read_pid_file "$pid_path" || true)"
460
- if [[ -z "$pid" ]]; then
461
- return 0
462
- fi
463
- if ! is_running "$pid"; then
464
- rm -f "$pid_path"
465
- return 0
466
- fi
467
- if ! pid_controls_app "$pid"; then
468
- rm -f "$pid_path"
469
- return 0
470
- fi
471
-
472
- kill -TERM "$pid" 2>/dev/null || true
473
- local deadline=$((SECONDS + STOP_TIMEOUT_SECONDS))
474
- while (( SECONDS <= deadline )); do
475
- if ! is_running "$pid"; then
476
- rm -f "$pid_path"
477
- return 0
478
- fi
479
- sleep 0.25
480
- done
481
-
482
- kill -KILL "$pid" 2>/dev/null || true
483
- rm -f "$pid_path"
484
- }
485
-
486
680
  clean_state_paths() {
487
681
  local state_path="$1"
488
682
  local pid_path="$2"
489
683
  local log_path="$3"
684
+ local monitor_pid_path="${4:-}"
490
685
  local state_dir
491
686
  state_dir="$(dirname "$state_path")"
492
687
  rm -f "$state_path" "$pid_path" "$log_path" "$state_dir/update.lock"
688
+ if [[ -n "$monitor_pid_path" ]]; then
689
+ rm -f "$monitor_pid_path"
690
+ fi
493
691
  rm -rf "$state_dir/notification-assets"
494
692
  }
495
693
 
@@ -519,7 +717,7 @@ cmd_clean() {
519
717
 
520
718
  local state_dir
521
719
  state_dir="$(dirname "$STATE_PATH")"
522
- clean_state_paths "$STATE_PATH" "$PID_PATH" "$LOG_PATH"
720
+ clean_state_paths "$STATE_PATH" "$PID_PATH" "$LOG_PATH" "$MONITOR_PID_PATH"
523
721
  if (( remove_env == 1 )); then
524
722
  rm -f "$ENV_FILE"
525
723
  fi
@@ -545,11 +743,11 @@ cmd_uninstall() {
545
743
 
546
744
  ensure_app_dir
547
745
  unload_launchd_if_controlled
548
- stop_pid_file "$PID_PATH"
746
+ cmd_stop >/dev/null
549
747
  if (( kill_tmux == 1 )) && command -v tmux >/dev/null 2>&1; then
550
748
  tmux -L nterminal kill-server >/dev/null 2>&1 || true
551
749
  fi
552
- clean_state_paths "$STATE_PATH" "$PID_PATH" "$LOG_PATH"
750
+ clean_state_paths "$STATE_PATH" "$PID_PATH" "$LOG_PATH" "$MONITOR_PID_PATH"
553
751
  rm -f "$ENV_FILE"
554
752
  remove_owned_launchd_plist
555
753
 
@@ -610,10 +808,10 @@ cmd_doctor() {
610
808
  fi
611
809
 
612
810
  ensure_directories
613
- if [[ -w "$(dirname "$PID_PATH")" && -w "$(dirname "$LOG_PATH")" ]]; then
614
- echo "ok writable: pid/log directories"
811
+ if [[ -w "$(dirname "$PID_PATH")" && -w "$(dirname "$MONITOR_PID_PATH")" && -w "$(dirname "$LOG_PATH")" ]]; then
812
+ echo "ok writable: pid/monitor/log directories"
615
813
  else
616
- echo "fail writable: pid/log directories"
814
+ echo "fail writable: pid/monitor/log directories"
617
815
  failed=1
618
816
  fi
619
817
 
@@ -635,9 +833,9 @@ cmd_help() {
635
833
  Usage: nterminal <command>
636
834
 
637
835
  Commands:
638
- status Show process status. Exit 0 when running, 3 when stopped.
836
+ status Show process status. Exit 0 when running, 3 when stopped, 4 when the supervisor is restarting it.
639
837
  start Start the built server in the background.
640
- stop Stop the managed server with SIGTERM, then SIGKILL after timeout.
838
+ stop Stop the managed server and supervisor with SIGTERM, then SIGKILL after timeout.
641
839
  restart Stop and start the server.
642
840
  build No-op for package installs; verifies the bundled runtime exists.
643
841
  deploy Restart the installed package using the bundled runtime.
@@ -658,6 +856,7 @@ Environment:
658
856
  NTERMINAL_ENV_FILE Override .env path.
659
857
  NTERMINAL_STATE_PATH Default ~/.nterminal/state.json.
660
858
  NTERMINAL_PID_PATH Default ~/.nterminal/nterminal.pid.
859
+ NTERMINAL_MONITOR_PID_PATH Default ~/.nterminal/nterminal.monitor.pid.
661
860
  NTERMINAL_LOG_PATH Default ~/.nterminal/nterminal.log.
662
861
  NTERMINAL_HEALTH_TIMEOUT_SECONDS Default 15.
663
862
  NTERMINAL_STOP_TIMEOUT_SECONDS Default 20.
@@ -668,6 +867,7 @@ EOF
668
867
  case "$COMMAND" in
669
868
  status) cmd_status "$@" ;;
670
869
  start) cmd_start "$@" ;;
870
+ supervise) cmd_supervise "$@" ;;
671
871
  stop) cmd_stop "$@" ;;
672
872
  restart|reload) cmd_restart "$@" ;;
673
873
  build) cmd_build "$@" ;;