procd 1.0.2__tar.gz → 1.0.4__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.4
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.4"
8
8
  description = "The Native Stack Runner. Orchestration Without Containers."
9
9
  readme = "README_PYPI.md"
10
10
  license = "MIT"
@@ -50,10 +50,16 @@ def _resolve_services(names: list[str], conf: cfg.Config) -> list[str]:
50
50
  return result
51
51
 
52
52
 
53
- def _default_services(conf: cfg.Config) -> list[str]:
53
+ def _default_services(conf: cfg.Config) -> list[str] | None:
54
+ """default グループのサービス一覧。未定義なら None。"""
54
55
  if "default" in conf.groups:
55
56
  return conf.groups["default"].services
56
- return list(conf.services.keys())
57
+ return None
58
+
59
+
60
+ def _no_default_group(conf: cfg.Config) -> None:
61
+ _console.print("[yellow]No 'default' group defined. Specify NAME, or define \\[group.default].[/yellow]")
62
+ _print_list(conf)
57
63
 
58
64
 
59
65
  # " < file" " > file" " >> file" を検出する(スペース区切りで誤検出を防ぐ)
@@ -178,7 +184,17 @@ def _run_step(step: str, conf: cfg.Config, extra_env: dict[str, str] | None = No
178
184
 
179
185
  def cmd_up(args: argparse.Namespace) -> None:
180
186
  conf = _load(args)
181
- targets = _resolve_services(args.names, conf) if args.names else _default_services(conf)
187
+ if args.names:
188
+ targets = _resolve_services(args.names, conf)
189
+ else:
190
+ targets = _default_services(conf)
191
+ if targets is None:
192
+ _no_default_group(conf)
193
+ return
194
+
195
+ if args.follow:
196
+ _up_follow(args, conf, targets)
197
+ return
182
198
 
183
199
  if args.detach:
184
200
  # detach: 従来通り _logger サブプロセスで JSONL に書き続ける
@@ -199,6 +215,7 @@ def cmd_up(args: argparse.Namespace) -> None:
199
215
  color = process.COLORS[i % len(process.COLORS)]
200
216
  result = process.wait_ready(name, svc, project_dir=conf.project_dir)
201
217
  if result is None:
218
+ process.clear_pid(name)
202
219
  print(f"{color}[{name}]{process.RESET} Process exited immediately. Last log:", file=sys.stderr)
203
220
  tail = process.tail_log(name)
204
221
  if tail:
@@ -277,9 +294,93 @@ def cmd_up(args: argparse.Namespace) -> None:
277
294
  shutdown()
278
295
 
279
296
 
297
+ def _up_follow(args: argparse.Namespace, conf: cfg.Config, targets: list[str]) -> None:
298
+ """detach 起動してから jsonl ログをフォローし、終了時にそのサービスも停止する。"""
299
+ jsonl = process.jsonl_path()
300
+ log_start_pos = jsonl.stat().st_size if jsonl.exists() else 0
301
+
302
+ started_names: list[str] = []
303
+ for i, name in enumerate(targets):
304
+ if process.is_running(name):
305
+ print(f"[{name}] already running (pid {process.read_pid(name)})")
306
+ continue
307
+ svc = conf.services[name]
308
+ color = process.COLORS[i % len(process.COLORS)]
309
+ print(f"{color}[{name}]{process.RESET} Starting...")
310
+ process.start(name, svc, detach=True, project_dir=conf.project_dir, verbose=args.verbose)
311
+ started_names.append(name)
312
+ for name in started_names:
313
+ svc = conf.services[name]
314
+ i = list(conf.services.keys()).index(name)
315
+ color = process.COLORS[i % len(process.COLORS)]
316
+ result = process.wait_ready(name, svc, project_dir=conf.project_dir)
317
+ if result is None:
318
+ process.clear_pid(name)
319
+ print(f"{color}[{name}]{process.RESET} Process exited immediately.", file=sys.stderr)
320
+ elif result:
321
+ print(f"{color}[{name}]{process.RESET} Ready (pid {process.read_pid(name)})")
322
+ else:
323
+ print(f"{color}[{name}]{process.RESET} Timed out waiting for ready", file=sys.stderr)
324
+
325
+ def _shutdown_started() -> None:
326
+ if not started_names:
327
+ return
328
+ print("\nStopping services...")
329
+ for name in reversed(started_names):
330
+ if process.stop(name):
331
+ print(f" Stopped {name}")
332
+
333
+ if not jsonl.exists():
334
+ if not started_names:
335
+ _err_console.print("[yellow]no logs found[/yellow]")
336
+ return
337
+ deadline = time.time() + 5
338
+ while not jsonl.exists() and time.time() < deadline:
339
+ time.sleep(0.1)
340
+ if not jsonl.exists():
341
+ process.LOG_DIR.mkdir(parents=True, exist_ok=True)
342
+ jsonl.touch()
343
+
344
+ def _fmt(entry: dict) -> str:
345
+ s = entry.get("s", "?")
346
+ color = process.COLORS[list(conf.services).index(s) % len(process.COLORS)] if s in conf.services else ""
347
+ ts = f" {entry.get('t','')[:23]} " if args.timestamp else ""
348
+ return f"{color}[{s}]{process.RESET}{ts} {entry.get('m','')}"
349
+
350
+ try:
351
+ with open(jsonl, encoding="utf-8") as f:
352
+ f.seek(log_start_pos)
353
+ while True:
354
+ line = f.readline()
355
+ if line:
356
+ try:
357
+ entry = json.loads(line)
358
+ except json.JSONDecodeError:
359
+ continue
360
+ print(_fmt(entry))
361
+ sys.stdout.flush()
362
+ else:
363
+ time.sleep(0.05)
364
+ except KeyboardInterrupt:
365
+ pass
366
+ finally:
367
+ _shutdown_started()
368
+
369
+
280
370
  def cmd_down(args: argparse.Namespace) -> None:
281
371
  conf = _load(args)
282
- targets = _resolve_services(args.names, conf) if args.names else _default_services(conf)
372
+ if args.all:
373
+ if args.names:
374
+ _err_console.print("[bold red]Error:[/bold red] NAME と --all は同時に指定できません")
375
+ sys.exit(1)
376
+ targets = list(conf.services.keys())
377
+ elif args.names:
378
+ targets = _resolve_services(args.names, conf)
379
+ else:
380
+ targets = _default_services(conf)
381
+ if targets is None:
382
+ _no_default_group(conf)
383
+ return
283
384
  for name in reversed(targets):
284
385
  if process.stop(name):
285
386
  print(f"Stopped {name}")
@@ -289,7 +390,13 @@ def cmd_down(args: argparse.Namespace) -> None:
289
390
 
290
391
  def cmd_restart(args: argparse.Namespace) -> None:
291
392
  conf = _load(args)
292
- targets = _resolve_services(args.names, conf) if args.names else _default_services(conf)
393
+ if args.names:
394
+ targets = _resolve_services(args.names, conf)
395
+ else:
396
+ targets = _default_services(conf)
397
+ if targets is None:
398
+ _no_default_group(conf)
399
+ return
293
400
  for name in reversed(targets):
294
401
  process.stop(name)
295
402
  for i, name in enumerate(targets):
@@ -337,59 +444,13 @@ def cmd_logs(args: argparse.Namespace) -> None:
337
444
  ts = f" {entry.get('t','')[:23]} " if args.timestamp else ""
338
445
  return f"{color}[{s}]{process.RESET}{ts} {entry.get('m','')}"
339
446
 
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
447
  if args.follow:
378
- # --up でサービスを起動した場合はログファイルが存在するまで少し待つ
379
448
  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()
449
+ _err_console.print("[yellow]no logs found — run 'procd up -d' first[/yellow]")
450
+ return
389
451
  try:
390
452
  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)
