Document who can call the loopback API and let the user pick the listen port.
This commit is contained in:
@@ -12,7 +12,7 @@ Do not edit `places.sqlite` while Firefox is open if this API is reachable.
|
|||||||
2. Native host installed only if scripts should call in: `powershell -NoProfile -File tools/install-native-host.ps1`
|
2. Native host installed only if scripts should call in: `powershell -NoProfile -File tools/install-native-host.ps1`
|
||||||
3. Firefox **running** for those calls
|
3. Firefox **running** for those calls
|
||||||
|
|
||||||
If `http://127.0.0.1:17634/health` fails, the host is not up. If `/health` works but calls 401, you do not have a current token.
|
If `http://127.0.0.1:17634/health` fails, the host is not up or the listen port was changed in Manage clients. If `/health` works but calls 401, you do not have a current token. Use `FAB_URL` when the port is not the default.
|
||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ REST aliases use the same Bearer token. `GET /health` has no token (loopback liv
|
|||||||
|
|
||||||
| Symptom | Cause | What to do |
|
| Symptom | Cause | What to do |
|
||||||
|---------|--------|------------|
|
|---------|--------|------------|
|
||||||
| Connection refused on `:17634` | Firefox closed or add-on/host missing | Open Firefox; load add-on; install native host |
|
| Connection refused on the listen port | Firefox closed, add-on/host missing, or port changed | Open Firefox; load add-on; install native host; match `FAB_URL` to Manage clients |
|
||||||
| 401 `Bearer token required` | Unsigned request | Set `FAB_TOKEN` |
|
| 401 `Bearer token required` | Unsigned request | Set `FAB_TOKEN` |
|
||||||
| 401 `unknown token` | Revoked, typo, or never generated | Manage clients → generate again |
|
| 401 `unknown token` | Revoked, typo, or never generated | Manage clients → generate again |
|
||||||
| 502 `method not allowed` | Typo or API not in the allowlist | `meta.methods` |
|
| 502 `method not allowed` | Typo or API not in the allowlist | `meta.methods` |
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ An API add-on for Firefox so agents and scripts can manage your bookmarks while
|
|||||||
|
|
||||||
The shipped product is the add-on in `extension/`. This is not a bookmark sync engine, and it is not a Mozilla product.
|
The shipped product is the add-on in `extension/`. This is not a bookmark sync engine, and it is not a Mozilla product.
|
||||||
|
|
||||||
|
## Who can use this
|
||||||
|
|
||||||
|
The HTTP API listens on **127.0.0.1 only** — the same computer that is running this Firefox. A local script, an editor agent, or a session you have opened on that machine (SSH, remote desktop) can call it, if it presents a secret you issued.
|
||||||
|
|
||||||
|
It does **not** work from chatgpt.com or other sites in a browser tab. Those programs run somewhere else and cannot see this port. Pages open in Firefox are also blocked (requests that carry a browser `Origin` are rejected).
|
||||||
|
|
||||||
|
You can sit at a different computer and still use it *if* your tooling is actually running on the Firefox machine — for example you SSH in and run the client there. Pointing a remote tool at some other host’s loopback will not work.
|
||||||
|
|
||||||
|
If this Firefox profile syncs bookmarks with a Mozilla account, edits made here can appear on your other devices. Sync is Firefox’s feature; the API itself still only accepts connections on this machine.
|
||||||
|
|
||||||
|
The listen port defaults to **17634**. Change it under Manage clients if that port is already taken, and point your tools at the same port (`FAB_URL`).
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
Scripts and local agents call a loopback HTTP API. The add-on performs only `browser.bookmarks` operations (create, search, move, delete, and related reads). It does not read tabs, history, cookies, or the open web.
|
Scripts and local agents call a loopback HTTP API. The add-on performs only `browser.bookmarks` operations (create, search, move, delete, and related reads). It does not read tabs, history, cookies, or the open web.
|
||||||
@@ -11,7 +23,7 @@ Scripts and local agents call a loopback HTTP API. The add-on performs only `bro
|
|||||||
1. Load the add-on.
|
1. Load the add-on.
|
||||||
2. Toolbar → **Manage clients** → generate a secret → store that secret yourself.
|
2. Toolbar → **Manage clients** → generate a secret → store that secret yourself.
|
||||||
3. Register the native host once if scripts should call in (`tools/install-native-host.ps1` on Windows).
|
3. Register the native host once if scripts should call in (`tools/install-native-host.ps1` on Windows).
|
||||||
4. Keep Firefox open. Call `http://127.0.0.1:17634` with `Authorization: Bearer <secret>`.
|
4. Keep Firefox open. Call `http://127.0.0.1:<port>` (default `17634`) with `Authorization: Bearer <secret>`.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /v1/call
|
POST /v1/call
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ This add-on is scoped to bookmarks. Treat a leaked `fab_…` secret like a passw
|
|||||||
|
|
||||||
- Manifest permissions: `bookmarks`, `storage`, `nativeMessaging` only
|
- Manifest permissions: `bookmarks`, `storage`, `nativeMessaging` only
|
||||||
- No `tabs`, `history`, `cookies`, `<all_urls>`, `webRequest`, or `runtime.onMessageExternal`
|
- No `tabs`, `history`, `cookies`, `<all_urls>`, `webRequest`, or `runtime.onMessageExternal`
|
||||||
- Host bind: `127.0.0.1`
|
- Host bind: `127.0.0.1` (port defaults to 17634; user-configurable)
|
||||||
- Reject HTTP requests that carry a browser `Origin`
|
- Reject HTTP requests that carry a browser `Origin`
|
||||||
- Native host `allowed_extensions` pinned to this gecko id
|
- Native host `allowed_extensions` pinned to this gecko id
|
||||||
- Method allowlist in both `extension/background.js` and `host/firefox_agent_bridge_host.py`
|
- Method allowlist in both `extension/background.js` and `host/firefox_agent_bridge_host.py`
|
||||||
|
|||||||
+64
-1
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const HOST_NAME = "com.easygoingaming.firefox_agent_bridge";
|
const HOST_NAME = "com.easygoingaming.firefox_agent_bridge";
|
||||||
const RECONNECT_MS = 2000;
|
const RECONNECT_MS = 2000;
|
||||||
|
const DEFAULT_PORT = 17634;
|
||||||
|
|
||||||
const ALLOWED = {
|
const ALLOWED = {
|
||||||
"meta.ping": async () => ({ ok: true, extension: "bookmarks-api" }),
|
"meta.ping": async () => ({ ok: true, extension: "bookmarks-api" }),
|
||||||
@@ -22,6 +23,47 @@ const ALLOWED = {
|
|||||||
let port = null;
|
let port = null;
|
||||||
let reconnectTimer = null;
|
let reconnectTimer = null;
|
||||||
let hostUp = false;
|
let hostUp = false;
|
||||||
|
let listenPort = DEFAULT_PORT;
|
||||||
|
let bindError = "";
|
||||||
|
let configWaiter = null;
|
||||||
|
|
||||||
|
function normalizePort(value) {
|
||||||
|
const n = Number(value);
|
||||||
|
if (!Number.isInteger(n) || n < 1024 || n > 65535) {
|
||||||
|
throw new Error("Port must be an integer from 1024 to 65535");
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadListenPort() {
|
||||||
|
const data = await browser.storage.local.get("listenPort");
|
||||||
|
try {
|
||||||
|
return normalizePort(data.listenPort == null ? DEFAULT_PORT : data.listenPort);
|
||||||
|
} catch (err) {
|
||||||
|
return DEFAULT_PORT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushConfig() {
|
||||||
|
if (!port) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
listenPort = await loadListenPort();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
configWaiter = resolve;
|
||||||
|
port.postMessage({ type: "config", port: listenPort });
|
||||||
|
setTimeout(resolve, 2500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusInfo() {
|
||||||
|
return {
|
||||||
|
hostUp,
|
||||||
|
port: listenPort,
|
||||||
|
listenPort,
|
||||||
|
bindError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function asArray(value) {
|
function asArray(value) {
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
@@ -43,6 +85,16 @@ function attach(nextPort) {
|
|||||||
port = nextPort;
|
port = nextPort;
|
||||||
hostUp = true;
|
hostUp = true;
|
||||||
port.onMessage.addListener(async (message) => {
|
port.onMessage.addListener(async (message) => {
|
||||||
|
if (message && message.type === "config-result") {
|
||||||
|
listenPort = message.port || listenPort;
|
||||||
|
bindError = message.ok ? "" : (message.error || "could not bind port");
|
||||||
|
if (configWaiter) {
|
||||||
|
const done = configWaiter;
|
||||||
|
configWaiter = null;
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (message && message.type === "auth") {
|
if (message && message.type === "auth") {
|
||||||
try {
|
try {
|
||||||
const client = await findClientByTokenHash(message.token_hash);
|
const client = await findClientByTokenHash(message.token_hash);
|
||||||
@@ -80,8 +132,10 @@ function attach(nextPort) {
|
|||||||
port.onDisconnect.addListener(() => {
|
port.onDisconnect.addListener(() => {
|
||||||
port = null;
|
port = null;
|
||||||
hostUp = false;
|
hostUp = false;
|
||||||
|
bindError = "";
|
||||||
scheduleReconnect();
|
scheduleReconnect();
|
||||||
});
|
});
|
||||||
|
pushConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleReconnect() {
|
function scheduleReconnect() {
|
||||||
@@ -106,7 +160,16 @@ function connect() {
|
|||||||
|
|
||||||
browser.runtime.onMessage.addListener((message) => {
|
browser.runtime.onMessage.addListener((message) => {
|
||||||
if (message && message.type === "status") {
|
if (message && message.type === "status") {
|
||||||
return Promise.resolve({ hostUp, port: 17634 });
|
return Promise.resolve(statusInfo());
|
||||||
|
}
|
||||||
|
if (message && message.type === "set-port") {
|
||||||
|
return (async () => {
|
||||||
|
const n = normalizePort(message.port);
|
||||||
|
await browser.storage.local.set({ listenPort: n });
|
||||||
|
listenPort = n;
|
||||||
|
await pushConfig();
|
||||||
|
return statusInfo();
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Bookmarks API for Scripting and AI",
|
"name": "Bookmarks API for Scripting and AI",
|
||||||
"short_name": "Bookmarks API",
|
"short_name": "Bookmarks API",
|
||||||
"version": "0.4.1",
|
"version": "0.4.2",
|
||||||
"description": "An API add-on for Firefox to allow agentic and script-based management of user bookmarks.",
|
"description": "An API add-on for Firefox to allow agentic and script-based management of user bookmarks.",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
<header>
|
<header>
|
||||||
<h1>Bookmarks API for Scripting and AI</h1>
|
<h1>Bookmarks API for Scripting and AI</h1>
|
||||||
<p class="lead">An API add-on for Firefox to allow agentic and script-based management of user bookmarks.</p>
|
<p class="lead">An API add-on for Firefox to allow agentic and script-based management of user bookmarks.</p>
|
||||||
|
<section class="usage">
|
||||||
|
<h2>Who can use this</h2>
|
||||||
|
<p>Tools on <strong>this computer</strong> can call the API — a local script, an editor agent, or a session you opened here (SSH, remote desktop). They need a secret you issue below.</p>
|
||||||
|
<p>It does not work from chatgpt.com or other sites in a browser tab. Those run elsewhere and cannot see this machine’s loopback port. Pages inside Firefox are blocked too.</p>
|
||||||
|
<p class="muted">If this Firefox profile syncs bookmarks with a Mozilla account, changes made here can show up on your other devices. The API itself still only accepts connections on this machine.</p>
|
||||||
|
</section>
|
||||||
<ol class="process">
|
<ol class="process">
|
||||||
<li>Issue a secret for each agent or script that should be allowed to work with your bookmarks.</li>
|
<li>Issue a secret for each agent or script that should be allowed to work with your bookmarks.</li>
|
||||||
<li>Store that secret with your secrets. This add-on keeps only a hash.</li>
|
<li>Store that secret with your secrets. This add-on keeps only a hash.</li>
|
||||||
@@ -17,6 +23,16 @@
|
|||||||
<p id="status" class="muted"></p>
|
<p id="status" class="muted"></p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<section class="settings">
|
||||||
|
<h2>Listen port</h2>
|
||||||
|
<p class="muted">Loopback only (<code>127.0.0.1</code>). Change this if the default is already in use. Your tools must use the same port.</p>
|
||||||
|
<div class="row">
|
||||||
|
<label class="sr" for="port">Listen port</label>
|
||||||
|
<input id="port" type="number" min="1024" max="65535" step="1" value="17634">
|
||||||
|
<button id="save-port" type="button">Save port</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="row">
|
<section class="row">
|
||||||
<input id="name" type="text" maxlength="64" placeholder="Client name" autocomplete="off">
|
<input id="name" type="text" maxlength="64" placeholder="Client name" autocomplete="off">
|
||||||
<button id="create" type="button">Generate secret</button>
|
<button id="create" type="button">Generate secret</button>
|
||||||
|
|||||||
+25
-4
@@ -5,11 +5,26 @@ const nameInput = document.getElementById("name");
|
|||||||
const createBtn = document.getElementById("create");
|
const createBtn = document.getElementById("create");
|
||||||
const errorEl = document.getElementById("error");
|
const errorEl = document.getElementById("error");
|
||||||
const statusEl = document.getElementById("status");
|
const statusEl = document.getElementById("status");
|
||||||
|
const portInput = document.getElementById("port");
|
||||||
|
const savePortBtn = document.getElementById("save-port");
|
||||||
const modal = document.getElementById("secret-modal");
|
const modal = document.getElementById("secret-modal");
|
||||||
const secretEl = document.getElementById("secret");
|
const secretEl = document.getElementById("secret");
|
||||||
const copyBtn = document.getElementById("copy");
|
const copyBtn = document.getElementById("copy");
|
||||||
const closeBtn = document.getElementById("close");
|
const closeBtn = document.getElementById("close");
|
||||||
|
|
||||||
|
function applyStatus(info) {
|
||||||
|
if (info && info.listenPort) {
|
||||||
|
portInput.value = String(info.listenPort);
|
||||||
|
}
|
||||||
|
if (info && info.bindError) {
|
||||||
|
statusEl.textContent = `Could not listen on ${info.listenPort}: ${info.bindError}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
statusEl.textContent = info && info.hostUp
|
||||||
|
? `This add-on is ready on 127.0.0.1:${info.listenPort} while Firefox is open.`
|
||||||
|
: "You can issue and revoke secrets here anytime.";
|
||||||
|
}
|
||||||
|
|
||||||
function showError(text) {
|
function showError(text) {
|
||||||
errorEl.hidden = !text;
|
errorEl.hidden = !text;
|
||||||
errorEl.textContent = text || "";
|
errorEl.textContent = text || "";
|
||||||
@@ -43,15 +58,21 @@ function escapeHtml(value) {
|
|||||||
async function refresh() {
|
async function refresh() {
|
||||||
render(await listClients());
|
render(await listClients());
|
||||||
try {
|
try {
|
||||||
const info = await browser.runtime.sendMessage({ type: "status" });
|
applyStatus(await browser.runtime.sendMessage({ type: "status" }));
|
||||||
statusEl.textContent = info && info.hostUp
|
|
||||||
? "This add-on is ready for agents and scripts while Firefox is open."
|
|
||||||
: "You can issue and revoke secrets here anytime.";
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.textContent = "You can issue and revoke secrets here anytime.";
|
statusEl.textContent = "You can issue and revoke secrets here anytime.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
savePortBtn.addEventListener("click", async () => {
|
||||||
|
showError("");
|
||||||
|
try {
|
||||||
|
applyStatus(await browser.runtime.sendMessage({ type: "set-port", port: portInput.value }));
|
||||||
|
} catch (err) {
|
||||||
|
showError(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
createBtn.addEventListener("click", async () => {
|
createBtn.addEventListener("click", async () => {
|
||||||
showError("");
|
showError("");
|
||||||
try {
|
try {
|
||||||
|
|||||||
+8
-4
@@ -4,11 +4,15 @@ const statusEl = document.getElementById("status");
|
|||||||
const manage = document.getElementById("manage");
|
const manage = document.getElementById("manage");
|
||||||
|
|
||||||
browser.runtime.sendMessage({ type: "status" }).then((info) => {
|
browser.runtime.sendMessage({ type: "status" }).then((info) => {
|
||||||
if (info && info.hostUp) {
|
if (info && info.bindError) {
|
||||||
statusEl.textContent = "Ready for agents and scripts while Firefox is open.";
|
statusEl.textContent = `Port ${info.listenPort} is not available.`;
|
||||||
} else {
|
return;
|
||||||
statusEl.textContent = "You can issue and revoke secrets here.";
|
|
||||||
}
|
}
|
||||||
|
if (info && info.hostUp) {
|
||||||
|
statusEl.textContent = `Ready on 127.0.0.1:${info.listenPort} while Firefox is open.`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
statusEl.textContent = "You can issue and revoke secrets here.";
|
||||||
});
|
});
|
||||||
|
|
||||||
manage.addEventListener("click", () => {
|
manage.addEventListener("click", () => {
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ body {
|
|||||||
}
|
}
|
||||||
h1, h2 { margin: 0 0 8px; font-size: 1.15rem; }
|
h1, h2 { margin: 0 0 8px; font-size: 1.15rem; }
|
||||||
.lead { margin: 0 0 12px; }
|
.lead { margin: 0 0 12px; }
|
||||||
|
.usage, .settings { margin: 0 0 16px; }
|
||||||
|
.usage p { margin: 0 0 8px; }
|
||||||
|
input[type="number"] {
|
||||||
|
width: 8rem;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
.process { margin: 0 0 16px; padding-left: 1.2rem; }
|
.process { margin: 0 0 16px; padding-left: 1.2rem; }
|
||||||
.process li { margin: 6px 0; }
|
.process li { margin: 6px 0; }
|
||||||
.muted { color: GrayText; }
|
.muted { color: GrayText; }
|
||||||
|
|||||||
@@ -20,9 +20,12 @@ if sys.platform == "win32":
|
|||||||
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
||||||
|
|
||||||
HOST = "127.0.0.1"
|
HOST = "127.0.0.1"
|
||||||
PORT = int(os.environ.get("FAB_PORT", "17634"))
|
DEFAULT_PORT = int(os.environ.get("FAB_PORT", "17634"))
|
||||||
|
PORT = DEFAULT_PORT
|
||||||
MAX_BODY = 256 * 1024
|
MAX_BODY = 256 * 1024
|
||||||
ALLOWED_HOSTS = {f"127.0.0.1:{PORT}", f"localhost:{PORT}"}
|
ALLOWED_HOSTS = {f"127.0.0.1:{PORT}", f"localhost:{PORT}"}
|
||||||
|
HTTP_SERVER: ThreadingHTTPServer | None = None
|
||||||
|
SERVER_LOCK = threading.Lock()
|
||||||
ALLOWED_METHODS = frozenset(
|
ALLOWED_METHODS = frozenset(
|
||||||
{
|
{
|
||||||
"meta.ping",
|
"meta.ping",
|
||||||
@@ -45,6 +48,45 @@ PENDING: dict[str, tuple[threading.Event, dict[str, Any]]] = {}
|
|||||||
PENDING_LOCK = threading.Lock()
|
PENDING_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def set_port_globals(port: int) -> None:
|
||||||
|
global PORT, ALLOWED_HOSTS
|
||||||
|
PORT = port
|
||||||
|
ALLOWED_HOSTS = {f"127.0.0.1:{port}", f"localhost:{port}"}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_listen_port(requested: Any) -> dict[str, Any]:
|
||||||
|
global HTTP_SERVER
|
||||||
|
try:
|
||||||
|
port = int(requested)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return {"type": "config-result", "ok": False, "error": "invalid port"}
|
||||||
|
if port < 1024 or port > 65535:
|
||||||
|
return {
|
||||||
|
"type": "config-result",
|
||||||
|
"ok": False,
|
||||||
|
"port": port,
|
||||||
|
"error": "port must be 1024–65535",
|
||||||
|
}
|
||||||
|
with SERVER_LOCK:
|
||||||
|
if HTTP_SERVER is not None and PORT == port:
|
||||||
|
return {"type": "config-result", "ok": True, "port": port}
|
||||||
|
old = HTTP_SERVER
|
||||||
|
HTTP_SERVER = None
|
||||||
|
if old is not None:
|
||||||
|
old.shutdown()
|
||||||
|
old.server_close()
|
||||||
|
try:
|
||||||
|
set_port_globals(port)
|
||||||
|
HTTP_SERVER = ThreadingHTTPServer((HOST, port), Handler)
|
||||||
|
except OSError as exc:
|
||||||
|
return {"type": "config-result", "ok": False, "port": port, "error": str(exc)}
|
||||||
|
threading.Thread(
|
||||||
|
target=HTTP_SERVER.serve_forever, name="fab-http", daemon=True
|
||||||
|
).start()
|
||||||
|
log(f"listening on http://{HOST}:{port}")
|
||||||
|
return {"type": "config-result", "ok": True, "port": port}
|
||||||
|
|
||||||
|
|
||||||
def log(msg: str) -> None:
|
def log(msg: str) -> None:
|
||||||
sys.stderr.write(msg + "\n")
|
sys.stderr.write(msg + "\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
@@ -252,6 +294,9 @@ def stdin_loop() -> None:
|
|||||||
break
|
break
|
||||||
if message is None:
|
if message is None:
|
||||||
break
|
break
|
||||||
|
if message.get("type") == "config":
|
||||||
|
send_to_extension(apply_listen_port(message.get("port")))
|
||||||
|
continue
|
||||||
req_id = str(message.get("id") or "")
|
req_id = str(message.get("id") or "")
|
||||||
with PENDING_LOCK:
|
with PENDING_LOCK:
|
||||||
pending = PENDING.pop(req_id, None)
|
pending = PENDING.pop(req_id, None)
|
||||||
@@ -265,10 +310,9 @@ def stdin_loop() -> None:
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
threading.Thread(target=stdin_loop, name="fab-stdin", daemon=True).start()
|
threading.Thread(target=stdin_loop, name="fab-stdin", daemon=True).start()
|
||||||
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
apply_listen_port(DEFAULT_PORT)
|
||||||
log(f"listening on http://{HOST}:{PORT}")
|
|
||||||
try:
|
try:
|
||||||
server.serve_forever()
|
threading.Event().wait()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
return 0
|
return 0
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
Reference in New Issue
Block a user