evo-cli 0.28.0__tar.gz → 0.28.1__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.
Files changed (147) hide show
  1. {evo_cli-0.28.0 → evo_cli-0.28.1}/PKG-INFO +1 -1
  2. evo_cli-0.28.1/evo_cli/VERSION +1 -0
  3. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/openvpn.py +3 -1
  4. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/openvpn.py +53 -5
  5. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli.egg-info/PKG-INFO +1 -1
  6. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_openvpn.py +44 -3
  7. evo_cli-0.28.0/evo_cli/VERSION +0 -1
  8. {evo_cli-0.28.0 → evo_cli-0.28.1}/Containerfile +0 -0
  9. {evo_cli-0.28.0 → evo_cli-0.28.1}/HISTORY.md +0 -0
  10. {evo_cli-0.28.0 → evo_cli-0.28.1}/LICENSE +0 -0
  11. {evo_cli-0.28.0 → evo_cli-0.28.1}/MANIFEST.in +0 -0
  12. {evo_cli-0.28.0 → evo_cli-0.28.1}/README.md +0 -0
  13. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/__init__.py +0 -0
  14. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/__main__.py +0 -0
  15. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/base.py +0 -0
  16. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/cli.py +0 -0
  17. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/__init__.py +0 -0
  18. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/agent_toy.py +0 -0
  19. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/claude_code.py +0 -0
  20. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/cloudflare.py +0 -0
  21. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/cred.py +0 -0
  22. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/download.py +0 -0
  23. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/fix_claude.py +0 -0
  24. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/gdrive.py +0 -0
  25. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/gh.py +0 -0
  26. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/__init__.py +0 -0
  27. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/_dag.py +0 -0
  28. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/_git.py +0 -0
  29. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/_model.py +0 -0
  30. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/_mutate.py +0 -0
  31. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/_paths.py +0 -0
  32. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/_render.py +0 -0
  33. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/_server.py +0 -0
  34. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/check.py +0 -0
  35. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/clone.py +0 -0
  36. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/edit.py +0 -0
  37. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/pull.py +0 -0
  38. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/serve.py +0 -0
  39. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/show.py +0 -0
  40. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/views.py +0 -0
  41. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/index-Cs7hXRPJ.css +0 -0
  42. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/index-CuNOlaH7.js +0 -0
  43. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  44. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  45. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  46. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  47. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  48. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  49. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  50. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  51. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  52. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  53. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  54. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  55. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/harness/web/index.html +0 -0
  56. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/hwid.py +0 -0
  57. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/hwid_reset.py +0 -0
  58. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/image.py +0 -0
  59. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/localproxy.py +0 -0
  60. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/mcp.py +0 -0
  61. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/miniconda.py +0 -0
  62. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/netcheck.py +0 -0
  63. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/ngrok.py +0 -0
  64. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/opencode.py +0 -0
  65. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/plantuml.py +0 -0
  66. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/route/__init__.py +0 -0
  67. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/route/_dns.py +0 -0
  68. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/route/_dpi.py +0 -0
  69. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/route/_hosts.py +0 -0
  70. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/route/_probe.py +0 -0
  71. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/route/check.py +0 -0
  72. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/route/fix.py +0 -0
  73. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/route/probe.py +0 -0
  74. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/route/status.py +0 -0
  75. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/serp.py +0 -0
  76. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/site2s.py +0 -0
  77. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/ssh.py +0 -0
  78. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/storage.py +0 -0
  79. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/sysmon.py +0 -0
  80. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/tts.py +0 -0
  81. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/update.py +0 -0
  82. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/commands/wifi.py +0 -0
  83. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/console.py +0 -0
  84. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/credentials/__init__.py +0 -0
  85. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/credentials/doctor.py +0 -0
  86. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/credentials/google_oauth.py +0 -0
  87. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/credentials/migrate.py +0 -0
  88. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/credentials/oauth_flow.py +0 -0
  89. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/credentials/registry.py +0 -0
  90. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/credentials/store.py +0 -0
  91. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/credentials/sync.py +0 -0
  92. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/imaging/__init__.py +0 -0
  93. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/imaging/align.py +0 -0
  94. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/imaging/core.py +0 -0
  95. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/imaging/creds.py +0 -0
  96. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/imaging/errors.py +0 -0
  97. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/imaging/gemini.py +0 -0
  98. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/imaging/ncnn.py +0 -0
  99. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/mcp_registry.py +0 -0
  100. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/serp/__init__.py +0 -0
  101. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/serp/client.py +0 -0
  102. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/serp/creds.py +0 -0
  103. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/serp/errors.py +0 -0
  104. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/serp/install.py +0 -0
  105. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/serp/render.py +0 -0
  106. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/tts/__init__.py +0 -0
  107. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/tts/chunking.py +0 -0
  108. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/tts/core.py +0 -0
  109. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/tts/creds.py +0 -0
  110. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/tts/errors.py +0 -0
  111. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/tts/gemini.py +0 -0
  112. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/tts/http.py +0 -0
  113. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/tts/openai.py +0 -0
  114. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/tts/player.py +0 -0
  115. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli/tts/vbee.py +0 -0
  116. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli.egg-info/SOURCES.txt +0 -0
  117. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli.egg-info/dependency_links.txt +0 -0
  118. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli.egg-info/entry_points.txt +0 -0
  119. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli.egg-info/requires.txt +0 -0
  120. {evo_cli-0.28.0 → evo_cli-0.28.1}/evo_cli.egg-info/top_level.txt +0 -0
  121. {evo_cli-0.28.0 → evo_cli-0.28.1}/pyproject.toml +0 -0
  122. {evo_cli-0.28.0 → evo_cli-0.28.1}/setup.cfg +0 -0
  123. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/__init__.py +0 -0
  124. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_agent_toy.py +0 -0
  125. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_claude_code.py +0 -0
  126. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_cli.py +0 -0
  127. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_console.py +0 -0
  128. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_cred.py +0 -0
  129. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_download.py +0 -0
  130. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_fix_claude.py +0 -0
  131. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_gdrive.py +0 -0
  132. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_gh.py +0 -0
  133. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_harness.py +0 -0
  134. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_harness_clone.py +0 -0
  135. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_harness_dag.py +0 -0
  136. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_image.py +0 -0
  137. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_mcp.py +0 -0
  138. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_ngrok.py +0 -0
  139. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_opencode.py +0 -0
  140. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_plantuml.py +0 -0
  141. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_route.py +0 -0
  142. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_serp.py +0 -0
  143. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_storage.py +0 -0
  144. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_sysmon.py +0 -0
  145. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_tts.py +0 -0
  146. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_update.py +0 -0
  147. {evo_cli-0.28.0 → evo_cli-0.28.1}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.28.0
