Skip to content

feat: Rust tray runtime and universal macOS app for 2.0 - #16

Merged
mimaxon1 merged 1 commit into
mainfrom
feat/rust-2.0
Oct 15, 2026
Merged

mimaxon1 merged 1 commit into
mainfrom
feat/rust-2.0

Conversation

@mimaxon1

Copy link
Copy Markdown
Owner

PC Remote 2.0 replaces the Python desktop process with a Rust tray/menu-bar app that stays idle without periodic system polling, while keeping the existing phone browser controller and migrating legacy settings safely.

  • Add Windows tray and macOS menu-bar runtime with shared API/web workers and platform controls.
  • Build a universal macOS app for Apple Silicon and Intel, plus DMG/ZIP release assets; run Rust tests on Windows and both macOS architectures.
  • Preserve the existing PIN format and QR-token sign-in contract; apply one serialized per-peer and process-wide PIN limit across login and protected actions.
  • Refresh English/Russian README, changelog, testing, and security guidance.

Validation completed locally: 12 Rust tests pass; Windows release build and Clippy pass; ARM64 and Intel macOS targets pass cross-target check and Clippy; live Windows API smoke verified pairing, PIN blocking, and session-token behavior. Real macOS app packaging and runtime checks are assigned to the macOS CI runners.

The macOS app is ad-hoc signed and is not notarized.

