remote-cmd-manager 2.0.0__tar.gz → 2.2.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.
Files changed (84) hide show
  1. {remote_cmd_manager-2.0.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.2.0}/PKG-INFO +57 -25
  2. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/README.md +49 -20
  3. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/examples/basic_usage.py +2 -2
  4. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/pyproject.toml +10 -5
  5. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/__init__.py +1 -1
  6. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/_version.py +1 -1
  7. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/cli/main.py +16 -3
  8. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/async_connection_pool.py +24 -3
  9. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/async_ssh_client.py +16 -4
  10. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/ssh_client.py +155 -21
  11. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/sync_connection_pool.py +10 -2
  12. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/sqlite_host_repository.py +50 -7
  13. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/__init__.py +3 -0
  14. remote_cmd_manager-2.2.0/remote_cmd/service/async_batch_executor.py +357 -0
  15. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/batch_executor.py +179 -83
  16. remote_cmd_manager-2.2.0/remote_cmd/service/retry_policy.py +158 -0
  17. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/crypto.py +9 -2
  18. remote_cmd_manager-2.2.0/remote_cmd/utils/exceptions.py +247 -0
  19. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0/remote_cmd_manager.egg-info}/PKG-INFO +57 -25
  20. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/SOURCES.txt +4 -0
  21. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/requires.txt +1 -1
  22. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/performance/conftest.py +18 -0
  23. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/performance/test_benchmarks.py +9 -0
  24. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_async_batch_executor.py +409 -23
  25. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_async_ssh_client.py +54 -3
  26. remote_cmd_manager-2.2.0/tests/test_batch_executor.py +849 -0
  27. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_cli.py +46 -0
  28. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_crypto.py +7 -1
  29. remote_cmd_manager-2.2.0/tests/test_exceptions.py +115 -0
  30. remote_cmd_manager-2.2.0/tests/test_release_metadata.py +69 -0
  31. remote_cmd_manager-2.2.0/tests/test_retry_policy.py +154 -0
  32. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_sqlite_repository.py +167 -0
  33. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_ssh_client.py +198 -1
  34. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_sync_connection_pool.py +74 -7
  35. remote_cmd_manager-2.0.0/remote_cmd/service/async_batch_executor.py +0 -235
  36. remote_cmd_manager-2.0.0/remote_cmd/utils/exceptions.py +0 -133
  37. remote_cmd_manager-2.0.0/tests/test_batch_executor.py +0 -437
  38. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/LICENSE +0 -0
  39. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/MANIFEST.in +0 -0
  40. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/config.example.yaml +0 -0
  41. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/examples/deploy_script.py +0 -0
  42. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/examples/nginx_batch_update.py +0 -0
  43. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/examples/system_health_check.py +0 -0
  44. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/cli/__init__.py +0 -0
  45. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/__init__.py +0 -0
  46. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/host.py +0 -0
  47. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/host_manager.py +0 -0
  48. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/__init__.py +0 -0
  49. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/host_repository.py +0 -0
  50. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/json_host_repository.py +0 -0
  51. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/_host_runner.py +0 -0
  52. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/_pool_policy.py +0 -0
  53. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/_types.py +0 -0
  54. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/credential_provider.py +0 -0
  55. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/host_service.py +0 -0
  56. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/ssh_service.py +0 -0
  57. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/storage_factory.py +0 -0
  58. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/task_runner.py +0 -0
  59. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/__init__.py +0 -0
  60. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/config.py +0 -0
  61. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/credential_guard.py +0 -0
  62. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/logging_utils.py +0 -0
  63. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
  64. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
  65. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
  66. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/requirements.txt +0 -0
  67. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/setup.cfg +0 -0
  68. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/__init__.py +0 -0
  69. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/conftest.py +0 -0
  70. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/integration/conftest.py +0 -0
  71. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/integration/test_ssh_connection.py +0 -0
  72. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/performance/__init__.py +0 -0
  73. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_config.py +0 -0
  74. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_credential_provider.py +0 -0
  75. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_host.py +0 -0
  76. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_host_manager.py +0 -0
  77. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_host_service.py +0 -0
  78. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_keyring_provider.py +0 -0
  79. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_logging_utils.py +0 -0
  80. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_pool_policy.py +0 -0
  81. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_repository.py +0 -0
  82. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_ssh_service.py +0 -0
  83. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_storage_factory.py +0 -0
  84. {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_task_runner.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remote_cmd_manager
3
- Version: 2.0.0
4
- Summary: SSH 远程服务器管理工具 / Python SSH remote server management tool
3
+ Version: 2.2.0
4
+ Summary: Cross-platform SSH remote server management tool (Linux/macOS/Windows) — lightweight, zero-configuration CLI and Python API for managing servers over SSH. Supports host CRUD, batch commands, file transfer, credential encryption, async execution, and more.
5
5
  Author-email: Vae-Scrooge <vaescrooge@gmail.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/Vae-Scrooge/remote-cmd
@@ -9,8 +9,8 @@ Project-URL: Repository, https://github.com/Vae-Scrooge/remote-cmd
9
9
  Project-URL: Documentation, https://github.com/Vae-Scrooge/remote-cmd#readme
10
10
  Project-URL: Bug Tracker, https://github.com/Vae-Scrooge/remote-cmd/issues
11
11
  Project-URL: Download, https://pypi.org/project/remote_cmd_manager/
12
- Keywords: ssh,remote,server-management,sysadmin,devops
13
- Classifier: Development Status :: 4 - Beta
12
+ Keywords: ssh,remote,server-management,sysadmin,devops,cross-platform
13
+ Classifier: Development Status :: 5 - Production/Stable
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: System Administrators
16
16
  Classifier: License :: OSI Approved :: MIT License
@@ -21,6 +21,9 @@ Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Topic :: System :: Systems Administration
22
22
  Classifier: Topic :: System :: Networking
23
23
  Classifier: Environment :: Console
24
+ Classifier: Operating System :: POSIX :: Linux
25
+ Classifier: Operating System :: MacOS
26
+ Classifier: Operating System :: Microsoft :: Windows
24
27
  Requires-Python: >=3.9
25
28
  Description-Content-Type: text/markdown
26
29
  License-File: LICENSE
@@ -42,7 +45,7 @@ Requires-Dist: ruff>=0.4; extra == "dev"
42
45
  Requires-Dist: mypy>=1.10; extra == "dev"
43
46
  Requires-Dist: keyring>=24.0; extra == "dev"
44
47
  Provides-Extra: docs
45
- Requires-Dist: pdoc<16,>=15; extra == "docs"
48
+ Requires-Dist: pdoc==15.0.4; extra == "docs"
46
49
  Dynamic: license-file
47
50
 
48
51
  <p align="center">
@@ -54,6 +57,7 @@ Dynamic: license-file
54
57
  <img src="https://img.shields.io/github/actions/workflow/status/Vae-Scrooge/remote-cmd/ci.yml?style=for-the-badge&logo=githubactions&label=CI" alt="CI">
55
58
  </p>
56
59
 
60
+ <p align="center"><strong>Tested on Linux, macOS, and Windows</strong></p>
57
61
  <h1 align="center">Remote CMD — SSH Server Management<br><small>Without the Overhead</small></h1>
58
62
 
59
63
  <p align="center">
@@ -71,15 +75,9 @@ Dynamic: license-file
71
75
  <a href="#contributing">Contributing</a>
72
76
  </p>
73
77
 
74
- <p align="center">
75
- <a href="https://asciinema.org/a/9yLeYj73muPUuAQY" target="_blank">
76
- <img src="https://asciinema.org/a/9yLeYj73muPUuAQY.svg" width="720" alt="Demo">
77
- </a>
78
- </p>
79
-
80
78
  ---
81
79
 
82
- **Remote CMD** is a lightweight Python CLI + API for managing servers over SSH. Add hosts, run commands, transfer files, and target groups by tags — no Ansible DSL or shell loops required.
80
+ **Remote CMD** is a lightweight Python CLI + API for managing servers over SSH. Add hosts, run commands, transfer files, and organize hosts with tags — no Ansible DSL or shell loops required.
83
81
 
84
82
  ```bash
85
83
  # One command to get started
@@ -88,8 +86,27 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
88
86
 
89
87
  ---
90
88
 
89
+ ## v2.2.0 Release Highlights
90
+
91
+ v2.2.0 bounds batch-execution resource usage, makes concurrent SQLite writers safe, tightens
92
+ retry classification, and hardens the release pipeline. See the
93
+ [full migration notes](./CHANGELOG.md) before upgrading automated callers.
94
+
95
+ ### Resource & Reliability
96
+
97
+ - Internal batch pools are now created lazily per host, capped at one connection, and closed as soon as that host (including all retries) finishes; batch-wide live internal connections are bounded by `max_concurrency` instead of retaining one idle connection per host until the batch ends.
98
+ - SQLite writes now use `BEGIN IMMEDIATE` with a configurable `busy_timeout` (default 5000 ms), and the initial `journal_mode=WAL` switch is retried; concurrent `remote-cmd` processes writing the same `hosts.db` no longer fail with `database is locked` or lose updates. Schema and `db_version` are unchanged.
99
+ - Pool closure raises the new `PoolClosedError` (still catchable as `RuntimeError`); retry classification is corrected so bare `RuntimeError` is retryable again (v2.0 behavior) while pool closure remains non-retryable. Typed permanent and transient errors are unchanged.
100
+
101
+ ### Release Engineering
102
+
103
+ - Publish workflow: artifact actions aligned, run-scoped artifact names, duplicate version publication now fails instead of being skipped, `twine check` validates distributions, and the release tag is verified against the package version. Trusted Publishing and the GitHub Release → PyPI flow are unchanged.
104
+ - CI documentation drift detection: `docs/api` is regenerated and compared on relevant changes, failing when tracked generated docs are stale (`pdoc` pinned to `15.0.4` on Python 3.12).
105
+ - Release metadata validation: a lightweight gate checks the single-source version, the `pyproject.toml` dynamic-version attribute, `[Unreleased]` and matching `[x.y.z]` CHANGELOG headings, and (on release) that the git tag matches the package version.
106
+
91
107
  ## Table of Contents
92
108
 
109
+ - [v2.2.0 Release Highlights](#v220-release-highlights)
93
110
  - [Why Remote CMD?](#why-remote-cmd)
94
111
  - [Quick Start](#quick-start)
95
112
  - [Use Cases](#use-cases)
@@ -132,8 +149,8 @@ remote-cmd host add web-01 192.168.1.10 ubuntu --key ~/.ssh/id_rsa
132
149
  # 3. Run a command
133
150
  remote-cmd run web-01 "uptime"
134
151
 
135
- # 4. Run across all production servers
136
- remote-cmd batch-run -t production "df -h /"
152
+ # 4. Run across named production servers
153
+ remote-cmd batch-run web-01 web-02 db-01 "df -h /"
137
154
  ```
138
155
 
139
156
  ---
@@ -143,7 +160,7 @@ remote-cmd batch-run -t production "df -h /"
143
160
  ### 🖥️ System Administrators — Check disk across 20 servers in one command
144
161
 
145
162
  ```bash
146
- remote-cmd batch-run -t production "df -h / | tail -1"
163
+ remote-cmd batch-run web-01 web-02 db-01 "df -h / | tail -1"
147
164
  # Output:
148
165
  # ✓ web-01 → /dev/sda1 32G 12G 19G 40% /
149
166
  # ✓ web-02 → /dev/sda1 32G 28G 3G 90% / ⚠️
@@ -167,7 +184,7 @@ for host in service.list_hosts(tag="staging"):
167
184
  ### 🔥 Incident Response — Check logs across all servers
168
185
 
169
186
  ```bash
170
- remote-cmd batch-run -t web "journalctl -xe -n 50 | grep -i error"
187
+ remote-cmd batch-run web-01 web-02 "journalctl -xe -n 50 | grep -i error"
171
188
  ```
172
189
 
173
190
  ### 🔧 Config Update — Upload and reload nginx across tagged hosts
@@ -190,10 +207,10 @@ All operations are available from the terminal:
190
207
  | `remote-cmd host show <name>` | Show one host's details |
191
208
  | `remote-cmd host test <name>` | Test connectivity to a host |
192
209
  | `remote-cmd host remove <name>` | Remove a host |
193
- | `remote-cmd run <name> "<cmd>"` | Run a command on one host |
210
+ | `remote-cmd run <name> "<cmd>" [-T SECONDS]` | Run a command on one host (`--timeout/-T` sets the wall-clock limit) |
194
211
  | `remote-cmd upload <name> <local> <remote>` | Upload a file via SFTP |
195
212
  | `remote-cmd download <name> <remote> <local>` | Download a file via SFTP |
196
- | `remote-cmd batch-run -t <tag> "<cmd>"` | Run across all hosts in a tag (`-C` concurrency, `-T` timeout, `--async`, `--show-failures`) |
213
+ | `remote-cmd batch-run <name>... "<cmd>"` | Run across named hosts (`-C` concurrency, `-T` timeout, `-r` retries, `--async`, `--show-failures`) |
197
214
 
198
215
  ---
199
216
 
@@ -221,7 +238,7 @@ with SSHClient(config) as client:
221
238
 
222
239
  # List remote directory
223
240
  for entry in client.list_remote_directory("/var/log"):
224
- print(f"{entry['name']}: {entry['size']} bytes")
241
+ print(f"{entry.name}: {entry.size} bytes")
225
242
  ```
226
243
 
227
244
  ---
@@ -232,7 +249,7 @@ with SSHClient(config) as client:
232
249
  |---|---|
233
250
  | **SSH Auth** | Password + key file + ssh-agent, with pluggable credential providers |
234
251
  | **Credential Chain** | Source passwords from environment, keyring, or arbitrary providers, in priority order |
235
- | **Credential Encryption** | AES-encrypt secrets at rest (`CredentialEncryption`) |
252
+ | **Credential Encryption** | Fernet-encrypt secrets at rest (`CredentialEncryption`) |
236
253
  | **Commands** | Single, multi-line, sudo with password |
237
254
  | **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
238
255
  | **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
@@ -240,12 +257,26 @@ with SSHClient(config) as client:
240
257
  | **Batch Ops** | Run commands across any host group, synchronously or asynchronously |
241
258
  | **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
242
259
  | **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
243
- | **Connection Test** | Ping all hosts and report status |
260
+ | **Connection Test** | Test all host SSH connections and report status |
244
261
  | **Secure Logging** | Structured logging that filters sensitive data (`SensitiveDataFilter`) |
245
262
  | **Type Safety** | Full type annotations + mypy strict |
246
263
 
247
264
  ---
248
265
 
266
+ ## Cross-Platform Support
267
+
268
+ `remote-cmd` is a pure-Python client, **tested and supported on Linux, macOS, and Windows**. No native dependencies or platform-specific builds are required.
269
+
270
+ | Platform | Status | Notes |
271
+ |----------|--------|-------|
272
+ | Linux | ✅ Fully tested | Primary development platform |
273
+ | macOS | ✅ Supported | Use `brew install openssh` for a newer OpenSSH |
274
+ | Windows | ✅ Supported | PowerShell / CMD / WSL all work |
275
+
276
+ > **Note on Windows file permissions:** Credential encryption keys are protected with `0600` permissions on Unix. On Windows this restriction is not enforced by the filesystem — the key file is still encrypted at rest. For production Windows use, consider adding filesystem-level ACLs.
277
+
278
+ ---
279
+
249
280
  ## Installation
250
281
 
251
282
  ```bash
@@ -263,8 +294,9 @@ pip install -e ".[dev]"
263
294
 
264
295
  The `[async]` extra installs `asyncssh` and enables the native async execution
265
296
  kernel: `AsyncSSHClient`, `AsyncConnectionPool` and `AsyncBatchExecutor`
266
- (also available via `BatchExecutor(use_async=True)`). Without it, `import remote_cmd`
267
- still works — the async symbols are simply not exported.
297
+ (also available via `BatchExecutor(use_async=True)`). `BatchExecutor(use_async=True)`
298
+ also requires this extra. Without it, `import remote_cmd` still works — the async symbols
299
+ are simply not exported.
268
300
 
269
301
  ---
270
302
 
@@ -284,14 +316,14 @@ still works — the async symbols are simply not exported.
284
316
  | [Changelog](./CHANGELOG.md) | Release history |
285
317
  | [Mobile Remote Guide](./MOBILE-REMOTE-GUIDE.md) | Manage servers from your phone |
286
318
 
287
- > **Note:** The documentation center and tutorials are maintained in **Chinese**. See
319
+ > **Note:** The documentation center and tutorials are maintained in **English**. See
288
320
  > [README.zh-CN.md](./README.zh-CN.md) for the Chinese version of this page.
289
321
 
290
322
  ---
291
323
 
292
324
  ## Project Status
293
325
 
294
- **Beta.** The core API is stable. Breaking changes will be communicated via semantic versioning.
326
+ **Stable.** The core API is stable and versioned under semantic versioning. Breaking changes are communicated via major-version bumps, and the public API surface has been stable since the 1.x line.
295
327
 
296
328
  **Roadmap:**
297
329
  - [x] Async SSH operations (parallel execution) — v1.1.0
@@ -7,6 +7,7 @@
7
7
  <img src="https://img.shields.io/github/actions/workflow/status/Vae-Scrooge/remote-cmd/ci.yml?style=for-the-badge&logo=githubactions&label=CI" alt="CI">
8
8
  </p>
9
9
 
10
+ <p align="center"><strong>Tested on Linux, macOS, and Windows</strong></p>
10
11
  <h1 align="center">Remote CMD — SSH Server Management<br><small>Without the Overhead</small></h1>
11
12
 
12
13
  <p align="center">
@@ -24,15 +25,9 @@
24
25
  <a href="#contributing">Contributing</a>
25
26
  </p>
26
27
 
27
- <p align="center">
28
- <a href="https://asciinema.org/a/9yLeYj73muPUuAQY" target="_blank">
29
- <img src="https://asciinema.org/a/9yLeYj73muPUuAQY.svg" width="720" alt="Demo">
30
- </a>
31
- </p>
32
-
33
28
  ---
34
29
 
35
- **Remote CMD** is a lightweight Python CLI + API for managing servers over SSH. Add hosts, run commands, transfer files, and target groups by tags — no Ansible DSL or shell loops required.
30
+ **Remote CMD** is a lightweight Python CLI + API for managing servers over SSH. Add hosts, run commands, transfer files, and organize hosts with tags — no Ansible DSL or shell loops required.
36
31
 
37
32
  ```bash
38
33
  # One command to get started
@@ -41,8 +36,27 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
41
36
 
42
37
  ---
43
38
 
39
+ ## v2.2.0 Release Highlights
40
+
41
+ v2.2.0 bounds batch-execution resource usage, makes concurrent SQLite writers safe, tightens
42
+ retry classification, and hardens the release pipeline. See the
43
+ [full migration notes](./CHANGELOG.md) before upgrading automated callers.
44
+
45
+ ### Resource & Reliability
46
+
47
+ - Internal batch pools are now created lazily per host, capped at one connection, and closed as soon as that host (including all retries) finishes; batch-wide live internal connections are bounded by `max_concurrency` instead of retaining one idle connection per host until the batch ends.
48
+ - SQLite writes now use `BEGIN IMMEDIATE` with a configurable `busy_timeout` (default 5000 ms), and the initial `journal_mode=WAL` switch is retried; concurrent `remote-cmd` processes writing the same `hosts.db` no longer fail with `database is locked` or lose updates. Schema and `db_version` are unchanged.
49
+ - Pool closure raises the new `PoolClosedError` (still catchable as `RuntimeError`); retry classification is corrected so bare `RuntimeError` is retryable again (v2.0 behavior) while pool closure remains non-retryable. Typed permanent and transient errors are unchanged.
50
+
51
+ ### Release Engineering
52
+
53
+ - Publish workflow: artifact actions aligned, run-scoped artifact names, duplicate version publication now fails instead of being skipped, `twine check` validates distributions, and the release tag is verified against the package version. Trusted Publishing and the GitHub Release → PyPI flow are unchanged.
54
+ - CI documentation drift detection: `docs/api` is regenerated and compared on relevant changes, failing when tracked generated docs are stale (`pdoc` pinned to `15.0.4` on Python 3.12).
55
+ - Release metadata validation: a lightweight gate checks the single-source version, the `pyproject.toml` dynamic-version attribute, `[Unreleased]` and matching `[x.y.z]` CHANGELOG headings, and (on release) that the git tag matches the package version.
56
+
44
57
  ## Table of Contents
45
58
 
59
+ - [v2.2.0 Release Highlights](#v220-release-highlights)
46
60
  - [Why Remote CMD?](#why-remote-cmd)
47
61
  - [Quick Start](#quick-start)
48
62
  - [Use Cases](#use-cases)
@@ -85,8 +99,8 @@ remote-cmd host add web-01 192.168.1.10 ubuntu --key ~/.ssh/id_rsa
85
99
  # 3. Run a command
86
100
  remote-cmd run web-01 "uptime"
87
101
 
88
- # 4. Run across all production servers
89
- remote-cmd batch-run -t production "df -h /"
102
+ # 4. Run across named production servers
103
+ remote-cmd batch-run web-01 web-02 db-01 "df -h /"
90
104
  ```
91
105
 
92
106
  ---
@@ -96,7 +110,7 @@ remote-cmd batch-run -t production "df -h /"
96
110
  ### 🖥️ System Administrators — Check disk across 20 servers in one command
97
111
 
98
112
  ```bash
99
- remote-cmd batch-run -t production "df -h / | tail -1"
113
+ remote-cmd batch-run web-01 web-02 db-01 "df -h / | tail -1"
100
114
  # Output:
101
115
  # ✓ web-01 → /dev/sda1 32G 12G 19G 40% /
102
116
  # ✓ web-02 → /dev/sda1 32G 28G 3G 90% / ⚠️
@@ -120,7 +134,7 @@ for host in service.list_hosts(tag="staging"):
120
134
  ### 🔥 Incident Response — Check logs across all servers
121
135
 
122
136
  ```bash
123
- remote-cmd batch-run -t web "journalctl -xe -n 50 | grep -i error"
137
+ remote-cmd batch-run web-01 web-02 "journalctl -xe -n 50 | grep -i error"
124
138
  ```
125
139
 
126
140
  ### 🔧 Config Update — Upload and reload nginx across tagged hosts
@@ -143,10 +157,10 @@ All operations are available from the terminal:
143
157
  | `remote-cmd host show <name>` | Show one host's details |
144
158
  | `remote-cmd host test <name>` | Test connectivity to a host |
145
159
  | `remote-cmd host remove <name>` | Remove a host |
146
- | `remote-cmd run <name> "<cmd>"` | Run a command on one host |
160
+ | `remote-cmd run <name> "<cmd>" [-T SECONDS]` | Run a command on one host (`--timeout/-T` sets the wall-clock limit) |
147
161
  | `remote-cmd upload <name> <local> <remote>` | Upload a file via SFTP |
148
162
  | `remote-cmd download <name> <remote> <local>` | Download a file via SFTP |
149
- | `remote-cmd batch-run -t <tag> "<cmd>"` | Run across all hosts in a tag (`-C` concurrency, `-T` timeout, `--async`, `--show-failures`) |
163
+ | `remote-cmd batch-run <name>... "<cmd>"` | Run across named hosts (`-C` concurrency, `-T` timeout, `-r` retries, `--async`, `--show-failures`) |
150
164
 
151
165
  ---
152
166
 
@@ -174,7 +188,7 @@ with SSHClient(config) as client:
174
188
 
175
189
  # List remote directory
176
190
  for entry in client.list_remote_directory("/var/log"):
177
- print(f"{entry['name']}: {entry['size']} bytes")
191
+ print(f"{entry.name}: {entry.size} bytes")
178
192
  ```
179
193
 
180
194
  ---
@@ -185,7 +199,7 @@ with SSHClient(config) as client:
185
199
  |---|---|
186
200
  | **SSH Auth** | Password + key file + ssh-agent, with pluggable credential providers |
187
201
  | **Credential Chain** | Source passwords from environment, keyring, or arbitrary providers, in priority order |
188
- | **Credential Encryption** | AES-encrypt secrets at rest (`CredentialEncryption`) |
202
+ | **Credential Encryption** | Fernet-encrypt secrets at rest (`CredentialEncryption`) |
189
203
  | **Commands** | Single, multi-line, sudo with password |
190
204
  | **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
191
205
  | **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
@@ -193,12 +207,26 @@ with SSHClient(config) as client:
193
207
  | **Batch Ops** | Run commands across any host group, synchronously or asynchronously |
194
208
  | **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
195
209
  | **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
196
- | **Connection Test** | Ping all hosts and report status |
210
+ | **Connection Test** | Test all host SSH connections and report status |
197
211
  | **Secure Logging** | Structured logging that filters sensitive data (`SensitiveDataFilter`) |
198
212
  | **Type Safety** | Full type annotations + mypy strict |
199
213
 
200
214
  ---
201
215
 
216
+ ## Cross-Platform Support
217
+
218
+ `remote-cmd` is a pure-Python client, **tested and supported on Linux, macOS, and Windows**. No native dependencies or platform-specific builds are required.
219
+
220
+ | Platform | Status | Notes |
221
+ |----------|--------|-------|
222
+ | Linux | ✅ Fully tested | Primary development platform |
223
+ | macOS | ✅ Supported | Use `brew install openssh` for a newer OpenSSH |
224
+ | Windows | ✅ Supported | PowerShell / CMD / WSL all work |
225
+
226
+ > **Note on Windows file permissions:** Credential encryption keys are protected with `0600` permissions on Unix. On Windows this restriction is not enforced by the filesystem — the key file is still encrypted at rest. For production Windows use, consider adding filesystem-level ACLs.
227
+
228
+ ---
229
+
202
230
  ## Installation
203
231
 
204
232
  ```bash
@@ -216,8 +244,9 @@ pip install -e ".[dev]"
216
244
 
217
245
  The `[async]` extra installs `asyncssh` and enables the native async execution
218
246
  kernel: `AsyncSSHClient`, `AsyncConnectionPool` and `AsyncBatchExecutor`
219
- (also available via `BatchExecutor(use_async=True)`). Without it, `import remote_cmd`
220
- still works — the async symbols are simply not exported.
247
+ (also available via `BatchExecutor(use_async=True)`). `BatchExecutor(use_async=True)`
248
+ also requires this extra. Without it, `import remote_cmd` still works — the async symbols
249
+ are simply not exported.
221
250
 
222
251
  ---
223
252
 
@@ -237,14 +266,14 @@ still works — the async symbols are simply not exported.
237
266
  | [Changelog](./CHANGELOG.md) | Release history |
238
267
  | [Mobile Remote Guide](./MOBILE-REMOTE-GUIDE.md) | Manage servers from your phone |
239
268
 
240
- > **Note:** The documentation center and tutorials are maintained in **Chinese**. See
269
+ > **Note:** The documentation center and tutorials are maintained in **English**. See
241
270
  > [README.zh-CN.md](./README.zh-CN.md) for the Chinese version of this page.
242
271
 
243
272
  ---
244
273
 
245
274
  ## Project Status
246
275
 
247
- **Beta.** The core API is stable. Breaking changes will be communicated via semantic versioning.
276
+ **Stable.** The core API is stable and versioned under semantic versioning. Breaking changes are communicated via major-version bumps, and the public API surface has been stable since the 1.x line.
248
277
 
249
278
  **Roadmap:**
250
279
  - [x] Async SSH operations (parallel execution) — v1.1.0
@@ -107,8 +107,8 @@ def example_file_transfer():
107
107
  entries = client.list_remote_directory("/tmp")
108
108
  print("\n/tmp 目录内容:")
109
109
  for entry in entries:
110
- type_str = "D" if entry["is_dir"] else "F"
111
- print(f" [{type_str}] {entry['name']:30} {entry['size']:>10} bytes")
110
+ type_str = "D" if entry.is_dir else "F"
111
+ print(f" [{type_str}] {entry.name:30} {entry.size:>10} bytes")
112
112
 
113
113
 
114
114
  def example_sudo_command():
@@ -5,16 +5,16 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "remote_cmd_manager"
7
7
  dynamic = ["version"]
8
- description = "SSH 远程服务器管理工具 / Python SSH remote server management tool"
8
+ description = "Cross-platform SSH remote server management tool (Linux/macOS/Windows) — lightweight, zero-configuration CLI and Python API for managing servers over SSH. Supports host CRUD, batch commands, file transfer, credential encryption, async execution, and more."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = { text = "MIT" }
12
12
  authors = [
13
13
  { name = "Vae-Scrooge", email = "vaescrooge@gmail.com" },
14
14
  ]
15
- keywords = ["ssh", "remote", "server-management", "sysadmin", "devops"]
15
+ keywords = ["ssh", "remote", "server-management", "sysadmin", "devops", "cross-platform"]
16
16
  classifiers = [
17
- "Development Status :: 4 - Beta",
17
+ "Development Status :: 5 - Production/Stable",
18
18
  "Intended Audience :: Developers",
19
19
  "Intended Audience :: System Administrators",
20
20
  "License :: OSI Approved :: MIT License",
@@ -25,6 +25,9 @@ classifiers = [
25
25
  "Topic :: System :: Systems Administration",
26
26
  "Topic :: System :: Networking",
27
27
  "Environment :: Console",
28
+ "Operating System :: POSIX :: Linux",
29
+ "Operating System :: MacOS",
30
+ "Operating System :: Microsoft :: Windows",
28
31
  ]
29
32
  dependencies = [
30
33
  "paramiko>=3.0",
@@ -50,8 +53,10 @@ dev = [
50
53
  # 测试 keyring 凭据提供者(生产为可选依赖,测试环境固定安装)
51
54
  "keyring>=24.0",
52
55
  ]
53
- # 文档生成(pdoc>=16 需 Python>=3.10,固定到 15.x 以兼容 requires-python>=3.9)
54
- docs = ["pdoc>=15,<16"]
56
+ # 文档生成(pdoc>=16 需 Python>=3.10,固定到 15.x 以兼容 requires-python>=3.9)。
57
+ # 精确 pin 15.0.4:pdoc HTML 输出随版本/解释器变化,docs/api 漂移门禁
58
+ # (scripts/check_docs_drift.py)要求生成结果可复现。
59
+ docs = ["pdoc==15.0.4"]
55
60
 
56
61
  [project.urls]
57
62
  Homepage = "https://github.com/Vae-Scrooge/remote-cmd"
@@ -48,7 +48,7 @@ Remote CMD - SSH 远程服务器管理工具
48
48
  - 文档: 参见 docs/ 目录
49
49
 
50
50
  Author: Vae-Scrooge
51
- Version: 1.2.3
51
+ Version: 2.2.0(单一真相源见 remote_cmd._version)
52
52
  License: MIT
53
53
  """
54
54
 
@@ -9,4 +9,4 @@
9
9
  - ``__version__`` 由 ``remote_cmd/__init__.py`` 导入并继续作为公共 API 导出。
10
10
  """
11
11
 
12
- __version__ = "2.0.0"
12
+ __version__ = "2.2.0"
@@ -310,19 +310,27 @@ def host_test(ctx: click.Context, name: str) -> None:
310
310
  @cli.command()
311
311
  @click.argument("host_name", required=True)
312
312
  @click.argument("command", required=True)
313
+ @click.option(
314
+ "--timeout",
315
+ "-T",
316
+ default=None,
317
+ type=int,
318
+ help="Command timeout in seconds (default: no timeout)",
319
+ )
313
320
  @click.pass_context
314
- def run(ctx: click.Context, host_name: str, command: str) -> None:
321
+ def run(ctx: click.Context, host_name: str, command: str, timeout: Optional[int]) -> None:
315
322
  """
316
323
  Execute a command on a remote host
317
324
 
318
325
  HOST_NAME: host name
326
+
319
327
  COMMAND: command to execute
320
328
  """
321
329
  service: HostService = ctx.obj["service"]
322
330
 
323
331
  try:
324
332
  with service.connect_to_host(host_name) as client:
325
- result = client.execute(command)
333
+ result = client.execute(command, timeout=timeout)
326
334
 
327
335
  if result.stdout:
328
336
  click.echo(result.stdout)
@@ -397,7 +405,12 @@ def download(ctx: click.Context, host_name: str, local_path: str, remote_path: s
397
405
  @click.option("--concurrency", "-C", default=10, help="Max concurrency (default: 10)")
398
406
  @click.option("--timeout", "-T", default=30, help="Command timeout in seconds (default: 30)")
399
407
  @click.option("--retry", "-r", default=0, help="Failure retry count (default: 0)")
400
- @click.option("--retry-delay", default=1.0, help="Retry delay in seconds (default: 1.0)")
408
+ @click.option(
409
+ "--retry-delay",
410
+ default=1.0,
411
+ help="Base retry delay in seconds; actual wait is exponential "
412
+ "backoff with jitter (default: 1.0)",
413
+ )
401
414
  @click.option("--async", "use_async", is_flag=True, help="Use async execution engine (asyncssh)")
402
415
  @click.option("--show-failures", is_flag=True, help="Show only failed hosts")
403
416
  @click.pass_context
@@ -26,6 +26,7 @@ from remote_cmd.service._pool_policy import (
26
26
  lifetime_expired,
27
27
  should_close,
28
28
  )
29
+ from remote_cmd.utils.exceptions import PoolClosedError
29
30
 
30
31
  logger = logging.getLogger(__name__)
31
32
 
@@ -48,12 +49,25 @@ class AsyncConnectionPool:
48
49
  max_lifetime: int = 3600,
49
50
  idle_timeout: int = 300,
50
51
  health_check_interval: int = 60,
52
+ client_factory: Optional[Any] = None,
51
53
  ) -> None:
54
+ """
55
+ Args:
56
+ config: 用于建立 SSH 连接的配置
57
+ max_connections: 同一最大连接数(同一配置可复用)
58
+ max_lifetime: 连接最大生命周期(秒),超过自动关闭
59
+ idle_timeout: 空闲超时(秒),超过自动关闭
60
+ health_check_interval: 后台清理任务周期(秒)
61
+ client_factory: 客户端工厂,默认为 AsyncSSHClient;测试可注入
62
+ mock(与 SyncConnectionPool 对齐)
63
+ """
52
64
  self.config = config
53
65
  self._max = max_connections
54
66
  self._max_lifetime = max_lifetime
55
67
  self._idle_timeout = idle_timeout
56
68
  self._health_check_interval = health_check_interval
69
+ # 客户端工厂:默认为 AsyncSSHClient;测试可注入 mock
70
+ self._client_factory = client_factory or AsyncSSHClient
57
71
 
58
72
  # 容器
59
73
  self._connections: list[AsyncSSHClient] = []
@@ -107,11 +121,18 @@ class AsyncConnectionPool:
107
121
 
108
122
  Raises:
109
123
  SSHConnectionError: 创建连接失败
110
- RuntimeError: 连接池已关闭(close_all 之后)
124
+ PoolClosedError: 连接池已关闭(close_all 之后),
125
+ 同时是 RuntimeError 子类(既有捕获行为不变)
111
126
  """
112
127
  if self._closed:
113
- raise RuntimeError("connection pool is closed")
128
+ raise PoolClosedError("connection pool is closed")
114
129
  await self._semaphore.acquire()
130
+ # 竞态守卫:等待信号量期间 close_all() 可能已完成——
131
+ # 取得槽位后必须复查,已关闭则归还槽位并抛出既有错误,
132
+ # 否则会向调用方发放来自已关闭池的连接
133
+ if self._closed:
134
+ self._semaphore.release()
135
+ raise PoolClosedError("connection pool is closed")
115
136
  try:
116
137
  # 优先复用空闲连接
117
138
  while not self._free.empty():
@@ -168,7 +189,7 @@ class AsyncConnectionPool:
168
189
  # 内部
169
190
  # ------------------------------------------------------------------
170
191
  async def _create_connection(self) -> AsyncSSHClient:
171
- client = AsyncSSHClient(self.config)
192
+ client = self._client_factory(self.config)
172
193
  try:
173
194
  await client.connect()
174
195
  except Exception: # noqa: BLE001
@@ -22,11 +22,18 @@ from typing import Any, Optional
22
22
 
23
23
  import asyncssh
24
24
 
25
- from remote_cmd.core.ssh_client import CommandResult, ConnectionConfig, RemoteFileEntry
25
+ from remote_cmd.core.ssh_client import (
26
+ CommandResult,
27
+ ConnectionConfig,
28
+ RemoteFileEntry,
29
+ validate_environment,
30
+ )
26
31
  from remote_cmd.utils.exceptions import (
32
+ SSHAuthenticationError,
27
33
  SSHCommandError,
28
34
  SSHConnectionError,
29
35
  SSHFileTransferError,
36
+ SSHTimeoutError,
30
37
  )
31
38
 
32
39
  logger = logging.getLogger(__name__)
@@ -94,11 +101,12 @@ class AsyncSSHClient:
94
101
  try:
95
102
  self._conn = await asyncssh.connect(**connect_kwargs)
96
103
  except asyncssh.PermissionDenied as e:
97
- raise SSHConnectionError(f"authentication failed: {e}") from e
104
+ # 永久性错误:重试同一凭据只会加剧账号锁定(见 service/retry_policy.py)
105
+ raise SSHAuthenticationError(f"authentication failed: {e}") from e
98
106
  except (OSError, asyncssh.Error) as e:
99
107
  msg = str(e).lower()
100
108
  if "timed out" in msg or "timeout" in msg or isinstance(e, asyncssh.TimeoutError):
101
- raise SSHConnectionError(f"connection timeout: {self.config.hostname}") from e
109
+ raise SSHTimeoutError(f"connection timeout: {self.config.hostname}") from e
102
110
  raise SSHConnectionError(f"connection error: {e}") from e
103
111
 
104
112
  logger.info(f"connected to {self.config.hostname}")
@@ -187,6 +195,8 @@ class AsyncSSHClient:
187
195
  SSHConnectionError: 未连接时抛出
188
196
  """
189
197
  conn = await self._get_conn()
198
+ # 安全:键必须为合法 shell 标识符(与同步实现一致,防止命令注入)
199
+ validate_environment(environment)
190
200
  # 安全:对 value 做 shlex.quote 转义,防止 shell 元字符注入
191
201
  env_str = ""
192
202
  if environment:
@@ -198,11 +208,13 @@ class AsyncSSHClient:
198
208
  # 安全:不记录命令全文(可能含敏感参数),仅记录执行事件
199
209
  logger.debug("executing remote command")
200
210
  try:
211
+ # 环境变量仅通过命令前缀的 export 注入(与同步 SSHClient 行为
212
+ # 一致):conn.run(env=...) 依赖服务端 AcceptEnv 且语义分叉,
213
+ # 不再重复传递
201
214
  result = await conn.run(
202
215
  full_command,
203
216
  timeout=timeout,
204
217
  check=False,
205
- env={k: str(v) for k, v in (environment or {}).items()},
206
218
  )
207
219
  except (OSError, asyncssh.Error) as e:
208
220
  raise SSHCommandError(f"command execution failed: {e}") from e