sshmd 0.1.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.
- sshmd-0.1.0/LICENSE +21 -0
- sshmd-0.1.0/PKG-INFO +309 -0
- sshmd-0.1.0/README.md +289 -0
- sshmd-0.1.0/pyproject.toml +45 -0
- sshmd-0.1.0/src/sshm/__init__.py +3 -0
- sshmd-0.1.0/src/sshm/__main__.py +4 -0
- sshmd-0.1.0/src/sshm/autostart.py +163 -0
- sshmd-0.1.0/src/sshm/cli.py +740 -0
- sshmd-0.1.0/src/sshm/completions/sshm.fish +137 -0
- sshmd-0.1.0/src/sshm/config.py +344 -0
- sshmd-0.1.0/src/sshm/daemon.py +289 -0
- sshmd-0.1.0/src/sshm/ipc.py +249 -0
- sshmd-0.1.0/src/sshm/process.py +545 -0
- sshmd-0.1.0/src/sshm/procutil.py +60 -0
- sshmd-0.1.0/src/sshm/protocol.py +53 -0
- sshmd-0.1.0/src/sshm/py.typed +0 -0
- sshmd-0.1.0/src/sshm/state.py +76 -0
- sshmd-0.1.0/src/sshm/terminal.py +199 -0
sshmd-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 revsearch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sshmd-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sshmd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cross-platform SSH session manager with a background daemon
|
|
5
|
+
Keywords: ssh,session-manager,daemon,port-forwarding,socks,terminal
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: System Administrators
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: System :: Networking
|
|
13
|
+
Classifier: Topic :: Utilities
|
|
14
|
+
Requires-Dist: click>=8.0
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Project-URL: Homepage, https://github.com/revsearch/sshm
|
|
17
|
+
Project-URL: Repository, https://github.com/revsearch/sshm
|
|
18
|
+
Project-URL: Issues, https://github.com/revsearch/sshm/issues
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# sshm
|
|
22
|
+
|
|
23
|
+
[](https://pypi.org/project/sshmd/) [](https://github.com/revsearch/sshm/actions/workflows/tests.yml)
|
|
24
|
+
|
|
25
|
+
An SSH session manager with a background daemon. Remote shells stay alive when you
|
|
26
|
+
close the terminal, reconnect on their own when the link drops, and reattach
|
|
27
|
+
instantly. State is kept in plain `~/.ssh/config`, so `ssh`, `scp`, and `rsync`
|
|
28
|
+
keep working alongside it.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
sshm add prod root@192.0.2.10 # keygen + copy key + write ~/.ssh/config
|
|
32
|
+
sshm prod # attach to a live shell (or start one)
|
|
33
|
+
# close the terminal — the shell keeps running; `sshm prod` reattaches it
|
|
34
|
+
|
|
35
|
+
ssh prod # plain ssh/scp/rsync work too — same alias & key
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
The package is published as **`sshmd`** and installs the `sshm` (and `sshmd`)
|
|
41
|
+
commands. With [uv](https://docs.astral.sh/uv/) or [pipx](https://pipx.pypa.io/):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv tool install sshmd
|
|
45
|
+
# or
|
|
46
|
+
pipx install sshmd
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Latest from git instead of PyPI:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv tool install git+https://github.com/revsearch/sshm
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
From a checkout:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/revsearch/sshm
|
|
59
|
+
cd sshm
|
|
60
|
+
uv tool install . # or: pip install .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Needs Python 3.12+. The `sshm` and `sshmd` commands are installed to your tool bin
|
|
64
|
+
directory (`~/.local/bin`, or the Python Scripts dir on Windows) — make sure it's
|
|
65
|
+
on your PATH. Update with `uv tool upgrade sshm`.
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Add a server: generates an ed25519 key, copies it to the remote, writes config
|
|
71
|
+
sshm add myserver root@192.168.1.100
|
|
72
|
+
|
|
73
|
+
# Connect (interactive shell via the daemon)
|
|
74
|
+
sshm myserver
|
|
75
|
+
|
|
76
|
+
# Or explicitly
|
|
77
|
+
sshm c myserver
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Works with `ssh`, `scp`, `rsync`
|
|
81
|
+
|
|
82
|
+
`sshm add` writes a normal `Host` entry to `~/.ssh/config`, so the alias works with
|
|
83
|
+
any tool that reads it — not just `sshm`. The generated key and port are picked up
|
|
84
|
+
automatically, so no `-i`/`-p` flags are needed:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
ssh myserver # plain SSH, same alias
|
|
88
|
+
ssh myserver htop # run a one-off command
|
|
89
|
+
scp ./backup.tar.gz myserver:/srv/ # copy a file up
|
|
90
|
+
scp myserver:/var/log/app.log . # ...and back down
|
|
91
|
+
rsync -avz ./site/ myserver:/var/www/ # sync a directory
|
|
92
|
+
sftp myserver # sftp, git over ssh, etc. all work too
|
|
93
|
+
git clone myserver:/srv/repo.git # alias works as the ssh host anywhere
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The difference: `sshm myserver` attaches to a persistent, auto-reconnecting shell
|
|
97
|
+
via the daemon, while `ssh myserver` is a plain one-off connection — both to the
|
|
98
|
+
same host, using the same key and config.
|
|
99
|
+
|
|
100
|
+
## Commands
|
|
101
|
+
|
|
102
|
+
Most commands have a short alias (shown first). `sshm <alias>` with no command is
|
|
103
|
+
shorthand for `connect`.
|
|
104
|
+
|
|
105
|
+
### Sessions
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
sshm <alias> # Connect (shorthand)
|
|
109
|
+
sshm c, connect <alias> [name] # Attach to a session or create a new one
|
|
110
|
+
sshm l, list # List all hosts
|
|
111
|
+
sshm l, list <alias> # List active sessions for a host
|
|
112
|
+
sshm a, add <alias> user@host # Add a server (keygen + copy key)
|
|
113
|
+
sshm r, remove <alias> # Remove a host and disconnect all sessions
|
|
114
|
+
sshm mv, rename <alias> <new> # Rename an alias (and its managed key)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`add` takes `user@host`, `user@host:port`, or bracketed IPv6
|
|
118
|
+
(`user@[2001:db8::1]:22`).
|
|
119
|
+
|
|
120
|
+
### Port forwarding and SOCKS
|
|
121
|
+
|
|
122
|
+
Forwards are written as native `LocalForward` / `RemoteForward` / `DynamicForward`
|
|
123
|
+
directives in `~/.ssh/config`, so any SSH client sees them. Direction is `-L` /
|
|
124
|
+
`-R` / `-D`, same as `ssh`:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
sshm p a, port add <alias> -L <local>:<host>:<remote> # Local forward
|
|
128
|
+
sshm p a, port add <alias> -R <remote>:<host>:<local> # Reverse forward
|
|
129
|
+
sshm p a, port add <alias> -D <port> # SOCKS proxy (ssh -D)
|
|
130
|
+
sshm p r, port remove <alias> -L <local>:<host>:<remote> # Remove a forward
|
|
131
|
+
sshm p r, port remove <alias> -D <port> # Remove a SOCKS proxy
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`-D <port>` is a dynamic forward — a SOCKS5 proxy on `127.0.0.1:<port>` tunnelled
|
|
135
|
+
through the host. Point a browser or any SOCKS-aware app at it.
|
|
136
|
+
|
|
137
|
+
### Auto-connect
|
|
138
|
+
|
|
139
|
+
When enabled, the daemon keeps at least one shell alive and reconnects it on
|
|
140
|
+
failure, so attaching is instant.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
sshm e, enable <alias> # Keep a session alive, auto-reconnect
|
|
144
|
+
sshm d, disable <alias> # Stop auto-connect
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Import / export
|
|
148
|
+
|
|
149
|
+
Move hosts, including their keys, between machines as JSON.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
sshm export servers.json # Export all hosts
|
|
153
|
+
sshm export prod.json web db api # Export specific hosts
|
|
154
|
+
sshm l servers.json # Preview a JSON file
|
|
155
|
+
sshm import servers.json # Import (skip existing)
|
|
156
|
+
sshm import servers.json -o # Import (override existing)
|
|
157
|
+
sshm import servers.json web db # Import only specific hosts
|
|
158
|
+
sshm import servers.json web=prod # Import 'web' under the alias 'prod'
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Daemon
|
|
162
|
+
|
|
163
|
+
The daemon (`sshmd`) starts on first use.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
sshm status # Daemon status
|
|
167
|
+
sshm stop # Stop the daemon
|
|
168
|
+
sshm install # Autostart on login (systemd / launchd / Task Scheduler)
|
|
169
|
+
sshm uninstall # Remove autostart
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Shell completions
|
|
173
|
+
|
|
174
|
+
### fish
|
|
175
|
+
|
|
176
|
+
`sshm` ships fish completions: subcommands, `port -L/-R/-D` flags, and host aliases
|
|
177
|
+
pulled live from `~/.ssh/config` (including the bare `sshm <alias>` shorthand).
|
|
178
|
+
`uv tool install` does **not** wire these up automatically, so install them once
|
|
179
|
+
(`exec fish` reloads the shell so they're active immediately):
|
|
180
|
+
|
|
181
|
+
```fish
|
|
182
|
+
sshm completions fish > ~/.config/fish/completions/sshm.fish && exec fish
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
fish autoloads from that directory — new sessions pick it up with no `source`. From
|
|
186
|
+
a checkout you can instead symlink the source file so edits are picked up live:
|
|
187
|
+
|
|
188
|
+
```fish
|
|
189
|
+
ln -s (path resolve src/sshm/completions/sshm.fish) ~/.config/fish/completions/sshm.fish
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Session states
|
|
193
|
+
|
|
194
|
+
| Icon | State | Meaning |
|
|
195
|
+
|------|----------|-----------------------------------|
|
|
196
|
+
| `●` | ready | Shell running, waiting for attach |
|
|
197
|
+
| `◆` | attached | A client is connected |
|
|
198
|
+
| `○` | dead | Process exited |
|
|
199
|
+
|
|
200
|
+
## How it works
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
sshm (CLI) ── TCP localhost:19222 ──> sshmd (daemon)
|
|
204
|
+
├── SSH processes (one shell per session)
|
|
205
|
+
├── Reader threads (scrollback buffer)
|
|
206
|
+
├── Watchdog (health, reconnect, keep-warm)
|
|
207
|
+
└── IPC server (JSON protocol + I/O bridge)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- The daemon spawns `ssh -tt <alias>` under a real PTY (POSIX; pipes on Windows)
|
|
211
|
+
and holds the shell process.
|
|
212
|
+
- `connect` bridges your terminal I/O to that process over TCP and forwards your
|
|
213
|
+
terminal size (and resizes / `SIGWINCH`) so the remote shell matches your window.
|
|
214
|
+
- Detaching (closing the terminal, Ctrl-C) leaves the shell running; reattach later.
|
|
215
|
+
- Typing `exit` in the shell removes the session cleanly — no reconnect.
|
|
216
|
+
- A lost connection (SSH exit 255) triggers reconnect with exponential backoff.
|
|
217
|
+
- Config stays in `~/.ssh/config`. Every rewrite goes to a temp file and is
|
|
218
|
+
replaced atomically (a `config.bak` is kept), so a crash mid-write won't corrupt it.
|
|
219
|
+
- Runtime state lives in `~/.sshm/`: pid file, IPC token, `sshmd.log`.
|
|
220
|
+
- The IPC server binds `127.0.0.1` only and checks a random per-daemon token
|
|
221
|
+
(stored `0600` in `~/.sshm/token`) on every request.
|
|
222
|
+
- The IPC port defaults to `19222`; set `SSHM_PORT` to change it (e.g. to run sshm
|
|
223
|
+
in both Windows and WSL when mirrored networking shares localhost). `sshm install`
|
|
224
|
+
persists the port to `~/.sshm/port` so the autostarted daemon — which doesn't see
|
|
225
|
+
your shell env — uses the same one.
|
|
226
|
+
|
|
227
|
+
## Platforms
|
|
228
|
+
|
|
229
|
+
- Linux — systemd user service for autostart.
|
|
230
|
+
- macOS — launchd LaunchAgent.
|
|
231
|
+
- Windows — Task Scheduler for autostart, Win32 console VT mode for terminal I/O.
|
|
232
|
+
|
|
233
|
+
On POSIX, ssh runs under a real PTY, so the remote shell tracks your window size
|
|
234
|
+
and resizes. Windows has no `pty` module, so there a session keeps the size it
|
|
235
|
+
attached with — full-screen apps (`vim`, `htop`) won't follow later resizes.
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full branch / commit / PR workflow.
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
uv sync # install dependencies (including the dev group)
|
|
243
|
+
uv run pytest # run the tests
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Module layout (`src/sshm/`):
|
|
247
|
+
|
|
248
|
+
| Module | Responsibility |
|
|
249
|
+
|----------------|-----------------------------------------------------------|
|
|
250
|
+
| `cli.py` | CLI commands (click), entry point `sshm` |
|
|
251
|
+
| `daemon.py` | `sshmd`: request dispatch, watchdog, entry `sshmd` |
|
|
252
|
+
| `process.py` | SSH sessions: PTY spawn, scrollback, reconnect, health |
|
|
253
|
+
| `ipc.py` | TCP IPC client/server on `127.0.0.1:19222` (token auth) |
|
|
254
|
+
| `protocol.py` | JSON message schema for IPC |
|
|
255
|
+
| `config.py` | `~/.ssh/config` parser/writer, port-forward rules |
|
|
256
|
+
| `terminal.py` | Raw terminal bridge (termios on Unix, Win32 console API) |
|
|
257
|
+
| `procutil.py` | Cross-platform process helpers (pid checks, Popen flags) |
|
|
258
|
+
| `state.py` | `~/.sshm` runtime files: pid, token, port, log |
|
|
259
|
+
| `autostart.py` | Task Scheduler / systemd / launchd integration |
|
|
260
|
+
|
|
261
|
+
## Troubleshooting
|
|
262
|
+
|
|
263
|
+
### Windows: `Bad owner or permissions on ~/.ssh/config`
|
|
264
|
+
|
|
265
|
+
OpenSSH on Windows refuses to read `~/.ssh/config` if the ACL contains extra
|
|
266
|
+
principals like `OWNER RIGHTS` (S-1-3-4). Symptom:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
Bad permissions. Try removing permissions for user: \\OWNER RIGHTS (S-1-3-4) on file C:/Users/<you>/.ssh/config.
|
|
270
|
+
Bad owner or permissions on C:\\Users\\<you>/.ssh/config
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Remove the offending principal:
|
|
274
|
+
|
|
275
|
+
```powershell
|
|
276
|
+
icacls "$env:USERPROFILE\.ssh\config" /remove "OWNER RIGHTS"
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
If other files in `.ssh` have the same issue (private keys, etc.):
|
|
280
|
+
|
|
281
|
+
```powershell
|
|
282
|
+
icacls "$env:USERPROFILE\.ssh\*" /remove "OWNER RIGHTS"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
If that's not enough (inheritance can pull in other groups), reset the ACL so only
|
|
286
|
+
you have access:
|
|
287
|
+
|
|
288
|
+
```powershell
|
|
289
|
+
$f = "$env:USERPROFILE\.ssh\config"
|
|
290
|
+
icacls $f /inheritance:r
|
|
291
|
+
icacls $f /grant:r "$($env:USERNAME):(F)"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
If `icacls` returns `Access is denied`, you don't own the file. Take ownership
|
|
295
|
+
first, then fix the ACL:
|
|
296
|
+
|
|
297
|
+
```powershell
|
|
298
|
+
takeown /F "$env:USERPROFILE\.ssh\config"
|
|
299
|
+
icacls "$env:USERPROFILE\.ssh\config" /grant "$($env:USERNAME):F"
|
|
300
|
+
icacls "$env:USERPROFILE\.ssh\config" /inheritance:r
|
|
301
|
+
icacls "$env:USERPROFILE\.ssh\config" /grant:r "$($env:USERNAME):(F)"
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
If `takeown` fails, run PowerShell as Administrator and repeat. Verify with
|
|
305
|
+
`icacls "$env:USERPROFILE\.ssh\config"` — only your user with `(F)` should remain.
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
MIT — see [LICENSE](LICENSE).
|
sshmd-0.1.0/README.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# sshm
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/sshmd/) [](https://github.com/revsearch/sshm/actions/workflows/tests.yml)
|
|
4
|
+
|
|
5
|
+
An SSH session manager with a background daemon. Remote shells stay alive when you
|
|
6
|
+
close the terminal, reconnect on their own when the link drops, and reattach
|
|
7
|
+
instantly. State is kept in plain `~/.ssh/config`, so `ssh`, `scp`, and `rsync`
|
|
8
|
+
keep working alongside it.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
sshm add prod root@192.0.2.10 # keygen + copy key + write ~/.ssh/config
|
|
12
|
+
sshm prod # attach to a live shell (or start one)
|
|
13
|
+
# close the terminal — the shell keeps running; `sshm prod` reattaches it
|
|
14
|
+
|
|
15
|
+
ssh prod # plain ssh/scp/rsync work too — same alias & key
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
The package is published as **`sshmd`** and installs the `sshm` (and `sshmd`)
|
|
21
|
+
commands. With [uv](https://docs.astral.sh/uv/) or [pipx](https://pipx.pypa.io/):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv tool install sshmd
|
|
25
|
+
# or
|
|
26
|
+
pipx install sshmd
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Latest from git instead of PyPI:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv tool install git+https://github.com/revsearch/sshm
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
From a checkout:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/revsearch/sshm
|
|
39
|
+
cd sshm
|
|
40
|
+
uv tool install . # or: pip install .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Needs Python 3.12+. The `sshm` and `sshmd` commands are installed to your tool bin
|
|
44
|
+
directory (`~/.local/bin`, or the Python Scripts dir on Windows) — make sure it's
|
|
45
|
+
on your PATH. Update with `uv tool upgrade sshm`.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Add a server: generates an ed25519 key, copies it to the remote, writes config
|
|
51
|
+
sshm add myserver root@192.168.1.100
|
|
52
|
+
|
|
53
|
+
# Connect (interactive shell via the daemon)
|
|
54
|
+
sshm myserver
|
|
55
|
+
|
|
56
|
+
# Or explicitly
|
|
57
|
+
sshm c myserver
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Works with `ssh`, `scp`, `rsync`
|
|
61
|
+
|
|
62
|
+
`sshm add` writes a normal `Host` entry to `~/.ssh/config`, so the alias works with
|
|
63
|
+
any tool that reads it — not just `sshm`. The generated key and port are picked up
|
|
64
|
+
automatically, so no `-i`/`-p` flags are needed:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
ssh myserver # plain SSH, same alias
|
|
68
|
+
ssh myserver htop # run a one-off command
|
|
69
|
+
scp ./backup.tar.gz myserver:/srv/ # copy a file up
|
|
70
|
+
scp myserver:/var/log/app.log . # ...and back down
|
|
71
|
+
rsync -avz ./site/ myserver:/var/www/ # sync a directory
|
|
72
|
+
sftp myserver # sftp, git over ssh, etc. all work too
|
|
73
|
+
git clone myserver:/srv/repo.git # alias works as the ssh host anywhere
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The difference: `sshm myserver` attaches to a persistent, auto-reconnecting shell
|
|
77
|
+
via the daemon, while `ssh myserver` is a plain one-off connection — both to the
|
|
78
|
+
same host, using the same key and config.
|
|
79
|
+
|
|
80
|
+
## Commands
|
|
81
|
+
|
|
82
|
+
Most commands have a short alias (shown first). `sshm <alias>` with no command is
|
|
83
|
+
shorthand for `connect`.
|
|
84
|
+
|
|
85
|
+
### Sessions
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
sshm <alias> # Connect (shorthand)
|
|
89
|
+
sshm c, connect <alias> [name] # Attach to a session or create a new one
|
|
90
|
+
sshm l, list # List all hosts
|
|
91
|
+
sshm l, list <alias> # List active sessions for a host
|
|
92
|
+
sshm a, add <alias> user@host # Add a server (keygen + copy key)
|
|
93
|
+
sshm r, remove <alias> # Remove a host and disconnect all sessions
|
|
94
|
+
sshm mv, rename <alias> <new> # Rename an alias (and its managed key)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`add` takes `user@host`, `user@host:port`, or bracketed IPv6
|
|
98
|
+
(`user@[2001:db8::1]:22`).
|
|
99
|
+
|
|
100
|
+
### Port forwarding and SOCKS
|
|
101
|
+
|
|
102
|
+
Forwards are written as native `LocalForward` / `RemoteForward` / `DynamicForward`
|
|
103
|
+
directives in `~/.ssh/config`, so any SSH client sees them. Direction is `-L` /
|
|
104
|
+
`-R` / `-D`, same as `ssh`:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
sshm p a, port add <alias> -L <local>:<host>:<remote> # Local forward
|
|
108
|
+
sshm p a, port add <alias> -R <remote>:<host>:<local> # Reverse forward
|
|
109
|
+
sshm p a, port add <alias> -D <port> # SOCKS proxy (ssh -D)
|
|
110
|
+
sshm p r, port remove <alias> -L <local>:<host>:<remote> # Remove a forward
|
|
111
|
+
sshm p r, port remove <alias> -D <port> # Remove a SOCKS proxy
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`-D <port>` is a dynamic forward — a SOCKS5 proxy on `127.0.0.1:<port>` tunnelled
|
|
115
|
+
through the host. Point a browser or any SOCKS-aware app at it.
|
|
116
|
+
|
|
117
|
+
### Auto-connect
|
|
118
|
+
|
|
119
|
+
When enabled, the daemon keeps at least one shell alive and reconnects it on
|
|
120
|
+
failure, so attaching is instant.
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
sshm e, enable <alias> # Keep a session alive, auto-reconnect
|
|
124
|
+
sshm d, disable <alias> # Stop auto-connect
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Import / export
|
|
128
|
+
|
|
129
|
+
Move hosts, including their keys, between machines as JSON.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
sshm export servers.json # Export all hosts
|
|
133
|
+
sshm export prod.json web db api # Export specific hosts
|
|
134
|
+
sshm l servers.json # Preview a JSON file
|
|
135
|
+
sshm import servers.json # Import (skip existing)
|
|
136
|
+
sshm import servers.json -o # Import (override existing)
|
|
137
|
+
sshm import servers.json web db # Import only specific hosts
|
|
138
|
+
sshm import servers.json web=prod # Import 'web' under the alias 'prod'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Daemon
|
|
142
|
+
|
|
143
|
+
The daemon (`sshmd`) starts on first use.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
sshm status # Daemon status
|
|
147
|
+
sshm stop # Stop the daemon
|
|
148
|
+
sshm install # Autostart on login (systemd / launchd / Task Scheduler)
|
|
149
|
+
sshm uninstall # Remove autostart
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Shell completions
|
|
153
|
+
|
|
154
|
+
### fish
|
|
155
|
+
|
|
156
|
+
`sshm` ships fish completions: subcommands, `port -L/-R/-D` flags, and host aliases
|
|
157
|
+
pulled live from `~/.ssh/config` (including the bare `sshm <alias>` shorthand).
|
|
158
|
+
`uv tool install` does **not** wire these up automatically, so install them once
|
|
159
|
+
(`exec fish` reloads the shell so they're active immediately):
|
|
160
|
+
|
|
161
|
+
```fish
|
|
162
|
+
sshm completions fish > ~/.config/fish/completions/sshm.fish && exec fish
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
fish autoloads from that directory — new sessions pick it up with no `source`. From
|
|
166
|
+
a checkout you can instead symlink the source file so edits are picked up live:
|
|
167
|
+
|
|
168
|
+
```fish
|
|
169
|
+
ln -s (path resolve src/sshm/completions/sshm.fish) ~/.config/fish/completions/sshm.fish
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Session states
|
|
173
|
+
|
|
174
|
+
| Icon | State | Meaning |
|
|
175
|
+
|------|----------|-----------------------------------|
|
|
176
|
+
| `●` | ready | Shell running, waiting for attach |
|
|
177
|
+
| `◆` | attached | A client is connected |
|
|
178
|
+
| `○` | dead | Process exited |
|
|
179
|
+
|
|
180
|
+
## How it works
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
sshm (CLI) ── TCP localhost:19222 ──> sshmd (daemon)
|
|
184
|
+
├── SSH processes (one shell per session)
|
|
185
|
+
├── Reader threads (scrollback buffer)
|
|
186
|
+
├── Watchdog (health, reconnect, keep-warm)
|
|
187
|
+
└── IPC server (JSON protocol + I/O bridge)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- The daemon spawns `ssh -tt <alias>` under a real PTY (POSIX; pipes on Windows)
|
|
191
|
+
and holds the shell process.
|
|
192
|
+
- `connect` bridges your terminal I/O to that process over TCP and forwards your
|
|
193
|
+
terminal size (and resizes / `SIGWINCH`) so the remote shell matches your window.
|
|
194
|
+
- Detaching (closing the terminal, Ctrl-C) leaves the shell running; reattach later.
|
|
195
|
+
- Typing `exit` in the shell removes the session cleanly — no reconnect.
|
|
196
|
+
- A lost connection (SSH exit 255) triggers reconnect with exponential backoff.
|
|
197
|
+
- Config stays in `~/.ssh/config`. Every rewrite goes to a temp file and is
|
|
198
|
+
replaced atomically (a `config.bak` is kept), so a crash mid-write won't corrupt it.
|
|
199
|
+
- Runtime state lives in `~/.sshm/`: pid file, IPC token, `sshmd.log`.
|
|
200
|
+
- The IPC server binds `127.0.0.1` only and checks a random per-daemon token
|
|
201
|
+
(stored `0600` in `~/.sshm/token`) on every request.
|
|
202
|
+
- The IPC port defaults to `19222`; set `SSHM_PORT` to change it (e.g. to run sshm
|
|
203
|
+
in both Windows and WSL when mirrored networking shares localhost). `sshm install`
|
|
204
|
+
persists the port to `~/.sshm/port` so the autostarted daemon — which doesn't see
|
|
205
|
+
your shell env — uses the same one.
|
|
206
|
+
|
|
207
|
+
## Platforms
|
|
208
|
+
|
|
209
|
+
- Linux — systemd user service for autostart.
|
|
210
|
+
- macOS — launchd LaunchAgent.
|
|
211
|
+
- Windows — Task Scheduler for autostart, Win32 console VT mode for terminal I/O.
|
|
212
|
+
|
|
213
|
+
On POSIX, ssh runs under a real PTY, so the remote shell tracks your window size
|
|
214
|
+
and resizes. Windows has no `pty` module, so there a session keeps the size it
|
|
215
|
+
attached with — full-screen apps (`vim`, `htop`) won't follow later resizes.
|
|
216
|
+
|
|
217
|
+
## Development
|
|
218
|
+
|
|
219
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full branch / commit / PR workflow.
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
uv sync # install dependencies (including the dev group)
|
|
223
|
+
uv run pytest # run the tests
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Module layout (`src/sshm/`):
|
|
227
|
+
|
|
228
|
+
| Module | Responsibility |
|
|
229
|
+
|----------------|-----------------------------------------------------------|
|
|
230
|
+
| `cli.py` | CLI commands (click), entry point `sshm` |
|
|
231
|
+
| `daemon.py` | `sshmd`: request dispatch, watchdog, entry `sshmd` |
|
|
232
|
+
| `process.py` | SSH sessions: PTY spawn, scrollback, reconnect, health |
|
|
233
|
+
| `ipc.py` | TCP IPC client/server on `127.0.0.1:19222` (token auth) |
|
|
234
|
+
| `protocol.py` | JSON message schema for IPC |
|
|
235
|
+
| `config.py` | `~/.ssh/config` parser/writer, port-forward rules |
|
|
236
|
+
| `terminal.py` | Raw terminal bridge (termios on Unix, Win32 console API) |
|
|
237
|
+
| `procutil.py` | Cross-platform process helpers (pid checks, Popen flags) |
|
|
238
|
+
| `state.py` | `~/.sshm` runtime files: pid, token, port, log |
|
|
239
|
+
| `autostart.py` | Task Scheduler / systemd / launchd integration |
|
|
240
|
+
|
|
241
|
+
## Troubleshooting
|
|
242
|
+
|
|
243
|
+
### Windows: `Bad owner or permissions on ~/.ssh/config`
|
|
244
|
+
|
|
245
|
+
OpenSSH on Windows refuses to read `~/.ssh/config` if the ACL contains extra
|
|
246
|
+
principals like `OWNER RIGHTS` (S-1-3-4). Symptom:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
Bad permissions. Try removing permissions for user: \\OWNER RIGHTS (S-1-3-4) on file C:/Users/<you>/.ssh/config.
|
|
250
|
+
Bad owner or permissions on C:\\Users\\<you>/.ssh/config
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Remove the offending principal:
|
|
254
|
+
|
|
255
|
+
```powershell
|
|
256
|
+
icacls "$env:USERPROFILE\.ssh\config" /remove "OWNER RIGHTS"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
If other files in `.ssh` have the same issue (private keys, etc.):
|
|
260
|
+
|
|
261
|
+
```powershell
|
|
262
|
+
icacls "$env:USERPROFILE\.ssh\*" /remove "OWNER RIGHTS"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
If that's not enough (inheritance can pull in other groups), reset the ACL so only
|
|
266
|
+
you have access:
|
|
267
|
+
|
|
268
|
+
```powershell
|
|
269
|
+
$f = "$env:USERPROFILE\.ssh\config"
|
|
270
|
+
icacls $f /inheritance:r
|
|
271
|
+
icacls $f /grant:r "$($env:USERNAME):(F)"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
If `icacls` returns `Access is denied`, you don't own the file. Take ownership
|
|
275
|
+
first, then fix the ACL:
|
|
276
|
+
|
|
277
|
+
```powershell
|
|
278
|
+
takeown /F "$env:USERPROFILE\.ssh\config"
|
|
279
|
+
icacls "$env:USERPROFILE\.ssh\config" /grant "$($env:USERNAME):F"
|
|
280
|
+
icacls "$env:USERPROFILE\.ssh\config" /inheritance:r
|
|
281
|
+
icacls "$env:USERPROFILE\.ssh\config" /grant:r "$($env:USERNAME):(F)"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
If `takeown` fails, run PowerShell as Administrator and repeat. Verify with
|
|
285
|
+
`icacls "$env:USERPROFILE\.ssh\config"` — only your user with `(F)` should remain.
|
|
286
|
+
|
|
287
|
+
## License
|
|
288
|
+
|
|
289
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sshmd"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Cross-platform SSH session manager with a background daemon"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = ["ssh", "session-manager", "daemon", "port-forwarding", "socks", "terminal"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Intended Audience :: System Administrators",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: System :: Networking",
|
|
16
|
+
"Topic :: Utilities",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"click>=8.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/revsearch/sshm"
|
|
24
|
+
Repository = "https://github.com/revsearch/sshm"
|
|
25
|
+
Issues = "https://github.com/revsearch/sshm/issues"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
sshm = "sshm.cli:main"
|
|
29
|
+
sshmd = "sshm.daemon:main"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.10.6,<0.12.0"]
|
|
33
|
+
build-backend = "uv_build"
|
|
34
|
+
|
|
35
|
+
# The distribution is published as `sshmd`, but the import package is `sshm`.
|
|
36
|
+
[tool.uv.build-backend]
|
|
37
|
+
module-name = "sshm"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=8.0",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|