Skip to content

Commit 562b541

Browse files
committed
Address SonarCloud + Codacy findings on PR #181
Round-up of every issue both scanners flagged on this branch: Library code: - Drop unused imports (NONCE_BYTES in host.py, dataclasses.field in file_transfer.py). - Replace the 17-parameter RemoteDesktopHost.__init__ with an AudioCaptureConfig dataclass (S107). GUI and tests now pass audio_config=AudioCaptureConfig(enabled=True, ...) instead of five separate kwargs, taking the parameter list down to 13. - Define module-level constants for repeated literals (S1192): _NOT_CONNECTED_MESSAGE in viewer.py, _OPEN_CLIPBOARD_FAILED in clipboard.py, _INVALID_TRANSFER_ID_MESSAGE in file_transfer.py. - Refactor RemoteDesktopViewer._recv_loop into a per-message dispatch table (S3776) — cognitive complexity 47 -> well under 15. - Float equality on host.py:638 sleep_for == 0.0 -> <= 0.0 (S1244). - Drop redundant exception classes from except tuples whenever a superclass is already listed (S5713). ConnectionError, ssl.SSLError and TimeoutError all derive from OSError. - ws_protocol.py: opposite-operator (S1940), reword 'commented-out' comment (S125), pass usedforsecurity=False on the SHA-1 used by the RFC 6455 handshake (Bandit B324 / Semgrep insecure-hash). - audio.py: replace the bare 'pass' in PortAudio's callback isolation with an explicit return + nosec B110 annotation. - All ssl.SSLContext(...) calls now set minimum_version = TLSv1_2 (S4423). User-opt-in insecure flows for self-signed certs are marked NOSONAR S5527/S4830 with a brief reason instead of changing behaviour. GUI: - Drop unused imports (os, QClipboard, QApplication, send_file). - Extract a _scroll_amount(angle_delta) helper to flatten the nested ternary on _FrameDisplay.wheelEvent (S3358). Tests: - Optional[_FakeStream] type hints (S5890); NOSONAR S100 on the two PascalCase mock methods that mirror the sounddevice API. - Replace bare 'pass' on the failure-stub stop() with an explanatory return (S1186). - NOSONAR S5655 on intentional bad-type tests for encode_text and dispatch_input. - Rename the unused 'tid' tuple element to '_tid' (S1481). - flow_control test: assert len + value before isinstance check so Sonar's flow analysis can prove seen[0] is safe (S6466). Behaviour is unchanged; tests still 295 pass on Windows.
1 parent a1df760 commit 562b541

13 files changed

Lines changed: 191 additions & 118 deletions

je_auto_control/gui/remote_desktop_tab.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,18 @@
1313
original remote-screen pixel space using the latest received frame's
1414
size.
1515
"""
16-
import os
1716
import secrets
1817
import ssl
1918
from pathlib import Path
2019
from typing import Optional
2120

2221
from PySide6.QtCore import QPoint, QRect, Qt, QThread, QTimer, Signal
2322
from PySide6.QtGui import (
24-
QClipboard, QDragEnterEvent, QDropEvent, QGuiApplication, QImage,
23+
QDragEnterEvent, QDropEvent, QGuiApplication, QImage,
2524
QKeyEvent, QMouseEvent, QPainter, QWheelEvent,
2625
)
2726
from PySide6.QtWidgets import (
28-
QApplication, QCheckBox, QComboBox, QFileDialog, QGroupBox, QHBoxLayout,
27+
QCheckBox, QComboBox, QFileDialog, QGroupBox, QHBoxLayout,
2928
QInputDialog, QLabel, QLineEdit, QMessageBox, QProgressBar, QPushButton,
3029
QSizePolicy, QSpinBox, QTabWidget, QVBoxLayout, QWidget,
3130
)
@@ -39,9 +38,9 @@
3938
WebSocketDesktopHost, WebSocketDesktopViewer,
4039
)
4140
from je_auto_control.utils.remote_desktop.audio import (
42-
AudioBackendError, AudioPlayer, is_audio_backend_available,
41+
AudioBackendError, AudioCaptureConfig, AudioPlayer,
42+
is_audio_backend_available,
4343
)
44-
from je_auto_control.utils.remote_desktop.file_transfer import send_file
4544
from je_auto_control.utils.remote_desktop.host_id import (
4645
HostIdError, format_host_id, parse_host_id,
4746
)
@@ -102,6 +101,15 @@ def _key_event_to_ac(event: QKeyEvent) -> Optional[str]:
102101
return None
103102

104103

104+
def _scroll_amount(angle_delta: int) -> int:
105+
"""Return ``+1`` / ``-1`` / ``0`` for a Qt wheel ``angleDelta`` value."""
106+
if angle_delta > 0:
107+
return 1
108+
if angle_delta < 0:
109+
return -1
110+
return 0
111+
112+
105113
class _FrameDisplay(QWidget):
106114
"""Paints the latest frame and emits remapped input events.
107115
@@ -209,8 +217,7 @@ def wheelEvent(self, event: QWheelEvent) -> None: # noqa: N802
209217
coords = self._to_remote(event.position().toPoint())
210218
if coords is None:
211219
return
212-
delta = event.angleDelta().y()
213-
amount = 1 if delta > 0 else -1 if delta < 0 else 0
220+
amount = _scroll_amount(event.angleDelta().y())
214221
if amount:
215222
self.mouse_scrolled.emit(coords[0], coords[1], amount)
216223