Closes #13

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T08:18:14.933305Z acb842b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@mimaxon1
mimaxon1 merged commit 2db4e93 into main Oct 15, 2026
8 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: acb842b187

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/api.rs
Comment on lines +472 to +474
if let Ok(executable) = std::env::current_exe() {
if std::process::Command::new(executable).spawn().is_ok() {
std::process::exit(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Relaunch only after the existing process releases its ports

When /app/restart is used, the replacement process is spawned while the current process still owns ports 8000 and 8080; only after spawn() succeeds does the parent exit. The child can therefore reach start_servers first, fail both fixed-port binds, and terminate, leaving the application stopped—especially harmful for clean_start, which has already cleared the PIN. Use an external watcher that waits for the old PID to exit, or otherwise relinquish the listeners before launching the replacement.

Useful? React with 👍 / 👎.

Comment thread src/platform.rs
Comment on lines +132 to +134
pub fn toggle_mute(state: &AppState) -> Result<bool, String> {
let next = !state.muted.load(std::sync::atomic::Ordering::Relaxed);
set_mute(state, next)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive mute toggles from the actual system state

When the machine starts with audio already muted, or mute is changed outside PC Remote, this cached flag still begins at false and is never synchronized from the OS. On macOS the first attempt to unmute consequently sets mute to true again; on Windows the synthetic key may unmute the device while the API reports muted: true. Query the current endpoint mute state before toggling and before returning status.

Useful? React with 👍 / 👎.

Comment thread src/platform.rs
Comment on lines +430 to +433
let actual = process.name().to_string_lossy();
let expected = format!("{}.exe", search.name);
if actual.eq_ignore_ascii_case(&expected)
|| actual.eq_ignore_ascii_case(&search.name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match Windows windows by executable identity, not display name

For Windows entries discovered from .lnk files, search.name is the shortcut's display name rather than the process executable name—for example, Google Chrome.lnk produces Google Chrome, while the process is chrome.exe. The minimize and close buttons therefore report that no window was found for many normal applications. Resolve the shortcut/executable or retain a process identity when building the app item and match against that instead.

Useful? React with 👍 / 👎.

Comment thread src/platform.rs
Comment on lines +15 to +18
pub fn local_ip() -> Ipv4Addr {
UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
.and_then(|socket| {
socket.connect((Ipv4Addr::new(8, 8, 8, 8), 80))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor the saved LAN interface when generating pairing URLs

On hosts with a VPN or multiple adapters, probing only the route to 8.8.8.8 can select an address that the phone cannot reach, even though migrated settings retain the user's network.preferred_ip and preferred_interface. That selected address is embedded in the QR URL and is also the only non-loopback address accepted by allowed_origin, so connecting through the intended LAN address is rejected. Restore preferred-interface handling and include all usable local addresses in the origin allowlist.

Useful? React with 👍 / 👎.

Comment thread src/state.rs
Comment on lines +509 to +513
let rows = entries
.iter()
.filter(|row| reset || since.is_none_or(|mark| row.id > mark))
.take(limit.min(1000))
.cloned()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Advance the log cursor only through returned entries

When more than limit entries are buffered, this takes the oldest limit matching rows but returns the ID of the newest buffered entry as next_since. The web client requests 120 rows and adopts that cursor, so an initial fetch with 500 buffered entries displays the oldest 120 and permanently skips the remaining 380. Return the newest selected row's ID and select the latest limit rows when resetting or initially loading.

Useful? React with 👍 / 👎.

Comment thread src/platform.rs
Comment on lines +29 to +32
let name = "System default audio output";
#[cfg(target_os = "windows")]
let name = "System default audio output";
json!({ "id": "default", "name": name })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report the actual default audio-output device

Both platform implementations return the same hard-coded default ID and display name without querying the operating system. Therefore /info and /audio/devices continue to show “System default audio output” regardless of whether the active device is speakers, headphones, HDMI, or another endpoint, so the advertised current-output status cannot reflect device changes. Query and return the active endpoint's real identifier and name.

Useful? React with 👍 / 👎.

Comment thread src/platform.rs
Comment on lines +157 to +161
let script = match action {
"play_pause" => "tell application \"Music\" to playpause",
"next" => "tell application \"Music\" to next track",
"prev" => "tell application \"Music\" to previous track",
"stop" => "tell application \"Music\" to stop",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Target the active macOS media session

On macOS every media action is sent specifically to the Music application. When playback is coming from Spotify, a browser, VLC, a podcast app, or another common player, the controls affect Music instead of the currently playing media and may launch or manipulate Music unexpectedly. Emit system media-key events or use the active Now Playing session rather than hard-coding one application.

Useful? React with 👍 / 👎.

Comment thread src/platform.rs
Comment on lines +236 to +239
for directory in [
PathBuf::from("/Applications"),
PathBuf::from("/System/Applications"),
] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scan the per-user macOS Applications directory

Applications installed only for the current user live under ~/Applications, but discovery checks only the two system-wide directories. Such apps never appear in the recent-app response and therefore cannot be launched or pinned from the controller. Include the user's Applications directory when collecting macOS candidates.

Useful? React with 👍 / 👎.

Comment thread src/platform.rs
Comment on lines +555 to +558
pub fn autostart_path() -> Option<PathBuf> {
env::var_os("APPDATA")
.map(PathBuf::from)
.map(|path| path.join("Microsoft/Windows/Start Menu/Programs/Startup/PC Remote.cmd"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Migrate the legacy Windows startup entry

Upgrading a user who still has the supported legacy PC-Android.cmd startup file leaves that entry active but invisible because the Rust runtime checks only PC Remote.cmd. The tray consequently reports autostart as disabled even though Windows still launches the app, and enabling it creates a second startup entry. Preserve the previous migration behavior by recognizing and replacing or removing the owned legacy entry.

Useful? React with 👍 / 👎.

Comment thread src/main.rs
Comment on lines +78 to +81
let state = Arc::new(state::AppState::new());
let api_port = config::port("PC_REMOTE_API_PORT", config::API_PORT_DEFAULT);
let web_port = config::port("PC_REMOTE_WEB_PORT", config::WEB_PORT_DEFAULT);
if let Err(error) = api::start_servers(Arc::clone(&state), api_port, web_port) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore a Windows single-instance guard

There is no process-level guard before the new runtime binds its fixed ports. When a user launches the installed shortcut while PC Remote is already running in the tray, the second process reports a fatal port-binding error instead of quietly exiting or activating the existing instance; duplicate startup entries can trigger the same modal error at every login. Acquire a named mutex before constructing the runtime, as the replaced Windows implementation did.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[security] Не позволять обходить rate limit перебором PIN через обычные endpoints

1 participant