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.
- {evo_cli-0.26.0 → evo_cli-0.26.1}/PKG-INFO +5 -1
- {evo_cli-0.26.0 → evo_cli-0.26.1}/README.md +4 -0
- evo_cli-0.26.1/evo_cli/VERSION +1 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/localproxy.py +6 -6
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/openvpn.py +13 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/ssh.py +1 -1
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/openvpn.py +21 -3
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/PKG-INFO +5 -1
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_openvpn.py +41 -8
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_route.py +1 -1
- evo_cli-0.26.0/evo_cli/VERSION +0 -1
- {evo_cli-0.26.0 → evo_cli-0.26.1}/Containerfile +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/HISTORY.md +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/LICENSE +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/MANIFEST.in +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/__init__.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/__main__.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/base.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/cli.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/agent_toy.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/claude_code.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/cred.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/download.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/gh.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/__init__.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_dag.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_git.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_model.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_mutate.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_paths.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_render.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/_server.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/check.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/clone.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/edit.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/pull.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/serve.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/show.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/views.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/index-Cs7hXRPJ.css +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/index-CuNOlaH7.js +0 -0
- {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
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- {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
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- {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
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- {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
- {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
- {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
- {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
- {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
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/harness/web/index.html +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/hwid.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/hwid_reset.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/image.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/mcp.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/ngrok.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/opencode.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/__init__.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/_dns.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/_dpi.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/_hosts.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/_probe.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/check.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/fix.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/probe.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/route/status.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/serp.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/sysmon.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/tts.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/update.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/commands/wifi.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/console.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/__init__.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/doctor.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/google_oauth.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/migrate.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/oauth_flow.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/registry.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/store.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/credentials/sync.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/__init__.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/align.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/core.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/creds.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/errors.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/gemini.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/imaging/ncnn.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/mcp_registry.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/__init__.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/client.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/creds.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/errors.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/install.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/serp/render.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/__init__.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/chunking.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/core.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/creds.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/errors.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/gemini.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/http.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/openai.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/player.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli/tts/vbee.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/SOURCES.txt +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/pyproject.toml +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/setup.cfg +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/__init__.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_agent_toy.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_claude_code.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_cli.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_console.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_cred.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_download.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_gdrive.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_gh.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_harness.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_harness_clone.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_harness_dag.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_image.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_mcp.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_ngrok.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_opencode.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_plantuml.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_serp.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_sysmon.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_tts.py +0 -0
- {evo_cli-0.26.0 → evo_cli-0.26.1}/tests/test_update.py +0 -0
- {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.
|
|
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://
|
|
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://
|
|
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
|
|
608
|
-
" [cyan]evo localproxy
|
|
609
|
-
" [cyan]evo localproxy
|
|
610
|
-
" [cyan]evo localproxy
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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,
|
|
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"] == "
|
|
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("
|
|
37
|
+
assert not _dns.is_bogus("203.0.113.7")
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def test_encode_qname_round_trip():
|
evo_cli-0.26.0/evo_cli/VERSION
DELETED
|
@@ -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
|
|
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
|
|
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
|
|
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
|