@@ -433,6 +440,7 @@ def _build_ssl_context(self) -> Optional[ssl.SSLContext]:
433440
if not cert_path or not key_path:
434441
raise ValueError(_t("rd_tls_both_required"))
435442
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
443+
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
436444
ctx.load_cert_chain(certfile=cert_path, keyfile=key_path)
437445
return ctx
438446

@@ -443,7 +451,7 @@ def _start(self) -> None:
443451
token = self._token.text().strip()
444452
try:
445453
ssl_context = self._build_ssl_context()
446-
except (OSError, ssl.SSLError, ValueError) as error:
454+
except (OSError, ValueError) as error:
447455
QMessageBox.warning(self, _t("rd_host_start"), str(error))
448456
return
449457
host_cls = (WebSocketDesktopHost
@@ -459,8 +467,10 @@ def _start(self) -> None:
459467
fps=float(self._fps.value()),
460468
quality=self._quality.value(),
461469
ssl_context=ssl_context,
462-
enable_audio=self._enable_audio.isChecked()
463-
and self._enable_audio.isEnabled(),
470+
audio_config=AudioCaptureConfig(
471+
enabled=self._enable_audio.isChecked()
472+
and self._enable_audio.isEnabled(),
473+
),
464474
)
465475
host.start()
466476
except (OSError, ValueError, RuntimeError, AudioBackendError) as error:
@@ -686,7 +696,7 @@ def _connect(self) -> None:
686696
except AuthenticationError as error:
687697
QMessageBox.warning(self, _t("rd_viewer_connect"), str(error))
688698
return
689-
except (OSError, ConnectionError, RuntimeError, ssl.SSLError) as error:
699+
except (OSError, RuntimeError) as error:
690700
QMessageBox.warning(self, _t("rd_viewer_connect"), str(error))
691701
return
692702
registry._viewer = viewer # noqa: SLF001 centralised lifecycle ownership
@@ -705,7 +715,9 @@ def _build_client_ssl_context(
705715
if transport not in ("TLS", "WSS"):
706716
return None
707717
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
718+
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
708719
if self._tls_insecure.isChecked():
720+
# NOSONAR S5527 S4830 # reason: explicit user opt-in for self-signed
709721
ctx.check_hostname = False
710722
ctx.verify_mode = ssl.CERT_NONE
711723
else:
@@ -827,7 +839,7 @@ def _send(self, action: dict) -> None:
827839
return
828840
try:
829841
viewer.send_input(action)
830-
except (OSError, ConnectionError) as error:
842+
except OSError as error:
831843
self._error_signal.emit(str(error))
832844

833845
def _send_mouse_move(self, x: int, y: int) -> None:
@@ -859,7 +871,7 @@ def _push_clipboard_to_host(self) -> None:
859871
return
860872
try:
861873
viewer.send_clipboard_text(text)
862-
except (OSError, ConnectionError) as error:
874+
except OSError as error:
863875
QMessageBox.warning(self, _t("rd_viewer_push_clipboard"),
864876
str(error))
865877
return
@@ -925,7 +937,7 @@ def relay(transfer_id, done, total):
925937
result = self._viewer.send_file(
926938
self._source, self._dest, on_progress=relay,
927939
)
928-
except (OSError, ConnectionError, RuntimeError) as error:
940+
except (OSError, RuntimeError) as error:
929941
self.completed.emit("", False, str(error), self._dest)
930942
return
931943
self.completed.emit(

je_auto_control/utils/clipboard/clipboard.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from io import BytesIO
1717
from typing import Optional
1818

19+
_OPEN_CLIPBOARD_FAILED = "OpenClipboard failed"
20+
1921

2022
def get_clipboard() -> str:
2123
"""Return the current clipboard text (empty string if empty)."""
@@ -83,7 +85,7 @@ def _win_get() -> str:
8385
kernel32.GlobalUnlock.argtypes = [wintypes.HGLOBAL]
8486

8587
if not user32.OpenClipboard(None):
86-
raise RuntimeError("OpenClipboard failed")
88+
raise RuntimeError(_OPEN_CLIPBOARD_FAILED)
8789
try:
8890
handle = user32.GetClipboardData(cf_unicodetext)
8991
if not handle:
@@ -131,7 +133,7 @@ def _win_set(text: str) -> None:
131133
ctypes.memmove(pointer, ctypes.addressof(data), size) # NOSONAR S5655 false positive — Array is accepted by addressof
132134
kernel32.GlobalUnlock(handle)
133135
if not user32.OpenClipboard(None):
134-
raise RuntimeError("OpenClipboard failed")
136+
raise RuntimeError(_OPEN_CLIPBOARD_FAILED)
135137
try:
136138
user32.EmptyClipboard()
137139
if not user32.SetClipboardData(cf_unicodetext, handle):
@@ -256,7 +258,7 @@ def _win_set_image(png_bytes: bytes) -> None:
256258
ctypes.memmove(pointer, dib, len(dib))
257259
kernel32.GlobalUnlock(handle)
258260
if not user32.OpenClipboard(None):
259-
raise RuntimeError("OpenClipboard failed")
261+
raise RuntimeError(_OPEN_CLIPBOARD_FAILED)
260262
try:
261263
user32.EmptyClipboard()
262264
if not user32.SetClipboardData(cf_dib, handle):

je_auto_control/utils/remote_desktop/audio.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
without noticeably starving the video pipe.
1212
"""
1313
import threading
14+
from dataclasses import dataclass
1415
from typing import Callable, Optional
1516

1617
DEFAULT_SAMPLE_RATE = 16_000
@@ -47,6 +48,17 @@ def is_audio_backend_available() -> bool:
4748
return True
4849

4950

51+
@dataclass(frozen=True)
52+
class AudioCaptureConfig:
53+
"""Bundled tuning knobs for :class:`RemoteDesktopHost` audio capture."""
54+
55+
enabled: bool = False
56+
device: Optional[int] = None
57+
sample_rate: int = DEFAULT_SAMPLE_RATE
58+
channels: int = DEFAULT_CHANNELS
59+
block_frames: int = DEFAULT_BLOCK_FRAMES
60+
61+
5062
class AudioCapture:
5163
"""Capture mono int16 PCM blocks and hand them to ``on_block`` as bytes.
5264
@@ -125,9 +137,9 @@ def _raw_callback(self, indata, frames, time_info, status) -> None:
125137
return
126138
try:
127139
self._on_block(bytes(indata))
128-
except Exception: # noqa: BLE001 callback isolation
140+
except Exception: # noqa: BLE001 callback isolation # nosec B110 # reason: PortAudio callback must never raise
129141
# We must not propagate user callback errors back into PortAudio.
130-
pass
142+
return
131143

132144

133145
class AudioPlayer:

je_auto_control/utils/remote_desktop/file_transfer.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import os
2121
import threading
2222
import uuid
23-
from dataclasses import dataclass, field
23+
from dataclasses import dataclass
2424
from pathlib import Path
2525
from typing import Any, Callable, Dict, Optional, Tuple
2626

@@ -30,6 +30,10 @@
3030
DEFAULT_CHUNK_SIZE = 256 * 1024
3131
TRANSFER_ID_LEN = 36 # str(uuid.uuid4()) length
3232

33+
_INVALID_TRANSFER_ID_MESSAGE = (
34+
f"transfer_id must be a {TRANSFER_ID_LEN}-char UUID string"
35+
)
36+
3337
ProgressCallback = Callable[[str, int, int], None]
3438
CompleteCallback = Callable[[str, bool, Optional[str], str], None]
3539

@@ -45,7 +49,7 @@ def new_transfer_id() -> str:
4549

4650
def encode_begin(transfer_id: str, dest_path: str, size: int) -> bytes:
4751
if len(transfer_id) != TRANSFER_ID_LEN:
48-
raise FileTransferError("transfer_id must be a 36-char UUID string")
52+
raise FileTransferError(_INVALID_TRANSFER_ID_MESSAGE)
4953
return json.dumps({
5054
"transfer_id": transfer_id,
5155
"dest_path": str(dest_path),
@@ -70,7 +74,7 @@ def decode_begin(payload: bytes) -> Tuple[str, str, int]:
7074

7175
def encode_chunk(transfer_id: str, chunk: bytes) -> bytes:
7276
if len(transfer_id) != TRANSFER_ID_LEN:
73-
raise FileTransferError("transfer_id must be a 36-char UUID string")
77+
raise FileTransferError(_INVALID_TRANSFER_ID_MESSAGE)
7478
return transfer_id.encode("ascii") + bytes(chunk)
7579

7680

@@ -84,7 +88,7 @@ def decode_chunk(payload: bytes) -> Tuple[str, bytes]:
8488
def encode_end(transfer_id: str, status: str = "ok",
8589
error: Optional[str] = None) -> bytes:
8690
if len(transfer_id) != TRANSFER_ID_LEN:
87-
raise FileTransferError("transfer_id must be a 36-char UUID string")
91+
raise FileTransferError(_INVALID_TRANSFER_ID_MESSAGE)
8892
body: Dict[str, Any] = {"transfer_id": transfer_id, "status": status}
8993
if error is not None:
9094
body["error"] = str(error)
@@ -257,7 +261,7 @@ def send_file(channel, source_path: str, dest_path: str,
257261
bytes_sent += len(chunk)
258262
if on_progress is not None:
259263
on_progress(transfer_id, bytes_sent, total_size)
260-
except (OSError, ConnectionError) as error:
264+
except OSError as error:
261265
channel.send_typed(
262266
MessageType.FILE_END,
263267
encode_end(transfer_id, status="error", error=str(error)),

0 commit comments

Comments
 (0)