procd 1.0.2__tar.gz → 1.0.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: procd
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: The Native Stack Runner. Orchestration Without Containers.
5
5
  Keywords:
6
6
  Author: alpine
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "procd"
7
- version = "1.0.2"
7
+ version = "1.0.3"
8
8
  description = "The Native Stack Runner. Orchestration Without Containers."
9
9
  readme = "README_PYPI.md"
10
10
  license = "MIT"
@@ -180,6 +180,10 @@ def cmd_up(args: argparse.Namespace) -> None:
180
180
  conf = _load(args)
181
181
  targets = _resolve_services(args.names, conf) if args.names else _default_services(conf)
182
182
 
183
+ if args.follow:
184
+ _up_follow(args, conf, targets)
185
+ return
186
+
183
187
  if args.detach:
184
188
  # detach: 従来通り _logger サブプロセスで JSONL に書き続ける
185
189
  procs: list[tuple[str, subprocess.Popen]] = []
@@ -277,6 +281,78 @@ def cmd_up(args: argparse.Namespace) -> None:
277
281
  shutdown()
278
282
 
279
283
 
284
+ def _up_follow(args: argparse.Namespace, conf: cfg.Config, targets: list[str]) -> None:
285
+ """detach 起動してから jsonl ログをフォローし、終了時にそのサービスも停止する。"""
286
+ jsonl = process.jsonl_path()
287
+ log_start_pos = jsonl.stat().st_size if jsonl.exists() else 0
288
+
289
+ started_names: list[str] = []
290
+ for i, name in enumerate(targets):
291
+ if process.is_running(name):
292
+ print(f"[{name}] already running (pid {process.read_pid(name)})")
293
+ continue
294
+ svc = conf.services[name]
295
+ color = process.COLORS[i % len(process.COLORS)]
296
+ print(f"{color}[{name}]{process.RESET} Starting...")
297
+ process.start(name, svc, detach=True, project_dir=conf.project_dir, verbose=args.verbose)
298
+ started_names.append(name)
299
+ for name in started_names:
300
+ svc = conf.services[name]
301
+ i = list(conf.services.keys()).index(name)
302
+ color = process.COLORS[i % len(process.COLORS)]
303
+ result = process.wait_ready(name, svc, project_dir=conf.project_dir)
304
+ if result is None:
305
+ print(f"{color}[{name}]{process.RESET} Process exited immediately.", file=sys.stderr)
306
+ elif result:
307
+ print(f"{color}[{name}]{process.RESET} Ready (pid {process.read_pid(name)})")
308
+ else:
309
+ print(f"{color}[{name}]{process.RESET} Timed out waiting for ready", file=sys.stderr)
310
+
311
+ def _shutdown_started() -> None:
312
+ if not started_names:
313
+ return
314
+ print("\nStopping services...")
315
+ for name in reversed(started_names):
316
+ if process.stop(name):
317
+ print(f" Stopped {name}")
318
+
319
+ if not jsonl.exists():
320
+ if not started_names:
321
+ _err_console.print("[yellow]no logs found[/yellow]")
322
+ return
323
+ deadline = time.time() + 5
324
+ while not jsonl.exists() and time.time() < deadline:
325
+ time.sleep(0.1)
326
+ if not jsonl.exists():
327
+ process.LOG_DIR.mkdir(parents=True, exist_ok=True)
328
+ jsonl.touch()
329
+
330
+ def _fmt(entry: dict) -> str:
331
+ s = entry.get("s", "?")
332
+ color = process.COLORS[list(conf.services).index(s) % len(process.COLORS)] if s in conf.services else ""
333
+ ts = f" {entry.get('t','')[:23]} " if args.timestamp else ""
334
+ return f"{color}[{s}]{process.RESET}{ts} {entry.get('m','')}"
335
+
336
+ try:
337
+ with open(jsonl, encoding="utf-8") as f:
338
+ f.seek(log_start_pos)
339
+ while True:
340
+ line = f.readline()
341
+ if line:
342
+ try:
343
+ entry = json.loads(line)
344
+ except json.JSONDecodeError:
345
+ continue
346
+ print(_fmt(entry))
347
+ sys.stdout.flush()
348
+ else:
349
+ time.sleep(0.05)
350
+ except KeyboardInterrupt:
351
+ pass
352
+ finally:
353
+ _shutdown_started()
354
+
355
+
280
356
  def cmd_down(args: argparse.Namespace) -> None:
281
357
  conf = _load(args)
282
358
  targets = _resolve_services(args.names, conf) if args.names else _default_services(conf)
@@ -337,59 +413,13 @@ def cmd_logs(args: argparse.Namespace) -> None:
337
413
  ts = f" {entry.get('t','')[:23]} " if args.timestamp else ""
338
414
  return f"{color}[{s}]{process.RESET}{ts} {entry.get('m','')}"
339
415
 
