Merge pull request #1982 from debpalash/fix/1974-dev-port-ownership

fix(dev): reclaim the port from a backend the app itself left running
This commit is contained in:
Palash Debnath
2026-09-09 22:39:58 -07:00
committed by GitHub
3 changed files with 59 additions and 0 deletions
+1
View File
@@ -9,6 +9,7 @@ the frozen-backend fallback mirror it for their toolchains.
## [Unreleased]
**Highlights**
- `bun run desktop` reclaims port 3900 from a backend the app itself left running, instead of refusing to start (#1974)
- A dictation shortcut another app already owns now says so, instead of silently doing nothing (#1858)
- Quitting on Windows is no longer reported as a crash on the next launch (#1898)
- A Reduce motion switch in Settings, for calm without changing your whole system (#1857)
+21
View File
@@ -76,6 +76,22 @@ function normalized(value, windows = process.platform === "win32") {
return path.posix.resolve(String(value || ""));
}
// The app's own reverse-DNS identity (tauri.conf.json `identifier`). A backend
// the Tauri shell spawned lives under a per-app directory named after this —
// `…/com.debpalash.omnivoice-studio/project/.venv/…` — so its path names
// VoiceStudio as unambiguously as the checkout path does, just from the other
// direction.
//
// Without this the ownership test only recognised a listener running out of
// the git checkout, so an app-managed backend left holding the port was
// treated as a stranger and the launcher refused to reclaim it — the run
// aborted with "Refusing to stop unrelated process" and no way forward
// except Task Manager (#1974).
//
// A reverse-DNS bundle id is specific enough to be safe here: nothing else
// on the machine carries it, which is the whole point of the namespace.
export const APP_BUNDLE_ID = "com.debpalash.omnivoice-studio";
export function belongsToCheckout(
cwd,
command,
@@ -91,6 +107,11 @@ export function belongsToCheckout(
return path === root || path.startsWith(prefix);
};
if (ownedPath(cwd) || ownedPath(executable)) return true;
// App-managed backend: the bundle id appears in the executable path or in
// the command line, whichever the platform gave us.
for (const value of [cwd, executable, command]) {
if (String(value || "").toLowerCase().includes(APP_BUNDLE_ID)) return true;
}
const haystack = windows
? String(command || "")
.replaceAll("/", "\\")
+37
View File
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
APP_BUNDLE_ID,
belongsToCheckout,
clearDevPortsWith,
isUninspectableProcessError,
@@ -199,3 +200,39 @@ test("windows stop fails when Terminate reports a non-zero ReturnValue", () => {
/Could not stop process 4242: Terminate returned 2/,
);
});
// #1974: a backend the Tauri shell spawned lives under a per-app directory
// named after the bundle id, not under the git checkout. The ownership test
// only knew about the checkout, so the launcher treated its own orphaned
// backend as a stranger, refused to reclaim port 3900, and aborted the run
// with no way forward but Task Manager.
test("an app-managed backend is recognised as ours", () => {
const root = "/work/VoiceStudio";
const macApp = `/Users/x/Library/Application Support/${APP_BUNDLE_ID}/project/.venv/bin/python`;
assert.equal(belongsToCheckout("", "", macApp, false, root), true);
assert.equal(belongsToCheckout("", `${macApp} -m uvicorn main:app`, "", false, root), true);
// Built by join so the Windows separators need no escaping in source.
const sep = String.fromCharCode(92);
const winApp = ["C:", "Users", "x", "AppData", "Roaming", APP_BUNDLE_ID, "project", ".venv", "Scripts", "python.exe"].join(sep);
assert.equal(belongsToCheckout("", "", winApp, true, ["C:", "repo"].join(sep)), true);
});
test("the bundle id match is case-insensitive", () => {
// Windows paths come back with inconsistent casing depending on the API.
const root = "/work/VoiceStudio";
const shouty = `/Users/x/Library/Application Support/${APP_BUNDLE_ID.toUpperCase()}/project/.venv/bin/python`;
assert.equal(belongsToCheckout("", "", shouty, false, root), true);
});
test("a foreign process is still refused", () => {
// The guard exists to avoid killing someone else's service on the port;
// widening ownership must not widen it to everything.
const root = "/work/VoiceStudio";
assert.equal(belongsToCheckout("", "python -m http.server 3900", "", false, root), false);
assert.equal(belongsToCheckout("", "", "/usr/bin/python3", false, root), false);
assert.equal(
belongsToCheckout("", "node /opt/com.someoneelse.app/server.js", "", false, root),
false,
);
});