arkaos 3.71.0 → 3.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/core/terminal/__init__.py +2 -0
- package/core/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/connections.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/session.cpython-313.pyc +0 -0
- package/core/terminal/connections.py +46 -0
- package/dashboard/app/layouts/default.vue +7 -0
- package/dashboard/app/pages/cognition.vue +311 -0
- package/dashboard/app/pages/settings.vue +215 -51
- package/dashboard/package.json +3 -1
- package/installer/autostart.js +178 -0
- package/installer/cli.js +7 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +178 -5
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-OS autostart for the ArkaOS dashboard (v3.72.0, feature #2).
|
|
3
|
+
*
|
|
4
|
+
* Opt-in: `npx arkaos autostart enable|disable|status`. Installs a per-OS
|
|
5
|
+
* boot item that runs `scripts/start-dashboard.sh` (macOS/Linux) or
|
|
6
|
+
* `scripts/start-dashboard.ps1` (Windows) — the same launcher the
|
|
7
|
+
* `dashboard` command uses, which already starts the Python API + the
|
|
8
|
+
* dashboard UI (preferring the production build, falling back to dev).
|
|
9
|
+
*
|
|
10
|
+
* ESM, Node + Bun, no interactive prompts. The unit-generation (`unitFor`)
|
|
11
|
+
* is pure so it can be unit-tested without touching the OS.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execSync } from "node:child_process";
|
|
15
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
export const AUTOSTART_LABEL = "io.wizardingcode.arkaos.dashboard";
|
|
21
|
+
const SERVICE_NAME = "arkaos-dashboard.service";
|
|
22
|
+
|
|
23
|
+
function defaultRepoRoot() {
|
|
24
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Pure: the boot unit for a given platform. Throws on unsupported OS. */
|
|
28
|
+
export function unitFor(os, { repoRoot, home }) {
|
|
29
|
+
if (os === "darwin") {
|
|
30
|
+
const startScript = `${repoRoot}/scripts/start-dashboard.sh`;
|
|
31
|
+
const log = `${home}/.arkaos/logs/dashboard-autostart.log`;
|
|
32
|
+
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
33
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
34
|
+
<plist version="1.0">
|
|
35
|
+
<dict>
|
|
36
|
+
<key>Label</key>
|
|
37
|
+
<string>${AUTOSTART_LABEL}</string>
|
|
38
|
+
<key>ProgramArguments</key>
|
|
39
|
+
<array>
|
|
40
|
+
<string>/bin/bash</string>
|
|
41
|
+
<string>${startScript}</string>
|
|
42
|
+
</array>
|
|
43
|
+
<key>EnvironmentVariables</key>
|
|
44
|
+
<dict>
|
|
45
|
+
<key>ARKAOS_ROOT</key>
|
|
46
|
+
<string>${repoRoot}</string>
|
|
47
|
+
</dict>
|
|
48
|
+
<key>RunAtLoad</key>
|
|
49
|
+
<true/>
|
|
50
|
+
<key>StandardOutPath</key>
|
|
51
|
+
<string>${log}</string>
|
|
52
|
+
<key>StandardErrorPath</key>
|
|
53
|
+
<string>${log}</string>
|
|
54
|
+
</dict>
|
|
55
|
+
</plist>
|
|
56
|
+
`;
|
|
57
|
+
return {
|
|
58
|
+
kind: "launchd",
|
|
59
|
+
path: join(home, "Library", "LaunchAgents", `${AUTOSTART_LABEL}.plist`),
|
|
60
|
+
content,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (os === "linux") {
|
|
65
|
+
const startScript = `${repoRoot}/scripts/start-dashboard.sh`;
|
|
66
|
+
const content = `[Unit]
|
|
67
|
+
Description=ArkaOS Dashboard (Python API + UI)
|
|
68
|
+
After=network.target
|
|
69
|
+
|
|
70
|
+
[Service]
|
|
71
|
+
Type=simple
|
|
72
|
+
Environment=ARKAOS_ROOT=${repoRoot}
|
|
73
|
+
ExecStart=/bin/bash ${startScript}
|
|
74
|
+
Restart=on-failure
|
|
75
|
+
|
|
76
|
+
[Install]
|
|
77
|
+
WantedBy=default.target
|
|
78
|
+
`;
|
|
79
|
+
return {
|
|
80
|
+
kind: "systemd",
|
|
81
|
+
path: join(home, ".config", "systemd", "user", SERVICE_NAME),
|
|
82
|
+
content,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (os === "win32") {
|
|
87
|
+
const ps1 = join(repoRoot, "scripts", "start-dashboard.ps1");
|
|
88
|
+
const content = `@echo off
|
|
89
|
+
rem ArkaOS dashboard autostart (login).
|
|
90
|
+
powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${ps1}"
|
|
91
|
+
`;
|
|
92
|
+
return {
|
|
93
|
+
kind: "startup",
|
|
94
|
+
path: join(
|
|
95
|
+
home,
|
|
96
|
+
"AppData", "Roaming", "Microsoft", "Windows",
|
|
97
|
+
"Start Menu", "Programs", "Startup",
|
|
98
|
+
"arkaos-dashboard.cmd",
|
|
99
|
+
),
|
|
100
|
+
content,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error(`unsupported platform for autostart: ${os}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function _silent(cmd) {
|
|
108
|
+
try {
|
|
109
|
+
execSync(cmd, { stdio: "pipe" });
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function enable({ repoRoot = defaultRepoRoot() } = {}) {
|
|
117
|
+
const unit = unitFor(process.platform, { repoRoot, home: homedir() });
|
|
118
|
+
mkdirSync(dirname(unit.path), { recursive: true });
|
|
119
|
+
mkdirSync(join(homedir(), ".arkaos", "logs"), { recursive: true });
|
|
120
|
+
writeFileSync(unit.path, unit.content, "utf8");
|
|
121
|
+
if (unit.kind === "launchd") {
|
|
122
|
+
_silent(`launchctl unload "${unit.path}"`);
|
|
123
|
+
if (!_silent(`launchctl load -w "${unit.path}"`)) {
|
|
124
|
+
return { ok: false, path: unit.path,
|
|
125
|
+
message: `Plist written but launchctl load failed — it will still run at next login.` };
|
|
126
|
+
}
|
|
127
|
+
} else if (unit.kind === "systemd") {
|
|
128
|
+
_silent("systemctl --user daemon-reload");
|
|
129
|
+
if (!_silent(`systemctl --user enable --now ${SERVICE_NAME}`)) {
|
|
130
|
+
return { ok: false, path: unit.path,
|
|
131
|
+
message: `Service written but systemctl enable failed — check 'systemctl --user' availability.` };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// startup: writing the .cmd is sufficient; it runs on next login.
|
|
135
|
+
return { ok: true, path: unit.path,
|
|
136
|
+
message: `Autostart enabled (${unit.kind}). The dashboard will start on login.` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function disable() {
|
|
140
|
+
const unit = unitFor(process.platform, { repoRoot: defaultRepoRoot(), home: homedir() });
|
|
141
|
+
if (unit.kind === "launchd") _silent(`launchctl unload "${unit.path}"`);
|
|
142
|
+
else if (unit.kind === "systemd") _silent(`systemctl --user disable --now ${SERVICE_NAME}`);
|
|
143
|
+
if (existsSync(unit.path)) rmSync(unit.path);
|
|
144
|
+
return { ok: true, path: unit.path, message: "Autostart disabled." };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function status() {
|
|
148
|
+
let unit;
|
|
149
|
+
try {
|
|
150
|
+
unit = unitFor(process.platform, { repoRoot: defaultRepoRoot(), home: homedir() });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return { installed: false, supported: false, message: err.message };
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
installed: existsSync(unit.path),
|
|
156
|
+
supported: true,
|
|
157
|
+
kind: unit.kind,
|
|
158
|
+
path: unit.path,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function autostart(args = []) {
|
|
163
|
+
const action = (args[0] || "status").toLowerCase();
|
|
164
|
+
if (action === "enable") {
|
|
165
|
+
const r = enable();
|
|
166
|
+
console.log(` ${r.ok ? "✓" : "⚠"} ${r.message}\n ${r.path}`);
|
|
167
|
+
} else if (action === "disable") {
|
|
168
|
+
const r = disable();
|
|
169
|
+
console.log(` ✓ ${r.message}`);
|
|
170
|
+
} else if (action === "status") {
|
|
171
|
+
const s = status();
|
|
172
|
+
if (!s.supported) console.log(` Autostart not supported here: ${s.message}`);
|
|
173
|
+
else console.log(` Autostart: ${s.installed ? "ENABLED" : "disabled"} (${s.kind})\n ${s.path}`);
|
|
174
|
+
} else {
|
|
175
|
+
console.error(` Unknown autostart action: ${action}. Use enable | disable | status.`);
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
package/installer/cli.js
CHANGED
|
@@ -45,6 +45,7 @@ Usage:
|
|
|
45
45
|
npx arkaos migrate Migrate from v1 to v2
|
|
46
46
|
npx arkaos migrate-user-data Move user data (~/.claude/skills/arka/ → ~/.arkaos/)
|
|
47
47
|
npx arkaos dashboard Start monitoring dashboard
|
|
48
|
+
npx arkaos autostart <enable|disable|status> Start dashboard on boot
|
|
48
49
|
npx arkaos keys Manage API keys (OpenAI, fal.ai, etc.)
|
|
49
50
|
npx arkaos doctor Run health checks
|
|
50
51
|
npx arkaos uninstall Remove ArkaOS
|
|
@@ -101,6 +102,12 @@ async function main() {
|
|
|
101
102
|
await update();
|
|
102
103
|
break;
|
|
103
104
|
|
|
105
|
+
case "autostart": {
|
|
106
|
+
const { autostart } = await import("./autostart.js");
|
|
107
|
+
await autostart(positionals.slice(1));
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
104
111
|
case "uninstall":
|
|
105
112
|
const { uninstall } = await import("./uninstall.js");
|
|
106
113
|
await uninstall();
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -20,7 +20,7 @@ from typing import Optional
|
|
|
20
20
|
ARKAOS_ROOT = Path(os.environ.get("ARKAOS_ROOT", Path(__file__).resolve().parent.parent))
|
|
21
21
|
sys.path.insert(0, str(ARKAOS_ROOT))
|
|
22
22
|
|
|
23
|
-
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
|
|
23
|
+
from fastapi import FastAPI, Query, Request, WebSocket, WebSocketDisconnect
|
|
24
24
|
from fastapi.middleware.cors import CORSMiddleware
|
|
25
25
|
|
|
26
26
|
app = FastAPI(title="ArkaOS Dashboard API", version="2.2.0")
|
|
@@ -2259,6 +2259,160 @@ def workflow_update_yaml(workflow_id: str, body: dict):
|
|
|
2259
2259
|
return {"updated": True, "id": workflow_id, "file": str(target)}
|
|
2260
2260
|
|
|
2261
2261
|
|
|
2262
|
+
# ============================================================================
|
|
2263
|
+
# v3.72.0 — Cognition: surface Dreaming insights (read-only) to the dashboard.
|
|
2264
|
+
# Reads existing markdown insights from <vault>/Projects/ArkaOS/Dreams via
|
|
2265
|
+
# core.cognition.dreams_reader. No new engine — pure read.
|
|
2266
|
+
# ============================================================================
|
|
2267
|
+
|
|
2268
|
+
|
|
2269
|
+
def _dreams_dir():
|
|
2270
|
+
"""Resolve the Dreams folder from the user's vault, or None."""
|
|
2271
|
+
from core.runtime.path_resolver import load_profile, ProfileMissingError
|
|
2272
|
+
try:
|
|
2273
|
+
profile = load_profile()
|
|
2274
|
+
except ProfileMissingError:
|
|
2275
|
+
return None
|
|
2276
|
+
except Exception: # noqa: BLE001 — never break the endpoint on profile errors
|
|
2277
|
+
return None
|
|
2278
|
+
vault = getattr(profile, "vault_path", None)
|
|
2279
|
+
return (Path(vault) / "Projects" / "ArkaOS" / "Dreams") if vault else None
|
|
2280
|
+
|
|
2281
|
+
|
|
2282
|
+
def _insight_to_dict(ins) -> dict:
|
|
2283
|
+
return {
|
|
2284
|
+
"date": ins.date,
|
|
2285
|
+
"title": ins.title,
|
|
2286
|
+
"confidence": ins.confidence,
|
|
2287
|
+
"sources": list(ins.sources),
|
|
2288
|
+
"tags": list(ins.tags),
|
|
2289
|
+
"body": ins.body,
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
|
|
2293
|
+
@app.get("/api/cognition/insights")
|
|
2294
|
+
def cognition_insights(days: int = 7):
|
|
2295
|
+
"""Recent Dreaming insights within the given window. Never 500s."""
|
|
2296
|
+
dreams = _dreams_dir()
|
|
2297
|
+
if dreams is None:
|
|
2298
|
+
return {"insights": [], "available": False}
|
|
2299
|
+
from core.cognition.dreams_reader import list_insights
|
|
2300
|
+
items = list_insights(dreams, since_days=max(1, int(days)))
|
|
2301
|
+
return {"insights": [_insight_to_dict(i) for i in items], "available": True}
|
|
2302
|
+
|
|
2303
|
+
|
|
2304
|
+
@app.get("/api/cognition/status")
|
|
2305
|
+
def cognition_status():
|
|
2306
|
+
"""Insight counts + confidence breakdown + last activity date."""
|
|
2307
|
+
dreams = _dreams_dir()
|
|
2308
|
+
if dreams is None:
|
|
2309
|
+
return {
|
|
2310
|
+
"today": 0, "week": 0, "total": 0,
|
|
2311
|
+
"by_confidence": {"high": 0, "medium": 0, "low": 0},
|
|
2312
|
+
"vault_configured": False, "last_date": None,
|
|
2313
|
+
}
|
|
2314
|
+
from core.cognition.dreams_reader import list_insights
|
|
2315
|
+
all_items = list_insights(dreams, since_days=36500)
|
|
2316
|
+
by_conf = {"high": 0, "medium": 0, "low": 0}
|
|
2317
|
+
for i in all_items:
|
|
2318
|
+
by_conf[i.confidence if i.confidence in by_conf else "medium"] += 1
|
|
2319
|
+
return {
|
|
2320
|
+
"today": len(list_insights(dreams, since_days=1)),
|
|
2321
|
+
"week": len(list_insights(dreams, since_days=7)),
|
|
2322
|
+
"total": len(all_items),
|
|
2323
|
+
"by_confidence": by_conf,
|
|
2324
|
+
"vault_configured": True,
|
|
2325
|
+
"last_date": all_items[0].date if all_items else None,
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
|
|
2329
|
+
# ============================================================================
|
|
2330
|
+
# v3.72.0 — System: version check + one-click core update (feature #3).
|
|
2331
|
+
# ============================================================================
|
|
2332
|
+
|
|
2333
|
+
_npm_latest_cache = {"version": None, "ts": 0.0}
|
|
2334
|
+
|
|
2335
|
+
|
|
2336
|
+
def _current_version() -> str:
|
|
2337
|
+
root = os.environ.get("ARKAOS_ROOT") or str(Path(__file__).resolve().parent.parent)
|
|
2338
|
+
try:
|
|
2339
|
+
return (Path(root) / "VERSION").read_text(encoding="utf-8").strip()
|
|
2340
|
+
except OSError:
|
|
2341
|
+
return "0.0.0"
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
def _npm_latest_version():
|
|
2345
|
+
import subprocess
|
|
2346
|
+
import time
|
|
2347
|
+
now = time.time()
|
|
2348
|
+
if _npm_latest_cache["version"] and now - _npm_latest_cache["ts"] < 600:
|
|
2349
|
+
return _npm_latest_cache["version"]
|
|
2350
|
+
try:
|
|
2351
|
+
out = subprocess.run(
|
|
2352
|
+
["npm", "view", "arkaos", "version"],
|
|
2353
|
+
capture_output=True, text=True, timeout=20,
|
|
2354
|
+
)
|
|
2355
|
+
latest = (out.stdout or "").strip() or None
|
|
2356
|
+
except Exception: # noqa: BLE001
|
|
2357
|
+
latest = None
|
|
2358
|
+
if latest:
|
|
2359
|
+
_npm_latest_cache["version"] = latest
|
|
2360
|
+
_npm_latest_cache["ts"] = now
|
|
2361
|
+
return latest
|
|
2362
|
+
|
|
2363
|
+
|
|
2364
|
+
def _is_newer(latest: str, current: str) -> bool:
|
|
2365
|
+
import re
|
|
2366
|
+
def _parts(v):
|
|
2367
|
+
nums = [int(n) for n in re.findall(r"\d+", v)[:3]]
|
|
2368
|
+
return nums or [0]
|
|
2369
|
+
try:
|
|
2370
|
+
return _parts(latest) > _parts(current)
|
|
2371
|
+
except Exception: # noqa: BLE001
|
|
2372
|
+
return False
|
|
2373
|
+
|
|
2374
|
+
|
|
2375
|
+
def _run_core_update() -> dict:
|
|
2376
|
+
import subprocess
|
|
2377
|
+
try:
|
|
2378
|
+
out = subprocess.run(
|
|
2379
|
+
["npx", "arkaos@latest", "update"],
|
|
2380
|
+
capture_output=True, text=True, timeout=600,
|
|
2381
|
+
)
|
|
2382
|
+
tail = ((out.stdout or "") + (out.stderr or ""))[-2000:]
|
|
2383
|
+
return {"ok": out.returncode == 0, "output": tail}
|
|
2384
|
+
except Exception as exc: # noqa: BLE001
|
|
2385
|
+
return {"ok": False, "output": f"update failed: {exc}"}
|
|
2386
|
+
|
|
2387
|
+
|
|
2388
|
+
@app.get("/api/system/version")
|
|
2389
|
+
def system_version():
|
|
2390
|
+
"""Current installed version vs the latest on npm."""
|
|
2391
|
+
current = _current_version()
|
|
2392
|
+
latest = _npm_latest_version()
|
|
2393
|
+
return {
|
|
2394
|
+
"current": current,
|
|
2395
|
+
"latest": latest,
|
|
2396
|
+
"update_available": bool(latest) and _is_newer(latest, current),
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
|
|
2400
|
+
@app.post("/api/system/update")
|
|
2401
|
+
def system_update(request: Request):
|
|
2402
|
+
"""Run `npx arkaos@latest update` (update step 1). Step 2 (project
|
|
2403
|
+
sync via /arka update) is a Claude Code action the UI prompts for.
|
|
2404
|
+
|
|
2405
|
+
A regular POST is CORS-protected; as defense-in-depth for an endpoint
|
|
2406
|
+
that runs an update, reject any explicitly non-localhost origin (an
|
|
2407
|
+
empty origin — local CLI / same-origin — is allowed).
|
|
2408
|
+
"""
|
|
2409
|
+
origin = request.headers.get("origin", "")
|
|
2410
|
+
if origin and not _terminal_origin_ok(origin):
|
|
2411
|
+
from fastapi import HTTPException
|
|
2412
|
+
raise HTTPException(status_code=403, detail="origin not allowed")
|
|
2413
|
+
return _run_core_update()
|
|
2414
|
+
|
|
2415
|
+
|
|
2262
2416
|
# ============================================================================
|
|
2263
2417
|
# PR99a v3.67.0 — Terminal PTY WebSocket + REST.
|
|
2264
2418
|
#
|
|
@@ -2282,6 +2436,12 @@ def _terminal_origin_ok(origin: str) -> bool:
|
|
|
2282
2436
|
)
|
|
2283
2437
|
|
|
2284
2438
|
|
|
2439
|
+
# v3.71.1 — enforce a single live WebSocket per session (latest wins), so a
|
|
2440
|
+
# reload or a second tab can't fight over the one PTY fd reader.
|
|
2441
|
+
from core.terminal.connections import ConnectionRegistry as _TerminalConnRegistry
|
|
2442
|
+
_terminal_conns = _TerminalConnRegistry()
|
|
2443
|
+
|
|
2444
|
+
|
|
2285
2445
|
@app.get("/api/terminal/token")
|
|
2286
2446
|
def terminal_token_endpoint():
|
|
2287
2447
|
"""Return the per-process bearer token used in WS handshakes.
|
|
@@ -2366,6 +2526,16 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
|
|
|
2366
2526
|
|
|
2367
2527
|
await ws.accept()
|
|
2368
2528
|
|
|
2529
|
+
# v3.71.1 — single live connection per session: a newer connection
|
|
2530
|
+
# (a reload, or a second tab) supersedes the previous one, which we
|
|
2531
|
+
# close so it stops pumping the shared PTY fd.
|
|
2532
|
+
superseded = _terminal_conns.acquire(session_id, ws)
|
|
2533
|
+
if superseded is not None:
|
|
2534
|
+
try:
|
|
2535
|
+
await superseded.close(code=4409, reason="superseded by a newer connection")
|
|
2536
|
+
except Exception:
|
|
2537
|
+
pass
|
|
2538
|
+
|
|
2369
2539
|
# v3.71.0 — replay recent scrollback so a client reconnecting after
|
|
2370
2540
|
# the operator navigated away / reloaded restores its session as it
|
|
2371
2541
|
# left it. Sent before the live reader is attached, so the historical
|
|
@@ -2437,10 +2607,13 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
|
|
|
2437
2607
|
pass
|
|
2438
2608
|
finally:
|
|
2439
2609
|
pump_task.cancel()
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2610
|
+
# Only the still-active connection owns the fd reader — a superseded
|
|
2611
|
+
# connection tearing down must not remove its replacement's reader.
|
|
2612
|
+
if _terminal_conns.release(session_id, ws):
|
|
2613
|
+
try:
|
|
2614
|
+
loop.remove_reader(session.master_fd)
|
|
2615
|
+
except (ValueError, OSError):
|
|
2616
|
+
pass
|
|
2444
2617
|
|
|
2445
2618
|
|
|
2446
2619
|
@app.on_event("startup")
|