msdev 0.14.0__tar.gz → 0.15.0__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.
- {msdev-0.14.0 → msdev-0.15.0}/PKG-INFO +36 -47
- {msdev-0.14.0 → msdev-0.15.0}/README.md +35 -46
- {msdev-0.14.0 → msdev-0.15.0}/pyproject.toml +1 -1
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/__init__.py +1 -1
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/cli.py +5 -8
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/compat.py +129 -19
- msdev-0.15.0/src/msdev/core/daemon_version.py +26 -0
- msdev-0.15.0/src/msdev/core/direct_exec.py +178 -0
- msdev-0.15.0/src/msdev/core/env_exec.py +173 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/node.py +16 -47
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/transport.py +364 -521
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdevd-linux-amd64 +0 -0
- msdev-0.15.0/src/msdev/helpers/msdevd-linux-amd64.sha256 +1 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdevd-linux-arm64 +0 -0
- msdev-0.15.0/src/msdev/helpers/msdevd-linux-arm64.sha256 +1 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/skills/msdev-cli/SKILL.md +17 -15
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev.egg-info/PKG-INFO +36 -47
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev.egg-info/SOURCES.txt +5 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_cli.py +2 -2
- msdev-0.15.0/tests/test_compat.py +111 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_config_bundle.py +3 -3
- msdev-0.15.0/tests/test_connect_env_scenario.py +75 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_daemon.py +23 -45
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_daemon_deploy.py +2 -0
- msdev-0.15.0/tests/test_env_exec_scenario.py +55 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_exec_command_scenario.py +3 -55
- msdev-0.15.0/tests/test_exec_default_timeout_scenario.py +75 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_go_msdevd_scenario.py +1 -1
- msdev-0.15.0/tests/test_node_bootstrap_version_scenario.py +78 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_ssh_auth.py +6 -13
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_target_exec_services.py +34 -18
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_transport.py +23 -0
- msdev-0.15.0/tests/test_windows_git_bash_ssh_scenario.py +45 -0
- msdev-0.14.0/src/msdev/helpers/msdevd-linux-amd64.sha256 +0 -1
- msdev-0.14.0/src/msdev/helpers/msdevd-linux-arm64.sha256 +0 -1
- msdev-0.14.0/tests/test_compat.py +0 -64
- msdev-0.14.0/tests/test_connect_env_scenario.py +0 -146
- msdev-0.14.0/tests/test_exec_default_timeout_scenario.py +0 -128
- msdev-0.14.0/tests/test_node_bootstrap_version_scenario.py +0 -173
- {msdev-0.14.0 → msdev-0.15.0}/LICENSE +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/setup.cfg +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/__init__.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/config.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/config_bundle.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/container_helper.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/daemon_deploy.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/env_transfer.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/exec_argv.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/guides.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/helper_deploy.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/host_files.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/host_mutations.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/init_setup.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/inventory.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/limits.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/local_rpc.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/multiplex.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/request_client.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/resources.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/__init__.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/capabilities.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/context.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/environment.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/execution.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/model.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/npu.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/results.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/sftp_transfer.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/skill_install.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/ssh_auth.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/ssh_config.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/ssh_tunnel.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/stream_limits.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/stream_server.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/stream_transport.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/streamproto.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/daemon.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdev-file-helper-linux-amd64 +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdev-file-helper-linux-amd64.sha256 +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdev-file-helper-linux-arm64 +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdev-file-helper-linux-arm64.sha256 +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev.egg-info/dependency_links.txt +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev.egg-info/entry_points.txt +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/src/msdev.egg-info/top_level.txt +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_cli_only_architecture.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_config.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_container_helper.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_daemon_container_stream.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_docker_stream_dispatch.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_env_put_get_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_env_read_write_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_env_transfer.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_exec_argv.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_helper_deploy.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_init_skill.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_inventory.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_model_cli.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_model_services.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_msdev_cli_skill.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_multiplex.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_node_connect_provision_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_node_connect_save_password_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_node_env_config.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_node_env_services.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_node_sync_ssh_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_request_client.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_sftp_transfer_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_ssh_config.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_ssh_tunnel.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_stream_server.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_stream_transport.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_streamproto.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_target_guides.py +0 -0
- {msdev-0.14.0 → msdev-0.15.0}/tests/test_windows_local_scenario.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: msdev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: Portable nodes and execution environments for msModelSlim development
|
|
5
5
|
Author: msModelSlim Agent Contributors
|
|
6
6
|
License-Expression: MulanPSL-2.0
|
|
@@ -25,12 +25,13 @@ Dynamic: license-file
|
|
|
25
25
|
|
|
26
26
|
# msdev
|
|
27
27
|
|
|
28
|
-
`msdev` exposes three capabilities: **`node`** (
|
|
28
|
+
`msdev` exposes three capabilities: **`node`** (SSH + connection reuse),
|
|
29
29
|
**`env`** (shared execution context, shell, files), and **`exec`** (commands).
|
|
30
30
|
|
|
31
|
-
A Node owns SSH reachability and
|
|
32
|
-
execution context: host or Docker runtime, conda/venv/uv layers,
|
|
33
|
-
`setup` script (root switch, proxy, etc.), command `wrap`, and static
|
|
31
|
+
A Node owns SSH reachability and optional ControlMaster multiplexing. An Env
|
|
32
|
+
owns the full execution context: host or Docker runtime, conda/venv/uv layers,
|
|
33
|
+
optional `setup` script (root switch, proxy, etc.), command `wrap`, and static
|
|
34
|
+
env vars. Commands run over direct SSH (no remote `msdevd`).
|
|
34
35
|
|
|
35
36
|
The CLI is intentionally stateless: execution always names the env as the first
|
|
36
37
|
`exec` argument.
|
|
@@ -45,7 +46,7 @@ python3 -m pip install -e .
|
|
|
45
46
|
msdev --help
|
|
46
47
|
```
|
|
47
48
|
|
|
48
|
-
The package installs `msdev`
|
|
49
|
+
The package installs the `msdev` CLI.
|
|
49
50
|
|
|
50
51
|
Install the agent skill for Cursor and Claude Code, and sync SSH config:
|
|
51
52
|
|
|
@@ -63,43 +64,39 @@ overwrite; use `-f` to force.
|
|
|
63
64
|
|
|
64
65
|
## Windows client
|
|
65
66
|
|
|
66
|
-
The typical setup is a **Windows laptop talking to Linux Nodes
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
`taskkill /T` on timeout or cancel;
|
|
67
|
+
The typical setup is a **Windows laptop talking to Linux Nodes**. Native
|
|
68
|
+
`C:\Windows\System32\OpenSSH\ssh.exe` does **not** support ControlMaster.
|
|
69
|
+
msdev therefore prefers **Git for Windows** OpenSSH when installed
|
|
70
|
+
(`C:\Program Files\Git\usr\bin\ssh.exe`), which does:
|
|
71
|
+
|
|
72
|
+
- auto-selects Git Bash `ssh`/`sftp`/`ssh-keygen` over PATH / System32 OpenSSH;
|
|
73
|
+
override with `MSDEV_SSH` / `MSDEV_SFTP` / `MSDEV_SSH_KEYGEN`;
|
|
74
|
+
- enables ControlMaster when the resolved client is Git/MSYS OpenSSH (disable
|
|
75
|
+
with `MSDEV_SSH_CONTROL_MASTER=0`, force with `=1`);
|
|
76
|
+
- falls back to System32 OpenSSH without mux if Git is not installed;
|
|
77
|
+
- hides SSH console windows (`CREATE_NO_WINDOW`) and kills local process trees
|
|
78
|
+
with `taskkill /T` on timeout or cancel;
|
|
79
79
|
- SSH is non-interactive: `BatchMode` unless a password is supplied, host keys
|
|
80
80
|
use `accept-new`, and there is no password/passphrase/yes-no prompt. When a
|
|
81
81
|
password is known, injects OpenSSH `SSH_ASKPASS` so `node connect` (and later
|
|
82
82
|
ssh/sftp) can authenticate without a TTY; the local public key is then
|
|
83
83
|
installed on the Node so later logins are passwordless. The password is
|
|
84
|
-
never written to the node registry
|
|
85
|
-
- serves a TCP loopback endpoint for local `msdevd` when this Python build has
|
|
86
|
-
no `AF_UNIX` sockets.
|
|
84
|
+
never written to the node registry.
|
|
87
85
|
|
|
88
86
|
Install with `python -m pip install -e .`. `install-user.py` writes
|
|
89
|
-
`msdev.cmd`
|
|
87
|
+
`msdev.cmd` under `%USERPROFILE%\.local\bin`.
|
|
90
88
|
|
|
91
89
|
Remote paths inside a Linux Env remain POSIX (`/srv/...`). Local `env put`/`get`
|
|
92
90
|
on Windows accept Windows absolute paths (`C:\...`).
|
|
93
91
|
|
|
94
92
|
Local `exec` on Windows wraps Conda with `conda run` and venvs with
|
|
95
93
|
`Scripts\activate.bat` (remote Linux Nodes still use POSIX `sh`/`bash` wrappers).
|
|
96
|
-
If `ssh` is not on PATH, msdev looks in `%SystemRoot%\System32\OpenSSH\`.
|
|
97
94
|
|
|
98
95
|
## Nodes and execution environments
|
|
99
96
|
|
|
100
97
|
Use **`node connect`** as the single entry point: it writes `~/.ssh/config`
|
|
101
98
|
when needed, registers the node, saves passwords, installs the local public
|
|
102
|
-
key, and
|
|
99
|
+
key, and opens an SSH ControlMaster when multiplexing is supported:
|
|
103
100
|
|
|
104
101
|
```bash
|
|
105
102
|
msdev node connect dev-122 --hostname 80.48.33.133 --user root --password PASSWORD
|
|
@@ -111,25 +108,20 @@ msdev node list
|
|
|
111
108
|
```
|
|
112
109
|
|
|
113
110
|
Connect writes or updates a dedicated `Host <name>` block in OpenSSH config.
|
|
114
|
-
The msdev registry stores
|
|
115
|
-
|
|
116
|
-
config. `node sync-ssh` registers Host aliases that were added manually.
|
|
111
|
+
The msdev registry stores the Host alias. User, port, keys, and proxy remain in
|
|
112
|
+
SSH config. `node sync-ssh` registers Host aliases that were added manually.
|
|
117
113
|
|
|
118
|
-
Connect atomically creates a same-named host Env on first use
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`ping` version does not match the local CLI. Use **`msdev node bootstrap NODE`**
|
|
122
|
-
to force a redeploy after upgrading msdev. No Python is required on the node.
|
|
114
|
+
Connect atomically creates a same-named host Env on first use. There is no
|
|
115
|
+
remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
|
|
116
|
+
re-check SSH readiness after connect problems.
|
|
123
117
|
|
|
124
|
-
Build
|
|
118
|
+
Build optional static helpers before release or local testing:
|
|
125
119
|
|
|
126
120
|
```bash
|
|
127
|
-
bash msdevd/build-static.sh
|
|
128
121
|
bash file-helper/build-static.sh
|
|
129
122
|
```
|
|
130
123
|
|
|
131
124
|
Packaged artifacts live under `src/msdev/helpers/` (gitignored until built).
|
|
132
|
-
The Go source is in `msdev/`.
|
|
133
125
|
|
|
134
126
|
Persistent OpenSSH masters are optional. All SSH from msdev is
|
|
135
127
|
non-interactive: host keys use `accept-new`, and password/passphrase prompts
|
|
@@ -259,11 +251,9 @@ CRLF on a Windows console so remote line output does not staircase; piped
|
|
|
259
251
|
CLI output keeps LF.
|
|
260
252
|
|
|
261
253
|
`--timeout-seconds` defaults to `-1` (no deadline). Any positive finite duration
|
|
262
|
-
is also accepted. A positive timeout kills the
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
SSH/Unix RPC deadlines include a short completion grace for output drain and
|
|
266
|
-
serialization.
|
|
254
|
+
is also accepted. A positive timeout kills the local SSH/command process tree
|
|
255
|
+
and returns a timed-out result. Cancelling the local CLI closes the SSH
|
|
256
|
+
session (best-effort remote cancel).
|
|
267
257
|
|
|
268
258
|
`--result-json` selects captured, non-streaming execution and prints one
|
|
269
259
|
structured JSON result containing stdout and stderr.
|
|
@@ -273,12 +263,11 @@ structured JSON result containing stdout and stderr.
|
|
|
273
263
|
```text
|
|
274
264
|
msdev CLI
|
|
275
265
|
-> typed core service
|
|
276
|
-
-> UnixRpcTransport or SshRpcTransport
|
|
277
|
-
->
|
|
266
|
+
-> UnixRpcTransport (local subprocess) or SshRpcTransport
|
|
267
|
+
-> SSH ControlMaster (when supported) + direct remote command / SFTP
|
|
278
268
|
```
|
|
279
269
|
|
|
280
|
-
Remote
|
|
281
|
-
locked atomic updates.
|
|
270
|
+
Remote command output is bounded. Node/Env registries use locked atomic updates.
|
|
282
271
|
|
|
283
272
|
## Testing
|
|
284
273
|
|
|
@@ -287,5 +276,5 @@ python3 -m unittest discover -s tests -v
|
|
|
287
276
|
```
|
|
288
277
|
|
|
289
278
|
Feature work must include at least one actual scenario test on the real
|
|
290
|
-
control path (`msdev` CLI → service →
|
|
291
|
-
|
|
279
|
+
control path (`msdev` CLI → service → SSH/SFTP → effect). Unit tests alone
|
|
280
|
+
are not sufficient.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# msdev
|
|
2
2
|
|
|
3
|
-
`msdev` exposes three capabilities: **`node`** (
|
|
3
|
+
`msdev` exposes three capabilities: **`node`** (SSH + connection reuse),
|
|
4
4
|
**`env`** (shared execution context, shell, files), and **`exec`** (commands).
|
|
5
5
|
|
|
6
|
-
A Node owns SSH reachability and
|
|
7
|
-
execution context: host or Docker runtime, conda/venv/uv layers,
|
|
8
|
-
`setup` script (root switch, proxy, etc.), command `wrap`, and static
|
|
6
|
+
A Node owns SSH reachability and optional ControlMaster multiplexing. An Env
|
|
7
|
+
owns the full execution context: host or Docker runtime, conda/venv/uv layers,
|
|
8
|
+
optional `setup` script (root switch, proxy, etc.), command `wrap`, and static
|
|
9
|
+
env vars. Commands run over direct SSH (no remote `msdevd`).
|
|
9
10
|
|
|
10
11
|
The CLI is intentionally stateless: execution always names the env as the first
|
|
11
12
|
`exec` argument.
|
|
@@ -20,7 +21,7 @@ python3 -m pip install -e .
|
|
|
20
21
|
msdev --help
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
The package installs `msdev`
|
|
24
|
+
The package installs the `msdev` CLI.
|
|
24
25
|
|
|
25
26
|
Install the agent skill for Cursor and Claude Code, and sync SSH config:
|
|
26
27
|
|
|
@@ -38,43 +39,39 @@ overwrite; use `-f` to force.
|
|
|
38
39
|
|
|
39
40
|
## Windows client
|
|
40
41
|
|
|
41
|
-
The typical setup is a **Windows laptop talking to Linux Nodes
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`taskkill /T` on timeout or cancel;
|
|
42
|
+
The typical setup is a **Windows laptop talking to Linux Nodes**. Native
|
|
43
|
+
`C:\Windows\System32\OpenSSH\ssh.exe` does **not** support ControlMaster.
|
|
44
|
+
msdev therefore prefers **Git for Windows** OpenSSH when installed
|
|
45
|
+
(`C:\Program Files\Git\usr\bin\ssh.exe`), which does:
|
|
46
|
+
|
|
47
|
+
- auto-selects Git Bash `ssh`/`sftp`/`ssh-keygen` over PATH / System32 OpenSSH;
|
|
48
|
+
override with `MSDEV_SSH` / `MSDEV_SFTP` / `MSDEV_SSH_KEYGEN`;
|
|
49
|
+
- enables ControlMaster when the resolved client is Git/MSYS OpenSSH (disable
|
|
50
|
+
with `MSDEV_SSH_CONTROL_MASTER=0`, force with `=1`);
|
|
51
|
+
- falls back to System32 OpenSSH without mux if Git is not installed;
|
|
52
|
+
- hides SSH console windows (`CREATE_NO_WINDOW`) and kills local process trees
|
|
53
|
+
with `taskkill /T` on timeout or cancel;
|
|
54
54
|
- SSH is non-interactive: `BatchMode` unless a password is supplied, host keys
|
|
55
55
|
use `accept-new`, and there is no password/passphrase/yes-no prompt. When a
|
|
56
56
|
password is known, injects OpenSSH `SSH_ASKPASS` so `node connect` (and later
|
|
57
57
|
ssh/sftp) can authenticate without a TTY; the local public key is then
|
|
58
58
|
installed on the Node so later logins are passwordless. The password is
|
|
59
|
-
never written to the node registry
|
|
60
|
-
- serves a TCP loopback endpoint for local `msdevd` when this Python build has
|
|
61
|
-
no `AF_UNIX` sockets.
|
|
59
|
+
never written to the node registry.
|
|
62
60
|
|
|
63
61
|
Install with `python -m pip install -e .`. `install-user.py` writes
|
|
64
|
-
`msdev.cmd`
|
|
62
|
+
`msdev.cmd` under `%USERPROFILE%\.local\bin`.
|
|
65
63
|
|
|
66
64
|
Remote paths inside a Linux Env remain POSIX (`/srv/...`). Local `env put`/`get`
|
|
67
65
|
on Windows accept Windows absolute paths (`C:\...`).
|
|
68
66
|
|
|
69
67
|
Local `exec` on Windows wraps Conda with `conda run` and venvs with
|
|
70
68
|
`Scripts\activate.bat` (remote Linux Nodes still use POSIX `sh`/`bash` wrappers).
|
|
71
|
-
If `ssh` is not on PATH, msdev looks in `%SystemRoot%\System32\OpenSSH\`.
|
|
72
69
|
|
|
73
70
|
## Nodes and execution environments
|
|
74
71
|
|
|
75
72
|
Use **`node connect`** as the single entry point: it writes `~/.ssh/config`
|
|
76
73
|
when needed, registers the node, saves passwords, installs the local public
|
|
77
|
-
key, and
|
|
74
|
+
key, and opens an SSH ControlMaster when multiplexing is supported:
|
|
78
75
|
|
|
79
76
|
```bash
|
|
80
77
|
msdev node connect dev-122 --hostname 80.48.33.133 --user root --password PASSWORD
|
|
@@ -86,25 +83,20 @@ msdev node list
|
|
|
86
83
|
```
|
|
87
84
|
|
|
88
85
|
Connect writes or updates a dedicated `Host <name>` block in OpenSSH config.
|
|
89
|
-
The msdev registry stores
|
|
90
|
-
|
|
91
|
-
config. `node sync-ssh` registers Host aliases that were added manually.
|
|
86
|
+
The msdev registry stores the Host alias. User, port, keys, and proxy remain in
|
|
87
|
+
SSH config. `node sync-ssh` registers Host aliases that were added manually.
|
|
92
88
|
|
|
93
|
-
Connect atomically creates a same-named host Env on first use
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`ping` version does not match the local CLI. Use **`msdev node bootstrap NODE`**
|
|
97
|
-
to force a redeploy after upgrading msdev. No Python is required on the node.
|
|
89
|
+
Connect atomically creates a same-named host Env on first use. There is no
|
|
90
|
+
remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
|
|
91
|
+
re-check SSH readiness after connect problems.
|
|
98
92
|
|
|
99
|
-
Build
|
|
93
|
+
Build optional static helpers before release or local testing:
|
|
100
94
|
|
|
101
95
|
```bash
|
|
102
|
-
bash msdevd/build-static.sh
|
|
103
96
|
bash file-helper/build-static.sh
|
|
104
97
|
```
|
|
105
98
|
|
|
106
99
|
Packaged artifacts live under `src/msdev/helpers/` (gitignored until built).
|
|
107
|
-
The Go source is in `msdev/`.
|
|
108
100
|
|
|
109
101
|
Persistent OpenSSH masters are optional. All SSH from msdev is
|
|
110
102
|
non-interactive: host keys use `accept-new`, and password/passphrase prompts
|
|
@@ -234,11 +226,9 @@ CRLF on a Windows console so remote line output does not staircase; piped
|
|
|
234
226
|
CLI output keeps LF.
|
|
235
227
|
|
|
236
228
|
`--timeout-seconds` defaults to `-1` (no deadline). Any positive finite duration
|
|
237
|
-
is also accepted. A positive timeout kills the
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
SSH/Unix RPC deadlines include a short completion grace for output drain and
|
|
241
|
-
serialization.
|
|
229
|
+
is also accepted. A positive timeout kills the local SSH/command process tree
|
|
230
|
+
and returns a timed-out result. Cancelling the local CLI closes the SSH
|
|
231
|
+
session (best-effort remote cancel).
|
|
242
232
|
|
|
243
233
|
`--result-json` selects captured, non-streaming execution and prints one
|
|
244
234
|
structured JSON result containing stdout and stderr.
|
|
@@ -248,12 +238,11 @@ structured JSON result containing stdout and stderr.
|
|
|
248
238
|
```text
|
|
249
239
|
msdev CLI
|
|
250
240
|
-> typed core service
|
|
251
|
-
-> UnixRpcTransport or SshRpcTransport
|
|
252
|
-
->
|
|
241
|
+
-> UnixRpcTransport (local subprocess) or SshRpcTransport
|
|
242
|
+
-> SSH ControlMaster (when supported) + direct remote command / SFTP
|
|
253
243
|
```
|
|
254
244
|
|
|
255
|
-
Remote
|
|
256
|
-
locked atomic updates.
|
|
245
|
+
Remote command output is bounded. Node/Env registries use locked atomic updates.
|
|
257
246
|
|
|
258
247
|
## Testing
|
|
259
248
|
|
|
@@ -262,5 +251,5 @@ python3 -m unittest discover -s tests -v
|
|
|
262
251
|
```
|
|
263
252
|
|
|
264
253
|
Feature work must include at least one actual scenario test on the real
|
|
265
|
-
control path (`msdev` CLI → service →
|
|
266
|
-
|
|
254
|
+
control path (`msdev` CLI → service → SSH/SFTP → effect). Unit tests alone
|
|
255
|
+
are not sufficient.
|
|
@@ -619,23 +619,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
619
619
|
node_status.add_argument("name")
|
|
620
620
|
node_bootstrap = node_sub.add_parser(
|
|
621
621
|
"bootstrap",
|
|
622
|
-
help=
|
|
623
|
-
"force-redeploy msdevd on the node "
|
|
624
|
-
"(use after local upgrade when remote version mismatches)"
|
|
625
|
-
),
|
|
622
|
+
help="ensure SSH reachability / ControlMaster for the node",
|
|
626
623
|
)
|
|
627
624
|
node_bootstrap.add_argument("name")
|
|
628
625
|
node_bootstrap.add_argument(
|
|
629
626
|
"--package",
|
|
630
627
|
type=Path,
|
|
631
628
|
default=None,
|
|
632
|
-
help="
|
|
629
|
+
help="ignored (kept for compatibility; remote msdevd is no longer deployed)",
|
|
633
630
|
)
|
|
634
631
|
node_connect = node_sub.add_parser(
|
|
635
632
|
"connect",
|
|
636
633
|
help=(
|
|
637
|
-
"register a node if needed, write ~/.ssh/config,
|
|
638
|
-
"and
|
|
634
|
+
"register a node if needed, write ~/.ssh/config, "
|
|
635
|
+
"and open an SSH ControlMaster when supported"
|
|
639
636
|
),
|
|
640
637
|
)
|
|
641
638
|
node_connect.add_argument(
|
|
@@ -671,7 +668,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
671
668
|
node_connect.add_argument(
|
|
672
669
|
"--no-auto-bootstrap",
|
|
673
670
|
action="store_true",
|
|
674
|
-
help="
|
|
671
|
+
help="ignored (kept for compatibility; no remote agent deploy)",
|
|
675
672
|
)
|
|
676
673
|
node_connect.add_argument(
|
|
677
674
|
"--password",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Platform helpers for POSIX and
|
|
1
|
+
"""Platform helpers for POSIX and Windows OpenSSH clients."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -9,6 +9,7 @@ import stat
|
|
|
9
9
|
import subprocess
|
|
10
10
|
import sys
|
|
11
11
|
import threading
|
|
12
|
+
from functools import lru_cache
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
@@ -25,46 +26,155 @@ def unix_sockets_supported() -> bool:
|
|
|
25
26
|
return True
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
def
|
|
29
|
-
root = Path(
|
|
29
|
+
def _windows_native_openssh(name: str) -> str | None:
|
|
30
|
+
root = Path(
|
|
31
|
+
os.environ.get("SystemRoot") or os.environ.get("WINDIR") or r"C:\Windows"
|
|
32
|
+
)
|
|
30
33
|
candidate = root / "System32" / "OpenSSH" / name
|
|
31
34
|
if candidate.is_file():
|
|
32
35
|
return str(candidate)
|
|
33
36
|
return None
|
|
34
37
|
|
|
35
38
|
|
|
39
|
+
def _windows_git_openssh_dirs() -> list[Path]:
|
|
40
|
+
"""Candidate Git for Windows ``usr\\bin`` directories (mux-capable OpenSSH)."""
|
|
41
|
+
dirs: list[Path] = []
|
|
42
|
+
for base in (
|
|
43
|
+
os.environ.get("ProgramW6432"),
|
|
44
|
+
os.environ.get("ProgramFiles"),
|
|
45
|
+
os.environ.get("ProgramFiles(x86)"),
|
|
46
|
+
):
|
|
47
|
+
if base:
|
|
48
|
+
dirs.append(Path(base) / "Git" / "usr" / "bin")
|
|
49
|
+
local = os.environ.get("LOCALAPPDATA")
|
|
50
|
+
if local:
|
|
51
|
+
dirs.append(Path(local) / "Programs" / "Git" / "usr" / "bin")
|
|
52
|
+
# Deduplicate while preserving order.
|
|
53
|
+
seen: set[str] = set()
|
|
54
|
+
unique: list[Path] = []
|
|
55
|
+
for path in dirs:
|
|
56
|
+
key = str(path).lower()
|
|
57
|
+
if key in seen:
|
|
58
|
+
continue
|
|
59
|
+
seen.add(key)
|
|
60
|
+
unique.append(path)
|
|
61
|
+
return unique
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _windows_git_openssh(name: str) -> str | None:
|
|
65
|
+
for directory in _windows_git_openssh_dirs():
|
|
66
|
+
candidate = directory / name
|
|
67
|
+
if candidate.is_file():
|
|
68
|
+
return str(candidate)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _normalize_exe_path(path: str) -> str:
|
|
73
|
+
return path.replace("/", "\\").lower()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _is_windows_native_openssh(path: str) -> bool:
|
|
77
|
+
normalized = _normalize_exe_path(path)
|
|
78
|
+
return "\\windows\\system32\\openssh\\" in normalized
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _is_windows_git_openssh(path: str) -> bool:
|
|
82
|
+
normalized = _normalize_exe_path(path)
|
|
83
|
+
return "\\git\\usr\\bin\\" in normalized or "\\git\\bin\\" in normalized
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _resolve_windows_tool(name: str, *, path_name: str) -> str:
|
|
87
|
+
"""Prefer Git Bash OpenSSH over PATH / native Win32-OpenSSH."""
|
|
88
|
+
override_key = {
|
|
89
|
+
"ssh.exe": "MSDEV_SSH",
|
|
90
|
+
"sftp.exe": "MSDEV_SFTP",
|
|
91
|
+
"ssh-keygen.exe": "MSDEV_SSH_KEYGEN",
|
|
92
|
+
}.get(name)
|
|
93
|
+
if override_key:
|
|
94
|
+
override = os.environ.get(override_key, "").strip()
|
|
95
|
+
if override:
|
|
96
|
+
return override
|
|
97
|
+
git = _windows_git_openssh(name)
|
|
98
|
+
if git:
|
|
99
|
+
return git
|
|
100
|
+
which = shutil.which(path_name)
|
|
101
|
+
if which:
|
|
102
|
+
return which
|
|
103
|
+
native = _windows_native_openssh(name)
|
|
104
|
+
if native:
|
|
105
|
+
return native
|
|
106
|
+
return path_name
|
|
107
|
+
|
|
108
|
+
|
|
36
109
|
def ssh_executable() -> str:
|
|
37
|
-
"""Resolve ssh
|
|
38
|
-
if
|
|
39
|
-
return "ssh"
|
|
40
|
-
|
|
41
|
-
return
|
|
110
|
+
"""Resolve ssh; on Windows prefer Git for Windows (ControlMaster-capable)."""
|
|
111
|
+
if IS_WINDOWS:
|
|
112
|
+
return _resolve_windows_tool("ssh.exe", path_name="ssh")
|
|
113
|
+
which = shutil.which("ssh")
|
|
114
|
+
return which or "ssh"
|
|
42
115
|
|
|
43
116
|
|
|
44
117
|
def sftp_executable() -> str:
|
|
45
|
-
"""Resolve sftp
|
|
46
|
-
if
|
|
47
|
-
return "sftp"
|
|
48
|
-
|
|
49
|
-
return
|
|
118
|
+
"""Resolve sftp; keep it paired with the preferred Windows ssh when possible."""
|
|
119
|
+
if IS_WINDOWS:
|
|
120
|
+
return _resolve_windows_tool("sftp.exe", path_name="sftp")
|
|
121
|
+
which = shutil.which("sftp")
|
|
122
|
+
return which or "sftp"
|
|
50
123
|
|
|
51
124
|
|
|
52
125
|
def ssh_keygen_executable() -> str:
|
|
53
126
|
"""Resolve ssh-keygen without requiring OpenSSH to be on PATH."""
|
|
54
|
-
if
|
|
55
|
-
return "ssh-keygen"
|
|
56
|
-
|
|
57
|
-
return
|
|
127
|
+
if IS_WINDOWS:
|
|
128
|
+
return _resolve_windows_tool("ssh-keygen.exe", path_name="ssh-keygen")
|
|
129
|
+
which = shutil.which("ssh-keygen")
|
|
130
|
+
return which or "ssh-keygen"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@lru_cache(maxsize=8)
|
|
134
|
+
def _ssh_version_text(ssh: str) -> str:
|
|
135
|
+
try:
|
|
136
|
+
kwargs: dict[str, Any] = {}
|
|
137
|
+
if IS_WINDOWS:
|
|
138
|
+
kwargs["creationflags"] = CREATE_NO_WINDOW
|
|
139
|
+
completed = subprocess.run(
|
|
140
|
+
[ssh, "-V"],
|
|
141
|
+
capture_output=True,
|
|
142
|
+
check=False,
|
|
143
|
+
timeout=5,
|
|
144
|
+
**kwargs,
|
|
145
|
+
)
|
|
146
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
147
|
+
return ""
|
|
148
|
+
# OpenSSH prints version on stderr.
|
|
149
|
+
return f"{completed.stderr or ''}{completed.stdout or ''}"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def ssh_supports_control_master(ssh: str | None = None) -> bool:
|
|
153
|
+
"""Return whether the resolved ssh client can use ControlMaster."""
|
|
154
|
+
path = ssh if ssh is not None else ssh_executable()
|
|
155
|
+
if not IS_WINDOWS:
|
|
156
|
+
return True
|
|
157
|
+
if _is_windows_git_openssh(path):
|
|
158
|
+
return True
|
|
159
|
+
if _is_windows_native_openssh(path):
|
|
160
|
+
return False
|
|
161
|
+
version = _ssh_version_text(path).lower()
|
|
162
|
+
if "openssh_for_windows" in version:
|
|
163
|
+
return False
|
|
164
|
+
# Non-native OpenSSH on PATH (Git/MSYS often) supports mux.
|
|
165
|
+
if "openssh_" in version:
|
|
166
|
+
return True
|
|
167
|
+
return False
|
|
58
168
|
|
|
59
169
|
|
|
60
170
|
def ssh_control_master_enabled() -> bool:
|
|
61
|
-
"""
|
|
171
|
+
"""Enable ControlMaster on POSIX, or on Windows with Git Bash OpenSSH."""
|
|
62
172
|
override = os.environ.get("MSDEV_SSH_CONTROL_MASTER", "").strip().lower()
|
|
63
173
|
if override in {"0", "false", "no", "off"}:
|
|
64
174
|
return False
|
|
65
175
|
if override in {"1", "true", "yes", "on"}:
|
|
66
176
|
return True
|
|
67
|
-
return
|
|
177
|
+
return ssh_supports_control_master()
|
|
68
178
|
|
|
69
179
|
|
|
70
180
|
def popen_kwargs(*, hidden: bool = True, new_group: bool = True) -> dict[str, Any]:
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Helpers for comparing local CLI and remote msdevd versions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .. import __version__
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def remote_daemon_version_matches(
|
|
11
|
+
ping: Any,
|
|
12
|
+
*,
|
|
13
|
+
expected: str | None = None,
|
|
14
|
+
) -> bool:
|
|
15
|
+
"""Return True when ping reports the same msdevd version as the local CLI."""
|
|
16
|
+
target = expected if expected is not None else __version__
|
|
17
|
+
if not isinstance(ping, dict):
|
|
18
|
+
return False
|
|
19
|
+
remote = ping.get("version")
|
|
20
|
+
return isinstance(remote, str) and remote == target
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_incompatible_daemon_rpc_error(exc: BaseException) -> bool:
|
|
24
|
+
"""True when the remote daemon rejected a method the current CLI requires."""
|
|
25
|
+
text = str(exc).lower()
|
|
26
|
+
return "unknown rpc method" in text
|