453
+ f.seek(0, 2) # 末尾からフォロー
393
454
  while True:
394
455
  line = f.readline()
395
456
  if line:
@@ -401,8 +462,6 @@ def cmd_logs(args: argparse.Namespace) -> None:
401
462
  time.sleep(0.05)
402
463
  except KeyboardInterrupt:
403
464
  pass
404
- finally:
405
- _shutdown_started()
406
465
  else:
407
466
  if not jsonl.exists():
408
467
  return
@@ -461,24 +520,42 @@ def cmd_run(args: argparse.Namespace) -> None:
461
520
 
462
521
  def cmd_list(args: argparse.Namespace) -> None:
463
522
  conf = _load(args)
523
+ _print_list(conf)
464
524
 
465
- if conf.services:
466
- _console.print("[bold]Services[/bold]")
467
- for name in conf.services:
468
- _console.print(f" [cyan]{name}[/cyan]")
469
- _console.print()
470
525
 
471
- if conf.groups:
472
- _console.print("[bold]Groups[/bold]")
473
- max_len = max(len(n) for n in conf.groups)
474
- for name, grp in conf.groups.items():
475
- members = ", ".join(grp.services)
476
- _console.print(f" [cyan]{name:<{max_len}}[/cyan] [dim]{members}[/dim]")
477
- _console.print()
526
+ def _print_list(conf: cfg.Config) -> None:
527
+ if not conf.services and not conf.groups:
528
+ return
529
+ from rich.table import Table
530
+ table = Table(show_header=True, header_style="bold")
531
+ table.add_column("NAME", style="cyan")
532
+ table.add_column("TYPE")
533
+ table.add_column("SERVICES")
534
+ for name in conf.services:
535
+ table.add_row(name, "service", "")
536
+ for name, grp in conf.groups.items():
537
+ table.add_row(name, "group", ", ".join(grp.services))
538
+ _console.print(table)
478
539
 