3
+ Version: 0.28.1
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -0,0 +1 @@
1
+ 0.28.1
@@ -294,12 +294,14 @@ def disconnect(name):
294
294
 
295
295
  def _disconnect(name):
296
296
  state = _guard(vpn.request, name, "disconnect")
297
+ if state["state"] == "STOPPED" and state.get("error"):
298
+ click.echo(f"{name} was not running; last error: {state['error']}", err=True)
297
299
  deadline = time.monotonic() + 20
298
300
  while state["state"] != "STOPPED" and time.monotonic() < deadline:
299
301
  time.sleep(0.2)
300
302
  state = _guard(vpn.request, name)
301
303
  if state["state"] != "STOPPED":
302
- raise click.ClickException("Disconnect not confirmed; inspect evo openvpn status.")
304
+ raise click.ClickException(state.get("error") or "Disconnect not confirmed; inspect evo openvpn status.")
303
305
  click.echo(f"{name}: stopped (OpenVPN Connect profiles unchanged).")
304
306
 
305
307
 
@@ -366,6 +366,58 @@ def last_profile():
366
366
  return None
367
367
 
368
368
 
369
+ def _worker_alive(base):
370
+ """The worker holds its lock for life, and the kernel releases it however the worker dies."""
371
+ import fcntl
372
+
373
+ try:
374
+ with open(str(base) + ".lock") as lock:
375
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
376
+ except FileNotFoundError:
377
+ return False
378
+ except BlockingIOError:
379
+ return True
380
+ return False
381
+
382
+
383
+ def _config_pids(base):
384
+ """PIDs whose command line names this session's config (OpenVPN, its sudo or wrapper); None if unknown."""
385
+ config = str(base) + ".ovpn"
386
+ try:
387
+ output = subprocess.run(
388
+ ["ps", "-A", "-o", "pid=", "-o", "args="], capture_output=True, text=True, timeout=5, check=True
389
+ ).stdout
390
+ except (OSError, subprocess.SubprocessError):
391
+ return None
392
+ return [line.split(None, 1)[0] for line in output.splitlines() if config in line]
393
+
394
+
395
+ def _reap(name, last):
396
+ """Finish the cleanup of a worker that died without it (SIGKILL, SIGTERM), once nothing of it runs.
397
+
398
+ Otherwise its last published state would read as UNKNOWN forever, blocking connect and disconnect.
399
+ """
400
+ base = session_path(name)
401
+ if _worker_alive(base):
402
+ return {"profile": name, "state": "UNKNOWN", "error": "Worker unavailable; inspect OpenVPN processes."}
403
+ pids = _config_pids(base)
404
+ if pids is None:
405
+ return {"profile": name, "state": "UNKNOWN", "error": "Worker gone; cannot list processes to confirm."}
406
+ if pids:
407
+ listed = " ".join(pids)
408
+ error = f"OpenVPN still runs without its worker (PID {listed}); stop it with: sudo kill {listed}"
409
+ return {"profile": name, "state": "UNKNOWN", "error": error}
410
+ for suffix in (".sock", ".mgmt", ".ovpn"):
411
+ Path(str(base) + suffix).unlink(missing_ok=True)
412
+ state = {
413
+ "profile": name,
414
+ "state": "STOPPED",
415
+ "error": f"Worker exited unexpectedly (last state {last['state']}); no OpenVPN process was left running.",
416
+ }
417
+ private_write(Path(str(base) + ".json"), json.dumps(state))
418
+ return state
419
+
420
+
369
421
  def request(name, command="status"):