340
- # --up: サービスを起動してからログをフォローし、終了時にサービスも停止する
341
- started_names: list[str] = []
342
- # 起動前のログ末尾位置を記録して古いログを表示しない
343
- log_start_pos: int = jsonl.stat().st_size if jsonl.exists() else 0
344
-
345
- if getattr(args, "up", None) is not None:
346
- targets = _resolve_services(args.up, conf) if args.up else _default_services(conf)
347
- for i, name in enumerate(targets):
348
- if process.is_running(name):
349
- print(f"[{name}] already running (pid {process.read_pid(name)})")
350
- continue
351
- svc = conf.services[name]
352
- color = process.COLORS[i % len(process.COLORS)]
353
- print(f"{color}[{name}]{process.RESET} Starting...")
354
- process.start(name, svc, detach=True, project_dir=conf.project_dir, verbose=args.verbose)
355
- started_names.append(name)
356
- for name in started_names:
357
- svc = conf.services[name]
358
- i = list(conf.services.keys()).index(name)
359
- color = process.COLORS[i % len(process.COLORS)]
360
- result = process.wait_ready(name, svc, project_dir=conf.project_dir)
361
- if result is None:
362
- print(f"{color}[{name}]{process.RESET} Process exited immediately.", file=sys.stderr)
363
- elif result:
364
- print(f"{color}[{name}]{process.RESET} Ready (pid {process.read_pid(name)})")
365
- else:
366
- print(f"{color}[{name}]{process.RESET} Timed out waiting for ready", file=sys.stderr)
367
- args.follow = True
368
-
369
- def _shutdown_started() -> None:
370
- if not started_names:
371
- return
372
- print("\nStopping services...")
373
- for name in reversed(started_names):
374
- if process.stop(name):
375
- print(f" Stopped {name}")
376
-
377
416
  if args.follow:
378
- # --up でサービスを起動した場合はログファイルが存在するまで少し待つ
379
417
  if not jsonl.exists():
380
- if not started_names:
381
- _err_console.print("[yellow]no logs found — run 'procd up -d' first[/yellow]")
382
- return
383
- deadline = time.time() + 5
384
- while not jsonl.exists() and time.time() < deadline:
385
- time.sleep(0.1)
386
- if not jsonl.exists():
387
- process.LOG_DIR.mkdir(parents=True, exist_ok=True)
388
- jsonl.touch()
418
+ _err_console.print("[yellow]no logs found — run 'procd up -d' first[/yellow]")
419
+ return
389
420
  try:
390
421
  with open(jsonl, encoding="utf-8") as f:
391
- # --up で起動した場合は起動前の末尾から、通常は最新末尾からフォロー
392
- f.seek(log_start_pos if started_names else 0, 0 if started_names else 2)
422
+ f.seek(0, 2) # 末尾からフォロー
393
423
  while True:
394
424
  line = f.readline()
395
425
  if line:
@@ -401,8 +431,6 @@ def cmd_logs(args: argparse.Namespace) -> None:
401
431
  time.sleep(0.05)
402
432
  except KeyboardInterrupt:
403
433
  pass
404
- finally:
405
- _shutdown_started()
406
434
  else:
407
435
  if not jsonl.exists():
408
436
  return
@@ -589,10 +617,10 @@ def cmd_link(args: argparse.Namespace) -> None:
589
617
  conf = _load(args)
590
618
  project_dir = conf.project_dir
591
619
 
592
- if args.logs_up is not None:
593
- names = " ".join(args.logs_up)
594
- procd_sub = f"logs --up{' ' + names if names else ''}"
595
- shortcut_name = f"procd logs --up{' ' + names if names else ''}"
620
+ if args.up_follow is not None:
621
+ names = " ".join(args.up_follow)
622
+ procd_sub = f"up --follow{' ' + names if names else ''}"
623
+ shortcut_name = f"procd up --follow{' ' + names if names else ''}"
596
624
  elif args.up is not None:
597
625
  names = " ".join(args.up)
598
626
  procd_sub = f"up{' ' + names if names else ''}"
@@ -632,7 +660,11 @@ def build_parser() -> argparse.ArgumentParser:
632
660
  # up
633
661
  p = sub.add_parser("up", help="サービスを起動する")
634
662
  p.add_argument("names", nargs="*", metavar="NAME")
635
- p.add_argument("-d", "--detach", action="store_true", help="バックグラウンド起動")
663
+ group = p.add_mutually_exclusive_group()
664
+ group.add_argument("-d", "--detach", action="store_true", help="バックグラウンド起動")
665
+ group.add_argument("-f", "--follow", action="store_true",
666
+ help="バックグラウンド起動してログをフォローし、終了時にサービスも停止する")
667
+ p.add_argument("-t", "--timestamp", action="store_true", help="--follow 時にタイムスタンプを表示する")
636
668
  p.set_defaults(func=cmd_up)
637
669
 
638
670
  # down
@@ -655,8 +687,6 @@ def build_parser() -> argparse.ArgumentParser:
655
687
  p.add_argument("-f", "--follow", action="store_true", help="tail -f スタイルで追跡")
656
688
  p.add_argument("-n", "--lines", type=int, default=200, metavar="N", help="表示する行数(デフォルト: 200)")
657
689
  p.add_argument("-t", "--timestamp", action="store_true", help="タイムスタンプを表示する")
658
- p.add_argument("--up", nargs="*", metavar="NAME",
659
- help="サービスを起動してからログをフォローし、終了時にサービスも停止する")
660
690
  p.set_defaults(func=cmd_logs)
661
691
 
662
692
  # run
@@ -684,8 +714,8 @@ def build_parser() -> argparse.ArgumentParser:
684
714
  group = p.add_mutually_exclusive_group(required=True)
685
715
  group.add_argument("--up", nargs="*", metavar="NAME",
686
716
  help="procd up のショートカット(NAME 省略で default グループ)")
687
- group.add_argument("--logs-up", nargs="*", metavar="NAME", dest="logs_up",
688
- help="procd logs --up のショートカット(NAME 省略で default グループ)")
717
+ group.add_argument("--up-follow", nargs="*", metavar="NAME", dest="up_follow",
718
+ help="procd up --follow のショートカット(NAME 省略で default グループ)")
689
719
  group.add_argument("--run", metavar="NAME",
690
720
  help="procd run <NAME> のショートカット")
691
721
  p.add_argument("--icon", metavar="FILE", help="アイコンファイル (.ico / .icns)")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes