rdp-pipe 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.
- rdp_pipe-0.1.0/LICENSE +21 -0
- rdp_pipe-0.1.0/PKG-INFO +264 -0
- rdp_pipe-0.1.0/README.md +245 -0
- rdp_pipe-0.1.0/pyproject.toml +44 -0
- rdp_pipe-0.1.0/setup.cfg +4 -0
- rdp_pipe-0.1.0/src/rdp_pipe/__init__.py +1 -0
- rdp_pipe-0.1.0/src/rdp_pipe/activity_stamp.py +19 -0
- rdp_pipe-0.1.0/src/rdp_pipe/command_socket.py +414 -0
- rdp_pipe-0.1.0/src/rdp_pipe/config/__init__.py +1 -0
- rdp_pipe-0.1.0/src/rdp_pipe/config/entrypoint/__init__.py +1 -0
- rdp_pipe-0.1.0/src/rdp_pipe/config/entrypoint/rdpr.py +3 -0
- rdp_pipe-0.1.0/src/rdp_pipe/connection_error.py +126 -0
- rdp_pipe-0.1.0/src/rdp_pipe/connection_progress.py +53 -0
- rdp_pipe-0.1.0/src/rdp_pipe/connection_url.py +187 -0
- rdp_pipe-0.1.0/src/rdp_pipe/display_control.py +256 -0
- rdp_pipe-0.1.0/src/rdp_pipe/entrypoint/__init__.py +1 -0
- rdp_pipe-0.1.0/src/rdp_pipe/entrypoint/rdp.py +319 -0
- rdp_pipe-0.1.0/src/rdp_pipe/entrypoint/rdpr.py +303 -0
- rdp_pipe-0.1.0/src/rdp_pipe/fastpath_input.py +80 -0
- rdp_pipe-0.1.0/src/rdp_pipe/keyboard_debug.py +20 -0
- rdp_pipe-0.1.0/src/rdp_pipe/mouse_debug.py +20 -0
- rdp_pipe-0.1.0/src/rdp_pipe/pointer_debug.py +48 -0
- rdp_pipe-0.1.0/src/rdp_pipe/pointer_update.py +684 -0
- rdp_pipe-0.1.0/src/rdp_pipe/qt_session_mapping.py +28 -0
- rdp_pipe-0.1.0/src/rdp_pipe/qt_session_window.py +1722 -0
- rdp_pipe-0.1.0/src/rdp_pipe/rdp_connection.py +1305 -0
- rdp_pipe-0.1.0/src/rdp_pipe/rdp_input.py +111 -0
- rdp_pipe-0.1.0/src/rdp_pipe/rdp_session_core.py +468 -0
- rdp_pipe-0.1.0/src/rdp_pipe/rdp_session_thread.py +578 -0
- rdp_pipe-0.1.0/src/rdp_pipe/remote_session_end.py +57 -0
- rdp_pipe-0.1.0/src/rdp_pipe/runtime_paths.py +30 -0
- rdp_pipe-0.1.0/src/rdp_pipe/trust_store.py +446 -0
- rdp_pipe-0.1.0/src/rdp_pipe.egg-info/PKG-INFO +264 -0
- rdp_pipe-0.1.0/src/rdp_pipe.egg-info/SOURCES.txt +60 -0
- rdp_pipe-0.1.0/src/rdp_pipe.egg-info/dependency_links.txt +1 -0
- rdp_pipe-0.1.0/src/rdp_pipe.egg-info/entry_points.txt +3 -0
- rdp_pipe-0.1.0/src/rdp_pipe.egg-info/requires.txt +12 -0
- rdp_pipe-0.1.0/src/rdp_pipe.egg-info/top_level.txt +1 -0
- rdp_pipe-0.1.0/tests/test_activity_stamp.py +33 -0
- rdp_pipe-0.1.0/tests/test_command_socket.py +270 -0
- rdp_pipe-0.1.0/tests/test_command_socket_server.py +264 -0
- rdp_pipe-0.1.0/tests/test_connection_error.py +130 -0
- rdp_pipe-0.1.0/tests/test_connection_progress.py +47 -0
- rdp_pipe-0.1.0/tests/test_connection_url.py +159 -0
- rdp_pipe-0.1.0/tests/test_display_control_pdu.py +139 -0
- rdp_pipe-0.1.0/tests/test_fastpath_input.py +68 -0
- rdp_pipe-0.1.0/tests/test_pointer_update.py +442 -0
- rdp_pipe-0.1.0/tests/test_qt_session_geometry.py +134 -0
- rdp_pipe-0.1.0/tests/test_qt_session_keyboard.py +225 -0
- rdp_pipe-0.1.0/tests/test_qt_session_mapping.py +59 -0
- rdp_pipe-0.1.0/tests/test_qt_session_mouse.py +265 -0
- rdp_pipe-0.1.0/tests/test_rdp_cli.py +225 -0
- rdp_pipe-0.1.0/tests/test_rdp_connection_factory.py +287 -0
- rdp_pipe-0.1.0/tests/test_rdp_session_core.py +251 -0
- rdp_pipe-0.1.0/tests/test_rdp_session_reconnect.py +206 -0
- rdp_pipe-0.1.0/tests/test_rdp_session_shutdown.py +732 -0
- rdp_pipe-0.1.0/tests/test_rdpeclip_clipboard.py +84 -0
- rdp_pipe-0.1.0/tests/test_rdpr_cli.py +211 -0
- rdp_pipe-0.1.0/tests/test_remote_session_end.py +67 -0
- rdp_pipe-0.1.0/tests/test_runtime_paths.py +28 -0
- rdp_pipe-0.1.0/tests/test_send_geometry.py +111 -0
- rdp_pipe-0.1.0/tests/test_trust_store.py +230 -0
rdp_pipe-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dustin Oprea
|
|
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.
|
rdp_pipe-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rdp_pipe
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PyQt6 desktop RDP client with seamless resize (MS-RDPEDISP)
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: aardwolf
|
|
9
|
+
Requires-Dist: PyQt6
|
|
10
|
+
Requires-Dist: Pillow
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: build; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
15
|
+
Requires-Dist: twine; extra == "dev"
|
|
16
|
+
Provides-Extra: mcp
|
|
17
|
+
Requires-Dist: mcp; extra == "mcp"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# rdp_pipe
|
|
21
|
+
|
|
22
|
+
Python desktop RDP client for Linux operators connecting to remote hosts over RDP with NLA (CredSSP) and NTLM password authentication. The UI is a native PyQt6 window with pointer-gated keyboard input and seamless resize via MS-RDPEDISP when the server supports it.
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
- **pyenv** with the Python version named in **`.python-version`** installed locally (the file is required in every clone; install that interpreter with pyenv before pip)
|
|
27
|
+
- An X11 or Wayland desktop session for PyQt6
|
|
28
|
+
- Network reachability to the target host on TCP 3389 (or the port in the URL)
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
From the repo root with pyenv active for **`.python-version`**:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Optional wrapper: **`./script/install.sh`** (runs the same command).
|
|
39
|
+
|
|
40
|
+
This project does not use a repo **`.venv`**. Console scripts (`rdp`, `rdpr`) land on your pyenv shim path after editable install.
|
|
41
|
+
|
|
42
|
+
If imports fail after moving the repo, remove any leftover **`.venv`** directory (it may still point at an old checkout path) and run **`pip install -e .`** again. The installable Python package is **`rdp_pipe`**, not `rdp`.
|
|
43
|
+
|
|
44
|
+
## Connect
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
rdp [-h] [--headless] [--pipe] [--no-autoresize] URL
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
GUI mode (default) opens a PyQt6 window. `--headless` runs without a display and **requires** `--pipe` for automation on the command socket under the system temp directory (`rdp.sock`).
|
|
51
|
+
|
|
52
|
+
Press **Ctrl+C in the terminal** where you launched `rdp` to disconnect and exit cleanly (this does not affect keyboard input forwarded inside the session window).
|
|
53
|
+
|
|
54
|
+
`URL` is an aardwolf-style connection string or bare host shorthand:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
rdp rdp://DOMAIN\\Administrator@10.0.0.5:3389
|
|
58
|
+
rdp rdp+ntlm-password://DOMAIN\\Administrator@10.0.0.5:3389
|
|
59
|
+
rdp 10.0.0.5
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Generic `rdp://` URLs prefer NTLM password authentication; the server selects NLA (CredSSP), TLS-only, or legacy RDP during X.224 negotiation.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
### Server certificate trust
|
|
66
|
+
|
|
67
|
+
On NLA connect, the client stores the server TLS certificate under `~/.config/rdpipe/`:
|
|
68
|
+
|
|
69
|
+
- `clients/<ip>` — one-line file mapping the remote IP to a certificate fingerprint
|
|
70
|
+
- `certificates/<fingerprint>/` — `certificate` (PEM), `ip`, and `metadata` (JSON)
|
|
71
|
+
|
|
72
|
+
The first connection to an IP is accepted automatically. If the server later presents a different certificate for the same IP, the client refuses to connect and prints paths to remove, for example:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
~/.config/rdpipe/clients/10.0.0.5
|
|
76
|
+
~/.config/rdpipe/certificates/<old-fingerprint>/
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Remove the `clients/<ip>` file to trust a new certificate on the next connect. IPv6 addresses use underscores instead of colons in client filenames (e.g. `2001_db8__1`).
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
### Password resolution
|
|
83
|
+
|
|
84
|
+
Passwords are resolved in order (first match wins):
|
|
85
|
+
|
|
86
|
+
1. Embedded in URL userinfo (`user:password@host`)
|
|
87
|
+
2. `RDP_PASSWORD` environment variable
|
|
88
|
+
3. Standard input — `getpass` on a TTY, otherwise one line from a pipe
|
|
89
|
+
|
|
90
|
+
If no password is available after these steps, the CLI exits with a non-zero status and an error on stderr.
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
export RDP_PASSWORD='your-password'
|
|
96
|
+
rdp 'rdp+ntlm-password://DOMAIN\Administrator@10.0.0.5'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
echo 'your-password' | rdp 'rdp+ntlm-password://Administrator@10.0.0.5'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Headless automation
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
export RDP_PASSWORD='your-password'
|
|
107
|
+
rdp --headless --pipe '10.0.0.5'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Headless mode uses a fixed 1280×800 session geometry (no window resize). The process listens on the Unix socket for newline-delimited JSON commands. Use `--color-depth 24` (or `16`) when you need a specific bpp for screen analysis via `receive_screenshot`.
|
|
111
|
+
|
|
112
|
+
Full wire format, command parameters, response shapes, and client examples: **[REMOTE_COMMAND_PROTOCOL.md](REMOTE_COMMAND_PROTOCOL.md)**.
|
|
113
|
+
|
|
114
|
+
Quick probe with `rdpr`:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
rdpr command_receive_geometry
|
|
118
|
+
rdpr command_send_geometry 1920 1080
|
|
119
|
+
rdpr command_send_click 640 400 --button left
|
|
120
|
+
rdpr command_receive_screenshot --format png
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Or with `nc`:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
printf '%s\n' '{"command":"receive_geometry"}' | nc -U "${TMPDIR:-/tmp}/rdp.sock"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`--pipe` is optional in GUI mode (automation socket at `{system temp}/rdp.sock` alongside the window). Every session also touches `{system temp}/rdp_activity.stamp` on remote framebuffer updates.
|
|
130
|
+
|
|
131
|
+
### MCP automation
|
|
132
|
+
|
|
133
|
+
A standard MCP host config is provided at [`mcp/mcp.json`](mcp/mcp.json). It launches [`mcp/run_server.sh`](mcp/run_server.sh), which selects the pyenv interpreter from `.python-version`, sets `RDPR_COMMAND` to that environment’s `rdpr`, and runs `mcp/server.py`. Install the optional MCP extra into that interpreter, then copy the file or merge its `rdp_pipe` entry into your host’s MCP settings:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pip install -e ".[mcp]"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Socket default: `{system temp}/rdp.sock` (honors `$TMPDIR`; typically `/tmp` on Linux).
|
|
140
|
+
|
|
141
|
+
For Cursor, copy or symlink into `.cursor/mcp.json` (that directory is gitignored):
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
mkdir -p .cursor
|
|
145
|
+
cp mcp/mcp.json .cursor/mcp.json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Typical LLM flow with an active `rdp --pipe` session:
|
|
149
|
+
|
|
150
|
+
1. `command_receive_geometry` — learn coordinate bounds
|
|
151
|
+
2. `command_receive_screenshot` — capture state (inline image; server always passes `--no-write` to `rdpr`)
|
|
152
|
+
3. `command_send_click` / `command_send_key` — act on the remote desktop
|
|
153
|
+
|
|
154
|
+
#### MCP test prompt
|
|
155
|
+
|
|
156
|
+
Paste this to an agent with **`rdp_pipe` connected** and a live **`rdp --headless --pipe`** (or GUI + `--pipe`) session:
|
|
157
|
+
|
|
158
|
+
```text
|
|
159
|
+
In RDP:
|
|
160
|
+
1. take screenshot
|
|
161
|
+
2. open calculator
|
|
162
|
+
3. take screenshot
|
|
163
|
+
4. close calculator
|
|
164
|
+
5. take screenshot
|
|
165
|
+
6. open command prompt
|
|
166
|
+
7. take screenshot
|
|
167
|
+
8. list directory
|
|
168
|
+
9. take screenshot
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Take a screenshot after steps **1**, **3**, **5**, **7**, and **9** only. After step **4**, confirm step **5** shows Calculator gone before opening Command Prompt.
|
|
172
|
+
|
|
173
|
+
On Windows Server with legacy Calculator (`win32calc.exe`), search-box automation works for launch; close with `taskkill /F /IM win32calc.exe` from search (title-bar **X** clicks are unreliable over RDP). Open Command Prompt with search → `cmd` → Enter; run `dir` in the CMD window.
|
|
174
|
+
|
|
175
|
+
#### MCP reference screenshots
|
|
176
|
+
|
|
177
|
+
Committed captures from a successful run (1280×800 session) live under [`asset/mcp/screenshot/`](asset/mcp/screenshot/):
|
|
178
|
+
|
|
179
|
+
| Step | Expected state | File |
|
|
180
|
+
|------|----------------|------|
|
|
181
|
+
| 1 | Clean desktop | [`01.png`](asset/mcp/screenshot/01.png) |
|
|
182
|
+
| 3 | Calculator open | [`03.png`](asset/mcp/screenshot/03.png) |
|
|
183
|
+
| 5 | Calculator closed | [`05.png`](asset/mcp/screenshot/05.png) |
|
|
184
|
+
| 7 | Command Prompt open | [`07.png`](asset/mcp/screenshot/07.png) |
|
|
185
|
+
| 9 | `dir` in `C:\Users\Administrator` | [`09.png`](asset/mcp/screenshot/09.png) |
|
|
186
|
+
|
|
187
|
+
Step 1 — initial desktop:
|
|
188
|
+
|
|
189
|
+

|
|
190
|
+
|
|
191
|
+
Step 3 — Calculator open:
|
|
192
|
+
|
|
193
|
+

|
|
194
|
+
|
|
195
|
+
Step 5 — Calculator closed:
|
|
196
|
+
|
|
197
|
+

|
|
198
|
+
|
|
199
|
+
Step 7 — Command Prompt open:
|
|
200
|
+
|
|
201
|
+

|
|
202
|
+
|
|
203
|
+
Step 9 — directory listing:
|
|
204
|
+
|
|
205
|
+

|
|
206
|
+
|
|
207
|
+
Wire protocol details: **[REMOTE_COMMAND_PROTOCOL.md](REMOTE_COMMAND_PROTOCOL.md)**.
|
|
208
|
+
|
|
209
|
+
## Monkey-patching
|
|
210
|
+
|
|
211
|
+
This client patches **aardwolf** at runtime instead of vendoring or forking the library. Patches live in production code under `src/rdp_pipe/` (not in tests). Each patch replaces a symbol on import; subclass overrides on `RdpDesktopConnection` are normal inheritance and are not listed here.
|
|
212
|
+
|
|
213
|
+
| Target | Replacement | Why |
|
|
214
|
+
|--------|-------------|-----|
|
|
215
|
+
| `aardwolf.protocol.T124.userdata.clientcoredata.TS_UD_CS_CORE.to_bytes` | `_patched_ts_ud_cs_core_to_bytes` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | During desktop connect, advertise `SUPPORT_MONITOR_LAYOUT_PDU` and (when `--color-depth` is 32) `WANT_32BPP_SESSION` / 24-bpp high color in Client Core Data. Active only while `RdpDesktopConnection.connect()` runs (`_CONNECTING_DESKTOP_CONNECTION` gate). |
|
|
216
|
+
| `aardwolf.protocol.pdu.capabilities.pointer.TS_POINTER_CAPABILITYSET.__init__` | `_patched_pointer_capabilityset_init` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | Advertise `colorPointerFlag=True` so the server sends color/cached pointer updates for hover cursor shapes. |
|
|
217
|
+
| `aardwolf.connection.RDPConnection.handle_out_data` | `_patched_handle_out_data` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | Before every Confirm Active (connect and mid-session reactivation), set `desktopResizeFlag=True` and add LARGE_POINTER / UNICODE / MOUSE_HWHEEL that aardwolf omits. |
|
|
218
|
+
| `RdpDesktopConnection._RDPConnection__process_fastpath` | `_rdp_desktop_process_fastpath` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | Stock aardwolf handles fast-path `BITMAP` only. Our handler also forwards bitmap tiles without inferring resolution from tile size, and emits pointer updates (`COLOR`, `POINTER`, `CACHED`, etc.) for remote cursor mirroring. |
|
|
219
|
+
| `aardwolf.extensions.RDPECLIP.channel.RDPECLIPChannel._handle_format_data_request` | `_patched_rdpeclip_handle_format_data_request` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | aardwolf dereferences `clipboard.data.datatype` without checking for `None`. After connect we advertise standard clipboard formats even when the local clipboard is empty; a remote `CB_FORMAT_DATA_REQUEST` then crashed the x224 reader. Reply with `CB_RESPONSE_FAIL` when `clipboard.data` is unset. |
|
|
220
|
+
| `aardwolf.connection.RDPConnection.terminate` | `_patched_aardwolf_terminate` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | Stock aardwolf always calls `send_disconnect()` and logs `Error while requesting shutdown` when the MCS channel is missing (failed connect) or the TCP socket is already reset. Skip graceful shutdown when MCS/server connect data are absent; suppress the warning for expected transport errors during reconnect teardown. |
|
|
221
|
+
| `aardwolf.connection.RDPConnection.check_logon_status` | `_patched_check_logon_status` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | After aardwolf records `SET_ERROR_INFO`, copy `SERVER_SHUTDOWN` / `SERVER_REBOOT` onto `RdpDesktopConnection.pending_remote_session_end_reason` so the GUI can prompt for reconnect vs exit. |
|
|
222
|
+
|
|
223
|
+
When adding or changing a runtime monkey-patch, update this section in the same change set.
|
|
224
|
+
|
|
225
|
+
## Release (PyPI)
|
|
226
|
+
|
|
227
|
+
Operator workflow mirrors a static frontend **build → package → deploy**, but artifacts are Python wheels/sdists and **deploy** uploads with **twine** (not S3).
|
|
228
|
+
|
|
229
|
+
Activate pyenv for **`.python-version`**, install dev tools once (`pip install -e ".[dev]"`), then from the repo root:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
./script/build.sh
|
|
233
|
+
ARCHIVE="$(./script/package.sh patch)"
|
|
234
|
+
./script/deploy.sh "${ARCHIVE}"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| Script | Role |
|
|
238
|
+
|--------|------|
|
|
239
|
+
| **`script/build.sh`** | `python -m build` → **`dist/`** (wheel + sdist). No version bump. |
|
|
240
|
+
| **`script/package.sh`** | **`patch` \| `minor` \| `major`**: clean tree, bump **`pyproject.toml`**, commit, annotated **`vX.Y.Z`**, run **`build.sh`**, pack **`build/archives/vX.Y.Z_rdp_pipe.tar.gz`**. Stdout = archive path. |
|
|
241
|
+
| **`script/deploy.sh`** | Positional archive path: extract, **`twine check`**, **`twine upload`**. Writes **`releases/pypi/X.Y.Z.md`** when the tree is clean. |
|
|
242
|
+
|
|
243
|
+
PyPI credentials: **`~/.pypirc`** or **`TWINE_USERNAME`** / **`TWINE_PASSWORD`**. For TestPyPI, set **`TWINE_REPOSITORY_URL=https://test.pypi.org/legacy/`** before **`deploy.sh`**.
|
|
244
|
+
|
|
245
|
+
After **`package.sh`**, push the release commit and tag when ready: **`git push origin HEAD vX.Y.Z`**.
|
|
246
|
+
|
|
247
|
+
## Test
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
pip install -e ".[dev]"
|
|
251
|
+
python -m pytest -q
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
MCP server tests need the **`mcp`** extra: `pip install -e ".[dev,mcp]"`.
|
|
255
|
+
|
|
256
|
+
Offline unit tests cover URL/password handling, RDPDISP PDU encoding, pointer mask decoding, command socket protocol, and CLI validation.
|
|
257
|
+
|
|
258
|
+
## Manual smoke
|
|
259
|
+
|
|
260
|
+
Connect to an RDP host with NLA enabled. Resize the client window and confirm the remote display resolution tracks when the server supports RDPDISP. Servers without RDPDISP keep a fixed session resolution; the local window may letterbox until disconnect.
|
|
261
|
+
|
|
262
|
+
To trace remote pointer mirroring (PDU types, apply/skip decisions, forwarded hover coordinates), run with `RDP_POINTER_DEBUG=1` and watch stderr while moving the mouse over window borders and text fields.
|
|
263
|
+
|
|
264
|
+
To trace keyboard capture after idle (Qt key events, pointer-inside gating, enqueue, forwarder dequeue), run with `RDP_KEYBOARD_DEBUG=1`. If GUI keys show no `keyPressEvent` lines but `rdpr send_key` still works, focus was stolen locally; if both fail, check stderr for `RDP deactivation-reactivation failed`.
|
rdp_pipe-0.1.0/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# rdp_pipe
|
|
2
|
+
|
|
3
|
+
Python desktop RDP client for Linux operators connecting to remote hosts over RDP with NLA (CredSSP) and NTLM password authentication. The UI is a native PyQt6 window with pointer-gated keyboard input and seamless resize via MS-RDPEDISP when the server supports it.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **pyenv** with the Python version named in **`.python-version`** installed locally (the file is required in every clone; install that interpreter with pyenv before pip)
|
|
8
|
+
- An X11 or Wayland desktop session for PyQt6
|
|
9
|
+
- Network reachability to the target host on TCP 3389 (or the port in the URL)
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
From the repo root with pyenv active for **`.python-version`**:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Optional wrapper: **`./script/install.sh`** (runs the same command).
|
|
20
|
+
|
|
21
|
+
This project does not use a repo **`.venv`**. Console scripts (`rdp`, `rdpr`) land on your pyenv shim path after editable install.
|
|
22
|
+
|
|
23
|
+
If imports fail after moving the repo, remove any leftover **`.venv`** directory (it may still point at an old checkout path) and run **`pip install -e .`** again. The installable Python package is **`rdp_pipe`**, not `rdp`.
|
|
24
|
+
|
|
25
|
+
## Connect
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
rdp [-h] [--headless] [--pipe] [--no-autoresize] URL
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
GUI mode (default) opens a PyQt6 window. `--headless` runs without a display and **requires** `--pipe` for automation on the command socket under the system temp directory (`rdp.sock`).
|
|
32
|
+
|
|
33
|
+
Press **Ctrl+C in the terminal** where you launched `rdp` to disconnect and exit cleanly (this does not affect keyboard input forwarded inside the session window).
|
|
34
|
+
|
|
35
|
+
`URL` is an aardwolf-style connection string or bare host shorthand:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
rdp rdp://DOMAIN\\Administrator@10.0.0.5:3389
|
|
39
|
+
rdp rdp+ntlm-password://DOMAIN\\Administrator@10.0.0.5:3389
|
|
40
|
+
rdp 10.0.0.5
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Generic `rdp://` URLs prefer NTLM password authentication; the server selects NLA (CredSSP), TLS-only, or legacy RDP during X.224 negotiation.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Server certificate trust
|
|
47
|
+
|
|
48
|
+
On NLA connect, the client stores the server TLS certificate under `~/.config/rdpipe/`:
|
|
49
|
+
|
|
50
|
+
- `clients/<ip>` — one-line file mapping the remote IP to a certificate fingerprint
|
|
51
|
+
- `certificates/<fingerprint>/` — `certificate` (PEM), `ip`, and `metadata` (JSON)
|
|
52
|
+
|
|
53
|
+
The first connection to an IP is accepted automatically. If the server later presents a different certificate for the same IP, the client refuses to connect and prints paths to remove, for example:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
~/.config/rdpipe/clients/10.0.0.5
|
|
57
|
+
~/.config/rdpipe/certificates/<old-fingerprint>/
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Remove the `clients/<ip>` file to trust a new certificate on the next connect. IPv6 addresses use underscores instead of colons in client filenames (e.g. `2001_db8__1`).
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Password resolution
|
|
64
|
+
|
|
65
|
+
Passwords are resolved in order (first match wins):
|
|
66
|
+
|
|
67
|
+
1. Embedded in URL userinfo (`user:password@host`)
|
|
68
|
+
2. `RDP_PASSWORD` environment variable
|
|
69
|
+
3. Standard input — `getpass` on a TTY, otherwise one line from a pipe
|
|
70
|
+
|
|
71
|
+
If no password is available after these steps, the CLI exits with a non-zero status and an error on stderr.
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
export RDP_PASSWORD='your-password'
|
|
77
|
+
rdp 'rdp+ntlm-password://DOMAIN\Administrator@10.0.0.5'
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
echo 'your-password' | rdp 'rdp+ntlm-password://Administrator@10.0.0.5'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Headless automation
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
export RDP_PASSWORD='your-password'
|
|
88
|
+
rdp --headless --pipe '10.0.0.5'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Headless mode uses a fixed 1280×800 session geometry (no window resize). The process listens on the Unix socket for newline-delimited JSON commands. Use `--color-depth 24` (or `16`) when you need a specific bpp for screen analysis via `receive_screenshot`.
|
|
92
|
+
|
|
93
|
+
Full wire format, command parameters, response shapes, and client examples: **[REMOTE_COMMAND_PROTOCOL.md](REMOTE_COMMAND_PROTOCOL.md)**.
|
|
94
|
+
|
|
95
|
+
Quick probe with `rdpr`:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
rdpr command_receive_geometry
|
|
99
|
+
rdpr command_send_geometry 1920 1080
|
|
100
|
+
rdpr command_send_click 640 400 --button left
|
|
101
|
+
rdpr command_receive_screenshot --format png
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Or with `nc`:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
printf '%s\n' '{"command":"receive_geometry"}' | nc -U "${TMPDIR:-/tmp}/rdp.sock"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`--pipe` is optional in GUI mode (automation socket at `{system temp}/rdp.sock` alongside the window). Every session also touches `{system temp}/rdp_activity.stamp` on remote framebuffer updates.
|
|
111
|
+
|
|
112
|
+
### MCP automation
|
|
113
|
+
|
|
114
|
+
A standard MCP host config is provided at [`mcp/mcp.json`](mcp/mcp.json). It launches [`mcp/run_server.sh`](mcp/run_server.sh), which selects the pyenv interpreter from `.python-version`, sets `RDPR_COMMAND` to that environment’s `rdpr`, and runs `mcp/server.py`. Install the optional MCP extra into that interpreter, then copy the file or merge its `rdp_pipe` entry into your host’s MCP settings:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install -e ".[mcp]"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Socket default: `{system temp}/rdp.sock` (honors `$TMPDIR`; typically `/tmp` on Linux).
|
|
121
|
+
|
|
122
|
+
For Cursor, copy or symlink into `.cursor/mcp.json` (that directory is gitignored):
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
mkdir -p .cursor
|
|
126
|
+
cp mcp/mcp.json .cursor/mcp.json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Typical LLM flow with an active `rdp --pipe` session:
|
|
130
|
+
|
|
131
|
+
1. `command_receive_geometry` — learn coordinate bounds
|
|
132
|
+
2. `command_receive_screenshot` — capture state (inline image; server always passes `--no-write` to `rdpr`)
|
|
133
|
+
3. `command_send_click` / `command_send_key` — act on the remote desktop
|
|
134
|
+
|
|
135
|
+
#### MCP test prompt
|
|
136
|
+
|
|
137
|
+
Paste this to an agent with **`rdp_pipe` connected** and a live **`rdp --headless --pipe`** (or GUI + `--pipe`) session:
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
In RDP:
|
|
141
|
+
1. take screenshot
|
|
142
|
+
2. open calculator
|
|
143
|
+
3. take screenshot
|
|
144
|
+
4. close calculator
|
|
145
|
+
5. take screenshot
|
|
146
|
+
6. open command prompt
|
|
147
|
+
7. take screenshot
|
|
148
|
+
8. list directory
|
|
149
|
+
9. take screenshot
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Take a screenshot after steps **1**, **3**, **5**, **7**, and **9** only. After step **4**, confirm step **5** shows Calculator gone before opening Command Prompt.
|
|
153
|
+
|
|
154
|
+
On Windows Server with legacy Calculator (`win32calc.exe`), search-box automation works for launch; close with `taskkill /F /IM win32calc.exe` from search (title-bar **X** clicks are unreliable over RDP). Open Command Prompt with search → `cmd` → Enter; run `dir` in the CMD window.
|
|
155
|
+
|
|
156
|
+
#### MCP reference screenshots
|
|
157
|
+
|
|
158
|
+
Committed captures from a successful run (1280×800 session) live under [`asset/mcp/screenshot/`](asset/mcp/screenshot/):
|
|
159
|
+
|
|
160
|
+
| Step | Expected state | File |
|
|
161
|
+
|------|----------------|------|
|
|
162
|
+
| 1 | Clean desktop | [`01.png`](asset/mcp/screenshot/01.png) |
|
|
163
|
+
| 3 | Calculator open | [`03.png`](asset/mcp/screenshot/03.png) |
|
|
164
|
+
| 5 | Calculator closed | [`05.png`](asset/mcp/screenshot/05.png) |
|
|
165
|
+
| 7 | Command Prompt open | [`07.png`](asset/mcp/screenshot/07.png) |
|
|
166
|
+
| 9 | `dir` in `C:\Users\Administrator` | [`09.png`](asset/mcp/screenshot/09.png) |
|
|
167
|
+
|
|
168
|
+
Step 1 — initial desktop:
|
|
169
|
+
|
|
170
|
+