370
422
  endpoint = str(session_path(name)) + ".sock"
371
423
  with socket.socket(socket.AF_UNIX) as client:
@@ -377,11 +429,7 @@ def request(name, command="status"):
377
429
  if path.exists():
378
430
  last = json.loads(path.read_text())
379
431
  if last["state"] not in ("FAILED", "STOPPED") or last.get("cleanup_failed"):
380
- return {
381
- "profile": name,
382
- "state": "UNKNOWN",
383
- "error": "Worker unavailable; inspect OpenVPN processes.",
384
- }
432
+ return _reap(name, last)
385
433
  if last.get("error"):
386
434
  return {"profile": name, "state": "STOPPED", "error": last["error"]}
387
435
  return {"profile": name, "state": "STOPPED"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.28.0
3
+ Version: 0.28.1
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -258,10 +258,51 @@ def test_plain_password_escaping_and_no_unrequested_otp():
258
258
  vpn.quote("x\nsignal SIGTERM")
259
259
 
260
260
 
261
- def test_stale_state_is_not_connected_or_confirmed_stopped(store):
262
- path = Path(str(vpn.session_path("m1")) + ".json")
263
- vpn.private_write(path, '{"state":"CONNECTED"}')
261
+ def _ps(monkeypatch, output):
262
+ def run(command, **kwargs):
263
+ assert command[0] == "ps"
264
+ if output is None:
265
+ raise OSError("no ps")
266
+ return subprocess.CompletedProcess(command, 0, stdout=output)
267
+
268
+ monkeypatch.setattr(vpn.subprocess, "run", run)
269
+
270
+
271
+ def test_stale_state_is_not_connected_or_confirmed_stopped(store, monkeypatch):
272
+ import fcntl
273
+
274
+ base = str(vpn.session_path("m1"))
275
+ vpn.private_write(Path(base + ".json"), '{"state":"CONNECTED"}')
276
+ _ps(monkeypatch, "")
277
+ with open(base + ".lock", "a") as held:
278
+ fcntl.flock(held, fcntl.LOCK_EX | fcntl.LOCK_NB)
279
+ assert vpn.request("m1")["state"] == "UNKNOWN" # the worker lives but does not answer
280
+ _ps(monkeypatch, None)
264
281
  assert vpn.request("m1")["state"] == "UNKNOWN"
282
+ _ps(monkeypatch, f" 4242 /usr/sbin/openvpn --config {base}.ovpn --verb 3\n 4300 -zsh\n")
283
+ state = vpn.request("m1")
284
+ assert state["state"] == "UNKNOWN" and state["error"].endswith("sudo kill 4242")
285
+
286
+
287
+ def test_dead_worker_is_reaped_and_disconnect_confirms_it(store, monkeypatch):
288
+ import socket
289
+
290
+ vpn.save_profile("m1", {"config": CONFIG})
291
+ base = str(vpn.session_path("m1"))
292
+ vpn.private_write(Path(base + ".json"), '{"profile":"m1","state":"CONNECTED","vpn_ip":"10.8.0.2"}')
293
+ # What a SIGKILLed worker leaves: a socket nobody listens on, the management socket and the config.
294
+ with socket.socket(socket.AF_UNIX) as stale:
295
+ stale.bind(base + ".sock")
296
+ for suffix in (".mgmt", ".ovpn", ".lock"):
297
+ Path(base + suffix).touch()
298
+ _ps(monkeypatch, " 4300 -zsh\n")
299
+ result = CliRunner().invoke(cli, ["openvpn", "disconnect", "m1"])
300
+ assert result.exit_code == 0, result.output
301
+ assert "m1 was not running; last error: Worker exited unexpectedly (last state CONNECTED)" in result.output
302
+ assert "m1: stopped" in result.output
303
+ assert not any(Path(base + suffix).exists() for suffix in (".sock", ".mgmt", ".ovpn"))
304
+ assert json.loads(Path(base + ".json").read_text())["state"] == "STOPPED"
305
+ assert vpn.request("m1")["state"] == "STOPPED" # so connect and import accept the profile again
265
306
 
266
307
 
267
308
  FAKE_OPENVPN = """
@@ -1 +0,0 @@
1
- 0.28.0
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
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
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes