allow to rename the profile without a browser restart and remove old sockets and registry entry when browser closes
This commit is contained in:
@@ -122,7 +122,7 @@ browser-cli/
|
||||
|
||||
All commands are run with `uv run browser-cli [--browser ALIAS] <command>`.
|
||||
|
||||
If exactly one browser instance is connected, commands auto-target it. Use `--browser ALIAS` when multiple browser instances are connected. You can inspect the active instances with `browser-cli clients` and assign a persistent profile alias from inside the target browser with `browser-cli rename-profile --browser <current-alias> <new-alias>`.
|
||||
If exactly one browser instance is connected, commands auto-target it. Use `--browser ALIAS` when multiple browser instances are connected. You can inspect the active instances with `browser-cli clients` and assign a persistent profile alias from inside the target browser with `browser-cli rename-profile --browser <current-alias> <new-alias>`. Closed browsers are removed from the client registry automatically.
|
||||
|
||||
Important: profile aliases are browser-instance aliases, not window aliases. Window aliases created with `windows rename` are only for targeting windows in commands like `nav open --window work`. If a browser instance has no explicit profile alias set, the native host gives it a generated UUID alias so multiple unaliased browsers stay distinct.
|
||||
|
||||
@@ -385,6 +385,6 @@ bash examples/demo.sh
|
||||
|
||||
- **Chrome internal pages** (`chrome://`, `brave://`, `about:`) cannot be scripted. DOM and extract commands only work on regular `http://` and `https://` pages.
|
||||
- **Profile switching** via `windows open --profile` depends on browser support and does not replace launching a separate browser profile externally with `--profile-directory`.
|
||||
- **Multiple browser instances can be auto-distinguished, but generated aliases are temporary**. Unaliased browsers get UUID aliases from the native host, which avoids collisions but is less ergonomic than setting a stable alias with `browser-cli rename-profile --browser <current-alias> <new-alias>` and restarting that browser.
|
||||
- **Multiple browser instances can be auto-distinguished, but generated aliases are temporary**. Unaliased browsers get UUID aliases from the native host, which avoids collisions but is less ergonomic than setting a stable alias with `browser-cli rename-profile --browser <current-alias> <new-alias>`.
|
||||
- **Supported install targets are explicit, not “all Chromium browsers”**. The installer currently supports Chrome, Chromium, Brave, Edge, and Vivaldi. Other Chromium-based browsers may use different or shared native messaging manifest locations, so they need browser-specific verification before being added safely.
|
||||
- **Linux and macOS only** — Windows native messaging paths are not yet handled.
|
||||
|
||||
@@ -191,7 +191,6 @@ def cmd_rename_profile(target_browser, alias):
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
sys.exit(1)
|
||||
console.print(f"[green]Profile renamed to '{alias}'[/green]")
|
||||
console.print(" Restart the browser for the change to take effect.")
|
||||
|
||||
|
||||
# ── install ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -98,6 +98,9 @@ def stdin_reader(alias: str):
|
||||
# Profile alias handshake
|
||||
if msg.get("type") == "hello":
|
||||
continue # already handled during startup
|
||||
if msg.get("type") == "bye":
|
||||
_cleanup(alias)
|
||||
os._exit(0)
|
||||
|
||||
msg_id = msg.get("id")
|
||||
if msg_id:
|
||||
|
||||
+53
-7
@@ -8,27 +8,53 @@
|
||||
|
||||
const NATIVE_HOST = "com.browsercli.host";
|
||||
let port = null;
|
||||
let keepaliveEnabled = true;
|
||||
|
||||
// ── Connection management ─────────────────────────────────────────────────────
|
||||
|
||||
function sendControlMessage(targetPort, message) {
|
||||
if (!targetPort) return;
|
||||
try {
|
||||
targetPort.postMessage(message);
|
||||
} catch (e) {
|
||||
console.warn("[browser-cli] Failed to send control message:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectPort({ sendBye = false } = {}) {
|
||||
const currentPort = port;
|
||||
if (!currentPort) return;
|
||||
|
||||
if (sendBye) sendControlMessage(currentPort, { type: "bye" });
|
||||
|
||||
if (port === currentPort) port = null;
|
||||
|
||||
try {
|
||||
currentPort.disconnect();
|
||||
} catch (e) {
|
||||
console.warn("[browser-cli] Failed to disconnect native port:", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function getProfileAlias() {
|
||||
const { profileAlias } = await chrome.storage.local.get("profileAlias");
|
||||
return profileAlias || "default";
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
if (port) return;
|
||||
if (port || !keepaliveEnabled) return;
|
||||
try {
|
||||
port = chrome.runtime.connectNative(NATIVE_HOST);
|
||||
port.onMessage.addListener(onMessage);
|
||||
port.onDisconnect.addListener(() => {
|
||||
port = null;
|
||||
const nativePort = chrome.runtime.connectNative(NATIVE_HOST);
|
||||
port = nativePort;
|
||||
nativePort.onMessage.addListener(onMessage);
|
||||
nativePort.onDisconnect.addListener(() => {
|
||||
if (port === nativePort) port = null;
|
||||
const err = chrome.runtime.lastError;
|
||||
if (err) console.warn("[browser-cli] Native host disconnected:", err.message);
|
||||
});
|
||||
// Send hello so native host knows which profile/alias this is
|
||||
const alias = await getProfileAlias();
|
||||
port.postMessage({ type: "hello", alias });
|
||||
nativePort.postMessage({ type: "hello", alias });
|
||||
console.log("[browser-cli] Connected to native host as profile:", alias);
|
||||
} catch (e) {
|
||||
port = null;
|
||||
@@ -38,12 +64,26 @@ async function connect() {
|
||||
|
||||
chrome.runtime.onInstalled.addListener(connect);
|
||||
chrome.runtime.onStartup.addListener(connect);
|
||||
chrome.runtime.onSuspend.addListener(() => {
|
||||
disconnectPort({ sendBye: true });
|
||||
});
|
||||
chrome.windows.onCreated.addListener(() => {
|
||||
keepaliveEnabled = true;
|
||||
if (!port) connect();
|
||||
});
|
||||
chrome.windows.onRemoved.addListener(async () => {
|
||||
const windows = await chrome.windows.getAll({});
|
||||
if (windows.length > 0) return;
|
||||
|
||||
keepaliveEnabled = false;
|
||||
disconnectPort({ sendBye: true });
|
||||
});
|
||||
|
||||
// Keepalive alarm — prevents service worker suspension and reconnects if needed
|
||||
chrome.alarms.create("keepalive", { periodInMinutes: 0.4 });
|
||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||
if (alarm.name === "keepalive") {
|
||||
if (!port) connect();
|
||||
if (!port && keepaliveEnabled) connect();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,6 +109,12 @@ async function onMessage(msg) {
|
||||
console.log("[browser-cli] →", command, data);
|
||||
port.postMessage({ id, success: true, data });
|
||||
}
|
||||
|
||||
if (command === "clients.rename_profile" && error === undefined) {
|
||||
disconnectPort({ sendBye: true });
|
||||
keepaliveEnabled = true;
|
||||
await connect();
|
||||
}
|
||||
}
|
||||
|
||||
async function dispatch(command, args) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "browser-cli",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"description": "Control your browser from the terminal via browser-cli",
|
||||
"permissions": [
|
||||
"tabs",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "browser-cli"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
description = "Control your real running browser from the terminal via a Chrome extension"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
|
||||
@@ -41,6 +41,7 @@ def test_rename_profile_uses_global_browser_target_when_set():
|
||||
|
||||
assert result.exit_code == 0
|
||||
send_command.assert_called_once_with("clients.rename_profile", {"alias": "work"}, profile=None)
|
||||
assert "Restart the browser" not in result.output
|
||||
|
||||
def test_install_help_lists_supported_browsers():
|
||||
result = CliRunner().invoke(main, ["install", "--help"])
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
import browser_cli.native_host as native_host
|
||||
|
||||
|
||||
def _raise_system_exit(code: int):
|
||||
raise SystemExit(code)
|
||||
|
||||
|
||||
def test_cleanup_removes_socket_and_registry_entry(monkeypatch, tmp_path):
|
||||
alias = "work"
|
||||
socket_path = tmp_path / "work.sock"
|
||||
socket_path.write_text("")
|
||||
registry_path = tmp_path / "registry.json"
|
||||
registry_path.write_text(json.dumps({alias: str(socket_path), "other": str(tmp_path / "other.sock")}))
|
||||
|
||||
monkeypatch.setattr(native_host, "SOCKET_DIR", tmp_path)
|
||||
monkeypatch.setattr(native_host, "REGISTRY_PATH", registry_path)
|
||||
|
||||
native_host._cleanup(alias)
|
||||
|
||||
assert not socket_path.exists()
|
||||
assert json.loads(registry_path.read_text()) == {"other": str(tmp_path / "other.sock")}
|
||||
|
||||
|
||||
def test_stdin_reader_cleans_up_on_eof(monkeypatch):
|
||||
cleaned = []
|
||||
|
||||
monkeypatch.setattr(native_host, "read_native_message", lambda stream: None)
|
||||
monkeypatch.setattr(native_host, "_cleanup", cleaned.append)
|
||||
monkeypatch.setattr(native_host.os, "_exit", _raise_system_exit)
|
||||
monkeypatch.setattr(native_host.sys, "stdin", SimpleNamespace(buffer=object()))
|
||||
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
native_host.stdin_reader("work")
|
||||
|
||||
assert cleaned == ["work"]
|
||||
|
||||
|
||||
def test_stdin_reader_cleans_up_on_bye(monkeypatch):
|
||||
cleaned = []
|
||||
messages = iter([{"type": "bye"}])
|
||||
|
||||
monkeypatch.setattr(native_host, "read_native_message", lambda stream: next(messages))
|
||||
monkeypatch.setattr(native_host, "_cleanup", cleaned.append)
|
||||
monkeypatch.setattr(native_host.os, "_exit", _raise_system_exit)
|
||||
monkeypatch.setattr(native_host.sys, "stdin", SimpleNamespace(buffer=object()))
|
||||
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
native_host.stdin_reader("work")
|
||||
|
||||
assert cleaned == ["work"]
|
||||
|
||||
|
||||
def test_stdin_reader_routes_response_messages(monkeypatch):
|
||||
response_queue = native_host.queue.Queue()
|
||||
native_host.PENDING["msg-1"] = response_queue
|
||||
messages = iter([{"type": "hello"}, {"id": "msg-1", "success": True}, None])
|
||||
|
||||
monkeypatch.setattr(native_host, "read_native_message", lambda stream: next(messages))
|
||||
monkeypatch.setattr(native_host, "_cleanup", lambda alias: None)
|
||||
monkeypatch.setattr(native_host.os, "_exit", _raise_system_exit)
|
||||
monkeypatch.setattr(native_host.sys, "stdin", SimpleNamespace(buffer=object()))
|
||||
|
||||
with pytest.raises(SystemExit, match="0"):
|
||||
native_host.stdin_reader("work")
|
||||
|
||||
assert response_queue.get_nowait() == {"id": "msg-1", "success": True}
|
||||
native_host.PENDING.clear()
|
||||
Reference in New Issue
Block a user