remote-cmd-manager 2.1.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.
- {remote_cmd_manager-2.1.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.2.0}/PKG-INFO +37 -30
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/README.md +29 -25
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/pyproject.toml +10 -5
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/__init__.py +1 -1
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/_version.py +1 -1
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/async_connection_pool.py +5 -3
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/ssh_client.py +8 -5
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/sync_connection_pool.py +5 -3
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/sqlite_host_repository.py +50 -7
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/async_batch_executor.py +96 -85
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/batch_executor.py +120 -113
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/retry_policy.py +14 -7
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/exceptions.py +28 -3
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0/remote_cmd_manager.egg-info}/PKG-INFO +37 -30
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/SOURCES.txt +1 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/requires.txt +1 -1
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_async_batch_executor.py +142 -6
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_batch_executor.py +182 -1
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_crypto.py +7 -1
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_exceptions.py +16 -0
- remote_cmd_manager-2.2.0/tests/test_release_metadata.py +69 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_retry_policy.py +17 -1
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_sqlite_repository.py +167 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_sync_connection_pool.py +19 -7
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/LICENSE +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/MANIFEST.in +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/config.example.yaml +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/examples/basic_usage.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/examples/deploy_script.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/examples/nginx_batch_update.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/examples/system_health_check.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/cli/__init__.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/cli/main.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/__init__.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/async_ssh_client.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/host.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/host_manager.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/__init__.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/host_repository.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/json_host_repository.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/__init__.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/_host_runner.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/_pool_policy.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/_types.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/credential_provider.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/host_service.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/ssh_service.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/storage_factory.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/task_runner.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/__init__.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/config.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/credential_guard.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/crypto.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/logging_utils.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/requirements.txt +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/setup.cfg +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/__init__.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/conftest.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/integration/conftest.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/integration/test_ssh_connection.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/performance/__init__.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/performance/conftest.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/performance/test_benchmarks.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_async_ssh_client.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_cli.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_config.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_credential_provider.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_host.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_host_manager.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_host_service.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_keyring_provider.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_logging_utils.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_pool_policy.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_repository.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_ssh_client.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_ssh_service.py +0 -0
- {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/tests/test_storage_factory.py +0 -0
- {remote_cmd_manager-2.1.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.
|
|
4
|
-
Summary: SSH
|
|
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 ::
|
|
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
|
|
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,12 +75,6 @@ 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
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.
|
|
@@ -88,32 +86,27 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
88
86
|
|
|
89
87
|
---
|
|
90
88
|
|
|
91
|
-
## v2.
|
|
89
|
+
## v2.2.0 Release Highlights
|
|
92
90
|
|
|
93
|
-
v2.
|
|
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
|
|
94
93
|
[full migration notes](./CHANGELOG.md) before upgrading automated callers.
|
|
95
94
|
|
|
96
|
-
###
|
|
95
|
+
### Resource & Reliability
|
|
97
96
|
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
- `
|
|
101
|
-
- `pool_factory` enables caller-supplied pools; external pools are caller-owned and never closed by either executor, while internally created pools are closed automatically after `execute()`.
|
|
102
|
-
- Retries classify permanent failures (authentication, credentials, configuration, validation, and programming errors) as non-retryable; unknown `Exception` subclasses remain retryable for backward compatibility. `retry_delay` is now the exponential-backoff base and full jitter is applied with a 60-second cap.
|
|
103
|
-
- Unknown hosts produce per-host failure results instead of aborting a multi-host batch, and duplicate host names are executed once.
|
|
104
|
-
- The exception hierarchy adds `SSHAuthenticationError`, `SSHTimeoutError`, `SSHCommandTimeoutError`, `CredentialError`, and the `ConfigurationError` alias without removing existing catch paths.
|
|
105
|
-
- Environment-variable names are validated before shell interpolation, and command text is excluded from library logs and execution errors.
|
|
106
|
-
- `remote-cmd run` supports `--timeout/-T` for command execution limits.
|
|
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.
|
|
107
100
|
|
|
108
|
-
###
|
|
101
|
+
### Release Engineering
|
|
109
102
|
|
|
110
|
-
-
|
|
111
|
-
- `
|
|
112
|
-
-
|
|
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.
|
|
113
106
|
|
|
114
107
|
## Table of Contents
|
|
115
108
|
|
|
116
|
-
- [v2.
|
|
109
|
+
- [v2.2.0 Release Highlights](#v220-release-highlights)
|
|
117
110
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
118
111
|
- [Quick Start](#quick-start)
|
|
119
112
|
- [Use Cases](#use-cases)
|
|
@@ -270,6 +263,20 @@ with SSHClient(config) as client:
|
|
|
270
263
|
|
|
271
264
|
---
|
|
272
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
|
+
|
|
273
280
|
## Installation
|
|
274
281
|
|
|
275
282
|
```bash
|
|
@@ -309,14 +316,14 @@ are simply not exported.
|
|
|
309
316
|
| [Changelog](./CHANGELOG.md) | Release history |
|
|
310
317
|
| [Mobile Remote Guide](./MOBILE-REMOTE-GUIDE.md) | Manage servers from your phone |
|
|
311
318
|
|
|
312
|
-
> **Note:** The documentation center and tutorials are maintained in **
|
|
319
|
+
> **Note:** The documentation center and tutorials are maintained in **English**. See
|
|
313
320
|
> [README.zh-CN.md](./README.zh-CN.md) for the Chinese version of this page.
|
|
314
321
|
|
|
315
322
|
---
|
|
316
323
|
|
|
317
324
|
## Project Status
|
|
318
325
|
|
|
319
|
-
**
|
|
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.
|
|
320
327
|
|
|
321
328
|
**Roadmap:**
|
|
322
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,12 +25,6 @@
|
|
|
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
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.
|
|
@@ -41,32 +36,27 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
41
36
|
|
|
42
37
|
---
|
|
43
38
|
|
|
44
|
-
## v2.
|
|
39
|
+
## v2.2.0 Release Highlights
|
|
45
40
|
|
|
46
|
-
v2.
|
|
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
|
|
47
43
|
[full migration notes](./CHANGELOG.md) before upgrading automated callers.
|
|
48
44
|
|
|
49
|
-
###
|
|
45
|
+
### Resource & Reliability
|
|
50
46
|
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
- `
|
|
54
|
-
- `pool_factory` enables caller-supplied pools; external pools are caller-owned and never closed by either executor, while internally created pools are closed automatically after `execute()`.
|
|
55
|
-
- Retries classify permanent failures (authentication, credentials, configuration, validation, and programming errors) as non-retryable; unknown `Exception` subclasses remain retryable for backward compatibility. `retry_delay` is now the exponential-backoff base and full jitter is applied with a 60-second cap.
|
|
56
|
-
- Unknown hosts produce per-host failure results instead of aborting a multi-host batch, and duplicate host names are executed once.
|
|
57
|
-
- The exception hierarchy adds `SSHAuthenticationError`, `SSHTimeoutError`, `SSHCommandTimeoutError`, `CredentialError`, and the `ConfigurationError` alias without removing existing catch paths.
|
|
58
|
-
- Environment-variable names are validated before shell interpolation, and command text is excluded from library logs and execution errors.
|
|
59
|
-
- `remote-cmd run` supports `--timeout/-T` for command execution limits.
|
|
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.
|
|
60
50
|
|
|
61
|
-
###
|
|
51
|
+
### Release Engineering
|
|
62
52
|
|
|
63
|
-
-
|
|
64
|
-
- `
|
|
65
|
-
-
|
|
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.
|
|
66
56
|
|
|
67
57
|
## Table of Contents
|
|
68
58
|
|
|
69
|
-
- [v2.
|
|
59
|
+
- [v2.2.0 Release Highlights](#v220-release-highlights)
|
|
70
60
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
71
61
|
- [Quick Start](#quick-start)
|
|
72
62
|
- [Use Cases](#use-cases)
|
|
@@ -223,6 +213,20 @@ with SSHClient(config) as client:
|
|
|
223
213
|
|
|
224
214
|
---
|
|
225
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
|
+
|
|
226
230
|
## Installation
|
|
227
231
|
|
|
228
232
|
```bash
|
|
@@ -262,14 +266,14 @@ are simply not exported.
|
|
|
262
266
|
| [Changelog](./CHANGELOG.md) | Release history |
|
|
263
267
|
| [Mobile Remote Guide](./MOBILE-REMOTE-GUIDE.md) | Manage servers from your phone |
|
|
264
268
|
|
|
265
|
-
> **Note:** The documentation center and tutorials are maintained in **
|
|
269
|
+
> **Note:** The documentation center and tutorials are maintained in **English**. See
|
|
266
270
|
> [README.zh-CN.md](./README.zh-CN.md) for the Chinese version of this page.
|
|
267
271
|
|
|
268
272
|
---
|
|
269
273
|
|
|
270
274
|
## Project Status
|
|
271
275
|
|
|
272
|
-
**
|
|
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.
|
|
273
277
|
|
|
274
278
|
**Roadmap:**
|
|
275
279
|
- [x] Async SSH operations (parallel execution) — v1.1.0
|
|
@@ -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
|
|
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 ::
|
|
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
|
-
|
|
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"
|
{remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/async_connection_pool.py
RENAMED
|
@@ -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
|
|
|
@@ -120,17 +121,18 @@ class AsyncConnectionPool:
|
|
|
120
121
|
|
|
121
122
|
Raises:
|
|
122
123
|
SSHConnectionError: 创建连接失败
|
|
123
|
-
|
|
124
|
+
PoolClosedError: 连接池已关闭(close_all 之后),
|
|
125
|
+
同时是 RuntimeError 子类(既有捕获行为不变)
|
|
124
126
|
"""
|
|
125
127
|
if self._closed:
|
|
126
|
-
raise
|
|
128
|
+
raise PoolClosedError("connection pool is closed")
|
|
127
129
|
await self._semaphore.acquire()
|
|
128
130
|
# 竞态守卫:等待信号量期间 close_all() 可能已完成——
|
|
129
131
|
# 取得槽位后必须复查,已关闭则归还槽位并抛出既有错误,
|
|
130
132
|
# 否则会向调用方发放来自已关闭池的连接
|
|
131
133
|
if self._closed:
|
|
132
134
|
self._semaphore.release()
|
|
133
|
-
raise
|
|
135
|
+
raise PoolClosedError("connection pool is closed")
|
|
134
136
|
try:
|
|
135
137
|
# 优先复用空闲连接
|
|
136
138
|
while not self._free.empty():
|
|
@@ -20,7 +20,7 @@ import socket
|
|
|
20
20
|
import stat
|
|
21
21
|
import threading
|
|
22
22
|
from dataclasses import dataclass
|
|
23
|
-
from pathlib import Path
|
|
23
|
+
from pathlib import Path, PurePosixPath
|
|
24
24
|
from typing import Any, Optional
|
|
25
25
|
|
|
26
26
|
import paramiko
|
|
@@ -729,13 +729,16 @@ class SSHClient:
|
|
|
729
729
|
sftp = self._get_sftp()
|
|
730
730
|
|
|
731
731
|
def _makedirs(sftp_client: paramiko.SFTPClient, remote_path: str) -> None:
|
|
732
|
-
|
|
732
|
+
# 远端路径始终是 POSIX 语义,必须用 PurePosixPath 处理,
|
|
733
|
+
# 不能用本地 Path(在 Windows 上 WindowsPath 对 "/" 的处理会导致无限递归)。
|
|
734
|
+
p = PurePosixPath(remote_path)
|
|
735
|
+
if p == PurePosixPath("/") or p == PurePosixPath("."):
|
|
733
736
|
return
|
|
734
737
|
try:
|
|
735
|
-
sftp_client.stat(
|
|
738
|
+
sftp_client.stat(str(p))
|
|
736
739
|
except OSError:
|
|
737
|
-
_makedirs(sftp_client, str(
|
|
738
|
-
sftp_client.mkdir(
|
|
740
|
+
_makedirs(sftp_client, str(p.parent))
|
|
741
|
+
sftp_client.mkdir(str(p))
|
|
739
742
|
|
|
740
743
|
try:
|
|
741
744
|
_makedirs(sftp, path)
|
{remote_cmd_manager-2.1.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/sync_connection_pool.py
RENAMED
|
@@ -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
|
|
|
@@ -111,17 +112,18 @@ class SyncConnectionPool:
|
|
|
111
112
|
|
|
112
113
|
Raises:
|
|
113
114
|
SSHConnectionError: 创建连接失败
|
|
114
|
-
|
|
115
|
+
PoolClosedError: 连接池已关闭(close_all 之后),
|
|
116
|
+
同时是 RuntimeError 子类(既有捕获行为不变)
|
|
115
117
|
"""
|
|
116
118
|
if self._closed:
|
|
117
|
-
raise
|
|
119
|
+
raise PoolClosedError("connection pool is closed")
|
|
118
120
|
self._semaphore.acquire()
|
|
119
121
|
# 竞态守卫:等待信号量期间 close_all() 可能已完成——
|
|
120
122
|
# 取得槽位后必须复查,已关闭则归还槽位并抛出既有错误,
|
|
121
123
|
# 否则会向调用方发放来自已关闭池的连接
|
|
122
124
|
if self._closed:
|
|
123
125
|
self._semaphore.release()
|
|
124
|
-
raise
|
|
126
|
+
raise PoolClosedError("connection pool is closed")
|
|
125
127
|
try:
|
|
126
128
|
# 优先复用空闲连接
|
|
127
129
|
while not self._free.empty():
|
|
@@ -21,6 +21,7 @@ import json
|
|
|
21
21
|
import logging
|
|
22
22
|
import sqlite3
|
|
23
23
|
import threading
|
|
24
|
+
import time
|
|
24
25
|
from typing import Optional
|
|
25
26
|
|
|
26
27
|
from remote_cmd.core.host import Host
|
|
@@ -64,6 +65,9 @@ CREATE TABLE IF NOT EXISTS meta (
|
|
|
64
65
|
);
|
|
65
66
|
"""
|
|
66
67
|
|
|
68
|
+
# 写锁等待上限(毫秒):多进程并发写同一数据库时,等待对方短事务提交/回滚
|
|
69
|
+
DEFAULT_BUSY_TIMEOUT_MS = 5000
|
|
70
|
+
|
|
67
71
|
|
|
68
72
|
class SqliteHostRepository(HostRepository):
|
|
69
73
|
"""
|
|
@@ -74,6 +78,7 @@ class SqliteHostRepository(HostRepository):
|
|
|
74
78
|
migrate_from: JSON 文件路径,用于自动迁移(仅首次使用)
|
|
75
79
|
auto_create: 是否自动创建表和数据库,默认 True
|
|
76
80
|
encryption: 可选的凭据加密器(设置后 save() 自动加密 password)
|
|
81
|
+
busy_timeout_ms: 写锁等待上限(毫秒),默认 5000
|
|
77
82
|
|
|
78
83
|
注意: 密码的加密依赖传入 encryption。若直接以明文密码调用 save()
|
|
79
84
|
且未提供 encryption,明文会被持久化到数据库。请勿绕过 HostService。
|
|
@@ -85,11 +90,13 @@ class SqliteHostRepository(HostRepository):
|
|
|
85
90
|
migrate_from: Optional[str] = None,
|
|
86
91
|
auto_create: bool = True,
|
|
87
92
|
encryption: Optional[CredentialEncryption] = None,
|
|
93
|
+
busy_timeout_ms: int = DEFAULT_BUSY_TIMEOUT_MS,
|
|
88
94
|
) -> None:
|
|
89
95
|
self._db_path = db_path
|
|
90
96
|
self._lock = threading.Lock()
|
|
91
97
|
self._encryption = encryption
|
|
92
98
|
self._guard = PasswordGuard(encryption)
|
|
99
|
+
self._busy_timeout_ms = busy_timeout_ms
|
|
93
100
|
|
|
94
101
|
if auto_create:
|
|
95
102
|
self._init_db()
|
|
@@ -103,7 +110,7 @@ class SqliteHostRepository(HostRepository):
|
|
|
103
110
|
|
|
104
111
|
def _init_db(self) -> None:
|
|
105
112
|
"""初始化数据库:创建表和索引"""
|
|
106
|
-
with self._txn() as conn:
|
|
113
|
+
with self._txn(write=True) as conn:
|
|
107
114
|
conn.execute(CREATE_TABLE_SQL)
|
|
108
115
|
conn.execute(CREATE_META_SQL)
|
|
109
116
|
for idx_sql in CREATE_INDEXES_SQL:
|
|
@@ -117,30 +124,66 @@ class SqliteHostRepository(HostRepository):
|
|
|
117
124
|
logger.debug(f"SQLite database initialized: {self._db_path}")
|
|
118
125
|
|
|
119
126
|
def _get_conn(self) -> sqlite3.Connection:
|
|
120
|
-
"""
|
|
127
|
+
"""获取数据库连接(线程安全)。
|
|
128
|
+
|
|
129
|
+
PRAGMA 顺序约定:``busy_timeout`` 必须最先设置——首次并发打开同一
|
|
130
|
+
数据库文件时,``journal_mode=WAL`` 本身需要短暂排他锁,若 busy
|
|
131
|
+
handler 尚未生效会立即抛 ``database is locked``(v2.2 多进程回归)。
|
|
132
|
+
|
|
133
|
+
注意:SQLite 的 journal_mode 切换不经过 busy handler,即使设置了
|
|
134
|
+
``busy_timeout`` 仍会直接返回 SQLITE_BUSY;因此在首次并发切换到
|
|
135
|
+
WAL 时使用有界重试(上限即 busy_timeout)等待其他连接的短事务结束。
|
|
136
|
+
"""
|
|
121
137
|
conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
|
122
138
|
conn.row_factory = sqlite3.Row
|
|
123
|
-
|
|
139
|
+
# 多进程/多连接写入争用:等待对方事务结束而不是立即失败
|
|
140
|
+
conn.execute(f"PRAGMA busy_timeout={self._busy_timeout_ms};")
|
|
141
|
+
self._enable_wal(conn)
|
|
124
142
|
conn.execute("PRAGMA foreign_keys=ON;")
|
|
125
143
|
return conn
|
|
126
144
|
|
|
145
|
+
def _enable_wal(self, conn: sqlite3.Connection) -> None:
|
|
146
|
+
"""启用 WAL;对 journal_mode 切换的 SQLITE_BUSY 做有界重试。
|
|
147
|
+
|
|
148
|
+
数据库已是 WAL 时该 PRAGMA 只读取模式、不取排他锁,直接返回;
|
|
149
|
+
仅首次从其他模式切换到 WAL 的竞态需要重试。
|
|
150
|
+
"""
|
|
151
|
+
deadline = time.monotonic() + self._busy_timeout_ms / 1000.0
|
|
152
|
+
while True:
|
|
153
|
+
try:
|
|
154
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
155
|
+
return
|
|
156
|
+
except sqlite3.OperationalError as e:
|
|
157
|
+
message = str(e).lower()
|
|
158
|
+
if "locked" not in message and "busy" not in message:
|
|
159
|
+
raise
|
|
160
|
+
if time.monotonic() >= deadline:
|
|
161
|
+
raise
|
|
162
|
+
time.sleep(0.05)
|
|
163
|
+
|
|
127
164
|
@contextlib.contextmanager
|
|
128
|
-
def _txn(self):
|
|
165
|
+
def _txn(self, write: bool = False):
|
|
129
166
|
"""
|
|
130
167
|
事务 + 连接生命周期上下文
|
|
131
168
|
|
|
132
169
|
包装 ``with conn:`` 与 ``conn.close()`` 为单一上下文:
|
|
133
170
|
- 进入时打开新连接并执行 PRAGMA
|
|
171
|
+
- ``write=True`` 时以 ``BEGIN IMMEDIATE`` 预先取得写锁(等待受
|
|
172
|
+
``busy_timeout`` 约束);避免 deferred 事务在写升级时因快照过期
|
|
173
|
+
直接返回 SQLITE_BUSY(busy handler 不适用该场景)
|
|
134
174
|
- 退出时先 ``conn.__exit__`` 提交/回滚,再 ``conn.close()`` 释放 fd
|
|
135
175
|
|
|
136
176
|
解决 ``with self._get_conn() as conn:`` 不自动 close 导致的 fd 累积泄漏
|
|
137
177
|
(sqlite3.Connection.__exit__ 仅管理事务边界,不释放连接句柄)。
|
|
138
178
|
|
|
139
179
|
所有读写操作都应通过 ``with self._lock, self._txn() as conn:`` 使用,
|
|
140
|
-
保证 ``self._lock`` 串行化的同时每次操作后释放 fd
|
|
180
|
+
保证 ``self._lock`` 串行化的同时每次操作后释放 fd;写操作使用
|
|
181
|
+
``self._txn(write=True)``。
|
|
141
182
|
"""
|
|
142
183
|
conn = self._get_conn()
|
|
143
184
|
try:
|
|
185
|
+
if write:
|
|
186
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
144
187
|
with conn: # 事务:commit 或 rollback
|
|
145
188
|
yield conn
|
|
146
189
|
finally:
|
|
@@ -204,7 +247,7 @@ class SqliteHostRepository(HostRepository):
|
|
|
204
247
|
|
|
205
248
|
def save(self, host: Host) -> None:
|
|
206
249
|
"""保存或更新主机"""
|
|
207
|
-
with self._lock, self._txn() as conn:
|
|
250
|
+
with self._lock, self._txn(write=True) as conn:
|
|
208
251
|
tags_json = json.dumps(host.tags or [], ensure_ascii=False)
|
|
209
252
|
# 配置了加密器时,明文密码先加密再落库
|
|
210
253
|
password = self._guard.encrypt(host.password)
|
|
@@ -248,7 +291,7 @@ class SqliteHostRepository(HostRepository):
|
|
|
248
291
|
|
|
249
292
|
def delete(self, name: str) -> None:
|
|
250
293
|
"""按名称删除主机"""
|
|
251
|
-
with self._lock, self._txn() as conn:
|
|
294
|
+
with self._lock, self._txn(write=True) as conn:
|
|
252
295
|
cursor = conn.execute("DELETE FROM hosts WHERE name = ?", (name,))
|
|
253
296
|
conn.commit()
|
|
254
297
|
|