evo-cli 0.26.0__tar.gz → 0.26.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 (145) hide show
  1. {evo_cli-0.26.0 → evo_cli-0.26.1}/PKG-INFO +5 -1
  2. {evo_cli-0.26.0 → evo_cli-0.26.1}/README.md +4 -0
  3. evo_cli-0.26.1/evo_cli/VERSION +1 -0
  4. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/localproxy.py +6 -6
  5. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/openvpn.py +13 -0
  6. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/ssh.py +1 -1
  7. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/openvpn.py +21 -3
  8. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/PKG-INFO +5 -1
  9. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_openvpn.py +41 -8
  10. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_route.py +1 -1
  11. evo_cli-0.26.0/evo_cli/VERSION +0 -1
  12. {evo_cli-0.26.0 → evo_cli-0.26.1}/Containerfile +0 -0
  13. {evo_cli-0.26.0 → evo_cli-0.26.1}/HISTORY.md +0 -0
  14. {evo_cli-0.26.0 → evo_cli-0.26.1}/LICENSE +0 -0
  15. {evo_cli-0.26.0 → evo_cli-0.26.1}/MANIFEST.in +0 -0
  16. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/__init__.py +0 -0
  17. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/__main__.py +0 -0
  18. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/base.py +0 -0
  19. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/cli.py +0 -0
  20. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/__init__.py +0 -0
  21. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/agent_toy.py +0 -0
  22. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/claude_code.py +0 -0
  23. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/cloudflare.py +0 -0
  24. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/cred.py +0 -0
  25. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/download.py +0 -0
  26. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/fix_claude.py +0 -0
  27. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/gdrive.py +0 -0
  28. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/gh.py +0 -0
  29. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/__init__.py +0 -0
  30. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_dag.py +0 -0
  31. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_git.py +0 -0
  32. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_model.py +0 -0
  33. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_mutate.py +0 -0
  34. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_paths.py +0 -0
  35. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_render.py +0 -0
  36. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_server.py +0 -0
  37. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/check.py +0 -0
  38. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/clone.py +0 -0
  39. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/edit.py +0 -0
  40. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/pull.py +0 -0
  41. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/serve.py +0 -0
  42. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/show.py +0 -0
  43. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/views.py +0 -0
  44. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/index-Cs7hXRPJ.css +0 -0
  45. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/index-CuNOlaH7.js +0 -0
  46. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  47. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  48. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  49. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  50. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  51. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  52. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  53. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  54. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  55. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  56. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  57. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  58. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/index.html +0 -0
  59. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/hwid.py +0 -0
  60. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/hwid_reset.py +0 -0
  61. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/image.py +0 -0
  62. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/mcp.py +0 -0
  63. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/miniconda.py +0 -0
  64. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/netcheck.py +0 -0
  65. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/ngrok.py +0 -0
  66. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/opencode.py +0 -0
  67. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/plantuml.py +0 -0
  68. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/__init__.py +0 -0
  69. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/_dns.py +0 -0
  70. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/_dpi.py +0 -0
  71. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/_hosts.py +0 -0
  72. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/_probe.py +0 -0
  73. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/check.py +0 -0
  74. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/fix.py +0 -0
  75. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/probe.py +0 -0
  76. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/status.py +0 -0
  77. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/serp.py +0 -0
  78. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/site2s.py +0 -0
  79. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/sysmon.py +0 -0
  80. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/tts.py +0 -0
  81. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/update.py +0 -0
  82. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/wifi.py +0 -0
  83. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/console.py +0 -0
  84. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/__init__.py +0 -0
  85. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/doctor.py +0 -0
  86. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/google_oauth.py +0 -0
  87. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/migrate.py +0 -0
  88. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/oauth_flow.py +0 -0
  89. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/registry.py +0 -0
  90. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/store.py +0 -0
  91. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/sync.py +0 -0
  92. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/__init__.py +0 -0
  93. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/align.py +0 -0
  94. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/core.py +0 -0
  95. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/creds.py +0 -0
  96. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/errors.py +0 -0
  97. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/gemini.py +0 -0
  98. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/ncnn.py +0 -0
  99. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/mcp_registry.py +0 -0
  100. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/__init__.py +0 -0
  101. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/client.py +0 -0
  102. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/creds.py +0 -0
  103. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/errors.py +0 -0
  104. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/install.py +0 -0
  105. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/render.py +0 -0
  106. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/__init__.py +0 -0
  107. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/chunking.py +0 -0
  108. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/core.py +0 -0
  109. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/creds.py +0 -0
  110. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/errors.py +0 -0
  111. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/gemini.py +0 -0
  112. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/http.py +0 -0
  113. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/openai.py +0 -0
  114. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/player.py +0 -0
  115. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/vbee.py +0 -0
  116. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/SOURCES.txt +0 -0
  117. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/dependency_links.txt +0 -0
  118. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/entry_points.txt +0 -0
  119. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/requires.txt +0 -0
  120. {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/top_level.txt +0 -0
  121. {evo_cli-0.26.0 → evo_cli-0.26.1}/pyproject.toml +0 -0
  122. {evo_cli-0.26.0 → evo_cli-0.26.1}/setup.cfg +0 -0
  123. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/__init__.py +0 -0
  124. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_agent_toy.py +0 -0
  125. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_claude_code.py +0 -0
  126. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_cli.py +0 -0
  127. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_console.py +0 -0
  128. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_cred.py +0 -0
  129. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_download.py +0 -0
  130. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_fix_claude.py +0 -0
  131. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_gdrive.py +0 -0
  132. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_gh.py +0 -0
  133. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_harness.py +0 -0
  134. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_harness_clone.py +0 -0
  135. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_harness_dag.py +0 -0
  136. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_image.py +0 -0
  137. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_mcp.py +0 -0
  138. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_ngrok.py +0 -0
  139. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_opencode.py +0 -0
  140. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_plantuml.py +0 -0
  141. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_serp.py +0 -0
  142. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_sysmon.py +0 -0
  143. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_tts.py +0 -0
  144. {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_update.py +0 -0
  145. {evo_cli-0.26.0 → evo_cli-0.26.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.26.0
3
+ Version: 0.26.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
@@ -132,6 +132,7 @@ evo openvpn list # numbered table: state, tunnel I
132
132
  evo openvpn connect work # hidden sudo prompt, then a live status view
133
133
  evo openvpn connect 2 # by # from list; omit to pick (last one is default)
134
134
  evo openvpn status -w # watch again later; alerts on drops
135
+ evo openvpn log -n 80 # recent OpenVPN log lines: why it dropped
135
136
  evo openvpn disconnect # picks the active tunnel
136
137
  evo cred sync push
137
138
  ```
@@ -145,6 +146,9 @@ reconnects (`--no-watch` returns immediately). Ctrl-C stops watching and offers
145
146
  disconnect; the tunnel otherwise keeps running. Soft drops (`ping-restart`,
146
147
  `connection-reset`) reconnect automatically with a fresh OTP; the view logs each one and
147
148
  raises a desktop notification, and exits 1 with the reason if the tunnel goes down.
149
+ `evo openvpn log` prints the worker's last 200 OpenVPN log lines, kept in memory only,
150
+ to show why a drop happened. The client sends a keepalive ping every 10 seconds so idle
151
+ TCP sessions survive NAT and firewalls; a keepalive pushed by the server takes precedence.
148
152
 
149
153
  Import each profile under a different name. Imports refuse to overwrite an existing
150
154
  name unless `--replace` is supplied; replacement preserves that profile's credentials.
@@ -99,6 +99,7 @@ evo openvpn list # numbered table: state, tunnel I
99
99
  evo openvpn connect work # hidden sudo prompt, then a live status view
100
100
  evo openvpn connect 2 # by # from list; omit to pick (last one is default)
101
101
  evo openvpn status -w # watch again later; alerts on drops
102
+ evo openvpn log -n 80 # recent OpenVPN log lines: why it dropped
102
103
  evo openvpn disconnect # picks the active tunnel
103
104
  evo cred sync push
104
105
  ```
@@ -112,6 +113,9 @@ reconnects (`--no-watch` returns immediately). Ctrl-C stops watching and offers
112
113
  disconnect; the tunnel otherwise keeps running. Soft drops (`ping-restart`,
113
114
  `connection-reset`) reconnect automatically with a fresh OTP; the view logs each one and
114
115
  raises a desktop notification, and exits 1 with the reason if the tunnel goes down.
116
+ `evo openvpn log` prints the worker's last 200 OpenVPN log lines, kept in memory only,
117
+ to show why a drop happened. The client sends a keepalive ping every 10 seconds so idle
118
+ TCP sessions survive NAT and firewalls; a keepalive pushed by the server takes precedence.
115
119
 
116
120
  Import each profile under a different name. Imports refuse to overwrite an existing
117
121
  name unless `--replace` is supplied; replacement preserves that profile's credentials.
@@ -0,0 +1 @@
1
+ 0.26.1
@@ -2,9 +2,9 @@
2
2
  evo localproxy — HTTPS reverse proxy mapping https://{domain}.local -> https://{domain}.
3
3
 
4
4
  Use case: browse / test a real HTTPS site under a local alias. A request to
5
- ``https://edunext.ptudev.net.local`` is TLS-terminated locally with a leaf cert
5
+ ``https://app.example.com.local`` is TLS-terminated locally with a leaf cert
6
6
  issued by a small local CA, the ``.local`` suffix is stripped, and the request is
7
- forwarded over TLS to the real ``https://edunext.ptudev.net``.
7
+ forwarded over TLS to the real ``https://app.example.com``.
8
8
 
9
9
  Rewrites that make logins / redirects stay inside the alias:
10
10
  - outbound (request): Host, Origin, Referer {domain}.local -> {domain}
@@ -604,10 +604,10 @@ class LocalReverseProxy:
604
604
 
605
605
  EPILOG = Text.from_markup(
606
606
  "[bold]Examples[/bold]\n\n"
607
- " [cyan]evo localproxy edunext.ptudev.net --install-ca[/cyan]\n"
608
- " [cyan]evo localproxy edunext.ptudev.net api.ptudev.net[/cyan]\n"
609
- " [cyan]evo localproxy edunext.ptudev.net -p 8443[/cyan] (if 443 is busy)\n"
610
- " [cyan]evo localproxy edunext.ptudev.net --no-hosts[/cyan]"
607
+ " [cyan]evo localproxy app.example.com --install-ca[/cyan]\n"
608
+ " [cyan]evo localproxy app.example.com api.example.com[/cyan]\n"
609
+ " [cyan]evo localproxy app.example.com -p 8443[/cyan] (if 443 is busy)\n"
610
+ " [cyan]evo localproxy app.example.com --no-hosts[/cyan]"
611
611
  )
612
612
 
613
613
 
@@ -141,6 +141,7 @@ def _event(out, name, state, previous):
141
141
  line.append(f" {detail}")
142
142
  out.print(line)
143
143
  if status == "RECONNECTING":
144
+ out.print(f"[dim] Why: evo openvpn log {name}[/dim]")
144
145
  _notify(f"{name} dropped ({state.get('last_reason', 'unknown')}); reconnecting")
145
146
  elif status == "CONNECTED" and previous != "CONNECTED" and state.get("reconnects"):
146
147
  _notify(f"{name} reconnected")
@@ -273,6 +274,18 @@ def status(name, watch):
273
274
  click.echo(json.dumps(_guard(vpn.request, name), indent=2))
274
275
 
275
276
 
277
+ @openvpn_group.command("log", help="Show recent OpenVPN log lines; kept in the worker's memory only, never on disk.")
278
+ @click.argument("name", required=False)
279
+ @click.option("-n", "--lines", type=click.IntRange(1, 200), default=40, show_default=True)
280
+ def show_log(name, lines):
281
+ name = _pick(name, prefer_active=True)
282
+ state = _guard(vpn.request, name, "log")
283
+ if "log" not in state:
284
+ raise click.ClickException(f"{name} has no running worker; logs exist only while it runs.")
285
+ for line in state["log"][-lines:]:
286
+ click.echo(line)
287
+
288
+
276
289
  @openvpn_group.command("disconnect", help="Disconnect only the named evo-managed connection, not the GUI profiles.")
277
290
  @click.argument("name", required=False)
278
291
  def disconnect(name):
@@ -14,7 +14,7 @@ warnings.filterwarnings("ignore", category=DeprecationWarning)
14
14
  EPILOG = Text.from_markup(
15
15
  "[bold]Examples[/bold]\n\n"
16
16
  " [cyan]evo setupssh[/cyan]\n"
17
- " [cyan]evo setupssh -H 42.96.16.233 -u root[/cyan]\n"
17
+ " [cyan]evo setupssh -H 203.0.113.10 -u root[/cyan]\n"
18
18
  " [cyan]evo setupssh -H host -u root -P 2222 -i ~/.ssh/id_ed25519[/cyan]"
19
19
  )
20
20
 
@@ -15,6 +15,7 @@ import struct
15
15
  import subprocess
16
16
  import sys
17
17
  import time
18
+ from collections import deque
18
19
  from pathlib import Path
19
20
  from urllib.parse import parse_qs, urlparse
20
21
 
@@ -235,7 +236,11 @@ def request(name, command="status"):
235
236
  return {"profile": name, "state": "STOPPED", "error": last["error"]}
236
237
  return {"profile": name, "state": "STOPPED"}
237
238
  client.sendall(command.encode() + b"\n")
238
- return json.loads(client.recv(8192))
239
+ # The worker closes after one reply; a log reply spans several reads.
240
+ chunks = []
241
+ while chunk := client.recv(65536):
242
+ chunks.append(chunk)
243
+ return json.loads(b"".join(chunks))
239
244
 
240
245
 
241
246
  def quote(value):
@@ -275,6 +280,8 @@ def worker(name, timeout):
275
280
  config = Path(str(base) + ".ovpn")
276
281
  control_path, management_path = str(base) + ".sock", str(base) + ".mgmt"
277
282
  state = {"profile": name, "state": "STARTING"}
283
+ # Served over the control socket for diagnosis; kept in memory only, gone with the worker.
284
+ log = deque(maxlen=200)
278
285
  lock = open(runtime_dir() / "active.lock", "a")
279
286
  try:
280
287
  fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
@@ -325,6 +332,9 @@ def worker(name, timeout):
325
332
  "none",
326
333
  "--connect-timeout",
327
334
  str(timeout),
335
+ # Keep idle TCP sessions alive through NAT/firewalls; a server-pushed keepalive overrides it.
336
+ "--ping",
337
+ "10",
328
338
  "--verb",
329
339
  "3",
330
340
  ]
@@ -353,7 +363,8 @@ def worker(name, timeout):
353
363
  state["state"] = "STOPPING"
354
364
  deadline = time.monotonic() + 15
355
365
  publish()
356
- client.sendall(json.dumps(state).encode())
366
+ reply = {**state, "log": list(log)} if command == b"log" else state
367
+ client.sendall(json.dumps(reply).encode())
357
368
  except (OSError, TimeoutError):
358
369
  pass
359
370
  elif key.data == "accept":
@@ -361,7 +372,9 @@ def worker(name, timeout):
361
372
  management.settimeout(2)
362
373
  selector.unregister(listener)
363
374
  selector.register(management, selectors.EVENT_READ, "management")
364
- management.sendall(b"state on\nbytecount 2\nhold release\n")
375
+ # The hold flag survives restarts and OpenVPN resets the release on each one: without
376
+ # "hold off" every reconnect would wait forever for another "hold release".
377
+ management.sendall(b"state on\nlog on\nbytecount 2\nhold off\nhold release\n")
365
378
  if stopping:
366
379
  management.sendall(b"signal SIGTERM\n")
367
380
  else:
@@ -385,6 +398,11 @@ def worker(name, timeout):
385
398
  received, _, sent = line[11:].partition(",")
386
399
  if received.isdigit() and sent.isdigit():
387
400
  state["bytes_in"], state["bytes_out"] = int(received), int(sent)
401
+ elif line.startswith(">LOG:"):
402
+ stamp, _, rest = line[5:].partition(",")
403
+ if stamp.isdigit():
404
+ clock = time.strftime("%H:%M:%S", time.localtime(int(stamp)))
405
+ log.append(f"{clock} {rest.partition(',')[2]}")
388
406
  elif line.startswith(">FATAL:"):
389
407
  raise CredentialError("OpenVPN reported a fatal error; check config, TLS and permissions.")
390
408
  elif line.startswith(">STATE:"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.26.0
3
+ Version: 0.26.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
@@ -132,6 +132,7 @@ evo openvpn list # numbered table: state, tunnel I
132
132
  evo openvpn connect work # hidden sudo prompt, then a live status view
133
133
  evo openvpn connect 2 # by # from list; omit to pick (last one is default)
134
134
  evo openvpn status -w # watch again later; alerts on drops
135
+ evo openvpn log -n 80 # recent OpenVPN log lines: why it dropped
135
136
  evo openvpn disconnect # picks the active tunnel
136
137
  evo cred sync push
137
138
  ```
@@ -145,6 +146,9 @@ reconnects (`--no-watch` returns immediately). Ctrl-C stops watching and offers
145
146
  disconnect; the tunnel otherwise keeps running. Soft drops (`ping-restart`,
146
147
  `connection-reset`) reconnect automatically with a fresh OTP; the view logs each one and
147
148
  raises a desktop notification, and exits 1 with the reason if the tunnel goes down.
149
+ `evo openvpn log` prints the worker's last 200 OpenVPN log lines, kept in memory only,
150
+ to show why a drop happened. The client sends a keepalive ping every 10 seconds so idle
151
+ TCP sessions survive NAT and firewalls; a keepalive pushed by the server takes precedence.
148
152
 
149
153
  Import each profile under a different name. Imports refuse to overwrite an existing
150
154
  name unless `--replace` is supplied; replacement preserves that profile's credentials.
@@ -39,6 +39,7 @@ def test_commands_registered():
39
39
  "connect",
40
40
  "status",
41
41
  "disconnect",
42
+ "log",
42
43
  }
43
44
 
44
45
 
@@ -182,20 +183,37 @@ sys.stdin.readline()
182
183
  with socket.socket(socket.AF_UNIX) as s:
183
184
  s.connect(sys.argv[1])
184
185
  stream = s.makefile('rb')
185
- assert stream.readline() == b'state on\\n'
186
- assert stream.readline() == b'bytecount 2\\n'
187
- assert stream.readline() == b'hold release\\n'
188
- s.sendall(b\">PASSWORD:Need 'Auth' username/password SC:1,OTP\\n\")
189
- assert stream.readline().startswith(b'username ')
190
- assert b'SCRV1:' in stream.readline()
186
+
187
+ # Like OpenVPN 2.6: every (re)start with the hold flag set waits for a fresh "hold release".
188
+ def hold():
189
+ s.sendall(b'>HOLD:Waiting for hold release:0\\n')
190
+ seen = set()
191
+ while (line := stream.readline()) != b'hold release\\n':
192
+ assert line, 'hold never released'
193
+ seen.add(line)
194
+ return seen
195
+
196
+ def auth():
197
+ s.sendall(b\">PASSWORD:Need 'Auth' username/password SC:1,OTP\\n\")
198
+ assert stream.readline().startswith(b'username ')
199
+ assert b'SCRV1:' in stream.readline()
200
+
201
+ seen = hold()
202
+ assert {b'state on\\n', b'log on\\n', b'bytecount 2\\n'} <= seen
203
+ held = b'hold off\\n' not in seen
204
+ auth()
191
205
  if sys.argv[2] == 'fail':
192
206
  s.sendall(b\">PASSWORD:Verification Failed: 'Auth'\\n\")
193
207
  else:
194
208
  s.sendall(b'>PASSWORD:Auth-Token:ephemeral-secret\\n')
209
+ s.sendall(b''.join(b'>LOG:1700000000,I,line %d\\n' % n for n in range(250)))
195
210
  s.sendall(b'>BYTECOUNT:1024,2048\\n')
196
211
  s.sendall(b'>STATE:1,CONNECTED,SUCCESS,10.8.0.2,192.0.2.1\\n')
197
212
  if sys.argv[2] == 'reconnect':
198
- s.sendall(b'>STATE:2,RECONNECTING,ping-restart,,\\n')
213
+ s.sendall(b'>STATE:2,RECONNECTING,connection-reset,,\\n')
214
+ if held:
215
+ hold()
216
+ auth() # --auth-nocache: a restart asks again, with a fresh OTP
199
217
  s.sendall(b'>STATE:3,CONNECTED,SUCCESS,10.8.0.2,192.0.2.1\\n')
200
218
  assert stream.readline() == b'signal SIGTERM\\n'
201
219
  """
@@ -217,6 +235,7 @@ def test_worker_real_sockets_auth_status_shutdown_without_root(store, monkeypatc
217
235
  assert "--script-security" in command and "--management-client" in command
218
236
  # A soft restart (ping-restart, connection-reset) must reconnect, not exit.
219
237
  assert "--remap-usr1" not in command and "--connect-retry-max" not in command
238
+ assert command[command.index("--ping") + 1] == "10"
220
239
  endpoint = command[command.index("--management") + 1]
221
240
  return popen([sys.executable, "-c", FAKE_OPENVPN, endpoint, mode], **kwargs)
222
241
 
@@ -236,7 +255,10 @@ def test_worker_real_sockets_auth_status_shutdown_without_root(store, monkeypatc
236
255
  assert state["since"] and (state["bytes_in"], state["bytes_out"]) == (1024, 2048)
237
256
  assert not fail
238
257
  if mode == "reconnect":
239
- assert state["reconnects"] == 1 and state["last_reason"] == "ping-restart"
258
+ assert state["reconnects"] == 1 and state["last_reason"] == "connection-reset"
259
+ # Only the newest lines are kept, and a reply larger than one read arrives whole.
260
+ log = vpn.request("m1", "log")["log"]
261
+ assert len(log) == 200 and log[-1].endswith(" line 249") and "log" not in vpn.request("m1")
240
262
  vpn.request("m1", "disconnect")
241
263
  break
242
264
  time.sleep(0.05)
@@ -324,6 +346,17 @@ def test_watch_clean_stop_and_ctrl_c_leave_no_alert(store, monkeypatch):
324
346
  assert result.exit_code == 0 and "keeps running in the background" in result.output and notes == []
325
347
 
326
348
 
349
+ def test_log_reads_the_live_worker_only(store, monkeypatch):
350
+ vpn.save_profile("m1", {"config": CONFIG})
351
+ replies = {"log": {"state": "CONNECTED", "log": [f"09:38:{n:02} line {n}" for n in range(50)]}}
352
+ monkeypatch.setattr(vpn, "request", lambda name, command="status": replies.get(command, {"state": "CONNECTED"}))
353
+ result = CliRunner().invoke(cli, ["openvpn", "log", "m1", "-n", "2"])
354
+ assert result.exit_code == 0 and result.output == "09:38:48 line 48\n09:38:49 line 49\n"
355
+ replies.clear()
356
+ result = CliRunner().invoke(cli, ["openvpn", "log", "m1"])
357
+ assert result.exit_code == 1 and "no running worker" in result.output
358
+
359
+
327
360
  def test_qr_decoder_is_local_and_output_is_not_printed(monkeypatch):
328
361
  monkeypatch.setattr(vpn.shutil, "which", lambda _: "/usr/bin/zbarimg")
329
362
  monkeypatch.setattr(
@@ -34,7 +34,7 @@ def test_is_bogus():
34
34
  assert _dns.is_bogus("127.0.0.1")
35
35
  assert _dns.is_bogus("0.0.0.0")
36
36
  assert not _dns.is_bogus("162.159.152.4")
37
- assert not _dns.is_bogus("118.68.82.144")
37
+ assert not _dns.is_bogus("203.0.113.7")
38
38
 
39
39
 
40
40
  def test_encode_qname_round_trip():
@@ -1 +0,0 @@
1
- 0.26.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