remotedesktop 0.0.1__tar.gz → 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.
- remotedesktop-0.1.0/.github/workflows/ci.yml +49 -0
- remotedesktop-0.1.0/CLAUDE.md +52 -0
- remotedesktop-0.1.0/PKG-INFO +113 -0
- remotedesktop-0.1.0/badges/coverage.svg +16 -0
- {remotedesktop-0.0.1 → remotedesktop-0.1.0}/pyproject.toml +8 -0
- remotedesktop-0.1.0/readme.md +94 -0
- remotedesktop-0.1.0/run_client.bat +14 -0
- remotedesktop-0.1.0/run_server.bat +14 -0
- remotedesktop-0.1.0/scripts/make_coverage_badge.py +57 -0
- {remotedesktop-0.0.1 → remotedesktop-0.1.0}/src/remotedesktop/__init__.py +1 -1
- remotedesktop-0.1.0/src/remotedesktop/client.py +243 -0
- remotedesktop-0.1.0/src/remotedesktop/clipboard.py +93 -0
- remotedesktop-0.1.0/src/remotedesktop/config.py +122 -0
- remotedesktop-0.1.0/src/remotedesktop/db.py +49 -0
- remotedesktop-0.1.0/src/remotedesktop/discovery.py +146 -0
- remotedesktop-0.1.0/src/remotedesktop/input_injection.py +123 -0
- remotedesktop-0.1.0/src/remotedesktop/inventory.py +218 -0
- remotedesktop-0.1.0/src/remotedesktop/protocol.py +75 -0
- remotedesktop-0.1.0/src/remotedesktop/server.py +171 -0
- remotedesktop-0.1.0/src/remotedesktop/sharing.py +540 -0
- remotedesktop-0.1.0/src/remotedesktop/tls.py +90 -0
- remotedesktop-0.1.0/src/remotedesktop/viewer.py +170 -0
- remotedesktop-0.1.0/src/remotedesktop/window_state.py +26 -0
- remotedesktop-0.1.0/tests/conftest.py +16 -0
- remotedesktop-0.1.0/tests/test_clipboard.py +115 -0
- remotedesktop-0.1.0/tests/test_config.py +58 -0
- remotedesktop-0.1.0/tests/test_discovery.py +74 -0
- remotedesktop-0.1.0/tests/test_input.py +176 -0
- remotedesktop-0.1.0/tests/test_inventory.py +95 -0
- remotedesktop-0.1.0/tests/test_protocol.py +83 -0
- remotedesktop-0.1.0/tests/test_sharing.py +276 -0
- remotedesktop-0.1.0/tests/test_smoke.py +58 -0
- remotedesktop-0.1.0/tests/test_window_state.py +28 -0
- remotedesktop-0.1.0/uv.lock +332 -0
- remotedesktop-0.0.1/CLAUDE.md +0 -38
- remotedesktop-0.0.1/PKG-INFO +0 -35
- remotedesktop-0.0.1/readme.md +0 -18
- remotedesktop-0.0.1/src/remotedesktop/client.py +0 -27
- remotedesktop-0.0.1/src/remotedesktop/server.py +0 -27
- remotedesktop-0.0.1/src/remotedesktop/viewer.py +0 -15
- remotedesktop-0.0.1/tests/conftest.py +0 -7
- remotedesktop-0.0.1/tests/test_smoke.py +0 -18
- remotedesktop-0.0.1/uv.lock +0 -142
- {remotedesktop-0.0.1 → remotedesktop-0.1.0}/.gitignore +0 -0
- {remotedesktop-0.0.1 → remotedesktop-0.1.0}/LICENSE +0 -0
- {remotedesktop-0.0.1 → remotedesktop-0.1.0}/run_claude.bat +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
ruff:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v6
|
|
14
|
+
- run: uvx ruff check .
|
|
15
|
+
|
|
16
|
+
ty:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: astral-sh/setup-uv@v6
|
|
21
|
+
# ty resolves imports from the project venv; pyproject pins the
|
|
22
|
+
# type-checking platform to win32 (the app's target).
|
|
23
|
+
- run: uv sync
|
|
24
|
+
- run: uvx ty check .
|
|
25
|
+
|
|
26
|
+
test:
|
|
27
|
+
# The app targets Windows: input injection, schannel TLS, and the GUI
|
|
28
|
+
# tests all assume it.
|
|
29
|
+
runs-on: windows-latest
|
|
30
|
+
permissions:
|
|
31
|
+
contents: write # to push the updated coverage badge
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
- uses: astral-sh/setup-uv@v6
|
|
35
|
+
- run: uv sync
|
|
36
|
+
- run: uv run pytest --cov=remotedesktop --cov-report=term
|
|
37
|
+
- name: Update coverage badge
|
|
38
|
+
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
|
39
|
+
run: |
|
|
40
|
+
uv run python scripts/make_coverage_badge.py
|
|
41
|
+
git add badges/coverage.svg
|
|
42
|
+
git diff --cached --quiet
|
|
43
|
+
if ($LASTEXITCODE -ne 0) {
|
|
44
|
+
git config user.name "github-actions[bot]"
|
|
45
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
46
|
+
git commit -m "Update coverage badge [skip ci]"
|
|
47
|
+
git push
|
|
48
|
+
}
|
|
49
|
+
exit 0
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What This Project Is
|
|
6
|
+
|
|
7
|
+
A Python client/server remote desktop GUI application (PySide6) for Windows computers on the same LAN, per `readme.md`:
|
|
8
|
+
|
|
9
|
+
- **Autodiscovery and connection** of servers on the LAN.
|
|
10
|
+
- **In scope:** desktop screen, keyboard, mouse, and clipboard.
|
|
11
|
+
- **Out of scope:** shared drives, devices, and multimedia (e.g., audio).
|
|
12
|
+
- **Two GUI apps:** a client and a server, each run on its respective computer.
|
|
13
|
+
- **Trust model:** on first connection, the user on the server side must explicitly permit the client. After that, the client may reconnect whenever the server is running without further approval.
|
|
14
|
+
- **Constraint:** does not use Windows RDP and does not rely on any Microsoft-based authentication.
|
|
15
|
+
|
|
16
|
+
LAN autodiscovery, screen sharing, keyboard/mouse forwarding, clipboard sync, and a TLS-encrypted transport with a token-authenticated handshake are all implemented — the readme feature set plus security hardening.
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
Managed with `uv` (hatchling build backend, src layout):
|
|
21
|
+
|
|
22
|
+
- `uv sync` — create/update the venv with the project and dev dependencies
|
|
23
|
+
- `uv run pytest` — run all tests
|
|
24
|
+
- `uv run pytest tests/test_smoke.py::test_version` — run a single test
|
|
25
|
+
- `uv run remotedesktop-client` / `uv run remotedesktop-server` — launch the apps. These are `gui-scripts`, so they run detached with no console output; use `uv run python -m remotedesktop.client` (or `.server`) when you need stdout/tracebacks.
|
|
26
|
+
- `uv build` — build sdist and wheel into `dist/`
|
|
27
|
+
- `uv publish` — publish to PyPI
|
|
28
|
+
|
|
29
|
+
## Architecture
|
|
30
|
+
|
|
31
|
+
- Both apps are PySide6 GUI applications. `src/remotedesktop/client.py` (`ClientWindow`) and `src/remotedesktop/server.py` (`ServerWindow`) hold the app entry points; `main()` in each is wired to the `remotedesktop-client` / `remotedesktop-server` GUI scripts in `pyproject.toml`.
|
|
32
|
+
- The client-side remote desktop view is a widget, `ViewerWidget` in `src/remotedesktop/viewer.py`, hosted as `ClientWindow`'s central widget. Screen display and keyboard/mouse/clipboard forwarding belong in this widget, not in the window.
|
|
33
|
+
- **Autodiscovery** (`src/remotedesktop/discovery.py`) is a stdlib-only UDP probe/response protocol, deliberately not mDNS: the client broadcasts a JSON probe to `DISCOVERY_PORT` (48653) and servers reply with `{name, port}`; datagrams with the wrong magic/version/type are dropped. The server runs a `DiscoveryResponder` thread while its window is open; the client's `DiscoveryPanel` (dock in `ClientWindow`) calls the blocking `discover_servers()` on a worker thread and delivers results to the GUI via a queued signal. `DEFAULT_CONNECT_PORT` (48654) is reserved for the future desktop connection.
|
|
34
|
+
- Discovery tests run over loopback with ephemeral ports (`bind_host`/`discovery_port`/`broadcast_hosts` parameters exist for this), so they never touch the real LAN or fixed ports.
|
|
35
|
+
- **Screen sharing** (`sharing.py` + `protocol.py`) runs entirely on the Qt event loop — no threads or locks. `MessageStream` frames messages over a QTcpSocket (4-byte length + kind byte; JSON control messages or JPEG frames; malformed input aborts the socket). `ShareServer` owns one QTimer that grabs the primary screen via `QScreen.grabWindow(0)`, JPEG-encodes once, and fans out to all clients, skipping clients whose send buffer is backlogged; the timer only runs while clients are connected. `ShareClient` decodes frames to QImage for the viewer.
|
|
36
|
+
- **All persistence is one SQLite database** (`db.py`): `db.connect(path)` opens the DB and ensures every table (`settings`, `paired_clients`, `known_servers`, and the inventory tables). The file lives at `config.default_db_path()` — `platformdirs.user_data_dir("remotedesktop")` (`%LOCALAPPDATA%\remotedesktop\remotedesktop.db` on Windows). **Use `platformdirs` for any new data location; don't hardcode `%APPDATA%`.** Every store (`Settings`, `PairedClients`, `KnownServers`, `ConnectionInventory`) takes a `sqlite3.Connection`; the GUIs open one connection per window and share it. Tests pass `db.connect(tmp_path/"x.db")` (or omit for in-memory), and use **separate DB files for the server and client** since they model different machines.
|
|
37
|
+
- **Trust model plumbing** (`config.py`): the client has a stable UUID identity (`load_client_identity`, in the `settings` table) and the server maps each approved client-id to a token (`PairedClients`). `ShareServer` takes an `approve_client(client_id, name) -> bool` callback; `ServerWindow` implements it as a modal QMessageBox. Tests inject stores backed by a temp DB and explicit identities so they never touch real data or prompt.
|
|
38
|
+
- **Status/debug logging is a feature**: `ShareServer`, `ShareClient`, and `DiscoveryPanel` emit human-readable `status` signals for every connection phase, and both windows show them in a timestamped "Connection log" pane. When adding connection behavior, emit a status message for each new phase or failure path — there's a test asserting the server's phase messages.
|
|
39
|
+
- **Connection inventory** (`inventory.py`): both windows have a tab (server: "Clients on LAN"; client: "Servers on LAN") backed by a `ConnectionInventory` — one `PeerRecord` per peer with attempts count, current state, first/last seen. The server drives it from `ShareServer.peerEvent` (a structured `{key, event, name, address, detail}` signal emitted alongside the status strings at each phase — keep both in sync when adding phases). The client drives it from `ClientWindow`'s own handlers (discovered servers from `DiscoveryPanel.serversFound`, plus attempt/connected/denied/disconnected). Server peers key by client-id; client peers key by `host:port`. **Persisted in SQLite** and reloaded on startup, so it survives restarts.
|
|
40
|
+
- **Revoking access**: `InventoryTab` takes an optional `(action_label, action_callback)`; the callback gets the selected peer's key. The server's tab wires "Revoke access" → `ShareServer.revoke_client(client_id)` (removes the token via `PairedClients.revoke`, disconnects any live stream with a `denied: access revoked`, and `_drop` reports state "revoked"; a `_revoked` set distinguishes it from a plain disconnect). The client's tab wires "Forget server" → `KnownServers.forget(key)` + disconnect. After either, the next connection needs a fresh approval. Server and client each use a *separate table* (`server_peers` / `client_peers`, selected via the `table=` arg) so a machine running both apps doesn't commingle them; `peers` is the default table used by tests. `ConnectionInventory` never lets a DB error break connectivity (load/save are wrapped).
|
|
41
|
+
- Sharing tests drive real sockets on the GUI thread by pumping `qapp.processEvents()` until a condition holds (see `pump()` in `tests/test_sharing.py`). Use the `make_server`/`make_client` helpers there and the session-scoped `credentials` fixture (one generated cert reused across tests — generating per test is ~100ms each). Tests exercise the real pairing flow (auto-approve), so there is no "pre-approve" shortcut anymore.
|
|
42
|
+
- **Input forwarding**: `ViewerWidget` captures mouse/keyboard events and emits `inputEvent` dicts with coordinates normalized 0..1 over the *displayed* frame rect (letterboxing is reversed via `_display_rect()`; events outside the frame are dropped). Keys carry the client's `nativeVirtualKey()` — since both ends are Windows, the server injects that VK directly with no key-translation table. The client sends these as `{"type": "input", ...}`; `ShareServer` only injects input from streams that completed the hello/approval handshake.
|
|
43
|
+
- **Injection is isolated behind `InputInjector`** (`input_injection.py`, Windows `SendInput` via ctypes; inert stub off-Windows). ShareServer takes an `injector=` param so tests pass a recording fake — **never let tests construct a real `InputInjector`, or they will move the host's actual mouse/keyboard.** Normalized 0..1 coords map directly to SendInput's 0..65535 absolute range over the primary monitor.
|
|
44
|
+
- **Transport is TLS + a token handshake** (`tls.py`, `sharing.py`, `config.py`). The Qt SSL backend here is Windows **schannel** (no OpenSSL); it does complete server-side TLS with a `cryptography`-generated self-signed cert loaded from PEM (verified), but be wary of schannel-specific quirks if you change the config. The server persists a self-signed cert/key under the config dir (`tls.load_or_create_credentials`; `ShareServer(credentials=...)`, or ephemeral if omitted). The client connects with `connectToHostEncrypted`, ignores the expected self-signed cert errors (`PeerVerifyMode.VerifyNone`), and pins the cert fingerprint **softly** — a change is logged but does *not* block the connection (robustness over strict security, per the trusted-LAN intent). Authentication: on first connect the server user approves and the server issues a random token (`PairedClients.pair`, stored server-side by client-id; client stores it in `KnownServers` keyed by `host:port` with the fingerprint). On reconnect the client sends the token in its hello and is admitted with no prompt; a missing/invalid token just falls back to re-approval (never hard-fails). **Do not reintroduce hard cert-pinning or challenge-response** — the user explicitly wanted robust connections over maximum security.
|
|
45
|
+
- **Clipboard sync is bidirectional** (`clipboard.py` `ClipboardSync`): client copy → server and server copy → all clients, as `{"type": "clipboard", "text"/"image_png"}` messages. It's opt-in — `ShareServer`/`ShareClient` take a `clipboard=` param and do nothing with clipboard if it's None, so only the GUIs (and clipboard tests) enable it; **plain sharing/input tests must leave it off to avoid touching the real OS clipboard.** Echo/loop prevention is by *content signature*, not just a guard flag, because Windows fires `QClipboard.dataChanged` asynchronously (after a flag would be cleared); image signatures hash canonical RGBA pixels so a re-encoded PNG round trip doesn't loop. Server only accepts clipboard from streams past the approval handshake. Tests use a `FakeClipboard(QObject)` with a `changed` signal and recording `apply()` so two independent clipboards exist in one process.
|
|
46
|
+
- The package version lives only in `src/remotedesktop/__init__.py` (`__version__`); hatchling reads it from there (`[tool.hatch.version]`), so bump it in that one place.
|
|
47
|
+
- Widget tests need a `QApplication`; use the session-scoped `qapp` fixture in `tests/conftest.py`.
|
|
48
|
+
|
|
49
|
+
## Environment Notes
|
|
50
|
+
|
|
51
|
+
- Target platform is Windows; development happens on Windows 11. Requires Python >=3.14.
|
|
52
|
+
- **Run the tests from PowerShell/cmd, not Git Bash.** Git Bash puts `C:\Program Files\Git\mingw64\bin` on PATH, so Qt loads its `openssl` TLS backend against Git's MinGW-built OpenSSL DLLs and crashes with an access violation on the first TLS connection. From PowerShell those DLLs aren't on PATH and Qt uses schannel, as intended.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: remotedesktop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Remote desktop client/server for Windows computers on the same LAN, with autodiscovery. Provides screen, keyboard, mouse, and clipboard sharing without RDP or Microsoft authentication.
|
|
5
|
+
Author-email: James Abel <j@abel.co>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
10
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: System :: Networking
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Requires-Dist: cryptography>=49.0.0
|
|
16
|
+
Requires-Dist: platformdirs>=4.10.0
|
|
17
|
+
Requires-Dist: pyside6>=6.11.1
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Remote Desktop
|
|
21
|
+
|
|
22
|
+
[](https://github.com/jamesabel/remotedesktop/actions/workflows/ci.yml)
|
|
23
|
+

|
|
24
|
+
[](https://pypi.org/project/remotedesktop/)
|
|
25
|
+

|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
A Python client/server application that provides remote desktop for Windows
|
|
29
|
+
computers on the same LAN, with autodiscovery of servers.
|
|
30
|
+
|
|
31
|
+
Connections are made to the desktop screen, keyboard, mouse, and clipboard.
|
|
32
|
+
Other connections are not provided, such as shared drives, devices, or
|
|
33
|
+
multimedia (e.g., audio).
|
|
34
|
+
|
|
35
|
+
The client and server are PySide6 (Qt) GUI apps, each run on its respective
|
|
36
|
+
computer. The client requests connection to the server, and for the initial
|
|
37
|
+
connection the user on the server side must permit the connection. After
|
|
38
|
+
that, the client can connect to the server whenever the server is running,
|
|
39
|
+
without the server having to grant permission again.
|
|
40
|
+
|
|
41
|
+
This does not use Windows RDP nor rely on any Microsoft-based authentication.
|
|
42
|
+
|
|
43
|
+
## Status
|
|
44
|
+
|
|
45
|
+
The core feature set works: autodiscovery, live screen viewing,
|
|
46
|
+
mouse/keyboard control, and two-way clipboard sync. A running server appears
|
|
47
|
+
in the client's *Servers* panel; opening it (after the server user approves
|
|
48
|
+
the first connection) shows the remote screen, clicking into the view
|
|
49
|
+
forwards mouse and keyboard input, and text/images copied on either side
|
|
50
|
+
appear on the other. Input forwarding is safe against interruptions: if the
|
|
51
|
+
viewer loses focus, a drag ends outside the view, or a client disconnects
|
|
52
|
+
mid-keystroke, anything still held down is released on the server — no stuck
|
|
53
|
+
keys or mouse buttons.
|
|
54
|
+
|
|
55
|
+
The connection is encrypted with TLS, and after the server user approves a
|
|
56
|
+
client once, that client reconnects automatically using a stored token.
|
|
57
|
+
Unapproved connections are limited to small handshake messages until the
|
|
58
|
+
server user admits them. The security model is tuned for a trusted LAN: the
|
|
59
|
+
server's certificate is self-signed and trusted on first use, favoring
|
|
60
|
+
reliable reconnection over strict certificate checking — if the server's
|
|
61
|
+
certificate ever changes, the client logs a warning and updates its stored
|
|
62
|
+
fingerprint rather than refusing to connect.
|
|
63
|
+
|
|
64
|
+
Both apps have a second tab listing every peer seen on the LAN — on the
|
|
65
|
+
server, the clients that have connected or attempted to; on the client, the
|
|
66
|
+
servers it has discovered or tried to reach — with each peer's current state,
|
|
67
|
+
number of attempts, and when it was first and last seen. From that tab the
|
|
68
|
+
server can revoke a client (disconnecting it and requiring approval to
|
|
69
|
+
reconnect) and the client can forget a server. This inventory, and all other
|
|
70
|
+
state (identity, pairings, settings), is stored in a SQLite database and
|
|
71
|
+
persists across restarts. Its location is chosen by `platformdirs` (on
|
|
72
|
+
Windows, `%LOCALAPPDATA%\remotedesktop`).
|
|
73
|
+
|
|
74
|
+
## Requirements
|
|
75
|
+
|
|
76
|
+
- Windows
|
|
77
|
+
- Python 3.14+
|
|
78
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
79
|
+
|
|
80
|
+
## Running
|
|
81
|
+
|
|
82
|
+
From a clone of this repository, double-click `run_server.bat` on the
|
|
83
|
+
computer to be shared and `run_client.bat` on the viewing computer. Each
|
|
84
|
+
script prepares the environment (installing dependencies on first use) and
|
|
85
|
+
then launches the app, closing its console window once the app is running.
|
|
86
|
+
|
|
87
|
+
To run from a terminal instead:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
uv run remotedesktop-server
|
|
91
|
+
uv run remotedesktop-client
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The apps are also published on PyPI as
|
|
95
|
+
[`remotedesktop`](https://pypi.org/project/remotedesktop/).
|
|
96
|
+
|
|
97
|
+
## How discovery works
|
|
98
|
+
|
|
99
|
+
The client broadcasts a small JSON probe over UDP (port 48653); each server
|
|
100
|
+
on the LAN replies with its hostname and connection port. Windows Firewall
|
|
101
|
+
must allow Python to receive inbound UDP on that port for a server to be
|
|
102
|
+
discoverable from other machines.
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
uv sync # set up the environment
|
|
108
|
+
uv run pytest # run the tests
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Run the tests from PowerShell or cmd, not Git Bash: Git Bash puts Git's
|
|
112
|
+
MinGW OpenSSL DLLs on `PATH`, which Qt's TLS backend loads and crashes on.
|
|
113
|
+
From PowerShell, Qt uses the Windows schannel backend as intended.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="20" role="img" aria-label="coverage: 83%">
|
|
2
|
+
<linearGradient id="s" x2="0" y2="100%">
|
|
3
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
4
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
5
|
+
</linearGradient>
|
|
6
|
+
<clipPath id="r"><rect width="104" height="20" rx="3" fill="#fff"/></clipPath>
|
|
7
|
+
<g clip-path="url(#r)">
|
|
8
|
+
<rect width="61" height="20" fill="#555"/>
|
|
9
|
+
<rect x="61" width="43" height="20" fill="#a4a61d"/>
|
|
10
|
+
<rect width="104" height="20" fill="url(#s)"/>
|
|
11
|
+
</g>
|
|
12
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
|
13
|
+
<text x="31.5" y="14">coverage</text>
|
|
14
|
+
<text x="82.5" y="14">83%</text>
|
|
15
|
+
</g>
|
|
16
|
+
</svg>
|
|
@@ -15,6 +15,8 @@ classifiers = [
|
|
|
15
15
|
"Topic :: System :: Networking",
|
|
16
16
|
]
|
|
17
17
|
dependencies = [
|
|
18
|
+
"cryptography>=49.0.0",
|
|
19
|
+
"platformdirs>=4.10.0",
|
|
18
20
|
"pyside6>=6.11.1",
|
|
19
21
|
]
|
|
20
22
|
|
|
@@ -25,6 +27,7 @@ remotedesktop-server = "remotedesktop.server:main"
|
|
|
25
27
|
[dependency-groups]
|
|
26
28
|
dev = [
|
|
27
29
|
"pytest>=8",
|
|
30
|
+
"pytest-cov>=7.1.0",
|
|
28
31
|
]
|
|
29
32
|
|
|
30
33
|
[build-system]
|
|
@@ -36,3 +39,8 @@ path = "src/remotedesktop/__init__.py"
|
|
|
36
39
|
|
|
37
40
|
[tool.pytest.ini_options]
|
|
38
41
|
testpaths = ["tests"]
|
|
42
|
+
|
|
43
|
+
[tool.ty.environment]
|
|
44
|
+
# The app targets Windows (SendInput via ctypes.windll); type-check as such
|
|
45
|
+
# even when ty runs on a Linux CI runner.
|
|
46
|
+
python-platform = "win32"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Remote Desktop
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jamesabel/remotedesktop/actions/workflows/ci.yml)
|
|
4
|
+

|
|
5
|
+
[](https://pypi.org/project/remotedesktop/)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
A Python client/server application that provides remote desktop for Windows
|
|
10
|
+
computers on the same LAN, with autodiscovery of servers.
|
|
11
|
+
|
|
12
|
+
Connections are made to the desktop screen, keyboard, mouse, and clipboard.
|
|
13
|
+
Other connections are not provided, such as shared drives, devices, or
|
|
14
|
+
multimedia (e.g., audio).
|
|
15
|
+
|
|
16
|
+
The client and server are PySide6 (Qt) GUI apps, each run on its respective
|
|
17
|
+
computer. The client requests connection to the server, and for the initial
|
|
18
|
+
connection the user on the server side must permit the connection. After
|
|
19
|
+
that, the client can connect to the server whenever the server is running,
|
|
20
|
+
without the server having to grant permission again.
|
|
21
|
+
|
|
22
|
+
This does not use Windows RDP nor rely on any Microsoft-based authentication.
|
|
23
|
+
|
|
24
|
+
## Status
|
|
25
|
+
|
|
26
|
+
The core feature set works: autodiscovery, live screen viewing,
|
|
27
|
+
mouse/keyboard control, and two-way clipboard sync. A running server appears
|
|
28
|
+
in the client's *Servers* panel; opening it (after the server user approves
|
|
29
|
+
the first connection) shows the remote screen, clicking into the view
|
|
30
|
+
forwards mouse and keyboard input, and text/images copied on either side
|
|
31
|
+
appear on the other. Input forwarding is safe against interruptions: if the
|
|
32
|
+
viewer loses focus, a drag ends outside the view, or a client disconnects
|
|
33
|
+
mid-keystroke, anything still held down is released on the server — no stuck
|
|
34
|
+
keys or mouse buttons.
|
|
35
|
+
|
|
36
|
+
The connection is encrypted with TLS, and after the server user approves a
|
|
37
|
+
client once, that client reconnects automatically using a stored token.
|
|
38
|
+
Unapproved connections are limited to small handshake messages until the
|
|
39
|
+
server user admits them. The security model is tuned for a trusted LAN: the
|
|
40
|
+
server's certificate is self-signed and trusted on first use, favoring
|
|
41
|
+
reliable reconnection over strict certificate checking — if the server's
|
|
42
|
+
certificate ever changes, the client logs a warning and updates its stored
|
|
43
|
+
fingerprint rather than refusing to connect.
|
|
44
|
+
|
|
45
|
+
Both apps have a second tab listing every peer seen on the LAN — on the
|
|
46
|
+
server, the clients that have connected or attempted to; on the client, the
|
|
47
|
+
servers it has discovered or tried to reach — with each peer's current state,
|
|
48
|
+
number of attempts, and when it was first and last seen. From that tab the
|
|
49
|
+
server can revoke a client (disconnecting it and requiring approval to
|
|
50
|
+
reconnect) and the client can forget a server. This inventory, and all other
|
|
51
|
+
state (identity, pairings, settings), is stored in a SQLite database and
|
|
52
|
+
persists across restarts. Its location is chosen by `platformdirs` (on
|
|
53
|
+
Windows, `%LOCALAPPDATA%\remotedesktop`).
|
|
54
|
+
|
|
55
|
+
## Requirements
|
|
56
|
+
|
|
57
|
+
- Windows
|
|
58
|
+
- Python 3.14+
|
|
59
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
60
|
+
|
|
61
|
+
## Running
|
|
62
|
+
|
|
63
|
+
From a clone of this repository, double-click `run_server.bat` on the
|
|
64
|
+
computer to be shared and `run_client.bat` on the viewing computer. Each
|
|
65
|
+
script prepares the environment (installing dependencies on first use) and
|
|
66
|
+
then launches the app, closing its console window once the app is running.
|
|
67
|
+
|
|
68
|
+
To run from a terminal instead:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
uv run remotedesktop-server
|
|
72
|
+
uv run remotedesktop-client
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The apps are also published on PyPI as
|
|
76
|
+
[`remotedesktop`](https://pypi.org/project/remotedesktop/).
|
|
77
|
+
|
|
78
|
+
## How discovery works
|
|
79
|
+
|
|
80
|
+
The client broadcasts a small JSON probe over UDP (port 48653); each server
|
|
81
|
+
on the LAN replies with its hostname and connection port. Windows Firewall
|
|
82
|
+
must allow Python to receive inbound UDP on that port for a server to be
|
|
83
|
+
discoverable from other machines.
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
uv sync # set up the environment
|
|
89
|
+
uv run pytest # run the tests
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Run the tests from PowerShell or cmd, not Git Bash: Git Bash puts Git's
|
|
93
|
+
MinGW OpenSSL DLLs on `PATH`, which Qt's TLS backend loads and crashes on.
|
|
94
|
+
From PowerShell, Qt uses the Windows schannel backend as intended.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal
|
|
3
|
+
cd /d "%~dp0"
|
|
4
|
+
|
|
5
|
+
rem Prepare the environment (installs/updates dependencies) before launching.
|
|
6
|
+
uv sync --quiet
|
|
7
|
+
if errorlevel 1 (
|
|
8
|
+
echo Failed to prepare the environment.
|
|
9
|
+
pause
|
|
10
|
+
exit /b 1
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
rem Launch the GUI detached so this console window can close immediately.
|
|
14
|
+
start "" ".venv\Scripts\remotedesktop-client.exe" %*
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal
|
|
3
|
+
cd /d "%~dp0"
|
|
4
|
+
|
|
5
|
+
rem Prepare the environment (installs/updates dependencies) before launching.
|
|
6
|
+
uv sync --quiet
|
|
7
|
+
if errorlevel 1 (
|
|
8
|
+
echo Failed to prepare the environment.
|
|
9
|
+
pause
|
|
10
|
+
exit /b 1
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
rem Launch the GUI detached so this console window can close immediately.
|
|
14
|
+
start "" ".venv\Scripts\remotedesktop-server.exe" %*
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Generate a shields-style SVG coverage badge from the .coverage data file.
|
|
2
|
+
|
|
3
|
+
Reads the total percentage via `coverage report --format=total` (so run the
|
|
4
|
+
tests with `--cov` first) and writes the badge, by default to
|
|
5
|
+
badges/coverage.svg. CI runs this and commits the badge so the readme can
|
|
6
|
+
reference it without any external service (the repo is private).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
_COLORS = [ # same thresholds the coverage-badge package used
|
|
14
|
+
(100, "#4c1"),
|
|
15
|
+
(90, "#97CA00"),
|
|
16
|
+
(75, "#a4a61d"),
|
|
17
|
+
(60, "#dfb317"),
|
|
18
|
+
(40, "#fe7d37"),
|
|
19
|
+
(0, "#e05d44"),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
_SVG = """<svg xmlns="http://www.w3.org/2000/svg" width="104" height="20" role="img" aria-label="coverage: {pct}%">
|
|
23
|
+
<linearGradient id="s" x2="0" y2="100%">
|
|
24
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
25
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
26
|
+
</linearGradient>
|
|
27
|
+
<clipPath id="r"><rect width="104" height="20" rx="3" fill="#fff"/></clipPath>
|
|
28
|
+
<g clip-path="url(#r)">
|
|
29
|
+
<rect width="61" height="20" fill="#555"/>
|
|
30
|
+
<rect x="61" width="43" height="20" fill="{color}"/>
|
|
31
|
+
<rect width="104" height="20" fill="url(#s)"/>
|
|
32
|
+
</g>
|
|
33
|
+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
|
34
|
+
<text x="31.5" y="14">coverage</text>
|
|
35
|
+
<text x="82.5" y="14">{pct}%</text>
|
|
36
|
+
</g>
|
|
37
|
+
</svg>
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> None:
|
|
42
|
+
total = subprocess.run(
|
|
43
|
+
[sys.executable, "-m", "coverage", "report", "--format=total"],
|
|
44
|
+
check=True,
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
).stdout.strip()
|
|
48
|
+
pct = round(float(total))
|
|
49
|
+
color = next(c for threshold, c in _COLORS if pct >= threshold)
|
|
50
|
+
out = Path(sys.argv[1] if len(sys.argv) > 1 else "badges/coverage.svg")
|
|
51
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
out.write_text(_SVG.format(pct=pct, color=color), encoding="utf-8")
|
|
53
|
+
print(f"{out}: {pct}%")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
main()
|