remote-cmd-manager 2.1.0__tar.gz → 2.3.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 (81) hide show
  1. {remote_cmd_manager-2.1.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.3.0}/PKG-INFO +41 -30
  2. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/README.md +30 -25
  3. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/pyproject.toml +13 -5
  4. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/__init__.py +1 -1
  5. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/_version.py +1 -1
  6. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/async_connection_pool.py +5 -3
  7. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/async_ssh_client.py +141 -13
  8. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/ssh_client.py +169 -33
  9. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/sync_connection_pool.py +5 -3
  10. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/repository/sqlite_host_repository.py +50 -7
  11. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/async_batch_executor.py +96 -85
  12. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/batch_executor.py +120 -113
  13. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/retry_policy.py +14 -7
  14. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/exceptions.py +28 -3
  15. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0/remote_cmd_manager.egg-info}/PKG-INFO +41 -30
  16. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd_manager.egg-info/SOURCES.txt +1 -0
  17. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd_manager.egg-info/requires.txt +1 -1
  18. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_async_batch_executor.py +142 -6
  19. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_async_ssh_client.py +253 -0
  20. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_batch_executor.py +182 -1
  21. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_crypto.py +7 -1
  22. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_exceptions.py +16 -0
  23. remote_cmd_manager-2.3.0/tests/test_release_metadata.py +69 -0
  24. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_retry_policy.py +17 -1
  25. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_sqlite_repository.py +167 -0
  26. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_ssh_client.py +132 -0
  27. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_sync_connection_pool.py +19 -7
  28. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/LICENSE +0 -0
  29. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/MANIFEST.in +0 -0
  30. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/config.example.yaml +0 -0
  31. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/examples/basic_usage.py +0 -0
  32. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/examples/deploy_script.py +0 -0
  33. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/examples/nginx_batch_update.py +0 -0
  34. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/examples/system_health_check.py +0 -0
  35. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/cli/__init__.py +0 -0
  36. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/cli/main.py +0 -0
  37. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/__init__.py +0 -0
  38. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/host.py +0 -0
  39. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/host_manager.py +0 -0
  40. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/repository/__init__.py +0 -0
  41. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/repository/host_repository.py +0 -0
  42. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/repository/json_host_repository.py +0 -0
  43. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/__init__.py +0 -0
  44. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/_host_runner.py +0 -0
  45. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/_pool_policy.py +0 -0
  46. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/_types.py +0 -0
  47. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/credential_provider.py +0 -0
  48. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/host_service.py +0 -0
  49. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/ssh_service.py +0 -0
  50. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/storage_factory.py +0 -0
  51. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/task_runner.py +0 -0
  52. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/__init__.py +0 -0
  53. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/config.py +0 -0
  54. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/credential_guard.py +0 -0
  55. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/crypto.py +0 -0
  56. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/logging_utils.py +0 -0
  57. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
  58. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
  59. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
  60. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/requirements.txt +0 -0
  61. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/setup.cfg +0 -0
  62. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/__init__.py +0 -0
  63. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/conftest.py +0 -0
  64. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/integration/conftest.py +0 -0
  65. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/integration/test_ssh_connection.py +0 -0
  66. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/performance/__init__.py +0 -0
  67. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/performance/conftest.py +0 -0
  68. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/performance/test_benchmarks.py +0 -0
  69. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_cli.py +0 -0
  70. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_config.py +0 -0
  71. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_credential_provider.py +0 -0
  72. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_host.py +0 -0
  73. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_host_manager.py +0 -0
  74. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_host_service.py +0 -0
  75. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_keyring_provider.py +0 -0
  76. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_logging_utils.py +0 -0
  77. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_pool_policy.py +0 -0
  78. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_repository.py +0 -0
  79. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_ssh_service.py +0 -0
  80. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.0}/tests/test_storage_factory.py +0 -0
  81. {remote_cmd_manager-2.1.0 → remote_cmd_manager-2.3.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.1.0
4
- Summary: SSH 远程服务器管理工具 / Python SSH remote server management tool
3
+ Version: 2.3.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
@@ -18,9 +18,15 @@ Classifier: Programming Language :: Python :: 3
18
18
  Classifier: Programming Language :: Python :: 3.9
19
19
  Classifier: Programming Language :: Python :: 3.10
20
20
  Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
21
24
  Classifier: Topic :: System :: Systems Administration
22
25
  Classifier: Topic :: System :: Networking
23
26
  Classifier: Environment :: Console
27
+ Classifier: Operating System :: POSIX :: Linux
28
+ Classifier: Operating System :: MacOS
29
+ Classifier: Operating System :: Microsoft :: Windows
24
30
  Requires-Python: >=3.9
25
31
  Description-Content-Type: text/markdown
26
32
  License-File: LICENSE
@@ -42,7 +48,7 @@ Requires-Dist: ruff>=0.4; extra == "dev"
42
48
  Requires-Dist: mypy>=1.10; extra == "dev"
43
49
  Requires-Dist: keyring>=24.0; extra == "dev"
44
50
  Provides-Extra: docs
45
- Requires-Dist: pdoc<16,>=15; extra == "docs"
51
+ Requires-Dist: pdoc==15.0.4; extra == "docs"
46
52
  Dynamic: license-file
47
53
 
48
54
  <p align="center">
@@ -54,6 +60,7 @@ Dynamic: license-file
54
60
  <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
61
  </p>
56
62
 
63
+ <p align="center"><strong>Tested on Linux, macOS, and Windows</strong></p>
57
64
  <h1 align="center">Remote CMD — SSH Server Management<br><small>Without the Overhead</small></h1>
58
65
 
59
66
  <p align="center">
@@ -71,12 +78,6 @@ Dynamic: license-file
71
78
  <a href="#contributing">Contributing</a>
72
79
  </p>
73
80
 
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
81
  ---
81
82
 
82
83
  **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 +89,28 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
88
89
 
89
90
  ---
90
91
 
91
- ## v2.1.0 Release Highlights
92
+ ## v2.3.0 Release Highlights
92
93
 
93
- v2.1.0 contains both major implementation changes and a final release-hardening pass. See the
94
+ v2.3.0 makes blocking SFTP operations timeout-safe, hardens async cancellation cleanup,
95
+ and aligns the supported Python range and release tooling. See the
94
96
  [full migration notes](./CHANGELOG.md) before upgrading automated callers.
95
97
 
96
- ### Major Implementation Changes
98
+ ### SFTP Timeouts & Reliability
97
99
 
98
- - Paramiko command execution drains stdout and stderr before retrieving the exit status, preventing SSH channel-window deadlocks on large output.
99
- - Paramiko command timeouts are wall-clock enforced; a silent or hung command closes its channel and raises `SSHCommandTimeoutError`.
100
- - `AsyncBatchExecutor` now uses `AsyncConnectionPool` for multi-host and retry workloads, reusing connections across attempts.
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.
100
+ - Blocking SFTP operations now support an inactivity timeout (default: `ConnectionConfig.timeout`, 30 s): uploads, downloads, directory listing, directory creation, removal, and file-info calls accept an optional `timeout` argument.
101
+ - The synchronous `SSHClient` applies the timeout to the Paramiko SFTP channel, so a stalled read/write aborts the actual operation and raises `SSHFileTransferError`.
102
+ - The asynchronous `AsyncSSHClient` bounds SFTP channel startup with a timeout and uses a progress-based inactivity watchdog to cancel stalled transfers, awaiting the aborted operation and preserving caller cancellation during cleanup.
103
+ - Timed-out or cancelled SFTP sessions are closed and discarded, so a stale or desynchronized channel can never be reused.
107
104
 
108
- ### Final Release Hardening
105
+ ### Release Engineering
109
106
 
110
- - Sync and async pools re-check their closed state after semaphore acquisition, preventing a shutdown race from issuing a connection from a closed pool.
111
- - `BatchExecutor(use_async=True)` now reports an actionable error when called from an active event loop; use `AsyncBatchExecutor.execute()` there instead.
112
- - The Paramiko stderr drain join is bounded to prevent an exceptional reader path from blocking the caller indefinitely.
107
+ - Python 3.9–3.14 are officially supported and CI-tested; `Requires-Python` remains `>=3.9`.
108
+ - CI runs release-metadata validation for CHANGELOG.md-only changes.
109
+ - Publish builds pin `build==1.5.0` and `twine==7.0.0`.
113
110
 
114
111
  ## Table of Contents
115
112
 
116
- - [v2.1.0 Release Highlights](#v210-release-highlights)
113
+ - [v2.3.0 Release Highlights](#v230-release-highlights)
117
114
  - [Why Remote CMD?](#why-remote-cmd)
118
115
  - [Quick Start](#quick-start)
119
116
  - [Use Cases](#use-cases)
@@ -270,6 +267,20 @@ with SSHClient(config) as client:
270
267
 
271
268
  ---
272
269
 
270
+ ## Cross-Platform Support
271
+
272
+ `remote-cmd` is a pure-Python client, **tested and supported on Linux, macOS, and Windows**. No native dependencies or platform-specific builds are required.
273
+
274
+ | Platform | Status | Notes |
275
+ |----------|--------|-------|
276
+ | Linux | ✅ Fully tested | Primary development platform |
277
+ | macOS | ✅ Supported | Use `brew install openssh` for a newer OpenSSH |
278
+ | Windows | ✅ Supported | PowerShell / CMD / WSL all work |
279
+
280
+ > **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.
281
+
282
+ ---
283
+
273
284
  ## Installation
274
285
 
275
286
  ```bash
@@ -309,14 +320,14 @@ are simply not exported.
309
320
  | [Changelog](./CHANGELOG.md) | Release history |
310
321
  | [Mobile Remote Guide](./MOBILE-REMOTE-GUIDE.md) | Manage servers from your phone |
311
322
 
312
- > **Note:** The documentation center and tutorials are maintained in **Chinese**. See
323
+ > **Note:** The documentation center and tutorials are maintained in **English**. See
313
324
  > [README.zh-CN.md](./README.zh-CN.md) for the Chinese version of this page.
314
325
 
315
326
  ---
316
327
 
317
328
  ## Project Status
318
329
 
319
- **Beta.** The core API is stable. Breaking changes will be communicated via semantic versioning.
330
+ **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
331
 
321
332
  **Roadmap:**
322
333
  - [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,28 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
41
36
 
42
37
  ---
43
38
 
44
- ## v2.1.0 Release Highlights
39
+ ## v2.3.0 Release Highlights
45
40
 
46
- v2.1.0 contains both major implementation changes and a final release-hardening pass. See the
41
+ v2.3.0 makes blocking SFTP operations timeout-safe, hardens async cancellation cleanup,
42
+ and aligns the supported Python range and release tooling. See the
47
43
  [full migration notes](./CHANGELOG.md) before upgrading automated callers.
48
44
 
49
- ### Major Implementation Changes
45
+ ### SFTP Timeouts & Reliability
50
46
 
51
- - Paramiko command execution drains stdout and stderr before retrieving the exit status, preventing SSH channel-window deadlocks on large output.
52
- - Paramiko command timeouts are wall-clock enforced; a silent or hung command closes its channel and raises `SSHCommandTimeoutError`.
53
- - `AsyncBatchExecutor` now uses `AsyncConnectionPool` for multi-host and retry workloads, reusing connections across attempts.
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
+ - Blocking SFTP operations now support an inactivity timeout (default: `ConnectionConfig.timeout`, 30 s): uploads, downloads, directory listing, directory creation, removal, and file-info calls accept an optional `timeout` argument.
48
+ - The synchronous `SSHClient` applies the timeout to the Paramiko SFTP channel, so a stalled read/write aborts the actual operation and raises `SSHFileTransferError`.
49
+ - The asynchronous `AsyncSSHClient` bounds SFTP channel startup with a timeout and uses a progress-based inactivity watchdog to cancel stalled transfers, awaiting the aborted operation and preserving caller cancellation during cleanup.
50
+ - Timed-out or cancelled SFTP sessions are closed and discarded, so a stale or desynchronized channel can never be reused.
60
51
 
61
- ### Final Release Hardening
52
+ ### Release Engineering
62
53
 
63
- - Sync and async pools re-check their closed state after semaphore acquisition, preventing a shutdown race from issuing a connection from a closed pool.
64
- - `BatchExecutor(use_async=True)` now reports an actionable error when called from an active event loop; use `AsyncBatchExecutor.execute()` there instead.
65
- - The Paramiko stderr drain join is bounded to prevent an exceptional reader path from blocking the caller indefinitely.
54
+ - Python 3.9–3.14 are officially supported and CI-tested; `Requires-Python` remains `>=3.9`.
55
+ - CI runs release-metadata validation for CHANGELOG.md-only changes.
56
+ - Publish builds pin `build==1.5.0` and `twine==7.0.0`.
66
57
 
67
58
  ## Table of Contents
68
59
 
69
- - [v2.1.0 Release Highlights](#v210-release-highlights)
60
+ - [v2.3.0 Release Highlights](#v230-release-highlights)
70
61
  - [Why Remote CMD?](#why-remote-cmd)
71
62
  - [Quick Start](#quick-start)
72
63
  - [Use Cases](#use-cases)
@@ -223,6 +214,20 @@ with SSHClient(config) as client:
223
214
 
224
215
  ---
225
216
 
217
+ ## Cross-Platform Support
218
+
219
+ `remote-cmd` is a pure-Python client, **tested and supported on Linux, macOS, and Windows**. No native dependencies or platform-specific builds are required.
220
+
221
+ | Platform | Status | Notes |
222
+ |----------|--------|-------|
223
+ | Linux | ✅ Fully tested | Primary development platform |
224
+ | macOS | ✅ Supported | Use `brew install openssh` for a newer OpenSSH |
225
+ | Windows | ✅ Supported | PowerShell / CMD / WSL all work |
226
+
227
+ > **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.
228
+
229
+ ---
230
+
226
231
  ## Installation
227
232
 
228
233
  ```bash
@@ -262,14 +267,14 @@ are simply not exported.
262
267
  | [Changelog](./CHANGELOG.md) | Release history |
263
268
  | [Mobile Remote Guide](./MOBILE-REMOTE-GUIDE.md) | Manage servers from your phone |
264
269
 
265
- > **Note:** The documentation center and tutorials are maintained in **Chinese**. See
270
+ > **Note:** The documentation center and tutorials are maintained in **English**. See
266
271
  > [README.zh-CN.md](./README.zh-CN.md) for the Chinese version of this page.
267
272
 
268
273
  ---
269
274
 
270
275
  ## Project Status
271
276
 
272
- **Beta.** The core API is stable. Breaking changes will be communicated via semantic versioning.
277
+ **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
278
 
274
279
  **Roadmap:**
275
280
  - [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 远程服务器管理工具 / 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",
@@ -22,9 +22,15 @@ classifiers = [
22
22
  "Programming Language :: Python :: 3.9",
23
23
  "Programming Language :: Python :: 3.10",
24
24
  "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
25
28
  "Topic :: System :: Systems Administration",
26
29
  "Topic :: System :: Networking",
27
30
  "Environment :: Console",
31
+ "Operating System :: POSIX :: Linux",
32
+ "Operating System :: MacOS",
33
+ "Operating System :: Microsoft :: Windows",
28
34
  ]
29
35
  dependencies = [
30
36
  "paramiko>=3.0",
@@ -50,8 +56,10 @@ dev = [
50
56
  # 测试 keyring 凭据提供者(生产为可选依赖,测试环境固定安装)
51
57
  "keyring>=24.0",
52
58
  ]
53
- # 文档生成(pdoc>=16 需 Python>=3.10,固定到 15.x 以兼容 requires-python>=3.9)
54
- docs = ["pdoc>=15,<16"]
59
+ # 文档生成(pdoc>=16 需 Python>=3.10,固定到 15.x 以兼容 requires-python>=3.9)。
60
+ # 精确 pin 15.0.4:pdoc HTML 输出随版本/解释器变化,docs/api 漂移门禁
61
+ # (scripts/check_docs_drift.py)要求生成结果可复现。
62
+ docs = ["pdoc==15.0.4"]
55
63
 
56
64
  [project.urls]
57
65
  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: 2.1.0(单一真相源见 remote_cmd._version)
51
+ Version: 2.3.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.1.0"
12
+ __version__ = "2.3.0"
@@ -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
- RuntimeError: 连接池已关闭(close_all 之后)
124
+ PoolClosedError: 连接池已关闭(close_all 之后),
125
+ 同时是 RuntimeError 子类(既有捕获行为不变)
124
126
  """
125
127
  if self._closed:
126
- raise RuntimeError("connection pool is closed")
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 RuntimeError("connection pool is closed")
135
+ raise PoolClosedError("connection pool is closed")
134
136
  try:
135
137
  # 优先复用空闲连接
136
138
  while not self._free.empty():
@@ -14,9 +14,12 @@
14
14
  - 密码、密钥等敏感信息不入日志(沿用项目 SensitiveDataFilter 规范)。
15
15
  """
16
16
 
17
+ import asyncio
18
+ import contextlib
17
19
  import logging
18
20
  import shlex
19
21
  import stat
22
+ from collections.abc import Awaitable, Callable, Sequence
20
23
  from pathlib import Path
21
24
  from typing import Any, Optional
22
25
 
@@ -34,6 +37,7 @@ from remote_cmd.utils.exceptions import (
34
37
  SSHConnectionError,
35
38
  SSHFileTransferError,
36
39
  SSHTimeoutError,
40
+ ValidationError,
37
41
  )
38
42
 
39
43
  logger = logging.getLogger(__name__)
@@ -286,45 +290,169 @@ class AsyncSSHClient:
286
290
  # ------------------------------------------------------------------
287
291
  # SFTP / 文件传输
288
292
  # ------------------------------------------------------------------
289
- async def _get_sftp(self) -> asyncssh.SFTPClient:
293
+ async def _get_sftp(self, timeout: Optional[float] = None) -> asyncssh.SFTPClient:
294
+ """获取 SFTP 客户端(延迟初始化)。
295
+
296
+ v2.3:打开 SFTP channel 本身也受有效超时约束,避免握手阶段永久挂起。
297
+ """
290
298
  conn = await self._get_conn()
291
299
  if self._sftp is None:
300
+ effective = self._effective_sftp_timeout(timeout)
292
301
  try:
293
- self._sftp = await conn.start_sftp_client()
302
+ self._sftp = await asyncio.wait_for(conn.start_sftp_client(), timeout=effective)
303
+ except asyncio.TimeoutError as e:
304
+ raise SSHFileTransferError(
305
+ f"failed to open SFTP channel: timed out after {effective:g} seconds"
306
+ ) from e
294
307
  except (OSError, asyncssh.Error) as e:
295
308
  raise SSHFileTransferError(f"failed to open SFTP channel: {e}") from e
296
309
  return self._sftp
297
310
 
298
- async def upload_file(self, local_path: str, remote_path: str) -> None:
299
- """异步上传本地文件到远程服务器。"""
300
- sftp = await self._get_sftp()
311
+ def _effective_sftp_timeout(self, timeout: Optional[float]) -> float:
312
+ """返回 SFTP 操作的有效 inactivity 超时(秒)。
313
+
314
+ 显式 timeout 优先,否则回退到 ``ConnectionConfig.timeout``。
315
+ """
316
+ if timeout is not None:
317
+ if timeout <= 0:
318
+ raise ValidationError(f"timeout must be > 0, got: {timeout}")
319
+ return timeout
320
+ return self.config.timeout
321
+
322
+ def _discard_sftp(self) -> None:
323
+ """关闭并丢弃缓存的 SFTP 会话(超时后防止复用可能失步的会话)。"""
324
+ sftp, self._sftp = self._sftp, None
325
+ if sftp is not None:
326
+ with contextlib.suppress(Exception):
327
+ sftp.exit()
328
+
329
+ async def _run_sftp_operation(
330
+ self,
331
+ operation: Callable[[Callable[..., None]], Awaitable[Any]],
332
+ timeout: Optional[float],
333
+ description: str,
334
+ ) -> Any:
335
+ """运行 SFTP 操作并施加 inactivity(静默)超时。
336
+
337
+ asyncssh 的 SFTP API 没有原生超时参数,因此使用 watchdog:
338
+ ``operation`` 收到一个 progress 回调(传给 ``put``/``get`` 的
339
+ ``progress_handler``),每次有数据进展就刷新时间戳;超过有效超时
340
+ 没有任何进展则取消操作、丢弃失步的 SFTP 会话并抛出
341
+ ``SSHFileTransferError``。只要数据持续流动,长时间传输不会被打断。
342
+ """
343
+ effective = self._effective_sftp_timeout(timeout)
344
+ loop = asyncio.get_running_loop()
345
+ last_activity = loop.time()
346
+
347
+ def _mark_progress(*_args: Any) -> None:
348
+ nonlocal last_activity
349
+ last_activity = loop.time()
350
+
351
+ task = asyncio.ensure_future(operation(_mark_progress))
352
+ interval = min(0.5, max(effective / 10.0, 0.01))
353
+ cancelled: Optional[asyncio.CancelledError] = None
354
+ try:
355
+ while True:
356
+ done, _pending = await asyncio.wait({task}, timeout=interval)
357
+ if task in done:
358
+ return task.result()
359
+ if loop.time() - last_activity >= effective:
360
+ break
361
+ except asyncio.CancelledError as exc:
362
+ # 外层取消:记录后统一走清理路径(不在此处 await,避免
363
+ # 取消事件再次进入本处理器)
364
+ cancelled = exc
365
+
366
+ # 统一清理:立即丢弃可能失步的 SFTP 会话;取消并等待子任务收尾。
367
+ # shield 确保清理期间到达的再次取消只打断"等待"本身,而不会
368
+ # 把清理过程转换成超时错误或被整体吞掉。
369
+ self._discard_sftp()
370
+ task.cancel()
371
+ try:
372
+ await asyncio.shield(task)
373
+ except asyncio.CancelledError:
374
+ if not task.cancelled():
375
+ # 清理期间外层再次取消:保留取消语义
376
+ raise
377
+ except Exception: # noqa: BLE001 - 子任务失败不应掩盖超时/取消语义
378
+ pass
379
+
380
+ if cancelled is not None:
381
+ raise cancelled
382
+ raise SSHFileTransferError(
383
+ f"{description} timed out after {effective:g} seconds of inactivity"
384
+ )
385
+
386
+ async def upload_file(
387
+ self,
388
+ local_path: str,
389
+ remote_path: str,
390
+ timeout: Optional[float] = None,
391
+ ) -> None:
392
+ """异步上传本地文件到远程服务器。
393
+
394
+ Args:
395
+ local_path: 本地文件路径
396
+ remote_path: 远程目标路径
397
+ timeout: inactivity 超时(秒);None 表示使用
398
+ ``ConnectionConfig.timeout``。静默超过该时长即中止
399
+ """
400
+ sftp = await self._get_sftp(timeout)
301
401
  local_file = Path(local_path)
302
402
  if not local_file.exists():
303
403
  raise SSHFileTransferError(f"Local file not found: {local_path}")
304
404
  logger.info(f"uploading file: {local_path} -> {remote_path}")
405
+
406
+ async def _operation(progress: Callable[..., None]) -> None:
407
+ await sftp.put(str(local_file), remote_path, progress_handler=progress)
408
+
305
409
  try:
306
- await sftp.put(str(local_file), remote_path)
410
+ await self._run_sftp_operation(_operation, timeout, "file upload")
307
411
  except (OSError, asyncssh.Error) as e:
308
412
  raise SSHFileTransferError(f"file upload failed: {e}") from e
309
413
  logger.info("file upload finished")
310
414
 
311
- async def download_file(self, remote_path: str, local_path: str) -> None:
312
- """异步从远程服务器下载文件到本地。"""
313
- sftp = await self._get_sftp()
415
+ async def download_file(
416
+ self,
417
+ remote_path: str,
418
+ local_path: str,
419
+ timeout: Optional[float] = None,
420
+ ) -> None:
421
+ """异步从远程服务器下载文件。
422
+
423
+ Args:
424
+ remote_path: 远程文件路径
425
+ local_path: 本地目标路径
426
+ timeout: inactivity 超时(秒);None 表示使用
427
+ ``ConnectionConfig.timeout``。静默超过该时长即中止
428
+ """
429
+ sftp = await self._get_sftp(timeout)
314
430
  local_file = Path(local_path)
315
431
  local_file.parent.mkdir(parents=True, exist_ok=True)
316
432
  logger.info(f"downloading file: {remote_path} -> {local_path}")
433
+
434
+ async def _operation(progress: Callable[..., None]) -> None:
435
+ await sftp.get(remote_path, str(local_file), progress_handler=progress)
436
+
317
437
  try:
318
- await sftp.get(remote_path, str(local_file))
438
+ await self._run_sftp_operation(_operation, timeout, "file download")
319
439
  except (OSError, asyncssh.Error) as e:
320
440
  raise SSHFileTransferError(f"file download failed: {e}") from e
321
441
  logger.info("file download finished")
322
442
 
323
- async def list_remote_directory(self, remote_path: str = ".") -> list[RemoteFileEntry]:
443
+ async def list_remote_directory(
444
+ self,
445
+ remote_path: str = ".",
446
+ timeout: Optional[float] = None,
447
+ ) -> list[RemoteFileEntry]:
324
448
  """异步列出远程目录内容(结构与同步 SSHClient 一致)。"""
325
- sftp = await self._get_sftp()
449
+ sftp = await self._get_sftp(timeout)
450
+
451
+ async def _operation(_progress: Callable[..., None]) -> Sequence[Any]:
452
+ return await sftp.readdir(remote_path)
453
+
326
454
  try:
327
- names = await sftp.readdir(remote_path)
455
+ names = await self._run_sftp_operation(_operation, timeout, "list remote directory")
328
456
  except (OSError, asyncssh.Error) as e:
329
457
  raise SSHFileTransferError(f"failed to list remote directory: {e}") from e
330
458