479
540
 
480
541
  def cmd_get(args: argparse.Namespace) -> None:
481
542
  conf = _load(args)
543
+ if not args.names:
544
+ if not conf.downloads:
545
+ _console.print("[yellow]No downloads defined in procd.toml.[/yellow]")
546
+ return
547
+ from rich.table import Table
548
+ table = Table(show_header=True, header_style="bold")
549
+ table.add_column("NAME", style="cyan")
550
+ table.add_column("STATUS")
551
+ for name in conf.downloads:
552
+ if dl_mod.is_installed(name, conf):
553
+ status = "[green]installed[/green]"
554
+ else:
555
+ status = "[red]not installed[/red]"
556
+ table.add_row(name, status)
557
+ _console.print(table)
558
+ return
482
559
  unknown = [n for n in args.names if n not in conf.downloads]
483
560
  if unknown:
484
561
  for n in unknown:
@@ -490,6 +567,55 @@ def cmd_get(args: argparse.Namespace) -> None:
490
567
  dl_mod.do_get_parallel(args.names, conf, force=args.force)
491
568
 
492
569
 
570
+ def _print_clean_preview(conf: cfg.Config) -> None:
571
+ stale = process.stale_pid_names()
572
+ if stale:
573
+ _console.print("[bold]Stale PID files[/bold] ([dim]--pids で削除[/dim])")
574
+ for name in stale:
575
+ _console.print(f" [cyan]{name}[/cyan]")
576
+ else:
577
+ _console.print("[dim]No stale PID files.[/dim]")
578
+ _console.print()
579
+ installed = [n for n in conf.downloads if dl_mod.is_installed(n, conf)]
580
+ if installed:
581
+ _console.print("[bold]Downloaded[/bold] ([dim]--downloads で削除[/dim])")
582
+ for name in installed:
583
+ _console.print(f" [cyan]{name}[/cyan]")
584
+ else:
585
+ _console.print("[dim]No downloaded files.[/dim]")
586
+
587
+
588
+ def cmd_clean(args: argparse.Namespace) -> None:
589
+ conf = _load(args)
590
+ do_pids = args.all or args.pids
591
+ do_downloads = args.all or args.downloads is not None
592
+
593
+ if not do_pids and not do_downloads:
594
+ _print_clean_preview(conf)
595
+ return
596
+
597
+ if do_pids:
598
+ stale = process.stale_pid_names()
599
+ if not stale:
600
+ print("No stale PID files.")
601
+ for name in stale:
602
+ process.clear_pid(name)
603
+ print(f"Removed stale pid file: {name}")
604
+
605
+ if do_downloads:
606
+ names = args.downloads if (not args.all and args.downloads) else list(conf.downloads.keys())
607
+ unknown = [n for n in names if n not in conf.downloads]
608
+ if unknown:
609
+ for n in unknown:
610
+ _err_console.print(f"[bold red]Error:[/bold red] unknown opt: {n}")
611
+ sys.exit(1)
612
+ for name in names:
613
+ if dl_mod.remove(name, conf):
614
+ print(f"Removed download: {name}")
615
+ else:
616
+ print(f"{name} was not downloaded")
617
+
618
+
493
619
  _INIT_TEMPLATE = '''\
494
620
  # ============================================================
495
621
  # Procd ローカルサービス定義
@@ -589,10 +715,10 @@ def cmd_link(args: argparse.Namespace) -> None:
589
715
  conf = _load(args)
590
716
  project_dir = conf.project_dir
591
717
 
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 ''}"
718
+ if args.up_follow is not None:
719
+ names = " ".join(args.up_follow)
720
+ procd_sub = f"up --follow{' ' + names if names else ''}"
721
+ shortcut_name = f"procd up --follow{' ' + names if names else ''}"
596
722
  elif args.up is not None:
597
723
  names = " ".join(args.up)
598
724
  procd_sub = f"up{' ' + names if names else ''}"
@@ -632,12 +758,17 @@ def build_parser() -> argparse.ArgumentParser:
632
758
  # up
633
759
  p = sub.add_parser("up", help="サービスを起動する")
634
760
  p.add_argument("names", nargs="*", metavar="NAME")
635
- p.add_argument("-d", "--detach", action="store_true", help="バックグラウンド起動")
761
+ group = p.add_mutually_exclusive_group()
762
+ group.add_argument("-d", "--detach", action="store_true", help="バックグラウンド起動")
763
+ group.add_argument("-f", "--follow", action="store_true",
764
+ help="バックグラウンド起動してログをフォローし、終了時にサービスも停止する")
765
+ p.add_argument("-t", "--timestamp", action="store_true", help="--follow 時にタイムスタンプを表示する")
636
766
  p.set_defaults(func=cmd_up)
637
767
 
638
768
  # down
639
769
  p = sub.add_parser("down", help="サービスを停止する")
640
770
  p.add_argument("names", nargs="*", metavar="NAME")
771
+ p.add_argument("-a", "--all", action="store_true", help="default グループの定義に関わらず全サービスを停止する")
641
772
  p.set_defaults(func=cmd_down)
642
773
 
643
774
  # restart
@@ -655,8 +786,6 @@ def build_parser() -> argparse.ArgumentParser:
655
786
  p.add_argument("-f", "--follow", action="store_true", help="tail -f スタイルで追跡")
656
787
  p.add_argument("-n", "--lines", type=int, default=200, metavar="N", help="表示する行数(デフォルト: 200)")
657
788
  p.add_argument("-t", "--timestamp", action="store_true", help="タイムスタンプを表示する")
658
- p.add_argument("--up", nargs="*", metavar="NAME",
659
- help="サービスを起動してからログをフォローし、終了時にサービスも停止する")
660
789
  p.set_defaults(func=cmd_logs)
661
790
 
662
791
  # run
@@ -675,17 +804,26 @@ def build_parser() -> argparse.ArgumentParser:
675
804
 
676
805
  # get
677
806
  p = sub.add_parser("get", help="[opt.*] をダウンロード・展開する")
678
- p.add_argument("names", nargs="+", metavar="NAME")
807
+ p.add_argument("names", nargs="*", metavar="NAME",
808
+ help="ダウンロードする opt 名(省略時は一覧を表示する)")
679
809
  p.add_argument("--force", action="store_true", help="既存でも強制的に再ダウンロードする")
680
810
  p.set_defaults(func=cmd_get)
681
811
 
812
+ # clean
813
+ p = sub.add_parser("clean", help="stale な pid ファイルやダウンロード済みファイルを削除する")
814
+ p.add_argument("--pids", action="store_true", help="動いていないプロセスの pid ファイルを削除する")
815
+ p.add_argument("--downloads", nargs="*", metavar="NAME",
816
+ help="ダウンロード済みファイルを削除する(NAME 省略で全件)")
817
+ p.add_argument("--all", action="store_true", help="--pids と --downloads の両方を実行する")
818
+ p.set_defaults(func=cmd_clean)
819
+
682
820
  # link
683
821
  p = sub.add_parser("link", help="デスクトップショートカットを作成する")
684
822
  group = p.add_mutually_exclusive_group(required=True)
685
823
  group.add_argument("--up", nargs="*", metavar="NAME",
686
824
  help="procd up のショートカット(NAME 省略で default グループ)")
687
- group.add_argument("--logs-up", nargs="*", metavar="NAME", dest="logs_up",
688
- help="procd logs --up のショートカット(NAME 省略で default グループ)")
825
+ group.add_argument("--up-follow", nargs="*", metavar="NAME", dest="up_follow",
826
+ help="procd up --follow のショートカット(NAME 省略で default グループ)")
689
827
  group.add_argument("--run", metavar="NAME",
690
828
  help="procd run <NAME> のショートカット")
691
829
  p.add_argument("--icon", metavar="FILE", help="アイコンファイル (.ico / .icns)")
@@ -5,6 +5,7 @@ cli.cmd_get と builtin._do_get の両方から呼ばれる。
5
5
  from __future__ import annotations
6
6
 
7
7
  import hashlib
8
+ import shutil
8
9
  import stat
9
10
  import sys
10
11
  import tarfile
@@ -43,6 +44,25 @@ def _already_exists(dl: "DownloadConfig", dest: Path, url_lower: str) -> bool:
43
44
  return (dest / Path(dl.url.split("?")[0]).name).exists()
44
45
 
45
46
 
47
+ def is_installed(name: str, conf: "Config") -> bool:
48
+ """opt.<name> が展開済み(ダウンロード済み)かどうかを返す。"""
49
+ dl = conf.downloads[name]
50
+ dest = Path(dl.dest)
51
+ if not dest.exists():
52
+ return False
53
+ url_lower = dl.url.lower().split("?")[0]
54
+ return _already_exists(dl, dest, url_lower)
55
+
56
+
57
+ def remove(name: str, conf: "Config") -> bool:
58
+ """opt.<name> の展開先ディレクトリを削除する。存在しなければ False。"""
59
+ dest = Path(conf.downloads[name].dest)
60
+ if not dest.exists():
61
+ return False
62
+ shutil.rmtree(dest)
63
+ return True
64
+
65
+
46
66
  def _finalize(name: str, dl: "DownloadConfig", tmp: Path, dest: Path, url_lower: str) -> None:
47
67
  """verify → extract/rename → 後処理。do_get / do_get_parallel の共通後半処理。"""
48
68
  _verify_checksum(tmp, dl.checksum, name)
@@ -60,30 +60,63 @@ def _append_jsonl(name: str, message: str) -> None:
60
60
 
61
61
  def write_pid(name: str, pid: int) -> None:
62
62
  RUN_DIR.mkdir(parents=True, exist_ok=True)
63
- pid_path(name).write_text(str(pid))
63
+ try:
64
+ create_time = psutil.Process(pid).create_time()
65
+ except psutil.NoSuchProcess:
66
+ create_time = 0.0
67
+ pid_path(name).write_text(f"{pid}:{create_time}")
64
68
 
65
69
 
66
- def read_pid(name: str) -> int | None:
70
+ def _read_pid_file(name: str) -> tuple[int, float | None] | None:
71
+ """pid ファイルを (pid, create_time) で読む。旧形式(PID のみ)なら create_time は None。"""
67
72
  p = pid_path(name)
68
73
  if not p.exists():
69
74
  return None
75
+ text = p.read_text().strip()
70
76
  try:
71
- return int(p.read_text().strip())
77
+ if ":" in text:
78
+ pid_str, ct_str = text.split(":", 1)
79
+ return int(pid_str), float(ct_str)
80
+ return int(text), None
72
81
  except ValueError:
73
82
  return None
74
83
 
75
84
 
85
+ def read_pid(name: str) -> int | None:
86
+ parsed = _read_pid_file(name)
87
+ return parsed[0] if parsed else None
88
+
89
+
76
90
  def clear_pid(name: str) -> None:
77
91
  p = pid_path(name)
78
92
  if p.exists():
79
93
  p.unlink()
80
94
 
81
95
 
96
+ def stale_pid_names() -> list[str]:
97
+ """var/run/*.pid のうち、対応するプロセスが実際には動いていない名前一覧。"""
98
+ if not RUN_DIR.exists():
99
+ return []
100
+ return [p.stem for p in sorted(RUN_DIR.glob("*.pid")) if not is_running(p.stem)]
101
+
102
+
103
+ def _is_same_process(pid: int, create_time: float | None) -> bool:
104
+ """PID が存在し、記録した起動時刻と一致するか(PID 再利用検出)。"""
105
+ if not psutil.pid_exists(pid):
106
+ return False
107
+ if create_time is None:
108
+ return True # 旧形式の pid ファイル: create_time 比較をスキップ
109
+ try:
110
+ return abs(psutil.Process(pid).create_time() - create_time) < 1
111
+ except psutil.NoSuchProcess:
112
+ return False
113
+
114
+
82
115
  def is_running(name: str) -> bool:
83
- pid = read_pid(name)
84
- if pid is None:
116
+ parsed = _read_pid_file(name)
117
+ if parsed is None:
85
118
  return False
86
- return psutil.pid_exists(pid)
119
+ return _is_same_process(*parsed)
87
120
 
88
121
 
89
122
  # ---------- プロセス起動・停止 ----------
@@ -205,11 +238,12 @@ def _kill_tree(pid: int) -> None:
205
238
 
206
239
 
207
240
  def stop(name: str) -> bool:
208
- """PID ファイルを元にプロセスを停止する。既に停止済みなら False を返す。"""
209
- pid = read_pid(name)
210
- if pid is None:
241
+ """PID ファイルを元にプロセスを停止する。既に停止済み、または PID が別プロセスに再利用されている場合は False を返す。"""
242
+ parsed = _read_pid_file(name)
243
+ if parsed is None:
211
244
  return False
212
- if not psutil.pid_exists(pid):
245
+ pid, create_time = parsed
246
+ if not _is_same_process(pid, create_time):
213
247
  clear_pid(name)
214
248
  return False
215
249
  _kill_tree(pid)
@@ -246,9 +280,8 @@ def wait_ready(name: str, svc: "ServiceConfig", timeout: int = 30, project_dir:
246
280
  port = int(port_str)
247
281
  deadline = time.time() + timeout
248
282
  while time.time() < deadline:
249
- pid = read_pid(name)
250
- if pid is not None and not psutil.pid_exists(pid):
251
- return None # 起動直後にクラッシュ
283
+ if not is_running(name):
284
+ return None # 起動直後にクラッシュ(または PID が別プロセスに再利用された)
252
285
  try:
253
286
  with socket.create_connection((host, port), timeout=1):
254
287
  return True
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes