Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions emrg/gui/conn-manager.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,7 @@
const { DaemonClient } = require("./daemon_client.js");

class ConnManager {
constructor({ projectDir, logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.logger = logger;
this.isPackaged = isPackaged;
this._conns = new Map(); // sid -> { conn, projectPath }
Expand All@@ -49,7 +48,6 @@ class ConnManager {
async ensureDaemon() {
if (this._daemonConn && this._daemonConn.connected) return this._daemonConn;
const boot = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
});
Expand DownExpand Up@@ -79,7 +77,6 @@ class ConnManager {
}
await this.ensureDaemon();
const conn = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
deltaBatchMs: 16, // P2:delta 批量(G122 16ms)每连接一份(#626)
Expand Down
75 changes: 27 additions & 48 deletions emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,22 +18,13 @@ const crypto = require("crypto");
const { spawn } = require("child_process");
const WebSocket = require("ws");

// G129 (rant 2026-08-09T08:03:46): TOKEN_FILE 必须接受 projectDir——硬编码
// os.homedir() 时,Windows 测试的 setupTempHome() 只设 HOME 不设 USERPROFILE,
// os.homedir() 仍读 USERPROFILE(真实用户目录)→ 测试把假 token 写进
// 真实的 ~/.emrg/emrgd.token → 演化周期 10 小时连不上 daemon(WinError 1225)。
// 所有调用点必须传 this.projectDir(默认 os.homedir() 保持生产行为不变)。
const TOKEN_FILE = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.token");
const EMRGD_LOG = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.log");
// Rant 2026-08-09T18:47:37(GUI 连不上 daemon 回归):daemon 的规范运行时目录永远是
// ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件读
// ~/.emrg/emrgd.token)。GUI 的 projectDir 若被 config gui.project_dir 指向别处
// (非 home),按 projectDir 读 token/pid/log 全部落空 → 误判 daemon 不存在 →
// 反复 spawn 撞 PID 锁 → "failed to start after 3 attempts" 假错误,而真 daemon 一直活着。
// 规范位置常量:作为 projectDir 读取失败时的权威回退。
const HOME_TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
// Rant 2026-08-20T16:03:31:GUI"工作目录"概念已删除——daemon 运行时文件固定读取
// 规范位置 ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件
// 读 ~/.emrg/emrgd.token)。projectDir 参数与 G129 回退逻辑随概念一起清理(#884 后
// emrgd.token 已是唯一规范位置,回退冗余)。
const TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
const EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
const HOME_PID_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.pid");
const HOME_EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
// Fixed daemon port (rant 2026-08-19T08:05:21 + 2026-08-20T14:32:52): the
// daemon always listens on this constant — keep in sync with emrg/connect.py
// EMRGD_PORT and emrg/_stop_all.py _EMRGD_PORT. The token file no longer
Expand DownExpand Up@@ -86,8 +77,7 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
Expand DownExpand Up@@ -125,9 +115,9 @@ class DaemonClient {

// ── 生命周期 ────────────────────────────────────────────

// Rant 2026-08-09T18:47:37:读 token 的权威入口。先试 projectDir(G129 语义),
// 缺失/畸形时回退 daemon 规范位置 ~/.emrg。文件仅含单行 token(rant
// 2026-08-20T14:32:52);端口一律用 EMRGD_PORT 常量。返回 {token, source, port} 或 null。
// Rant 2026-08-20T16:03:31:读 token 的权威入口——固定读 daemon 规范位置
// ~/.emrg/emrgd.token(#884 后唯一位置)。文件仅含单行 token;端口一律用
// EMRGD_PORT 常量。返回 {token, source, port} 或 null。
_readPortToken() {
const tryRead = (file) => {
try {
Expand All@@ -137,16 +127,8 @@ class DaemonClient {
} catch { /* missing/unreadable → try next */ }
return null;
};
const project = tryRead(TOKEN_FILE(this.projectDir));
if (project) return { ...project, source: "projectDir", port: EMRGD_PORT };
const home = tryRead(HOME_TOKEN_FILE());
if (home) {
this.logger.warn(
`[gui] token file not found at projectDir (${TOKEN_FILE(this.projectDir)}) — ` +
`reusing canonical ~/.emrg/emrgd.token`
);
return { ...home, source: "home", port: EMRGD_PORT };
}
const token = tryRead(TOKEN_FILE());
if (token) return { ...token, source: "canonical", port: EMRGD_PORT };
return null;
}

Expand All@@ -168,8 +150,8 @@ class DaemonClient {
// R124 对应(daemon_manager.py):spawn 超时后读 emrgd.log 尾部,
// 让宿主看到真实失败原因(缺 DLL / PATH / 端口冲突),而不是干巴巴的
// "failed to start within timeout"(rant 2026-08-09T13:16:36 验收项 ②)。
// 18:47:37:log 也在规范 ~/.emrg 下——projectDir 读不到就回退 home
for (const file of [EMRGD_LOG(this.projectDir), HOME_EMRGD_LOG()]) {
// 18:47:37:log 在规范 ~/.emrg 下;16:03:31 后固定读该位置
for (const file of [EMRGD_LOG()]) {
try {
const data = fs.readFileSync(file, "utf8");
const tail = data.trim().split("\n").slice(-lines).join("\n");
Expand All@@ -193,7 +175,7 @@ class DaemonClient {
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore",
detached: true,
};
Expand All@@ -203,7 +185,7 @@ class DaemonClient {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${os.homedir()}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
Expand All@@ -218,9 +200,9 @@ class DaemonClient {
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${os.homedir()}`);
const child = spawn(python, args, {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore", // G68:对照 DEVNULL
detached: true, // 对照 start_new_session=True
// windowsHide: python.exe 是 console 子系统——GUI spawn 时不隐藏会
Expand All@@ -242,12 +224,9 @@ class DaemonClient {
// Rant 2026-08-09T13:16:36 G43 加固:daemon 进程是否存活(emrgd.pid 探测)。
// 存活 → ws 连接失败视为瞬时(daemon 重启/启动中),保留 port 文件交给退避重试;
// 死亡 → 允许 G43 删文件重拉。
// 18:47:37:pid 文件也在规范 ~/.emrg —— projectDir 读不到回退 home
// 18:47:37:pid 文件在规范 ~/.emrg;16:03:31 后固定读该位置
_daemonProcessAlive() {
const pidFiles = [
path.join(this.projectDir, ".emrg", "emrgd.pid"),
HOME_PID_FILE(),
];
const pidFiles = [HOME_PID_FILE()];
for (const pidFile of pidFiles) {
try {
const pid = Number(String(fs.readFileSync(pidFile, "utf8")).trim());
Expand DownExpand Up@@ -290,8 +269,8 @@ class DaemonClient {

// Rant 2026-08-09T18:47:37(A1 + B1):探测"已存在的 daemon"——4 状态诊断日志
// (token_file_exists / token_file_content / daemon_alive(ping) / spawn_result)。
// spawn 失败 ≠ daemon 不存在:GUI 可能因 projectDir≠home 读错 token 文件,
// 或 daemon 早已被 scheduler/TUI 拉起。返回 {token, source, port} 或 null。
// spawn 失败 ≠ daemon 不存在:daemon 可能早已被 scheduler/TUI 拉起。
// 返回 {token, source, port} 或 null。
async _probeExistingDaemon(spawnResult = "n/a") {
const pt = this._readPortToken();
const tokenFileExists = !!(pt || this._readPortTokenRaw());
Expand All@@ -306,7 +285,7 @@ class DaemonClient {

// 读 token 文件原始存在性(不含解析),供 probe 日志用。
_readPortTokenRaw() {
for (const file of [TOKEN_FILE(this.projectDir), HOME_TOKEN_FILE()]) {
for (const file of [TOKEN_FILE()]) {
try { if (fs.readFileSync(file, "utf8").trim()) return true; } catch { /* next */ }
}
return false;
Expand All@@ -329,15 +308,15 @@ class DaemonClient {
throw spawnErr;
}
// spawn 成功:daemon 永远写规范 ~/.emrg/emrgd.token(daemon.py config_dir()),
// 用权威读取(projectDir 回退 home),不假设 projectDir==home
// 权威读取固定该位置(16:03:31)
const pt = this._readPortToken();
if (!pt) throw new Error("token file not written after spawn");
this.logger.info(`[gui] daemon spawned ok: port=${EMRGD_PORT}`);
return pt;
}

async ensureConnected({ skipStart = false } = {}) {
// Rant 2026-08-09T18:47:37:1. 读 token 文件(projectDir → 规范 ~/.emrg 回退)→
// Rant 2026-08-09T18:47:37:1. 读 token 文件(固定规范 ~/.emrg)→
// 无则拉 daemon;spawn 失败先探测已有 daemon,活着直接复用,不再盲报
// "failed to start after 3 attempts"。每步打结构化诊断日志(B1-B5)。
// P2 connManager(rant 2026-08-10T15:07:19):skipStart=true 时 daemon 生命周期
Expand All@@ -353,7 +332,7 @@ class DaemonClient {
} else {
if (skipStart) {
throw new Error(
`daemon not running (skipStart): no token file at ${TOKEN_FILE(this.projectDir)}`
`daemon not running (skipStart): no token file at ${TOKEN_FILE()}`
);
}
this.logger.info(`[gui] ensureConnected: token_file_exists=false — spawning daemon`);
Expand DownExpand Up@@ -392,7 +371,7 @@ class DaemonClient {
}
this.logger.warn(`[gui] ws connect failed: ${e.message} — stale token, respawning daemon`);
try { this.ws.close(); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE(this.projectDir)); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE()); } catch { /* ignore */ }
const r = await this._spawnOrProbe();
port = r.port;
token = r.token;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions emrg/gui/conn-manager.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,7 @@
const { DaemonClient } = require("./daemon_client.js");

class ConnManager {
constructor({ projectDir, logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.logger = logger;
this.isPackaged = isPackaged;
this._conns = new Map(); // sid -> { conn, projectPath }
Expand All@@ -49,7 +48,6 @@ class ConnManager {
async ensureDaemon() {
if (this._daemonConn && this._daemonConn.connected) return this._daemonConn;
const boot = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
});
Expand DownExpand Up@@ -79,7 +77,6 @@ class ConnManager {
}
await this.ensureDaemon();
const conn = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
deltaBatchMs: 16, // P2:delta 批量(G122 16ms)每连接一份(#626)
Expand Down
75 changes: 27 additions & 48 deletions emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,22 +18,13 @@ const crypto = require("crypto");
const { spawn } = require("child_process");
const WebSocket = require("ws");

// G129 (rant 2026-08-09T08:03:46): TOKEN_FILE 必须接受 projectDir——硬编码
// os.homedir() 时,Windows 测试的 setupTempHome() 只设 HOME 不设 USERPROFILE,
// os.homedir() 仍读 USERPROFILE(真实用户目录)→ 测试把假 token 写进
// 真实的 ~/.emrg/emrgd.token → 演化周期 10 小时连不上 daemon(WinError 1225)。
// 所有调用点必须传 this.projectDir(默认 os.homedir() 保持生产行为不变)。
const TOKEN_FILE = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.token");
const EMRGD_LOG = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.log");
// Rant 2026-08-09T18:47:37(GUI 连不上 daemon 回归):daemon 的规范运行时目录永远是
// ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件读
// ~/.emrg/emrgd.token)。GUI 的 projectDir 若被 config gui.project_dir 指向别处
// (非 home),按 projectDir 读 token/pid/log 全部落空 → 误判 daemon 不存在 →
// 反复 spawn 撞 PID 锁 → "failed to start after 3 attempts" 假错误,而真 daemon 一直活着。
// 规范位置常量:作为 projectDir 读取失败时的权威回退。
const HOME_TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
// Rant 2026-08-20T16:03:31:GUI"工作目录"概念已删除——daemon 运行时文件固定读取
// 规范位置 ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件
// 读 ~/.emrg/emrgd.token)。projectDir 参数与 G129 回退逻辑随概念一起清理(#884 后
// emrgd.token 已是唯一规范位置,回退冗余)。
const TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
const EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
const HOME_PID_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.pid");
const HOME_EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
// Fixed daemon port (rant 2026-08-19T08:05:21 + 2026-08-20T14:32:52): the
// daemon always listens on this constant — keep in sync with emrg/connect.py
// EMRGD_PORT and emrg/_stop_all.py _EMRGD_PORT. The token file no longer
Expand DownExpand Up@@ -86,8 +77,7 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
Expand DownExpand Up@@ -125,9 +115,9 @@ class DaemonClient {

// ── 生命周期 ────────────────────────────────────────────

// Rant 2026-08-09T18:47:37:读 token 的权威入口。先试 projectDir(G129 语义),
// 缺失/畸形时回退 daemon 规范位置 ~/.emrg。文件仅含单行 token(rant
// 2026-08-20T14:32:52);端口一律用 EMRGD_PORT 常量。返回 {token, source, port} 或 null。
// Rant 2026-08-20T16:03:31:读 token 的权威入口——固定读 daemon 规范位置
// ~/.emrg/emrgd.token(#884 后唯一位置)。文件仅含单行 token;端口一律用
// EMRGD_PORT 常量。返回 {token, source, port} 或 null。
_readPortToken() {
const tryRead = (file) => {
try {
Expand All@@ -137,16 +127,8 @@ class DaemonClient {
} catch { /* missing/unreadable → try next */ }
return null;
};
const project = tryRead(TOKEN_FILE(this.projectDir));
if (project) return { ...project, source: "projectDir", port: EMRGD_PORT };
const home = tryRead(HOME_TOKEN_FILE());
if (home) {
this.logger.warn(
`[gui] token file not found at projectDir (${TOKEN_FILE(this.projectDir)}) — ` +
`reusing canonical ~/.emrg/emrgd.token`
);
return { ...home, source: "home", port: EMRGD_PORT };
}
const token = tryRead(TOKEN_FILE());
if (token) return { ...token, source: "canonical", port: EMRGD_PORT };
return null;
}

Expand All@@ -168,8 +150,8 @@ class DaemonClient {
// R124 对应(daemon_manager.py):spawn 超时后读 emrgd.log 尾部,
// 让宿主看到真实失败原因(缺 DLL / PATH / 端口冲突),而不是干巴巴的
// "failed to start within timeout"(rant 2026-08-09T13:16:36 验收项 ②)。
// 18:47:37:log 也在规范 ~/.emrg 下——projectDir 读不到就回退 home
for (const file of [EMRGD_LOG(this.projectDir), HOME_EMRGD_LOG()]) {
// 18:47:37:log 在规范 ~/.emrg 下;16:03:31 后固定读该位置
for (const file of [EMRGD_LOG()]) {
try {
const data = fs.readFileSync(file, "utf8");
const tail = data.trim().split("\n").slice(-lines).join("\n");
Expand All@@ -193,7 +175,7 @@ class DaemonClient {
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore",
detached: true,
};
Expand All@@ -203,7 +185,7 @@ class DaemonClient {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${os.homedir()}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
Expand All@@ -218,9 +200,9 @@ class DaemonClient {
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${os.homedir()}`);
const child = spawn(python, args, {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore", // G68:对照 DEVNULL
detached: true, // 对照 start_new_session=True
// windowsHide: python.exe 是 console 子系统——GUI spawn 时不隐藏会
Expand All@@ -242,12 +224,9 @@ class DaemonClient {
// Rant 2026-08-09T13:16:36 G43 加固:daemon 进程是否存活(emrgd.pid 探测)。
// 存活 → ws 连接失败视为瞬时(daemon 重启/启动中),保留 port 文件交给退避重试;
// 死亡 → 允许 G43 删文件重拉。
// 18:47:37:pid 文件也在规范 ~/.emrg —— projectDir 读不到回退 home
// 18:47:37:pid 文件在规范 ~/.emrg;16:03:31 后固定读该位置
_daemonProcessAlive() {
const pidFiles = [
path.join(this.projectDir, ".emrg", "emrgd.pid"),
HOME_PID_FILE(),
];
const pidFiles = [HOME_PID_FILE()];
for (const pidFile of pidFiles) {
try {
const pid = Number(String(fs.readFileSync(pidFile, "utf8")).trim());
Expand DownExpand Up@@ -290,8 +269,8 @@ class DaemonClient {

// Rant 2026-08-09T18:47:37(A1 + B1):探测"已存在的 daemon"——4 状态诊断日志
// (token_file_exists / token_file_content / daemon_alive(ping) / spawn_result)。
// spawn 失败 ≠ daemon 不存在:GUI 可能因 projectDir≠home 读错 token 文件,
// 或 daemon 早已被 scheduler/TUI 拉起。返回 {token, source, port} 或 null。
// spawn 失败 ≠ daemon 不存在:daemon 可能早已被 scheduler/TUI 拉起。
// 返回 {token, source, port} 或 null。
async _probeExistingDaemon(spawnResult = "n/a") {
const pt = this._readPortToken();
const tokenFileExists = !!(pt || this._readPortTokenRaw());
Expand All@@ -306,7 +285,7 @@ class DaemonClient {

// 读 token 文件原始存在性(不含解析),供 probe 日志用。
_readPortTokenRaw() {
for (const file of [TOKEN_FILE(this.projectDir), HOME_TOKEN_FILE()]) {
for (const file of [TOKEN_FILE()]) {
try { if (fs.readFileSync(file, "utf8").trim()) return true; } catch { /* next */ }
}
return false;
Expand All@@ -329,15 +308,15 @@ class DaemonClient {
throw spawnErr;
}
// spawn 成功:daemon 永远写规范 ~/.emrg/emrgd.token(daemon.py config_dir()),
// 用权威读取(projectDir 回退 home),不假设 projectDir==home
// 权威读取固定该位置(16:03:31)
const pt = this._readPortToken();
if (!pt) throw new Error("token file not written after spawn");
this.logger.info(`[gui] daemon spawned ok: port=${EMRGD_PORT}`);
return pt;
}

async ensureConnected({ skipStart = false } = {}) {
// Rant 2026-08-09T18:47:37:1. 读 token 文件(projectDir → 规范 ~/.emrg 回退)→
// Rant 2026-08-09T18:47:37:1. 读 token 文件(固定规范 ~/.emrg)→
// 无则拉 daemon;spawn 失败先探测已有 daemon,活着直接复用,不再盲报
// "failed to start after 3 attempts"。每步打结构化诊断日志(B1-B5)。
// P2 connManager(rant 2026-08-10T15:07:19):skipStart=true 时 daemon 生命周期
Expand All@@ -353,7 +332,7 @@ class DaemonClient {
} else {
if (skipStart) {
throw new Error(
`daemon not running (skipStart): no token file at ${TOKEN_FILE(this.projectDir)}`
`daemon not running (skipStart): no token file at ${TOKEN_FILE()}`
);
}
this.logger.info(`[gui] ensureConnected: token_file_exists=false — spawning daemon`);
Expand DownExpand Up@@ -392,7 +371,7 @@ class DaemonClient {
}
this.logger.warn(`[gui] ws connect failed: ${e.message} — stale token, respawning daemon`);
try { this.ws.close(); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE(this.projectDir)); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE()); } catch { /* ignore */ }
const r = await this._spawnOrProbe();
port = r.port;
token = r.token;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions emrg/gui/conn-manager.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,7 @@
const { DaemonClient } = require("./daemon_client.js");

class ConnManager {
constructor({ projectDir, logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.logger = logger;
this.isPackaged = isPackaged;
this._conns = new Map(); // sid -> { conn, projectPath }
Expand All@@ -49,7 +48,6 @@ class ConnManager {
async ensureDaemon() {
if (this._daemonConn && this._daemonConn.connected) return this._daemonConn;
const boot = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
});
Expand DownExpand Up@@ -79,7 +77,6 @@ class ConnManager {
}
await this.ensureDaemon();
const conn = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
deltaBatchMs: 16, // P2:delta 批量(G122 16ms)每连接一份(#626)
Expand Down
75 changes: 27 additions & 48 deletions emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,22 +18,13 @@ const crypto = require("crypto");
const { spawn } = require("child_process");
const WebSocket = require("ws");

// G129 (rant 2026-08-09T08:03:46): TOKEN_FILE 必须接受 projectDir——硬编码
// os.homedir() 时,Windows 测试的 setupTempHome() 只设 HOME 不设 USERPROFILE,
// os.homedir() 仍读 USERPROFILE(真实用户目录)→ 测试把假 token 写进
// 真实的 ~/.emrg/emrgd.token → 演化周期 10 小时连不上 daemon(WinError 1225)。
// 所有调用点必须传 this.projectDir(默认 os.homedir() 保持生产行为不变)。
const TOKEN_FILE = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.token");
const EMRGD_LOG = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.log");
// Rant 2026-08-09T18:47:37(GUI 连不上 daemon 回归):daemon 的规范运行时目录永远是
// ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件读
// ~/.emrg/emrgd.token)。GUI 的 projectDir 若被 config gui.project_dir 指向别处
// (非 home),按 projectDir 读 token/pid/log 全部落空 → 误判 daemon 不存在 →
// 反复 spawn 撞 PID 锁 → "failed to start after 3 attempts" 假错误,而真 daemon 一直活着。
// 规范位置常量:作为 projectDir 读取失败时的权威回退。
const HOME_TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
// Rant 2026-08-20T16:03:31:GUI"工作目录"概念已删除——daemon 运行时文件固定读取
// 规范位置 ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件
// 读 ~/.emrg/emrgd.token)。projectDir 参数与 G129 回退逻辑随概念一起清理(#884 后
// emrgd.token 已是唯一规范位置,回退冗余)。
const TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
const EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
const HOME_PID_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.pid");
const HOME_EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
// Fixed daemon port (rant 2026-08-19T08:05:21 + 2026-08-20T14:32:52): the
// daemon always listens on this constant — keep in sync with emrg/connect.py
// EMRGD_PORT and emrg/_stop_all.py _EMRGD_PORT. The token file no longer
Expand DownExpand Up@@ -86,8 +77,7 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
Expand DownExpand Up@@ -125,9 +115,9 @@ class DaemonClient {

// ── 生命周期 ────────────────────────────────────────────

// Rant 2026-08-09T18:47:37:读 token 的权威入口。先试 projectDir(G129 语义),
// 缺失/畸形时回退 daemon 规范位置 ~/.emrg。文件仅含单行 token(rant
// 2026-08-20T14:32:52);端口一律用 EMRGD_PORT 常量。返回 {token, source, port} 或 null。
// Rant 2026-08-20T16:03:31:读 token 的权威入口——固定读 daemon 规范位置
// ~/.emrg/emrgd.token(#884 后唯一位置)。文件仅含单行 token;端口一律用
// EMRGD_PORT 常量。返回 {token, source, port} 或 null。
_readPortToken() {
const tryRead = (file) => {
try {
Expand All@@ -137,16 +127,8 @@ class DaemonClient {
} catch { /* missing/unreadable → try next */ }
return null;
};
const project = tryRead(TOKEN_FILE(this.projectDir));
if (project) return { ...project, source: "projectDir", port: EMRGD_PORT };
const home = tryRead(HOME_TOKEN_FILE());
if (home) {
this.logger.warn(
`[gui] token file not found at projectDir (${TOKEN_FILE(this.projectDir)}) — ` +
`reusing canonical ~/.emrg/emrgd.token`
);
return { ...home, source: "home", port: EMRGD_PORT };
}
const token = tryRead(TOKEN_FILE());
if (token) return { ...token, source: "canonical", port: EMRGD_PORT };
return null;
}

Expand All@@ -168,8 +150,8 @@ class DaemonClient {
// R124 对应(daemon_manager.py):spawn 超时后读 emrgd.log 尾部,
// 让宿主看到真实失败原因(缺 DLL / PATH / 端口冲突),而不是干巴巴的
// "failed to start within timeout"(rant 2026-08-09T13:16:36 验收项 ②)。
// 18:47:37:log 也在规范 ~/.emrg 下——projectDir 读不到就回退 home
for (const file of [EMRGD_LOG(this.projectDir), HOME_EMRGD_LOG()]) {
// 18:47:37:log 在规范 ~/.emrg 下;16:03:31 后固定读该位置
for (const file of [EMRGD_LOG()]) {
try {
const data = fs.readFileSync(file, "utf8");
const tail = data.trim().split("\n").slice(-lines).join("\n");
Expand All@@ -193,7 +175,7 @@ class DaemonClient {
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore",
detached: true,
};
Expand All@@ -203,7 +185,7 @@ class DaemonClient {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${os.homedir()}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
Expand All@@ -218,9 +200,9 @@ class DaemonClient {
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${os.homedir()}`);
const child = spawn(python, args, {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore", // G68:对照 DEVNULL
detached: true, // 对照 start_new_session=True
// windowsHide: python.exe 是 console 子系统——GUI spawn 时不隐藏会
Expand All@@ -242,12 +224,9 @@ class DaemonClient {
// Rant 2026-08-09T13:16:36 G43 加固:daemon 进程是否存活(emrgd.pid 探测)。
// 存活 → ws 连接失败视为瞬时(daemon 重启/启动中),保留 port 文件交给退避重试;
// 死亡 → 允许 G43 删文件重拉。
// 18:47:37:pid 文件也在规范 ~/.emrg —— projectDir 读不到回退 home
// 18:47:37:pid 文件在规范 ~/.emrg;16:03:31 后固定读该位置
_daemonProcessAlive() {
const pidFiles = [
path.join(this.projectDir, ".emrg", "emrgd.pid"),
HOME_PID_FILE(),
];
const pidFiles = [HOME_PID_FILE()];
for (const pidFile of pidFiles) {
try {
const pid = Number(String(fs.readFileSync(pidFile, "utf8")).trim());
Expand DownExpand Up@@ -290,8 +269,8 @@ class DaemonClient {

// Rant 2026-08-09T18:47:37(A1 + B1):探测"已存在的 daemon"——4 状态诊断日志
// (token_file_exists / token_file_content / daemon_alive(ping) / spawn_result)。
// spawn 失败 ≠ daemon 不存在:GUI 可能因 projectDir≠home 读错 token 文件,
// 或 daemon 早已被 scheduler/TUI 拉起。返回 {token, source, port} 或 null。
// spawn 失败 ≠ daemon 不存在:daemon 可能早已被 scheduler/TUI 拉起。
// 返回 {token, source, port} 或 null。
async _probeExistingDaemon(spawnResult = "n/a") {
const pt = this._readPortToken();
const tokenFileExists = !!(pt || this._readPortTokenRaw());
Expand All@@ -306,7 +285,7 @@ class DaemonClient {

// 读 token 文件原始存在性(不含解析),供 probe 日志用。
_readPortTokenRaw() {
for (const file of [TOKEN_FILE(this.projectDir), HOME_TOKEN_FILE()]) {
for (const file of [TOKEN_FILE()]) {
try { if (fs.readFileSync(file, "utf8").trim()) return true; } catch { /* next */ }
}
return false;
Expand All@@ -329,15 +308,15 @@ class DaemonClient {
throw spawnErr;
}
// spawn 成功:daemon 永远写规范 ~/.emrg/emrgd.token(daemon.py config_dir()),
// 用权威读取(projectDir 回退 home),不假设 projectDir==home
// 权威读取固定该位置(16:03:31)
const pt = this._readPortToken();
if (!pt) throw new Error("token file not written after spawn");
this.logger.info(`[gui] daemon spawned ok: port=${EMRGD_PORT}`);
return pt;
}

async ensureConnected({ skipStart = false } = {}) {
// Rant 2026-08-09T18:47:37:1. 读 token 文件(projectDir → 规范 ~/.emrg 回退)→
// Rant 2026-08-09T18:47:37:1. 读 token 文件(固定规范 ~/.emrg)→
// 无则拉 daemon;spawn 失败先探测已有 daemon,活着直接复用,不再盲报
// "failed to start after 3 attempts"。每步打结构化诊断日志(B1-B5)。
// P2 connManager(rant 2026-08-10T15:07:19):skipStart=true 时 daemon 生命周期
Expand All@@ -353,7 +332,7 @@ class DaemonClient {
} else {
if (skipStart) {
throw new Error(
`daemon not running (skipStart): no token file at ${TOKEN_FILE(this.projectDir)}`
`daemon not running (skipStart): no token file at ${TOKEN_FILE()}`
);
}
this.logger.info(`[gui] ensureConnected: token_file_exists=false — spawning daemon`);
Expand DownExpand Up@@ -392,7 +371,7 @@ class DaemonClient {
}
this.logger.warn(`[gui] ws connect failed: ${e.message} — stale token, respawning daemon`);
try { this.ws.close(); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE(this.projectDir)); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE()); } catch { /* ignore */ }
const r = await this._spawnOrProbe();
port = r.port;
token = r.token;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions emrg/gui/conn-manager.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,7 @@
const { DaemonClient } = require("./daemon_client.js");

class ConnManager {
constructor({ projectDir, logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.logger = logger;
this.isPackaged = isPackaged;
this._conns = new Map(); // sid -> { conn, projectPath }
Expand All@@ -49,7 +48,6 @@ class ConnManager {
async ensureDaemon() {
if (this._daemonConn && this._daemonConn.connected) return this._daemonConn;
const boot = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
});
Expand DownExpand Up@@ -79,7 +77,6 @@ class ConnManager {
}
await this.ensureDaemon();
const conn = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
deltaBatchMs: 16, // P2:delta 批量(G122 16ms)每连接一份(#626)
Expand Down
75 changes: 27 additions & 48 deletions emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,22 +18,13 @@ const crypto = require("crypto");
const { spawn } = require("child_process");
const WebSocket = require("ws");

// G129 (rant 2026-08-09T08:03:46): TOKEN_FILE 必须接受 projectDir——硬编码
// os.homedir() 时,Windows 测试的 setupTempHome() 只设 HOME 不设 USERPROFILE,
// os.homedir() 仍读 USERPROFILE(真实用户目录)→ 测试把假 token 写进
// 真实的 ~/.emrg/emrgd.token → 演化周期 10 小时连不上 daemon(WinError 1225)。
// 所有调用点必须传 this.projectDir(默认 os.homedir() 保持生产行为不变)。
const TOKEN_FILE = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.token");
const EMRGD_LOG = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.log");
// Rant 2026-08-09T18:47:37(GUI 连不上 daemon 回归):daemon 的规范运行时目录永远是
// ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件读
// ~/.emrg/emrgd.token)。GUI 的 projectDir 若被 config gui.project_dir 指向别处
// (非 home),按 projectDir 读 token/pid/log 全部落空 → 误判 daemon 不存在 →
// 反复 spawn 撞 PID 锁 → "failed to start after 3 attempts" 假错误,而真 daemon 一直活着。
// 规范位置常量:作为 projectDir 读取失败时的权威回退。
const HOME_TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
// Rant 2026-08-20T16:03:31:GUI"工作目录"概念已删除——daemon 运行时文件固定读取
// 规范位置 ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件
// 读 ~/.emrg/emrgd.token)。projectDir 参数与 G129 回退逻辑随概念一起清理(#884 后
// emrgd.token 已是唯一规范位置,回退冗余)。
const TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
const EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
const HOME_PID_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.pid");
const HOME_EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
// Fixed daemon port (rant 2026-08-19T08:05:21 + 2026-08-20T14:32:52): the
// daemon always listens on this constant — keep in sync with emrg/connect.py
// EMRGD_PORT and emrg/_stop_all.py _EMRGD_PORT. The token file no longer
Expand DownExpand Up@@ -86,8 +77,7 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
Expand DownExpand Up@@ -125,9 +115,9 @@ class DaemonClient {

// ── 生命周期 ────────────────────────────────────────────

// Rant 2026-08-09T18:47:37:读 token 的权威入口。先试 projectDir(G129 语义),
// 缺失/畸形时回退 daemon 规范位置 ~/.emrg。文件仅含单行 token(rant
// 2026-08-20T14:32:52);端口一律用 EMRGD_PORT 常量。返回 {token, source, port} 或 null。
// Rant 2026-08-20T16:03:31:读 token 的权威入口——固定读 daemon 规范位置
// ~/.emrg/emrgd.token(#884 后唯一位置)。文件仅含单行 token;端口一律用
// EMRGD_PORT 常量。返回 {token, source, port} 或 null。
_readPortToken() {
const tryRead = (file) => {
try {
Expand All@@ -137,16 +127,8 @@ class DaemonClient {
} catch { /* missing/unreadable → try next */ }
return null;
};
const project = tryRead(TOKEN_FILE(this.projectDir));
if (project) return { ...project, source: "projectDir", port: EMRGD_PORT };
const home = tryRead(HOME_TOKEN_FILE());
if (home) {
this.logger.warn(
`[gui] token file not found at projectDir (${TOKEN_FILE(this.projectDir)}) — ` +
`reusing canonical ~/.emrg/emrgd.token`
);
return { ...home, source: "home", port: EMRGD_PORT };
}
const token = tryRead(TOKEN_FILE());
if (token) return { ...token, source: "canonical", port: EMRGD_PORT };
return null;
}

Expand All@@ -168,8 +150,8 @@ class DaemonClient {
// R124 对应(daemon_manager.py):spawn 超时后读 emrgd.log 尾部,
// 让宿主看到真实失败原因(缺 DLL / PATH / 端口冲突),而不是干巴巴的
// "failed to start within timeout"(rant 2026-08-09T13:16:36 验收项 ②)。
// 18:47:37:log 也在规范 ~/.emrg 下——projectDir 读不到就回退 home
for (const file of [EMRGD_LOG(this.projectDir), HOME_EMRGD_LOG()]) {
// 18:47:37:log 在规范 ~/.emrg 下;16:03:31 后固定读该位置
for (const file of [EMRGD_LOG()]) {
try {
const data = fs.readFileSync(file, "utf8");
const tail = data.trim().split("\n").slice(-lines).join("\n");
Expand All@@ -193,7 +175,7 @@ class DaemonClient {
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore",
detached: true,
};
Expand All@@ -203,7 +185,7 @@ class DaemonClient {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${os.homedir()}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
Expand All@@ -218,9 +200,9 @@ class DaemonClient {
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${os.homedir()}`);
const child = spawn(python, args, {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore", // G68:对照 DEVNULL
detached: true, // 对照 start_new_session=True
// windowsHide: python.exe 是 console 子系统——GUI spawn 时不隐藏会
Expand All@@ -242,12 +224,9 @@ class DaemonClient {
// Rant 2026-08-09T13:16:36 G43 加固:daemon 进程是否存活(emrgd.pid 探测)。
// 存活 → ws 连接失败视为瞬时(daemon 重启/启动中),保留 port 文件交给退避重试;
// 死亡 → 允许 G43 删文件重拉。
// 18:47:37:pid 文件也在规范 ~/.emrg —— projectDir 读不到回退 home
// 18:47:37:pid 文件在规范 ~/.emrg;16:03:31 后固定读该位置
_daemonProcessAlive() {
const pidFiles = [
path.join(this.projectDir, ".emrg", "emrgd.pid"),
HOME_PID_FILE(),
];
const pidFiles = [HOME_PID_FILE()];
for (const pidFile of pidFiles) {
try {
const pid = Number(String(fs.readFileSync(pidFile, "utf8")).trim());
Expand DownExpand Up@@ -290,8 +269,8 @@ class DaemonClient {

// Rant 2026-08-09T18:47:37(A1 + B1):探测"已存在的 daemon"——4 状态诊断日志
// (token_file_exists / token_file_content / daemon_alive(ping) / spawn_result)。
// spawn 失败 ≠ daemon 不存在:GUI 可能因 projectDir≠home 读错 token 文件,
// 或 daemon 早已被 scheduler/TUI 拉起。返回 {token, source, port} 或 null。
// spawn 失败 ≠ daemon 不存在:daemon 可能早已被 scheduler/TUI 拉起。
// 返回 {token, source, port} 或 null。
async _probeExistingDaemon(spawnResult = "n/a") {
const pt = this._readPortToken();
const tokenFileExists = !!(pt || this._readPortTokenRaw());
Expand All@@ -306,7 +285,7 @@ class DaemonClient {

// 读 token 文件原始存在性(不含解析),供 probe 日志用。
_readPortTokenRaw() {
for (const file of [TOKEN_FILE(this.projectDir), HOME_TOKEN_FILE()]) {
for (const file of [TOKEN_FILE()]) {
try { if (fs.readFileSync(file, "utf8").trim()) return true; } catch { /* next */ }
}
return false;
Expand All@@ -329,15 +308,15 @@ class DaemonClient {
throw spawnErr;
}
// spawn 成功:daemon 永远写规范 ~/.emrg/emrgd.token(daemon.py config_dir()),
// 用权威读取(projectDir 回退 home),不假设 projectDir==home
// 权威读取固定该位置(16:03:31)
const pt = this._readPortToken();
if (!pt) throw new Error("token file not written after spawn");
this.logger.info(`[gui] daemon spawned ok: port=${EMRGD_PORT}`);
return pt;
}

async ensureConnected({ skipStart = false } = {}) {
// Rant 2026-08-09T18:47:37:1. 读 token 文件(projectDir → 规范 ~/.emrg 回退)→
// Rant 2026-08-09T18:47:37:1. 读 token 文件(固定规范 ~/.emrg)→
// 无则拉 daemon;spawn 失败先探测已有 daemon,活着直接复用,不再盲报
// "failed to start after 3 attempts"。每步打结构化诊断日志(B1-B5)。
// P2 connManager(rant 2026-08-10T15:07:19):skipStart=true 时 daemon 生命周期
Expand All@@ -353,7 +332,7 @@ class DaemonClient {
} else {
if (skipStart) {
throw new Error(
`daemon not running (skipStart): no token file at ${TOKEN_FILE(this.projectDir)}`
`daemon not running (skipStart): no token file at ${TOKEN_FILE()}`
);
}
this.logger.info(`[gui] ensureConnected: token_file_exists=false — spawning daemon`);
Expand DownExpand Up@@ -392,7 +371,7 @@ class DaemonClient {
}
this.logger.warn(`[gui] ws connect failed: ${e.message} — stale token, respawning daemon`);
try { this.ws.close(); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE(this.projectDir)); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE()); } catch { /* ignore */ }
const r = await this._spawnOrProbe();
port = r.port;
token = r.token;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions emrg/gui/conn-manager.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,7 @@
const { DaemonClient } = require("./daemon_client.js");

class ConnManager {
constructor({ projectDir, logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.logger = logger;
this.isPackaged = isPackaged;
this._conns = new Map(); // sid -> { conn, projectPath }
Expand All@@ -49,7 +48,6 @@ class ConnManager {
async ensureDaemon() {
if (this._daemonConn && this._daemonConn.connected) return this._daemonConn;
const boot = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
});
Expand DownExpand Up@@ -79,7 +77,6 @@ class ConnManager {
}
await this.ensureDaemon();
const conn = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
deltaBatchMs: 16, // P2:delta 批量(G122 16ms)每连接一份(#626)
Expand Down
75 changes: 27 additions & 48 deletions emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,22 +18,13 @@ const crypto = require("crypto");
const { spawn } = require("child_process");
const WebSocket = require("ws");

// G129 (rant 2026-08-09T08:03:46): TOKEN_FILE 必须接受 projectDir——硬编码
// os.homedir() 时,Windows 测试的 setupTempHome() 只设 HOME 不设 USERPROFILE,
// os.homedir() 仍读 USERPROFILE(真实用户目录)→ 测试把假 token 写进
// 真实的 ~/.emrg/emrgd.token → 演化周期 10 小时连不上 daemon(WinError 1225)。
// 所有调用点必须传 this.projectDir(默认 os.homedir() 保持生产行为不变)。
const TOKEN_FILE = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.token");
const EMRGD_LOG = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.log");
// Rant 2026-08-09T18:47:37(GUI 连不上 daemon 回归):daemon 的规范运行时目录永远是
// ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件读
// ~/.emrg/emrgd.token)。GUI 的 projectDir 若被 config gui.project_dir 指向别处
// (非 home),按 projectDir 读 token/pid/log 全部落空 → 误判 daemon 不存在 →
// 反复 spawn 撞 PID 锁 → "failed to start after 3 attempts" 假错误,而真 daemon 一直活着。
// 规范位置常量:作为 projectDir 读取失败时的权威回退。
const HOME_TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
// Rant 2026-08-20T16:03:31:GUI"工作目录"概念已删除——daemon 运行时文件固定读取
// 规范位置 ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件
// 读 ~/.emrg/emrgd.token)。projectDir 参数与 G129 回退逻辑随概念一起清理(#884 后
// emrgd.token 已是唯一规范位置,回退冗余)。
const TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
const EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
const HOME_PID_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.pid");
const HOME_EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
// Fixed daemon port (rant 2026-08-19T08:05:21 + 2026-08-20T14:32:52): the
// daemon always listens on this constant — keep in sync with emrg/connect.py
// EMRGD_PORT and emrg/_stop_all.py _EMRGD_PORT. The token file no longer
Expand DownExpand Up@@ -86,8 +77,7 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
Expand DownExpand Up@@ -125,9 +115,9 @@ class DaemonClient {

// ── 生命周期 ────────────────────────────────────────────

// Rant 2026-08-09T18:47:37:读 token 的权威入口。先试 projectDir(G129 语义),
// 缺失/畸形时回退 daemon 规范位置 ~/.emrg。文件仅含单行 token(rant
// 2026-08-20T14:32:52);端口一律用 EMRGD_PORT 常量。返回 {token, source, port} 或 null。
// Rant 2026-08-20T16:03:31:读 token 的权威入口——固定读 daemon 规范位置
// ~/.emrg/emrgd.token(#884 后唯一位置)。文件仅含单行 token;端口一律用
// EMRGD_PORT 常量。返回 {token, source, port} 或 null。
_readPortToken() {
const tryRead = (file) => {
try {
Expand All@@ -137,16 +127,8 @@ class DaemonClient {
} catch { /* missing/unreadable → try next */ }
return null;
};
const project = tryRead(TOKEN_FILE(this.projectDir));
if (project) return { ...project, source: "projectDir", port: EMRGD_PORT };
const home = tryRead(HOME_TOKEN_FILE());
if (home) {
this.logger.warn(
`[gui] token file not found at projectDir (${TOKEN_FILE(this.projectDir)}) — ` +
`reusing canonical ~/.emrg/emrgd.token`
);
return { ...home, source: "home", port: EMRGD_PORT };
}
const token = tryRead(TOKEN_FILE());
if (token) return { ...token, source: "canonical", port: EMRGD_PORT };
return null;
}

Expand All@@ -168,8 +150,8 @@ class DaemonClient {
// R124 对应(daemon_manager.py):spawn 超时后读 emrgd.log 尾部,
// 让宿主看到真实失败原因(缺 DLL / PATH / 端口冲突),而不是干巴巴的
// "failed to start within timeout"(rant 2026-08-09T13:16:36 验收项 ②)。
// 18:47:37:log 也在规范 ~/.emrg 下——projectDir 读不到就回退 home
for (const file of [EMRGD_LOG(this.projectDir), HOME_EMRGD_LOG()]) {
// 18:47:37:log 在规范 ~/.emrg 下;16:03:31 后固定读该位置
for (const file of [EMRGD_LOG()]) {
try {
const data = fs.readFileSync(file, "utf8");
const tail = data.trim().split("\n").slice(-lines).join("\n");
Expand All@@ -193,7 +175,7 @@ class DaemonClient {
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore",
detached: true,
};
Expand All@@ -203,7 +185,7 @@ class DaemonClient {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${os.homedir()}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
Expand All@@ -218,9 +200,9 @@ class DaemonClient {
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${os.homedir()}`);
const child = spawn(python, args, {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore", // G68:对照 DEVNULL
detached: true, // 对照 start_new_session=True
// windowsHide: python.exe 是 console 子系统——GUI spawn 时不隐藏会
Expand All@@ -242,12 +224,9 @@ class DaemonClient {
// Rant 2026-08-09T13:16:36 G43 加固:daemon 进程是否存活(emrgd.pid 探测)。
// 存活 → ws 连接失败视为瞬时(daemon 重启/启动中),保留 port 文件交给退避重试;
// 死亡 → 允许 G43 删文件重拉。
// 18:47:37:pid 文件也在规范 ~/.emrg —— projectDir 读不到回退 home
// 18:47:37:pid 文件在规范 ~/.emrg;16:03:31 后固定读该位置
_daemonProcessAlive() {
const pidFiles = [
path.join(this.projectDir, ".emrg", "emrgd.pid"),
HOME_PID_FILE(),
];
const pidFiles = [HOME_PID_FILE()];
for (const pidFile of pidFiles) {
try {
const pid = Number(String(fs.readFileSync(pidFile, "utf8")).trim());
Expand DownExpand Up@@ -290,8 +269,8 @@ class DaemonClient {

// Rant 2026-08-09T18:47:37(A1 + B1):探测"已存在的 daemon"——4 状态诊断日志
// (token_file_exists / token_file_content / daemon_alive(ping) / spawn_result)。
// spawn 失败 ≠ daemon 不存在:GUI 可能因 projectDir≠home 读错 token 文件,
// 或 daemon 早已被 scheduler/TUI 拉起。返回 {token, source, port} 或 null。
// spawn 失败 ≠ daemon 不存在:daemon 可能早已被 scheduler/TUI 拉起。
// 返回 {token, source, port} 或 null。
async _probeExistingDaemon(spawnResult = "n/a") {
const pt = this._readPortToken();
const tokenFileExists = !!(pt || this._readPortTokenRaw());
Expand All@@ -306,7 +285,7 @@ class DaemonClient {

// 读 token 文件原始存在性(不含解析),供 probe 日志用。
_readPortTokenRaw() {
for (const file of [TOKEN_FILE(this.projectDir), HOME_TOKEN_FILE()]) {
for (const file of [TOKEN_FILE()]) {
try { if (fs.readFileSync(file, "utf8").trim()) return true; } catch { /* next */ }
}
return false;
Expand All@@ -329,15 +308,15 @@ class DaemonClient {
throw spawnErr;
}
// spawn 成功:daemon 永远写规范 ~/.emrg/emrgd.token(daemon.py config_dir()),
// 用权威读取(projectDir 回退 home),不假设 projectDir==home
// 权威读取固定该位置(16:03:31)
const pt = this._readPortToken();
if (!pt) throw new Error("token file not written after spawn");
this.logger.info(`[gui] daemon spawned ok: port=${EMRGD_PORT}`);
return pt;
}

async ensureConnected({ skipStart = false } = {}) {
// Rant 2026-08-09T18:47:37:1. 读 token 文件(projectDir → 规范 ~/.emrg 回退)→
// Rant 2026-08-09T18:47:37:1. 读 token 文件(固定规范 ~/.emrg)→
// 无则拉 daemon;spawn 失败先探测已有 daemon,活着直接复用,不再盲报
// "failed to start after 3 attempts"。每步打结构化诊断日志(B1-B5)。
// P2 connManager(rant 2026-08-10T15:07:19):skipStart=true 时 daemon 生命周期
Expand All@@ -353,7 +332,7 @@ class DaemonClient {
} else {
if (skipStart) {
throw new Error(
`daemon not running (skipStart): no token file at ${TOKEN_FILE(this.projectDir)}`
`daemon not running (skipStart): no token file at ${TOKEN_FILE()}`
);
}
this.logger.info(`[gui] ensureConnected: token_file_exists=false — spawning daemon`);
Expand DownExpand Up@@ -392,7 +371,7 @@ class DaemonClient {
}
this.logger.warn(`[gui] ws connect failed: ${e.message} — stale token, respawning daemon`);
try { this.ws.close(); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE(this.projectDir)); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE()); } catch { /* ignore */ }
const r = await this._spawnOrProbe();
port = r.port;
token = r.token;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions emrg/gui/conn-manager.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,7 @@
const { DaemonClient } = require("./daemon_client.js");

class ConnManager {
constructor({ projectDir, logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.logger = logger;
this.isPackaged = isPackaged;
this._conns = new Map(); // sid -> { conn, projectPath }
Expand All@@ -49,7 +48,6 @@ class ConnManager {
async ensureDaemon() {
if (this._daemonConn && this._daemonConn.connected) return this._daemonConn;
const boot = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
});
Expand DownExpand Up@@ -79,7 +77,6 @@ class ConnManager {
}
await this.ensureDaemon();
const conn = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
deltaBatchMs: 16, // P2:delta 批量(G122 16ms)每连接一份(#626)
Expand Down
75 changes: 27 additions & 48 deletions emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,22 +18,13 @@ const crypto = require("crypto");
const { spawn } = require("child_process");
const WebSocket = require("ws");

// G129 (rant 2026-08-09T08:03:46): TOKEN_FILE 必须接受 projectDir——硬编码
// os.homedir() 时,Windows 测试的 setupTempHome() 只设 HOME 不设 USERPROFILE,
// os.homedir() 仍读 USERPROFILE(真实用户目录)→ 测试把假 token 写进
// 真实的 ~/.emrg/emrgd.token → 演化周期 10 小时连不上 daemon(WinError 1225)。
// 所有调用点必须传 this.projectDir(默认 os.homedir() 保持生产行为不变)。
const TOKEN_FILE = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.token");
const EMRGD_LOG = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.log");
// Rant 2026-08-09T18:47:37(GUI 连不上 daemon 回归):daemon 的规范运行时目录永远是
// ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件读
// ~/.emrg/emrgd.token)。GUI 的 projectDir 若被 config gui.project_dir 指向别处
// (非 home),按 projectDir 读 token/pid/log 全部落空 → 误判 daemon 不存在 →
// 反复 spawn 撞 PID 锁 → "failed to start after 3 attempts" 假错误,而真 daemon 一直活着。
// 规范位置常量:作为 projectDir 读取失败时的权威回退。
const HOME_TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
// Rant 2026-08-20T16:03:31:GUI"工作目录"概念已删除——daemon 运行时文件固定读取
// 规范位置 ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件
// 读 ~/.emrg/emrgd.token)。projectDir 参数与 G129 回退逻辑随概念一起清理(#884 后
// emrgd.token 已是唯一规范位置,回退冗余)。
const TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
const EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
const HOME_PID_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.pid");
const HOME_EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
// Fixed daemon port (rant 2026-08-19T08:05:21 + 2026-08-20T14:32:52): the
// daemon always listens on this constant — keep in sync with emrg/connect.py
// EMRGD_PORT and emrg/_stop_all.py _EMRGD_PORT. The token file no longer
Expand DownExpand Up@@ -86,8 +77,7 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
Expand DownExpand Up@@ -125,9 +115,9 @@ class DaemonClient {

// ── 生命周期 ────────────────────────────────────────────

// Rant 2026-08-09T18:47:37:读 token 的权威入口。先试 projectDir(G129 语义),
// 缺失/畸形时回退 daemon 规范位置 ~/.emrg。文件仅含单行 token(rant
// 2026-08-20T14:32:52);端口一律用 EMRGD_PORT 常量。返回 {token, source, port} 或 null。
// Rant 2026-08-20T16:03:31:读 token 的权威入口——固定读 daemon 规范位置
// ~/.emrg/emrgd.token(#884 后唯一位置)。文件仅含单行 token;端口一律用
// EMRGD_PORT 常量。返回 {token, source, port} 或 null。
_readPortToken() {
const tryRead = (file) => {
try {
Expand All@@ -137,16 +127,8 @@ class DaemonClient {
} catch { /* missing/unreadable → try next */ }
return null;
};
const project = tryRead(TOKEN_FILE(this.projectDir));
if (project) return { ...project, source: "projectDir", port: EMRGD_PORT };
const home = tryRead(HOME_TOKEN_FILE());
if (home) {
this.logger.warn(
`[gui] token file not found at projectDir (${TOKEN_FILE(this.projectDir)}) — ` +
`reusing canonical ~/.emrg/emrgd.token`
);
return { ...home, source: "home", port: EMRGD_PORT };
}
const token = tryRead(TOKEN_FILE());
if (token) return { ...token, source: "canonical", port: EMRGD_PORT };
return null;
}

Expand All@@ -168,8 +150,8 @@ class DaemonClient {
// R124 对应(daemon_manager.py):spawn 超时后读 emrgd.log 尾部,
// 让宿主看到真实失败原因(缺 DLL / PATH / 端口冲突),而不是干巴巴的
// "failed to start within timeout"(rant 2026-08-09T13:16:36 验收项 ②)。
// 18:47:37:log 也在规范 ~/.emrg 下——projectDir 读不到就回退 home
for (const file of [EMRGD_LOG(this.projectDir), HOME_EMRGD_LOG()]) {
// 18:47:37:log 在规范 ~/.emrg 下;16:03:31 后固定读该位置
for (const file of [EMRGD_LOG()]) {
try {
const data = fs.readFileSync(file, "utf8");
const tail = data.trim().split("\n").slice(-lines).join("\n");
Expand All@@ -193,7 +175,7 @@ class DaemonClient {
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore",
detached: true,
};
Expand All@@ -203,7 +185,7 @@ class DaemonClient {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${os.homedir()}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
Expand All@@ -218,9 +200,9 @@ class DaemonClient {
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${os.homedir()}`);
const child = spawn(python, args, {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore", // G68:对照 DEVNULL
detached: true, // 对照 start_new_session=True
// windowsHide: python.exe 是 console 子系统——GUI spawn 时不隐藏会
Expand All@@ -242,12 +224,9 @@ class DaemonClient {
// Rant 2026-08-09T13:16:36 G43 加固:daemon 进程是否存活(emrgd.pid 探测)。
// 存活 → ws 连接失败视为瞬时(daemon 重启/启动中),保留 port 文件交给退避重试;
// 死亡 → 允许 G43 删文件重拉。
// 18:47:37:pid 文件也在规范 ~/.emrg —— projectDir 读不到回退 home
// 18:47:37:pid 文件在规范 ~/.emrg;16:03:31 后固定读该位置
_daemonProcessAlive() {
const pidFiles = [
path.join(this.projectDir, ".emrg", "emrgd.pid"),
HOME_PID_FILE(),
];
const pidFiles = [HOME_PID_FILE()];
for (const pidFile of pidFiles) {
try {
const pid = Number(String(fs.readFileSync(pidFile, "utf8")).trim());
Expand DownExpand Up@@ -290,8 +269,8 @@ class DaemonClient {

// Rant 2026-08-09T18:47:37(A1 + B1):探测"已存在的 daemon"——4 状态诊断日志
// (token_file_exists / token_file_content / daemon_alive(ping) / spawn_result)。
// spawn 失败 ≠ daemon 不存在:GUI 可能因 projectDir≠home 读错 token 文件,
// 或 daemon 早已被 scheduler/TUI 拉起。返回 {token, source, port} 或 null。
// spawn 失败 ≠ daemon 不存在:daemon 可能早已被 scheduler/TUI 拉起。
// 返回 {token, source, port} 或 null。
async _probeExistingDaemon(spawnResult = "n/a") {
const pt = this._readPortToken();
const tokenFileExists = !!(pt || this._readPortTokenRaw());
Expand All@@ -306,7 +285,7 @@ class DaemonClient {

// 读 token 文件原始存在性(不含解析),供 probe 日志用。
_readPortTokenRaw() {
for (const file of [TOKEN_FILE(this.projectDir), HOME_TOKEN_FILE()]) {
for (const file of [TOKEN_FILE()]) {
try { if (fs.readFileSync(file, "utf8").trim()) return true; } catch { /* next */ }
}
return false;
Expand All@@ -329,15 +308,15 @@ class DaemonClient {
throw spawnErr;
}
// spawn 成功:daemon 永远写规范 ~/.emrg/emrgd.token(daemon.py config_dir()),
// 用权威读取(projectDir 回退 home),不假设 projectDir==home
// 权威读取固定该位置(16:03:31)
const pt = this._readPortToken();
if (!pt) throw new Error("token file not written after spawn");
this.logger.info(`[gui] daemon spawned ok: port=${EMRGD_PORT}`);
return pt;
}

async ensureConnected({ skipStart = false } = {}) {
// Rant 2026-08-09T18:47:37:1. 读 token 文件(projectDir → 规范 ~/.emrg 回退)→
// Rant 2026-08-09T18:47:37:1. 读 token 文件(固定规范 ~/.emrg)→
// 无则拉 daemon;spawn 失败先探测已有 daemon,活着直接复用,不再盲报
// "failed to start after 3 attempts"。每步打结构化诊断日志(B1-B5)。
// P2 connManager(rant 2026-08-10T15:07:19):skipStart=true 时 daemon 生命周期
Expand All@@ -353,7 +332,7 @@ class DaemonClient {
} else {
if (skipStart) {
throw new Error(
`daemon not running (skipStart): no token file at ${TOKEN_FILE(this.projectDir)}`
`daemon not running (skipStart): no token file at ${TOKEN_FILE()}`
);
}
this.logger.info(`[gui] ensureConnected: token_file_exists=false — spawning daemon`);
Expand DownExpand Up@@ -392,7 +371,7 @@ class DaemonClient {
}
this.logger.warn(`[gui] ws connect failed: ${e.message} — stale token, respawning daemon`);
try { this.ws.close(); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE(this.projectDir)); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE()); } catch { /* ignore */ }
const r = await this._spawnOrProbe();
port = r.port;
token = r.token;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions emrg/gui/conn-manager.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,7 @@
const { DaemonClient } = require("./daemon_client.js");

class ConnManager {
constructor({ projectDir, logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.logger = logger;
this.isPackaged = isPackaged;
this._conns = new Map(); // sid -> { conn, projectPath }
Expand All@@ -49,7 +48,6 @@ class ConnManager {
async ensureDaemon() {
if (this._daemonConn && this._daemonConn.connected) return this._daemonConn;
const boot = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
});
Expand DownExpand Up@@ -79,7 +77,6 @@ class ConnManager {
}
await this.ensureDaemon();
const conn = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
deltaBatchMs: 16, // P2:delta 批量(G122 16ms)每连接一份(#626)
Expand Down
75 changes: 27 additions & 48 deletions emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,22 +18,13 @@ const crypto = require("crypto");
const { spawn } = require("child_process");
const WebSocket = require("ws");

// G129 (rant 2026-08-09T08:03:46): TOKEN_FILE 必须接受 projectDir——硬编码
// os.homedir() 时,Windows 测试的 setupTempHome() 只设 HOME 不设 USERPROFILE,
// os.homedir() 仍读 USERPROFILE(真实用户目录)→ 测试把假 token 写进
// 真实的 ~/.emrg/emrgd.token → 演化周期 10 小时连不上 daemon(WinError 1225)。
// 所有调用点必须传 this.projectDir(默认 os.homedir() 保持生产行为不变)。
const TOKEN_FILE = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.token");
const EMRGD_LOG = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.log");
// Rant 2026-08-09T18:47:37(GUI 连不上 daemon 回归):daemon 的规范运行时目录永远是
// ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件读
// ~/.emrg/emrgd.token)。GUI 的 projectDir 若被 config gui.project_dir 指向别处
// (非 home),按 projectDir 读 token/pid/log 全部落空 → 误判 daemon 不存在 →
// 反复 spawn 撞 PID 锁 → "failed to start after 3 attempts" 假错误,而真 daemon 一直活着。
// 规范位置常量:作为 projectDir 读取失败时的权威回退。
const HOME_TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
// Rant 2026-08-20T16:03:31:GUI"工作目录"概念已删除——daemon 运行时文件固定读取
// 规范位置 ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件
// 读 ~/.emrg/emrgd.token)。projectDir 参数与 G129 回退逻辑随概念一起清理(#884 后
// emrgd.token 已是唯一规范位置,回退冗余)。
const TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
const EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
const HOME_PID_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.pid");
const HOME_EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
// Fixed daemon port (rant 2026-08-19T08:05:21 + 2026-08-20T14:32:52): the
// daemon always listens on this constant — keep in sync with emrg/connect.py
// EMRGD_PORT and emrg/_stop_all.py _EMRGD_PORT. The token file no longer
Expand DownExpand Up@@ -86,8 +77,7 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
Expand DownExpand Up@@ -125,9 +115,9 @@ class DaemonClient {

// ── 生命周期 ────────────────────────────────────────────

// Rant 2026-08-09T18:47:37:读 token 的权威入口。先试 projectDir(G129 语义),
// 缺失/畸形时回退 daemon 规范位置 ~/.emrg。文件仅含单行 token(rant
// 2026-08-20T14:32:52);端口一律用 EMRGD_PORT 常量。返回 {token, source, port} 或 null。
// Rant 2026-08-20T16:03:31:读 token 的权威入口——固定读 daemon 规范位置
// ~/.emrg/emrgd.token(#884 后唯一位置)。文件仅含单行 token;端口一律用
// EMRGD_PORT 常量。返回 {token, source, port} 或 null。
_readPortToken() {
const tryRead = (file) => {
try {
Expand All@@ -137,16 +127,8 @@ class DaemonClient {
} catch { /* missing/unreadable → try next */ }
return null;
};
const project = tryRead(TOKEN_FILE(this.projectDir));
if (project) return { ...project, source: "projectDir", port: EMRGD_PORT };
const home = tryRead(HOME_TOKEN_FILE());
if (home) {
this.logger.warn(
`[gui] token file not found at projectDir (${TOKEN_FILE(this.projectDir)}) — ` +
`reusing canonical ~/.emrg/emrgd.token`
);
return { ...home, source: "home", port: EMRGD_PORT };
}
const token = tryRead(TOKEN_FILE());
if (token) return { ...token, source: "canonical", port: EMRGD_PORT };
return null;
}

Expand All@@ -168,8 +150,8 @@ class DaemonClient {
// R124 对应(daemon_manager.py):spawn 超时后读 emrgd.log 尾部,
// 让宿主看到真实失败原因(缺 DLL / PATH / 端口冲突),而不是干巴巴的
// "failed to start within timeout"(rant 2026-08-09T13:16:36 验收项 ②)。
// 18:47:37:log 也在规范 ~/.emrg 下——projectDir 读不到就回退 home
for (const file of [EMRGD_LOG(this.projectDir), HOME_EMRGD_LOG()]) {
// 18:47:37:log 在规范 ~/.emrg 下;16:03:31 后固定读该位置
for (const file of [EMRGD_LOG()]) {
try {
const data = fs.readFileSync(file, "utf8");
const tail = data.trim().split("\n").slice(-lines).join("\n");
Expand All@@ -193,7 +175,7 @@ class DaemonClient {
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore",
detached: true,
};
Expand All@@ -203,7 +185,7 @@ class DaemonClient {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${os.homedir()}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
Expand All@@ -218,9 +200,9 @@ class DaemonClient {
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${os.homedir()}`);
const child = spawn(python, args, {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore", // G68:对照 DEVNULL
detached: true, // 对照 start_new_session=True
// windowsHide: python.exe 是 console 子系统——GUI spawn 时不隐藏会
Expand All@@ -242,12 +224,9 @@ class DaemonClient {
// Rant 2026-08-09T13:16:36 G43 加固:daemon 进程是否存活(emrgd.pid 探测)。
// 存活 → ws 连接失败视为瞬时(daemon 重启/启动中),保留 port 文件交给退避重试;
// 死亡 → 允许 G43 删文件重拉。
// 18:47:37:pid 文件也在规范 ~/.emrg —— projectDir 读不到回退 home
// 18:47:37:pid 文件在规范 ~/.emrg;16:03:31 后固定读该位置
_daemonProcessAlive() {
const pidFiles = [
path.join(this.projectDir, ".emrg", "emrgd.pid"),
HOME_PID_FILE(),
];
const pidFiles = [HOME_PID_FILE()];
for (const pidFile of pidFiles) {
try {
const pid = Number(String(fs.readFileSync(pidFile, "utf8")).trim());
Expand DownExpand Up@@ -290,8 +269,8 @@ class DaemonClient {

// Rant 2026-08-09T18:47:37(A1 + B1):探测"已存在的 daemon"——4 状态诊断日志
// (token_file_exists / token_file_content / daemon_alive(ping) / spawn_result)。
// spawn 失败 ≠ daemon 不存在:GUI 可能因 projectDir≠home 读错 token 文件,
// 或 daemon 早已被 scheduler/TUI 拉起。返回 {token, source, port} 或 null。
// spawn 失败 ≠ daemon 不存在:daemon 可能早已被 scheduler/TUI 拉起。
// 返回 {token, source, port} 或 null。
async _probeExistingDaemon(spawnResult = "n/a") {
const pt = this._readPortToken();
const tokenFileExists = !!(pt || this._readPortTokenRaw());
Expand All@@ -306,7 +285,7 @@ class DaemonClient {

// 读 token 文件原始存在性(不含解析),供 probe 日志用。
_readPortTokenRaw() {
for (const file of [TOKEN_FILE(this.projectDir), HOME_TOKEN_FILE()]) {
for (const file of [TOKEN_FILE()]) {
try { if (fs.readFileSync(file, "utf8").trim()) return true; } catch { /* next */ }
}
return false;
Expand All@@ -329,15 +308,15 @@ class DaemonClient {
throw spawnErr;
}
// spawn 成功:daemon 永远写规范 ~/.emrg/emrgd.token(daemon.py config_dir()),
// 用权威读取(projectDir 回退 home),不假设 projectDir==home
// 权威读取固定该位置(16:03:31)
const pt = this._readPortToken();
if (!pt) throw new Error("token file not written after spawn");
this.logger.info(`[gui] daemon spawned ok: port=${EMRGD_PORT}`);
return pt;
}

async ensureConnected({ skipStart = false } = {}) {
// Rant 2026-08-09T18:47:37:1. 读 token 文件(projectDir → 规范 ~/.emrg 回退)→
// Rant 2026-08-09T18:47:37:1. 读 token 文件(固定规范 ~/.emrg)→
// 无则拉 daemon;spawn 失败先探测已有 daemon,活着直接复用,不再盲报
// "failed to start after 3 attempts"。每步打结构化诊断日志(B1-B5)。
// P2 connManager(rant 2026-08-10T15:07:19):skipStart=true 时 daemon 生命周期
Expand All@@ -353,7 +332,7 @@ class DaemonClient {
} else {
if (skipStart) {
throw new Error(
`daemon not running (skipStart): no token file at ${TOKEN_FILE(this.projectDir)}`
`daemon not running (skipStart): no token file at ${TOKEN_FILE()}`
);
}
this.logger.info(`[gui] ensureConnected: token_file_exists=false — spawning daemon`);
Expand DownExpand Up@@ -392,7 +371,7 @@ class DaemonClient {
}
this.logger.warn(`[gui] ws connect failed: ${e.message} — stale token, respawning daemon`);
try { this.ws.close(); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE(this.projectDir)); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE()); } catch { /* ignore */ }
const r = await this._spawnOrProbe();
port = r.port;
token = r.token;
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions emrg/gui/conn-manager.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,7 @@
const { DaemonClient } = require("./daemon_client.js");

class ConnManager {
constructor({ projectDir, logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, isPackaged = false, restartWindowMs = 1000, singleRetryDelayMs = 1000 } = {}) {
this.logger = logger;
this.isPackaged = isPackaged;
this._conns = new Map(); // sid -> { conn, projectPath }
Expand All@@ -49,7 +48,6 @@ class ConnManager {
async ensureDaemon() {
if (this._daemonConn && this._daemonConn.connected) return this._daemonConn;
const boot = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
});
Expand DownExpand Up@@ -79,7 +77,6 @@ class ConnManager {
}
await this.ensureDaemon();
const conn = new DaemonClient({
projectDir: this.projectDir,
logger: this.logger,
isPackaged: this.isPackaged,
deltaBatchMs: 16, // P2:delta 批量(G122 16ms)每连接一份(#626)
Expand Down
75 changes: 27 additions & 48 deletions emrg/gui/daemon_client.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,22 +18,13 @@ const crypto = require("crypto");
const { spawn } = require("child_process");
const WebSocket = require("ws");

// G129 (rant 2026-08-09T08:03:46): TOKEN_FILE 必须接受 projectDir——硬编码
// os.homedir() 时,Windows 测试的 setupTempHome() 只设 HOME 不设 USERPROFILE,
// os.homedir() 仍读 USERPROFILE(真实用户目录)→ 测试把假 token 写进
// 真实的 ~/.emrg/emrgd.token → 演化周期 10 小时连不上 daemon(WinError 1225)。
// 所有调用点必须传 this.projectDir(默认 os.homedir() 保持生产行为不变)。
const TOKEN_FILE = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.token");
const EMRGD_LOG = (projectDir = os.homedir()) => path.join(projectDir, ".emrg", "emrgd.log");
// Rant 2026-08-09T18:47:37(GUI 连不上 daemon 回归):daemon 的规范运行时目录永远是
// ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件读
// ~/.emrg/emrgd.token)。GUI 的 projectDir 若被 config gui.project_dir 指向别处
// (非 home),按 projectDir 读 token/pid/log 全部落空 → 误判 daemon 不存在 →
// 反复 spawn 撞 PID 锁 → "failed to start after 3 attempts" 假错误,而真 daemon 一直活着。
// 规范位置常量:作为 projectDir 读取失败时的权威回退。
const HOME_TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
// Rant 2026-08-20T16:03:31:GUI"工作目录"概念已删除——daemon 运行时文件固定读取
// 规范位置 ~/.emrg(daemon.py config_dir() = Path.home()/".emrg";connect.py 无条件
// 读 ~/.emrg/emrgd.token)。projectDir 参数与 G129 回退逻辑随概念一起清理(#884 后
// emrgd.token 已是唯一规范位置,回退冗余)。
const TOKEN_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.token");
const EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
const HOME_PID_FILE = () => path.join(os.homedir(), ".emrg", "emrgd.pid");
const HOME_EMRGD_LOG = () => path.join(os.homedir(), ".emrg", "emrgd.log");
// Fixed daemon port (rant 2026-08-19T08:05:21 + 2026-08-20T14:32:52): the
// daemon always listens on this constant — keep in sync with emrg/connect.py
// EMRGD_PORT and emrg/_stop_all.py _EMRGD_PORT. The token file no longer
Expand DownExpand Up@@ -86,8 +77,7 @@ const RESPONSE_TYPES = {
};

class DaemonClient {
constructor({ projectDir = os.homedir(), logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.projectDir = projectDir;
constructor({ logger = console, authTimeoutMs = AUTH_TIMEOUT_MS, isPackaged = false, deltaBatchMs = 0 } = {}) {
this.logger = logger;
this._authTimeoutMs = authTimeoutMs; // G142 测试可注入短超时(默认 10s)
this._isPackaged = isPackaged; // Phase 4:打包模式(rant #12 §4)由 main.js 注入 app.isPackaged
Expand DownExpand Up@@ -125,9 +115,9 @@ class DaemonClient {

// ── 生命周期 ────────────────────────────────────────────

// Rant 2026-08-09T18:47:37:读 token 的权威入口。先试 projectDir(G129 语义),
// 缺失/畸形时回退 daemon 规范位置 ~/.emrg。文件仅含单行 token(rant
// 2026-08-20T14:32:52);端口一律用 EMRGD_PORT 常量。返回 {token, source, port} 或 null。
// Rant 2026-08-20T16:03:31:读 token 的权威入口——固定读 daemon 规范位置
// ~/.emrg/emrgd.token(#884 后唯一位置)。文件仅含单行 token;端口一律用
// EMRGD_PORT 常量。返回 {token, source, port} 或 null。
_readPortToken() {
const tryRead = (file) => {
try {
Expand All@@ -137,16 +127,8 @@ class DaemonClient {
} catch { /* missing/unreadable → try next */ }
return null;
};
const project = tryRead(TOKEN_FILE(this.projectDir));
if (project) return { ...project, source: "projectDir", port: EMRGD_PORT };
const home = tryRead(HOME_TOKEN_FILE());
if (home) {
this.logger.warn(
`[gui] token file not found at projectDir (${TOKEN_FILE(this.projectDir)}) — ` +
`reusing canonical ~/.emrg/emrgd.token`
);
return { ...home, source: "home", port: EMRGD_PORT };
}
const token = tryRead(TOKEN_FILE());
if (token) return { ...token, source: "canonical", port: EMRGD_PORT };
return null;
}

Expand All@@ -168,8 +150,8 @@ class DaemonClient {
// R124 对应(daemon_manager.py):spawn 超时后读 emrgd.log 尾部,
// 让宿主看到真实失败原因(缺 DLL / PATH / 端口冲突),而不是干巴巴的
// "failed to start within timeout"(rant 2026-08-09T13:16:36 验收项 ②)。
// 18:47:37:log 也在规范 ~/.emrg 下——projectDir 读不到就回退 home
for (const file of [EMRGD_LOG(this.projectDir), HOME_EMRGD_LOG()]) {
// 18:47:37:log 在规范 ~/.emrg 下;16:03:31 后固定读该位置
for (const file of [EMRGD_LOG()]) {
try {
const data = fs.readFileSync(file, "utf8");
const tail = data.trim().split("\n").slice(-lines).join("\n");
Expand All@@ -193,7 +175,7 @@ class DaemonClient {
if (this._isPackaged) {
const emrgdPath = this._findDaemonExecutable();
const opts = {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore",
detached: true,
};
Expand All@@ -203,7 +185,7 @@ class DaemonClient {
opts.shell = true;
opts.windowsHide = true;
}
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning packaged daemon: ${emrgdPath} cwd=${os.homedir()}`);
const child = spawn(emrgdPath, [], opts);
child.unref();
this._daemonChild = child;
Expand All@@ -218,9 +200,9 @@ class DaemonClient {
// G125:spawn 设 cwd=project_dir(daemon load_skills 用 Path.cwd() 加载项目级 skills)
const python = this._findPython();
const args = ["-m", "emrg.server"];
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${this.projectDir}`);
this.logger.info(`[gui] spawning daemon: ${python} ${args.join(" ")} cwd=${os.homedir()}`);
const child = spawn(python, args, {
cwd: this.projectDir,
cwd: os.homedir(),
stdio: "ignore", // G68:对照 DEVNULL
detached: true, // 对照 start_new_session=True
// windowsHide: python.exe 是 console 子系统——GUI spawn 时不隐藏会
Expand All@@ -242,12 +224,9 @@ class DaemonClient {
// Rant 2026-08-09T13:16:36 G43 加固:daemon 进程是否存活(emrgd.pid 探测)。
// 存活 → ws 连接失败视为瞬时(daemon 重启/启动中),保留 port 文件交给退避重试;
// 死亡 → 允许 G43 删文件重拉。
// 18:47:37:pid 文件也在规范 ~/.emrg —— projectDir 读不到回退 home
// 18:47:37:pid 文件在规范 ~/.emrg;16:03:31 后固定读该位置
_daemonProcessAlive() {
const pidFiles = [
path.join(this.projectDir, ".emrg", "emrgd.pid"),
HOME_PID_FILE(),
];
const pidFiles = [HOME_PID_FILE()];
for (const pidFile of pidFiles) {
try {
const pid = Number(String(fs.readFileSync(pidFile, "utf8")).trim());
Expand DownExpand Up@@ -290,8 +269,8 @@ class DaemonClient {

// Rant 2026-08-09T18:47:37(A1 + B1):探测"已存在的 daemon"——4 状态诊断日志
// (token_file_exists / token_file_content / daemon_alive(ping) / spawn_result)。
// spawn 失败 ≠ daemon 不存在:GUI 可能因 projectDir≠home 读错 token 文件,
// 或 daemon 早已被 scheduler/TUI 拉起。返回 {token, source, port} 或 null。
// spawn 失败 ≠ daemon 不存在:daemon 可能早已被 scheduler/TUI 拉起。
// 返回 {token, source, port} 或 null。
async _probeExistingDaemon(spawnResult = "n/a") {
const pt = this._readPortToken();
const tokenFileExists = !!(pt || this._readPortTokenRaw());
Expand All@@ -306,7 +285,7 @@ class DaemonClient {

// 读 token 文件原始存在性(不含解析),供 probe 日志用。
_readPortTokenRaw() {
for (const file of [TOKEN_FILE(this.projectDir), HOME_TOKEN_FILE()]) {
for (const file of [TOKEN_FILE()]) {
try { if (fs.readFileSync(file, "utf8").trim()) return true; } catch { /* next */ }
}
return false;
Expand All@@ -329,15 +308,15 @@ class DaemonClient {
throw spawnErr;
}
// spawn 成功:daemon 永远写规范 ~/.emrg/emrgd.token(daemon.py config_dir()),
// 用权威读取(projectDir 回退 home),不假设 projectDir==home
// 权威读取固定该位置(16:03:31)
const pt = this._readPortToken();
if (!pt) throw new Error("token file not written after spawn");
this.logger.info(`[gui] daemon spawned ok: port=${EMRGD_PORT}`);
return pt;
}

async ensureConnected({ skipStart = false } = {}) {
// Rant 2026-08-09T18:47:37:1. 读 token 文件(projectDir → 规范 ~/.emrg 回退)→
// Rant 2026-08-09T18:47:37:1. 读 token 文件(固定规范 ~/.emrg)→
// 无则拉 daemon;spawn 失败先探测已有 daemon,活着直接复用,不再盲报
// "failed to start after 3 attempts"。每步打结构化诊断日志(B1-B5)。
// P2 connManager(rant 2026-08-10T15:07:19):skipStart=true 时 daemon 生命周期
Expand All@@ -353,7 +332,7 @@ class DaemonClient {
} else {
if (skipStart) {
throw new Error(
`daemon not running (skipStart): no token file at ${TOKEN_FILE(this.projectDir)}`
`daemon not running (skipStart): no token file at ${TOKEN_FILE()}`
);
}
this.logger.info(`[gui] ensureConnected: token_file_exists=false — spawning daemon`);
Expand DownExpand Up@@ -392,7 +371,7 @@ class DaemonClient {
}
this.logger.warn(`[gui] ws connect failed: ${e.message} — stale token, respawning daemon`);
try { this.ws.close(); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE(this.projectDir)); } catch { /* ignore */ }
try { fs.unlinkSync(TOKEN_FILE()); } catch { /* ignore */ }
const r = await this._spawnOrProbe();
port = r.port;
token = r.token;
Expand Down
Loading
Loading