|
|
171
|
+
|
|
172
|
+
Step 3 — Calculator open:
|
|
173
|
+
|
|
174
|
+

|
|
175
|
+
|
|
176
|
+
Step 5 — Calculator closed:
|
|
177
|
+
|
|
178
|
+

|
|
179
|
+
|
|
180
|
+
Step 7 — Command Prompt open:
|
|
181
|
+
|
|
182
|
+

|
|
183
|
+
|
|
184
|
+
Step 9 — directory listing:
|
|
185
|
+
|
|
186
|
+

|
|
187
|
+
|
|
188
|
+
Wire protocol details: **[REMOTE_COMMAND_PROTOCOL.md](REMOTE_COMMAND_PROTOCOL.md)**.
|
|
189
|
+
|
|
190
|
+
## Monkey-patching
|
|
191
|
+
|
|
192
|
+
This client patches **aardwolf** at runtime instead of vendoring or forking the library. Patches live in production code under `src/rdp_pipe/` (not in tests). Each patch replaces a symbol on import; subclass overrides on `RdpDesktopConnection` are normal inheritance and are not listed here.
|
|
193
|
+
|
|
194
|
+
| Target | Replacement | Why |
|
|
195
|
+
|--------|-------------|-----|
|
|
196
|
+
| `aardwolf.protocol.T124.userdata.clientcoredata.TS_UD_CS_CORE.to_bytes` | `_patched_ts_ud_cs_core_to_bytes` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | During desktop connect, advertise `SUPPORT_MONITOR_LAYOUT_PDU` and (when `--color-depth` is 32) `WANT_32BPP_SESSION` / 24-bpp high color in Client Core Data. Active only while `RdpDesktopConnection.connect()` runs (`_CONNECTING_DESKTOP_CONNECTION` gate). |
|
|
197
|
+
| `aardwolf.protocol.pdu.capabilities.pointer.TS_POINTER_CAPABILITYSET.__init__` | `_patched_pointer_capabilityset_init` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | Advertise `colorPointerFlag=True` so the server sends color/cached pointer updates for hover cursor shapes. |
|
|
198
|
+
| `aardwolf.connection.RDPConnection.handle_out_data` | `_patched_handle_out_data` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | Before every Confirm Active (connect and mid-session reactivation), set `desktopResizeFlag=True` and add LARGE_POINTER / UNICODE / MOUSE_HWHEEL that aardwolf omits. |
|
|
199
|
+
| `RdpDesktopConnection._RDPConnection__process_fastpath` | `_rdp_desktop_process_fastpath` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | Stock aardwolf handles fast-path `BITMAP` only. Our handler also forwards bitmap tiles without inferring resolution from tile size, and emits pointer updates (`COLOR`, `POINTER`, `CACHED`, etc.) for remote cursor mirroring. |
|
|
200
|
+
| `aardwolf.extensions.RDPECLIP.channel.RDPECLIPChannel._handle_format_data_request` | `_patched_rdpeclip_handle_format_data_request` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | aardwolf dereferences `clipboard.data.datatype` without checking for `None`. After connect we advertise standard clipboard formats even when the local clipboard is empty; a remote `CB_FORMAT_DATA_REQUEST` then crashed the x224 reader. Reply with `CB_RESPONSE_FAIL` when `clipboard.data` is unset. |
|
|
201
|
+
| `aardwolf.connection.RDPConnection.terminate` | `_patched_aardwolf_terminate` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | Stock aardwolf always calls `send_disconnect()` and logs `Error while requesting shutdown` when the MCS channel is missing (failed connect) or the TCP socket is already reset. Skip graceful shutdown when MCS/server connect data are absent; suppress the warning for expected transport errors during reconnect teardown. |
|
|
202
|
+
| `aardwolf.connection.RDPConnection.check_logon_status` | `_patched_check_logon_status` in [`rdp_connection.py`](src/rdp_pipe/rdp_connection.py) | After aardwolf records `SET_ERROR_INFO`, copy `SERVER_SHUTDOWN` / `SERVER_REBOOT` onto `RdpDesktopConnection.pending_remote_session_end_reason` so the GUI can prompt for reconnect vs exit. |
|
|
203
|
+
|
|
204
|
+
When adding or changing a runtime monkey-patch, update this section in the same change set.
|
|
205
|
+
|
|
206
|
+
## Release (PyPI)
|
|
207
|
+
|
|
208
|
+
Operator workflow mirrors a static frontend **build → package → deploy**, but artifacts are Python wheels/sdists and **deploy** uploads with **twine** (not S3).
|
|
209
|
+
|
|
210
|
+
Activate pyenv for **`.python-version`**, install dev tools once (`pip install -e ".[dev]"`), then from the repo root:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
./script/build.sh
|
|
214
|
+
ARCHIVE="$(./script/package.sh patch)"
|
|
215
|
+
./script/deploy.sh "${ARCHIVE}"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
| Script | Role |
|
|
219
|
+
|--------|------|
|
|
220
|
+
| **`script/build.sh`** | `python -m build` → **`dist/`** (wheel + sdist). No version bump. |
|
|
221
|
+
| **`script/package.sh`** | **`patch` \| `minor` \| `major`**: clean tree, bump **`pyproject.toml`**, commit, annotated **`vX.Y.Z`**, run **`build.sh`**, pack **`build/archives/vX.Y.Z_rdp_pipe.tar.gz`**. Stdout = archive path. |
|
|
222
|
+
| **`script/deploy.sh`** | Positional archive path: extract, **`twine check`**, **`twine upload`**. Writes **`releases/pypi/X.Y.Z.md`** when the tree is clean. |
|
|
223
|
+
|
|
224
|
+
PyPI credentials: **`~/.pypirc`** or **`TWINE_USERNAME`** / **`TWINE_PASSWORD`**. For TestPyPI, set **`TWINE_REPOSITORY_URL=https://test.pypi.org/legacy/`** before **`deploy.sh`**.
|
|
225
|
+
|
|
226
|
+
After **`package.sh`**, push the release commit and tag when ready: **`git push origin HEAD vX.Y.Z`**.
|
|
227
|
+
|
|
228
|
+
## Test
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
pip install -e ".[dev]"
|
|
232
|
+
python -m pytest -q
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
MCP server tests need the **`mcp`** extra: `pip install -e ".[dev,mcp]"`.
|
|
236
|
+
|
|
237
|
+
Offline unit tests cover URL/password handling, RDPDISP PDU encoding, pointer mask decoding, command socket protocol, and CLI validation.
|
|
238
|
+
|
|
239
|
+
## Manual smoke
|
|
240
|
+
|
|
241
|
+
Connect to an RDP host with NLA enabled. Resize the client window and confirm the remote display resolution tracks when the server supports RDPDISP. Servers without RDPDISP keep a fixed session resolution; the local window may letterbox until disconnect.
|
|
242
|
+
|
|
243
|
+
To trace remote pointer mirroring (PDU types, apply/skip decisions, forwarded hover coordinates), run with `RDP_POINTER_DEBUG=1` and watch stderr while moving the mouse over window borders and text fields.
|
|
244
|
+
|
|
245
|
+
To trace keyboard capture after idle (Qt key events, pointer-inside gating, enqueue, forwarder dequeue), run with `RDP_KEYBOARD_DEBUG=1`. If GUI keys show no `keyPressEvent` lines but `rdpr send_key` still works, focus was stolen locally; if both fail, check stderr for `RDP deactivation-reactivation failed`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rdp_pipe"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "PyQt6 desktop RDP client with seamless resize (MS-RDPEDISP)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"aardwolf",
|
|
13
|
+
"PyQt6",
|
|
14
|
+
"Pillow",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"build",
|
|
20
|
+
"pytest",
|
|
21
|
+
"pytest-asyncio",
|
|
22
|
+
"twine",
|
|
23
|
+
]
|
|
24
|
+
mcp = [
|
|
25
|
+
"mcp",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
rdp = "rdp_pipe.entrypoint.rdp:main"
|
|
30
|
+
rdpr = "rdp_pipe.entrypoint.rdpr:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
asyncio_mode = "auto"
|
|
38
|
+
pythonpath = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.pyright]
|
|
41
|
+
include = ["src", "tests", "mcp"]
|
|
42
|
+
extraPaths = ["src"]
|
|
43
|
+
pythonVersion = "3.14"
|
|
44
|
+
typeCheckingMode = "basic"
|
rdp_pipe-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Desktop RDP client package built on aardwolf and PyQt6."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Touch a filepath when remote framebuffer updates arrive."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
_LOGGER = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def touch_activity_stamp_file(activity_stamp_filepath: str) -> None:
|
|
9
|
+
"""Create or update mtime on the activity stamp file."""
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
with open(activity_stamp_filepath, "a"):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
except OSError as error:
|
|
16
|
+
_LOGGER.warning(
|
|
17
|
+
"activity stamp touch failed filepath={activity_stamp_filepath} error={error}".format(
|
|
18
|
+
activity_stamp_filepath=activity_stamp_filepath,
|
|
19
|
+
error=error))
|