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
137 changes: 76 additions & 61 deletions bin/emrg-uninstall
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,15 +44,19 @@ from pathlib import Path
EMRG_DIR = Path.home() / ".emrg"
LOG_DIR = EMRG_DIR / "logs"
GRAVEYARD_DIR = EMRG_DIR / "graveyard"
PORT_FILE = EMRG_DIR / "emrgd.port"
PID_FILE = EMRG_DIR / "emrgd.pid"
INSTALL_DIR = EMRG_DIR / "install"

# Rant 2026-08-25T18:46(宿主):emrgd.pid / emrgd.port 已彻底废弃——不写不读。
# 固定端口(EMRGD_PORT=56031)是 daemon 存活唯一 ground truth(rant
# 2026-08-19T08:05:21);token 只由 emrgd.token 单行承载(rant 2026-08-20T14:32:52)。
# WHITELIST 里仍保留这两个文件名,仅为卸载时清理老安装的历史残留(不创建不读取)。

# R101 whitelist — known EMRG files. Anything else in ~/.emrg is user data
# and is preserved (listed in the report instead of deleted).
# R121: 补全运行时文件 — emrgd.log / emrg-gui.log / gui-window.json 为 daemon/GUI
# 运行日志与窗口状态;skills/ 是 daemon 启动时 mkdir 的运行时骨架(非用户数据),
# 用户自定义 skills 先入 graveyard 快照再删除(rant 2026-08-05T15:35:17)。
# emrgd.pid / emrgd.port:历史遗留(已废弃),仅卸载时清理。
WHITELIST = [
"install", "versions", "config.toml", "sessions", "memory", "logs",
"projects.yml", "tasks.yml", "rants.jsonl", "saturation",
Expand DownExpand Up@@ -99,33 +103,37 @@ def stop_gui() -> dict:


def stop_daemon() -> dict:
"""Step 1b — stop the daemon. Returns {method, ok}."""
"""Step 1b — stop the daemon. Returns {method, ok}.

Rant 2026-08-25T18:46(宿主):不读 emrgd.pid / emrgd.port(已废弃)。
顺序:① ws 协议关闭(固定端口 56031 + emrgd.token 单行 token)
② 命令行扫描 ``-m emrg.server`` 进程 SIGTERM/taskkill(与
emrg._stop_all.stop_daemon 同语义,仅剩的可靠身份标记)
"""
result = {"method": "none", "ok": False}
# Try protocol shutdown when websockets is importable (PYTHONPATH has lib/).
EMRGD_PORT = 56031 # 固定端口(connect.py EMRGD_PORT,rant 2026-08-19T08:05:21)
token_path = EMRG_DIR / "emrgd.token"
try:
import asyncio
import json as _json

from websockets.asyncio.client import connect # type: ignore
from websockets.exceptions import ConnectionClosed # type: ignore

port = None
token = None
if PORT_FILE.exists():
lines = PORT_FILE.read_text(encoding="utf-8").splitlines()
if lines:
port = lines[0].strip()
if len(lines) > 1:
token = lines[1].strip()
token = ""
try:
token = token_path.read_text(encoding="utf-8").strip()
except OSError:
pass

async def _shutdown() -> bool:
if not port:
if not token:
return False
try:
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{port}", open_timeout=3), timeout=4)
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{EMRGD_PORT}", open_timeout=3), timeout=4)
try:
# Auth handshake (mirrors connect.py connect_to_server).
await ws.send(_json.dumps({"type": "auth", "token": token or ""}, ensure_ascii=False))
await ws.send(_json.dumps({"type": "auth", "token": token}, ensure_ascii=False))
ack = _json.loads(await asyncio.wait_for(ws.recv(), timeout=10))
if ack.get("type") != "auth_ok":
await ws.close()
Expand All@@ -141,61 +149,68 @@ def stop_daemon() -> dict:
except (ConnectionClosed, OSError, asyncio.TimeoutError, _json.JSONDecodeError):
return False

if port:
if token:
ok = asyncio.run(_shutdown())
if ok:
result = {"method": "protocol-shutdown", "ok": True}
print(" [1] daemon stopped (protocol shutdown)")
return result
except Exception:
pass # degrade to pid-based stop
pass # degrade to cmdline-scan stop

# Fallback: SIGTERM via pid file (POSIX) / taskkill (Windows).
pid = None
if PID_FILE.exists():
try:
pid = int(PID_FILE.read_text(encoding="utf-8").strip())
except ValueError:
pid = None
if not pid and PORT_FILE.exists():
# port file second line is the token, not pid — skip
pass
if pid:
try:
if os.name == "nt":
subprocess.run(
["taskkill", "/PID", str(pid), "/F"],
capture_output=True, timeout=10,
)
# R121: 轮询确认进程退出(≤5s),避免 pid 文件被并发重建
for _ in range(35):
chk = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True, text=True, timeout=5,
)
if "No tasks" in chk.stdout:
break
time.sleep(0.15)
else:
os.kill(pid, signal.SIGTERM)
for _ in range(20):
try:
os.kill(pid, 0)
# Fallback: kill by command-line identity (``-m emrg.server``) — the only
# reliable marker on Windows (rant 2026-08-17T17:03:38; emrg._stop_all
# same approach). No pid file involved.
import subprocess as _sp
try:
if os.name == "nt":
out = _sp.run(
["wmic", "process", "where", "name='pythonw.exe'", "get", "processid,commandline"],
capture_output=True, text=True, timeout=10,
).stdout or ""
# PowerShell CIM fallback for Win11 24H2+ (no wmic).
if not out.strip() or "emrg.server" not in out:
ps_cmd = ("Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match "
"'emrg.server' } | ForEach-Object { $_.ProcessId }")
out = _sp.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(p) for p in re.findall(r"\d+", out) if p.strip()]
for pid in pids:
_sp.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, timeout=10)
else:
out = _sp.run(
["ps", "-axww", "-o", "pid=,command="],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(m.group(1)) for m in re.finditer(r"^\s*(\d+)\s+.*-m emrg\.server\b", out, re.M)]
for pid in pids:
try:
os.kill(pid, signal.SIGTERM)
except OSError:
pass
if pids:
# Poll ≤5s for the port to close (ground truth).
import socket as _socket
for _ in range(35):
try:
with _socket.create_connection(("127.0.0.1", EMRGD_PORT), timeout=0.3):
time.sleep(0.15)
except OSError:
break
result = {"method": "pid-sigterm", "ok": True}
print(f" [1] daemon stopped (pid {pid})")
except OSError:
break
result = {"method": "cmdline-scan", "ok": True}
print(f" [1] daemon stopped (cmdline scan, {len(pids)} pid(s))")
return result
except (OSError, subprocess.SubprocessError):
pass
# Fallback: remove stale runtime files.
for f in (PORT_FILE, PID_FILE):
try:
f.unlink()
except FileNotFoundError:
pass
print(" [1] daemon not running (or already stopped); removed stale port/pid files")
except (OSError, subprocess.SubprocessError, _sp.SubprocessError):
pass
# Token cleanup once port is confirmed closed (daemon removes it itself on
# graceful shutdown; a force-killed daemon cannot).
try:
token_path.unlink()
except OSError:
pass
print(" [1] daemon not running (or already stopped)")
return result


Expand Down
6 changes: 4 additions & 2 deletions packaging/make-installer.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -414,8 +414,10 @@ end;
// Inno CloseApplications 看不到无窗口的 pythonw daemon(emrgd.cmd → pythonw.exe
// -m emrg.server 常驻锁文件),覆盖 ~/.emrg\install 时卡在"停止已有进程"。
// PrepareToInstall 在安装开始前用 runtime 的 python 运行 bin\stop_all.py:
// ws 协议关闭 daemon → emrgd.pid 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、
// CIM 命令行过滤 TUI、install\git\ 前缀连坐强杀 bundled git、verify 残留检查;
// ws 协议关闭 daemon(固定端口 56031 + emrgd.token)→ 命令行扫描 -m emrg.server
// 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、CIM 命令行过滤 TUI、
// install\git\ 前缀连坐强杀 bundled git、verify 残留检查(emrgd.pid 已废弃,
// rant 2026-08-21T16:45:06:固定端口为存活 ground truth);
// 有残留 exit 1(脚本打印残留清单)→ 中止安装。干净安装(无旧 install)直接跳过。
// {app} 是旧版安装目录——不能依赖旧版 emrg 命令(可能无 stop 子命令),所以用
// 单文件脚本 + runtime python({app}\bin\python-dist\python.exe,R90 布局,
Expand Down
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
137 changes: 76 additions & 61 deletions bin/emrg-uninstall
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,15 +44,19 @@ from pathlib import Path
EMRG_DIR = Path.home() / ".emrg"
LOG_DIR = EMRG_DIR / "logs"
GRAVEYARD_DIR = EMRG_DIR / "graveyard"
PORT_FILE = EMRG_DIR / "emrgd.port"
PID_FILE = EMRG_DIR / "emrgd.pid"
INSTALL_DIR = EMRG_DIR / "install"

# Rant 2026-08-25T18:46(宿主):emrgd.pid / emrgd.port 已彻底废弃——不写不读。
# 固定端口(EMRGD_PORT=56031)是 daemon 存活唯一 ground truth(rant
# 2026-08-19T08:05:21);token 只由 emrgd.token 单行承载(rant 2026-08-20T14:32:52)。
# WHITELIST 里仍保留这两个文件名,仅为卸载时清理老安装的历史残留(不创建不读取)。

# R101 whitelist — known EMRG files. Anything else in ~/.emrg is user data
# and is preserved (listed in the report instead of deleted).
# R121: 补全运行时文件 — emrgd.log / emrg-gui.log / gui-window.json 为 daemon/GUI
# 运行日志与窗口状态;skills/ 是 daemon 启动时 mkdir 的运行时骨架(非用户数据),
# 用户自定义 skills 先入 graveyard 快照再删除(rant 2026-08-05T15:35:17)。
# emrgd.pid / emrgd.port:历史遗留(已废弃),仅卸载时清理。
WHITELIST = [
"install", "versions", "config.toml", "sessions", "memory", "logs",
"projects.yml", "tasks.yml", "rants.jsonl", "saturation",
Expand DownExpand Up@@ -99,33 +103,37 @@ def stop_gui() -> dict:


def stop_daemon() -> dict:
"""Step 1b — stop the daemon. Returns {method, ok}."""
"""Step 1b — stop the daemon. Returns {method, ok}.

Rant 2026-08-25T18:46(宿主):不读 emrgd.pid / emrgd.port(已废弃)。
顺序:① ws 协议关闭(固定端口 56031 + emrgd.token 单行 token)
② 命令行扫描 ``-m emrg.server`` 进程 SIGTERM/taskkill(与
emrg._stop_all.stop_daemon 同语义,仅剩的可靠身份标记)
"""
result = {"method": "none", "ok": False}
# Try protocol shutdown when websockets is importable (PYTHONPATH has lib/).
EMRGD_PORT = 56031 # 固定端口(connect.py EMRGD_PORT,rant 2026-08-19T08:05:21)
token_path = EMRG_DIR / "emrgd.token"
try:
import asyncio
import json as _json

from websockets.asyncio.client import connect # type: ignore
from websockets.exceptions import ConnectionClosed # type: ignore

port = None
token = None
if PORT_FILE.exists():
lines = PORT_FILE.read_text(encoding="utf-8").splitlines()
if lines:
port = lines[0].strip()
if len(lines) > 1:
token = lines[1].strip()
token = ""
try:
token = token_path.read_text(encoding="utf-8").strip()
except OSError:
pass

async def _shutdown() -> bool:
if not port:
if not token:
return False
try:
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{port}", open_timeout=3), timeout=4)
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{EMRGD_PORT}", open_timeout=3), timeout=4)
try:
# Auth handshake (mirrors connect.py connect_to_server).
await ws.send(_json.dumps({"type": "auth", "token": token or ""}, ensure_ascii=False))
await ws.send(_json.dumps({"type": "auth", "token": token}, ensure_ascii=False))
ack = _json.loads(await asyncio.wait_for(ws.recv(), timeout=10))
if ack.get("type") != "auth_ok":
await ws.close()
Expand All@@ -141,61 +149,68 @@ def stop_daemon() -> dict:
except (ConnectionClosed, OSError, asyncio.TimeoutError, _json.JSONDecodeError):
return False

if port:
if token:
ok = asyncio.run(_shutdown())
if ok:
result = {"method": "protocol-shutdown", "ok": True}
print(" [1] daemon stopped (protocol shutdown)")
return result
except Exception:
pass # degrade to pid-based stop
pass # degrade to cmdline-scan stop

# Fallback: SIGTERM via pid file (POSIX) / taskkill (Windows).
pid = None
if PID_FILE.exists():
try:
pid = int(PID_FILE.read_text(encoding="utf-8").strip())
except ValueError:
pid = None
if not pid and PORT_FILE.exists():
# port file second line is the token, not pid — skip
pass
if pid:
try:
if os.name == "nt":
subprocess.run(
["taskkill", "/PID", str(pid), "/F"],
capture_output=True, timeout=10,
)
# R121: 轮询确认进程退出(≤5s),避免 pid 文件被并发重建
for _ in range(35):
chk = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True, text=True, timeout=5,
)
if "No tasks" in chk.stdout:
break
time.sleep(0.15)
else:
os.kill(pid, signal.SIGTERM)
for _ in range(20):
try:
os.kill(pid, 0)
# Fallback: kill by command-line identity (``-m emrg.server``) — the only
# reliable marker on Windows (rant 2026-08-17T17:03:38; emrg._stop_all
# same approach). No pid file involved.
import subprocess as _sp
try:
if os.name == "nt":
out = _sp.run(
["wmic", "process", "where", "name='pythonw.exe'", "get", "processid,commandline"],
capture_output=True, text=True, timeout=10,
).stdout or ""
# PowerShell CIM fallback for Win11 24H2+ (no wmic).
if not out.strip() or "emrg.server" not in out:
ps_cmd = ("Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match "
"'emrg.server' } | ForEach-Object { $_.ProcessId }")
out = _sp.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(p) for p in re.findall(r"\d+", out) if p.strip()]
for pid in pids:
_sp.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, timeout=10)
else:
out = _sp.run(
["ps", "-axww", "-o", "pid=,command="],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(m.group(1)) for m in re.finditer(r"^\s*(\d+)\s+.*-m emrg\.server\b", out, re.M)]
for pid in pids:
try:
os.kill(pid, signal.SIGTERM)
except OSError:
pass
if pids:
# Poll ≤5s for the port to close (ground truth).
import socket as _socket
for _ in range(35):
try:
with _socket.create_connection(("127.0.0.1", EMRGD_PORT), timeout=0.3):
time.sleep(0.15)
except OSError:
break
result = {"method": "pid-sigterm", "ok": True}
print(f" [1] daemon stopped (pid {pid})")
except OSError:
break
result = {"method": "cmdline-scan", "ok": True}
print(f" [1] daemon stopped (cmdline scan, {len(pids)} pid(s))")
return result
except (OSError, subprocess.SubprocessError):
pass
# Fallback: remove stale runtime files.
for f in (PORT_FILE, PID_FILE):
try:
f.unlink()
except FileNotFoundError:
pass
print(" [1] daemon not running (or already stopped); removed stale port/pid files")
except (OSError, subprocess.SubprocessError, _sp.SubprocessError):
pass
# Token cleanup once port is confirmed closed (daemon removes it itself on
# graceful shutdown; a force-killed daemon cannot).
try:
token_path.unlink()
except OSError:
pass
print(" [1] daemon not running (or already stopped)")
return result


Expand Down
6 changes: 4 additions & 2 deletions packaging/make-installer.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -414,8 +414,10 @@ end;
// Inno CloseApplications 看不到无窗口的 pythonw daemon(emrgd.cmd → pythonw.exe
// -m emrg.server 常驻锁文件),覆盖 ~/.emrg\install 时卡在"停止已有进程"。
// PrepareToInstall 在安装开始前用 runtime 的 python 运行 bin\stop_all.py:
// ws 协议关闭 daemon → emrgd.pid 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、
// CIM 命令行过滤 TUI、install\git\ 前缀连坐强杀 bundled git、verify 残留检查;
// ws 协议关闭 daemon(固定端口 56031 + emrgd.token)→ 命令行扫描 -m emrg.server
// 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、CIM 命令行过滤 TUI、
// install\git\ 前缀连坐强杀 bundled git、verify 残留检查(emrgd.pid 已废弃,
// rant 2026-08-21T16:45:06:固定端口为存活 ground truth);
// 有残留 exit 1(脚本打印残留清单)→ 中止安装。干净安装(无旧 install)直接跳过。
// {app} 是旧版安装目录——不能依赖旧版 emrg 命令(可能无 stop 子命令),所以用
// 单文件脚本 + runtime python({app}\bin\python-dist\python.exe,R90 布局,
Expand Down
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
137 changes: 76 additions & 61 deletions bin/emrg-uninstall
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,15 +44,19 @@ from pathlib import Path
EMRG_DIR = Path.home() / ".emrg"
LOG_DIR = EMRG_DIR / "logs"
GRAVEYARD_DIR = EMRG_DIR / "graveyard"
PORT_FILE = EMRG_DIR / "emrgd.port"
PID_FILE = EMRG_DIR / "emrgd.pid"
INSTALL_DIR = EMRG_DIR / "install"

# Rant 2026-08-25T18:46(宿主):emrgd.pid / emrgd.port 已彻底废弃——不写不读。
# 固定端口(EMRGD_PORT=56031)是 daemon 存活唯一 ground truth(rant
# 2026-08-19T08:05:21);token 只由 emrgd.token 单行承载(rant 2026-08-20T14:32:52)。
# WHITELIST 里仍保留这两个文件名,仅为卸载时清理老安装的历史残留(不创建不读取)。

# R101 whitelist — known EMRG files. Anything else in ~/.emrg is user data
# and is preserved (listed in the report instead of deleted).
# R121: 补全运行时文件 — emrgd.log / emrg-gui.log / gui-window.json 为 daemon/GUI
# 运行日志与窗口状态;skills/ 是 daemon 启动时 mkdir 的运行时骨架(非用户数据),
# 用户自定义 skills 先入 graveyard 快照再删除(rant 2026-08-05T15:35:17)。
# emrgd.pid / emrgd.port:历史遗留(已废弃),仅卸载时清理。
WHITELIST = [
"install", "versions", "config.toml", "sessions", "memory", "logs",
"projects.yml", "tasks.yml", "rants.jsonl", "saturation",
Expand DownExpand Up@@ -99,33 +103,37 @@ def stop_gui() -> dict:


def stop_daemon() -> dict:
"""Step 1b — stop the daemon. Returns {method, ok}."""
"""Step 1b — stop the daemon. Returns {method, ok}.

Rant 2026-08-25T18:46(宿主):不读 emrgd.pid / emrgd.port(已废弃)。
顺序:① ws 协议关闭(固定端口 56031 + emrgd.token 单行 token)
② 命令行扫描 ``-m emrg.server`` 进程 SIGTERM/taskkill(与
emrg._stop_all.stop_daemon 同语义,仅剩的可靠身份标记)
"""
result = {"method": "none", "ok": False}
# Try protocol shutdown when websockets is importable (PYTHONPATH has lib/).
EMRGD_PORT = 56031 # 固定端口(connect.py EMRGD_PORT,rant 2026-08-19T08:05:21)
token_path = EMRG_DIR / "emrgd.token"
try:
import asyncio
import json as _json

from websockets.asyncio.client import connect # type: ignore
from websockets.exceptions import ConnectionClosed # type: ignore

port = None
token = None
if PORT_FILE.exists():
lines = PORT_FILE.read_text(encoding="utf-8").splitlines()
if lines:
port = lines[0].strip()
if len(lines) > 1:
token = lines[1].strip()
token = ""
try:
token = token_path.read_text(encoding="utf-8").strip()
except OSError:
pass

async def _shutdown() -> bool:
if not port:
if not token:
return False
try:
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{port}", open_timeout=3), timeout=4)
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{EMRGD_PORT}", open_timeout=3), timeout=4)
try:
# Auth handshake (mirrors connect.py connect_to_server).
await ws.send(_json.dumps({"type": "auth", "token": token or ""}, ensure_ascii=False))
await ws.send(_json.dumps({"type": "auth", "token": token}, ensure_ascii=False))
ack = _json.loads(await asyncio.wait_for(ws.recv(), timeout=10))
if ack.get("type") != "auth_ok":
await ws.close()
Expand All@@ -141,61 +149,68 @@ def stop_daemon() -> dict:
except (ConnectionClosed, OSError, asyncio.TimeoutError, _json.JSONDecodeError):
return False

if port:
if token:
ok = asyncio.run(_shutdown())
if ok:
result = {"method": "protocol-shutdown", "ok": True}
print(" [1] daemon stopped (protocol shutdown)")
return result
except Exception:
pass # degrade to pid-based stop
pass # degrade to cmdline-scan stop

# Fallback: SIGTERM via pid file (POSIX) / taskkill (Windows).
pid = None
if PID_FILE.exists():
try:
pid = int(PID_FILE.read_text(encoding="utf-8").strip())
except ValueError:
pid = None
if not pid and PORT_FILE.exists():
# port file second line is the token, not pid — skip
pass
if pid:
try:
if os.name == "nt":
subprocess.run(
["taskkill", "/PID", str(pid), "/F"],
capture_output=True, timeout=10,
)
# R121: 轮询确认进程退出(≤5s),避免 pid 文件被并发重建
for _ in range(35):
chk = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True, text=True, timeout=5,
)
if "No tasks" in chk.stdout:
break
time.sleep(0.15)
else:
os.kill(pid, signal.SIGTERM)
for _ in range(20):
try:
os.kill(pid, 0)
# Fallback: kill by command-line identity (``-m emrg.server``) — the only
# reliable marker on Windows (rant 2026-08-17T17:03:38; emrg._stop_all
# same approach). No pid file involved.
import subprocess as _sp
try:
if os.name == "nt":
out = _sp.run(
["wmic", "process", "where", "name='pythonw.exe'", "get", "processid,commandline"],
capture_output=True, text=True, timeout=10,
).stdout or ""
# PowerShell CIM fallback for Win11 24H2+ (no wmic).
if not out.strip() or "emrg.server" not in out:
ps_cmd = ("Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match "
"'emrg.server' } | ForEach-Object { $_.ProcessId }")
out = _sp.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(p) for p in re.findall(r"\d+", out) if p.strip()]
for pid in pids:
_sp.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, timeout=10)
else:
out = _sp.run(
["ps", "-axww", "-o", "pid=,command="],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(m.group(1)) for m in re.finditer(r"^\s*(\d+)\s+.*-m emrg\.server\b", out, re.M)]
for pid in pids:
try:
os.kill(pid, signal.SIGTERM)
except OSError:
pass
if pids:
# Poll ≤5s for the port to close (ground truth).
import socket as _socket
for _ in range(35):
try:
with _socket.create_connection(("127.0.0.1", EMRGD_PORT), timeout=0.3):
time.sleep(0.15)
except OSError:
break
result = {"method": "pid-sigterm", "ok": True}
print(f" [1] daemon stopped (pid {pid})")
except OSError:
break
result = {"method": "cmdline-scan", "ok": True}
print(f" [1] daemon stopped (cmdline scan, {len(pids)} pid(s))")
return result
except (OSError, subprocess.SubprocessError):
pass
# Fallback: remove stale runtime files.
for f in (PORT_FILE, PID_FILE):
try:
f.unlink()
except FileNotFoundError:
pass
print(" [1] daemon not running (or already stopped); removed stale port/pid files")
except (OSError, subprocess.SubprocessError, _sp.SubprocessError):
pass
# Token cleanup once port is confirmed closed (daemon removes it itself on
# graceful shutdown; a force-killed daemon cannot).
try:
token_path.unlink()
except OSError:
pass
print(" [1] daemon not running (or already stopped)")
return result


Expand Down
6 changes: 4 additions & 2 deletions packaging/make-installer.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -414,8 +414,10 @@ end;
// Inno CloseApplications 看不到无窗口的 pythonw daemon(emrgd.cmd → pythonw.exe
// -m emrg.server 常驻锁文件),覆盖 ~/.emrg\install 时卡在"停止已有进程"。
// PrepareToInstall 在安装开始前用 runtime 的 python 运行 bin\stop_all.py:
// ws 协议关闭 daemon → emrgd.pid 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、
// CIM 命令行过滤 TUI、install\git\ 前缀连坐强杀 bundled git、verify 残留检查;
// ws 协议关闭 daemon(固定端口 56031 + emrgd.token)→ 命令行扫描 -m emrg.server
// 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、CIM 命令行过滤 TUI、
// install\git\ 前缀连坐强杀 bundled git、verify 残留检查(emrgd.pid 已废弃,
// rant 2026-08-21T16:45:06:固定端口为存活 ground truth);
// 有残留 exit 1(脚本打印残留清单)→ 中止安装。干净安装(无旧 install)直接跳过。
// {app} 是旧版安装目录——不能依赖旧版 emrg 命令(可能无 stop 子命令),所以用
// 单文件脚本 + runtime python({app}\bin\python-dist\python.exe,R90 布局,
Expand Down
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
137 changes: 76 additions & 61 deletions bin/emrg-uninstall
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,15 +44,19 @@ from pathlib import Path
EMRG_DIR = Path.home() / ".emrg"
LOG_DIR = EMRG_DIR / "logs"
GRAVEYARD_DIR = EMRG_DIR / "graveyard"
PORT_FILE = EMRG_DIR / "emrgd.port"
PID_FILE = EMRG_DIR / "emrgd.pid"
INSTALL_DIR = EMRG_DIR / "install"

# Rant 2026-08-25T18:46(宿主):emrgd.pid / emrgd.port 已彻底废弃——不写不读。
# 固定端口(EMRGD_PORT=56031)是 daemon 存活唯一 ground truth(rant
# 2026-08-19T08:05:21);token 只由 emrgd.token 单行承载(rant 2026-08-20T14:32:52)。
# WHITELIST 里仍保留这两个文件名,仅为卸载时清理老安装的历史残留(不创建不读取)。

# R101 whitelist — known EMRG files. Anything else in ~/.emrg is user data
# and is preserved (listed in the report instead of deleted).
# R121: 补全运行时文件 — emrgd.log / emrg-gui.log / gui-window.json 为 daemon/GUI
# 运行日志与窗口状态;skills/ 是 daemon 启动时 mkdir 的运行时骨架(非用户数据),
# 用户自定义 skills 先入 graveyard 快照再删除(rant 2026-08-05T15:35:17)。
# emrgd.pid / emrgd.port:历史遗留(已废弃),仅卸载时清理。
WHITELIST = [
"install", "versions", "config.toml", "sessions", "memory", "logs",
"projects.yml", "tasks.yml", "rants.jsonl", "saturation",
Expand DownExpand Up@@ -99,33 +103,37 @@ def stop_gui() -> dict:


def stop_daemon() -> dict:
"""Step 1b — stop the daemon. Returns {method, ok}."""
"""Step 1b — stop the daemon. Returns {method, ok}.

Rant 2026-08-25T18:46(宿主):不读 emrgd.pid / emrgd.port(已废弃)。
顺序:① ws 协议关闭(固定端口 56031 + emrgd.token 单行 token)
② 命令行扫描 ``-m emrg.server`` 进程 SIGTERM/taskkill(与
emrg._stop_all.stop_daemon 同语义,仅剩的可靠身份标记)
"""
result = {"method": "none", "ok": False}
# Try protocol shutdown when websockets is importable (PYTHONPATH has lib/).
EMRGD_PORT = 56031 # 固定端口(connect.py EMRGD_PORT,rant 2026-08-19T08:05:21)
token_path = EMRG_DIR / "emrgd.token"
try:
import asyncio
import json as _json

from websockets.asyncio.client import connect # type: ignore
from websockets.exceptions import ConnectionClosed # type: ignore

port = None
token = None
if PORT_FILE.exists():
lines = PORT_FILE.read_text(encoding="utf-8").splitlines()
if lines:
port = lines[0].strip()
if len(lines) > 1:
token = lines[1].strip()
token = ""
try:
token = token_path.read_text(encoding="utf-8").strip()
except OSError:
pass

async def _shutdown() -> bool:
if not port:
if not token:
return False
try:
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{port}", open_timeout=3), timeout=4)
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{EMRGD_PORT}", open_timeout=3), timeout=4)
try:
# Auth handshake (mirrors connect.py connect_to_server).
await ws.send(_json.dumps({"type": "auth", "token": token or ""}, ensure_ascii=False))
await ws.send(_json.dumps({"type": "auth", "token": token}, ensure_ascii=False))
ack = _json.loads(await asyncio.wait_for(ws.recv(), timeout=10))
if ack.get("type") != "auth_ok":
await ws.close()
Expand All@@ -141,61 +149,68 @@ def stop_daemon() -> dict:
except (ConnectionClosed, OSError, asyncio.TimeoutError, _json.JSONDecodeError):
return False

if port:
if token:
ok = asyncio.run(_shutdown())
if ok:
result = {"method": "protocol-shutdown", "ok": True}
print(" [1] daemon stopped (protocol shutdown)")
return result
except Exception:
pass # degrade to pid-based stop
pass # degrade to cmdline-scan stop

# Fallback: SIGTERM via pid file (POSIX) / taskkill (Windows).
pid = None
if PID_FILE.exists():
try:
pid = int(PID_FILE.read_text(encoding="utf-8").strip())
except ValueError:
pid = None
if not pid and PORT_FILE.exists():
# port file second line is the token, not pid — skip
pass
if pid:
try:
if os.name == "nt":
subprocess.run(
["taskkill", "/PID", str(pid), "/F"],
capture_output=True, timeout=10,
)
# R121: 轮询确认进程退出(≤5s),避免 pid 文件被并发重建
for _ in range(35):
chk = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True, text=True, timeout=5,
)
if "No tasks" in chk.stdout:
break
time.sleep(0.15)
else:
os.kill(pid, signal.SIGTERM)
for _ in range(20):
try:
os.kill(pid, 0)
# Fallback: kill by command-line identity (``-m emrg.server``) — the only
# reliable marker on Windows (rant 2026-08-17T17:03:38; emrg._stop_all
# same approach). No pid file involved.
import subprocess as _sp
try:
if os.name == "nt":
out = _sp.run(
["wmic", "process", "where", "name='pythonw.exe'", "get", "processid,commandline"],
capture_output=True, text=True, timeout=10,
).stdout or ""
# PowerShell CIM fallback for Win11 24H2+ (no wmic).
if not out.strip() or "emrg.server" not in out:
ps_cmd = ("Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match "
"'emrg.server' } | ForEach-Object { $_.ProcessId }")
out = _sp.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(p) for p in re.findall(r"\d+", out) if p.strip()]
for pid in pids:
_sp.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, timeout=10)
else:
out = _sp.run(
["ps", "-axww", "-o", "pid=,command="],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(m.group(1)) for m in re.finditer(r"^\s*(\d+)\s+.*-m emrg\.server\b", out, re.M)]
for pid in pids:
try:
os.kill(pid, signal.SIGTERM)
except OSError:
pass
if pids:
# Poll ≤5s for the port to close (ground truth).
import socket as _socket
for _ in range(35):
try:
with _socket.create_connection(("127.0.0.1", EMRGD_PORT), timeout=0.3):
time.sleep(0.15)
except OSError:
break
result = {"method": "pid-sigterm", "ok": True}
print(f" [1] daemon stopped (pid {pid})")
except OSError:
break
result = {"method": "cmdline-scan", "ok": True}
print(f" [1] daemon stopped (cmdline scan, {len(pids)} pid(s))")
return result
except (OSError, subprocess.SubprocessError):
pass
# Fallback: remove stale runtime files.
for f in (PORT_FILE, PID_FILE):
try:
f.unlink()
except FileNotFoundError:
pass
print(" [1] daemon not running (or already stopped); removed stale port/pid files")
except (OSError, subprocess.SubprocessError, _sp.SubprocessError):
pass
# Token cleanup once port is confirmed closed (daemon removes it itself on
# graceful shutdown; a force-killed daemon cannot).
try:
token_path.unlink()
except OSError:
pass
print(" [1] daemon not running (or already stopped)")
return result


Expand Down
6 changes: 4 additions & 2 deletions packaging/make-installer.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -414,8 +414,10 @@ end;
// Inno CloseApplications 看不到无窗口的 pythonw daemon(emrgd.cmd → pythonw.exe
// -m emrg.server 常驻锁文件),覆盖 ~/.emrg\install 时卡在"停止已有进程"。
// PrepareToInstall 在安装开始前用 runtime 的 python 运行 bin\stop_all.py:
// ws 协议关闭 daemon → emrgd.pid 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、
// CIM 命令行过滤 TUI、install\git\ 前缀连坐强杀 bundled git、verify 残留检查;
// ws 协议关闭 daemon(固定端口 56031 + emrgd.token)→ 命令行扫描 -m emrg.server
// 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、CIM 命令行过滤 TUI、
// install\git\ 前缀连坐强杀 bundled git、verify 残留检查(emrgd.pid 已废弃,
// rant 2026-08-21T16:45:06:固定端口为存活 ground truth);
// 有残留 exit 1(脚本打印残留清单)→ 中止安装。干净安装(无旧 install)直接跳过。
// {app} 是旧版安装目录——不能依赖旧版 emrg 命令(可能无 stop 子命令),所以用
// 单文件脚本 + runtime python({app}\bin\python-dist\python.exe,R90 布局,
Expand Down
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
137 changes: 76 additions & 61 deletions bin/emrg-uninstall
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,15 +44,19 @@ from pathlib import Path
EMRG_DIR = Path.home() / ".emrg"
LOG_DIR = EMRG_DIR / "logs"
GRAVEYARD_DIR = EMRG_DIR / "graveyard"
PORT_FILE = EMRG_DIR / "emrgd.port"
PID_FILE = EMRG_DIR / "emrgd.pid"
INSTALL_DIR = EMRG_DIR / "install"

# Rant 2026-08-25T18:46(宿主):emrgd.pid / emrgd.port 已彻底废弃——不写不读。
# 固定端口(EMRGD_PORT=56031)是 daemon 存活唯一 ground truth(rant
# 2026-08-19T08:05:21);token 只由 emrgd.token 单行承载(rant 2026-08-20T14:32:52)。
# WHITELIST 里仍保留这两个文件名,仅为卸载时清理老安装的历史残留(不创建不读取)。

# R101 whitelist — known EMRG files. Anything else in ~/.emrg is user data
# and is preserved (listed in the report instead of deleted).
# R121: 补全运行时文件 — emrgd.log / emrg-gui.log / gui-window.json 为 daemon/GUI
# 运行日志与窗口状态;skills/ 是 daemon 启动时 mkdir 的运行时骨架(非用户数据),
# 用户自定义 skills 先入 graveyard 快照再删除(rant 2026-08-05T15:35:17)。
# emrgd.pid / emrgd.port:历史遗留(已废弃),仅卸载时清理。
WHITELIST = [
"install", "versions", "config.toml", "sessions", "memory", "logs",
"projects.yml", "tasks.yml", "rants.jsonl", "saturation",
Expand DownExpand Up@@ -99,33 +103,37 @@ def stop_gui() -> dict:


def stop_daemon() -> dict:
"""Step 1b — stop the daemon. Returns {method, ok}."""
"""Step 1b — stop the daemon. Returns {method, ok}.

Rant 2026-08-25T18:46(宿主):不读 emrgd.pid / emrgd.port(已废弃)。
顺序:① ws 协议关闭(固定端口 56031 + emrgd.token 单行 token)
② 命令行扫描 ``-m emrg.server`` 进程 SIGTERM/taskkill(与
emrg._stop_all.stop_daemon 同语义,仅剩的可靠身份标记)
"""
result = {"method": "none", "ok": False}
# Try protocol shutdown when websockets is importable (PYTHONPATH has lib/).
EMRGD_PORT = 56031 # 固定端口(connect.py EMRGD_PORT,rant 2026-08-19T08:05:21)
token_path = EMRG_DIR / "emrgd.token"
try:
import asyncio
import json as _json

from websockets.asyncio.client import connect # type: ignore
from websockets.exceptions import ConnectionClosed # type: ignore

port = None
token = None
if PORT_FILE.exists():
lines = PORT_FILE.read_text(encoding="utf-8").splitlines()
if lines:
port = lines[0].strip()
if len(lines) > 1:
token = lines[1].strip()
token = ""
try:
token = token_path.read_text(encoding="utf-8").strip()
except OSError:
pass

async def _shutdown() -> bool:
if not port:
if not token:
return False
try:
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{port}", open_timeout=3), timeout=4)
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{EMRGD_PORT}", open_timeout=3), timeout=4)
try:
# Auth handshake (mirrors connect.py connect_to_server).
await ws.send(_json.dumps({"type": "auth", "token": token or ""}, ensure_ascii=False))
await ws.send(_json.dumps({"type": "auth", "token": token}, ensure_ascii=False))
ack = _json.loads(await asyncio.wait_for(ws.recv(), timeout=10))
if ack.get("type") != "auth_ok":
await ws.close()
Expand All@@ -141,61 +149,68 @@ def stop_daemon() -> dict:
except (ConnectionClosed, OSError, asyncio.TimeoutError, _json.JSONDecodeError):
return False

if port:
if token:
ok = asyncio.run(_shutdown())
if ok:
result = {"method": "protocol-shutdown", "ok": True}
print(" [1] daemon stopped (protocol shutdown)")
return result
except Exception:
pass # degrade to pid-based stop
pass # degrade to cmdline-scan stop

# Fallback: SIGTERM via pid file (POSIX) / taskkill (Windows).
pid = None
if PID_FILE.exists():
try:
pid = int(PID_FILE.read_text(encoding="utf-8").strip())
except ValueError:
pid = None
if not pid and PORT_FILE.exists():
# port file second line is the token, not pid — skip
pass
if pid:
try:
if os.name == "nt":
subprocess.run(
["taskkill", "/PID", str(pid), "/F"],
capture_output=True, timeout=10,
)
# R121: 轮询确认进程退出(≤5s),避免 pid 文件被并发重建
for _ in range(35):
chk = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True, text=True, timeout=5,
)
if "No tasks" in chk.stdout:
break
time.sleep(0.15)
else:
os.kill(pid, signal.SIGTERM)
for _ in range(20):
try:
os.kill(pid, 0)
# Fallback: kill by command-line identity (``-m emrg.server``) — the only
# reliable marker on Windows (rant 2026-08-17T17:03:38; emrg._stop_all
# same approach). No pid file involved.
import subprocess as _sp
try:
if os.name == "nt":
out = _sp.run(
["wmic", "process", "where", "name='pythonw.exe'", "get", "processid,commandline"],
capture_output=True, text=True, timeout=10,
).stdout or ""
# PowerShell CIM fallback for Win11 24H2+ (no wmic).
if not out.strip() or "emrg.server" not in out:
ps_cmd = ("Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match "
"'emrg.server' } | ForEach-Object { $_.ProcessId }")
out = _sp.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(p) for p in re.findall(r"\d+", out) if p.strip()]
for pid in pids:
_sp.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, timeout=10)
else:
out = _sp.run(
["ps", "-axww", "-o", "pid=,command="],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(m.group(1)) for m in re.finditer(r"^\s*(\d+)\s+.*-m emrg\.server\b", out, re.M)]
for pid in pids:
try:
os.kill(pid, signal.SIGTERM)
except OSError:
pass
if pids:
# Poll ≤5s for the port to close (ground truth).
import socket as _socket
for _ in range(35):
try:
with _socket.create_connection(("127.0.0.1", EMRGD_PORT), timeout=0.3):
time.sleep(0.15)
except OSError:
break
result = {"method": "pid-sigterm", "ok": True}
print(f" [1] daemon stopped (pid {pid})")
except OSError:
break
result = {"method": "cmdline-scan", "ok": True}
print(f" [1] daemon stopped (cmdline scan, {len(pids)} pid(s))")
return result
except (OSError, subprocess.SubprocessError):
pass
# Fallback: remove stale runtime files.
for f in (PORT_FILE, PID_FILE):
try:
f.unlink()
except FileNotFoundError:
pass
print(" [1] daemon not running (or already stopped); removed stale port/pid files")
except (OSError, subprocess.SubprocessError, _sp.SubprocessError):
pass
# Token cleanup once port is confirmed closed (daemon removes it itself on
# graceful shutdown; a force-killed daemon cannot).
try:
token_path.unlink()
except OSError:
pass
print(" [1] daemon not running (or already stopped)")
return result


Expand Down
6 changes: 4 additions & 2 deletions packaging/make-installer.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -414,8 +414,10 @@ end;
// Inno CloseApplications 看不到无窗口的 pythonw daemon(emrgd.cmd → pythonw.exe
// -m emrg.server 常驻锁文件),覆盖 ~/.emrg\install 时卡在"停止已有进程"。
// PrepareToInstall 在安装开始前用 runtime 的 python 运行 bin\stop_all.py:
// ws 协议关闭 daemon → emrgd.pid 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、
// CIM 命令行过滤 TUI、install\git\ 前缀连坐强杀 bundled git、verify 残留检查;
// ws 协议关闭 daemon(固定端口 56031 + emrgd.token)→ 命令行扫描 -m emrg.server
// 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、CIM 命令行过滤 TUI、
// install\git\ 前缀连坐强杀 bundled git、verify 残留检查(emrgd.pid 已废弃,
// rant 2026-08-21T16:45:06:固定端口为存活 ground truth);
// 有残留 exit 1(脚本打印残留清单)→ 中止安装。干净安装(无旧 install)直接跳过。
// {app} 是旧版安装目录——不能依赖旧版 emrg 命令(可能无 stop 子命令),所以用
// 单文件脚本 + runtime python({app}\bin\python-dist\python.exe,R90 布局,
Expand Down
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
137 changes: 76 additions & 61 deletions bin/emrg-uninstall
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,15 +44,19 @@ from pathlib import Path
EMRG_DIR = Path.home() / ".emrg"
LOG_DIR = EMRG_DIR / "logs"
GRAVEYARD_DIR = EMRG_DIR / "graveyard"
PORT_FILE = EMRG_DIR / "emrgd.port"
PID_FILE = EMRG_DIR / "emrgd.pid"
INSTALL_DIR = EMRG_DIR / "install"

# Rant 2026-08-25T18:46(宿主):emrgd.pid / emrgd.port 已彻底废弃——不写不读。
# 固定端口(EMRGD_PORT=56031)是 daemon 存活唯一 ground truth(rant
# 2026-08-19T08:05:21);token 只由 emrgd.token 单行承载(rant 2026-08-20T14:32:52)。
# WHITELIST 里仍保留这两个文件名,仅为卸载时清理老安装的历史残留(不创建不读取)。

# R101 whitelist — known EMRG files. Anything else in ~/.emrg is user data
# and is preserved (listed in the report instead of deleted).
# R121: 补全运行时文件 — emrgd.log / emrg-gui.log / gui-window.json 为 daemon/GUI
# 运行日志与窗口状态;skills/ 是 daemon 启动时 mkdir 的运行时骨架(非用户数据),
# 用户自定义 skills 先入 graveyard 快照再删除(rant 2026-08-05T15:35:17)。
# emrgd.pid / emrgd.port:历史遗留(已废弃),仅卸载时清理。
WHITELIST = [
"install", "versions", "config.toml", "sessions", "memory", "logs",
"projects.yml", "tasks.yml", "rants.jsonl", "saturation",
Expand DownExpand Up@@ -99,33 +103,37 @@ def stop_gui() -> dict:


def stop_daemon() -> dict:
"""Step 1b — stop the daemon. Returns {method, ok}."""
"""Step 1b — stop the daemon. Returns {method, ok}.

Rant 2026-08-25T18:46(宿主):不读 emrgd.pid / emrgd.port(已废弃)。
顺序:① ws 协议关闭(固定端口 56031 + emrgd.token 单行 token)
② 命令行扫描 ``-m emrg.server`` 进程 SIGTERM/taskkill(与
emrg._stop_all.stop_daemon 同语义,仅剩的可靠身份标记)
"""
result = {"method": "none", "ok": False}
# Try protocol shutdown when websockets is importable (PYTHONPATH has lib/).
EMRGD_PORT = 56031 # 固定端口(connect.py EMRGD_PORT,rant 2026-08-19T08:05:21)
token_path = EMRG_DIR / "emrgd.token"
try:
import asyncio
import json as _json

from websockets.asyncio.client import connect # type: ignore
from websockets.exceptions import ConnectionClosed # type: ignore

port = None
token = None
if PORT_FILE.exists():
lines = PORT_FILE.read_text(encoding="utf-8").splitlines()
if lines:
port = lines[0].strip()
if len(lines) > 1:
token = lines[1].strip()
token = ""
try:
token = token_path.read_text(encoding="utf-8").strip()
except OSError:
pass

async def _shutdown() -> bool:
if not port:
if not token:
return False
try:
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{port}", open_timeout=3), timeout=4)
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{EMRGD_PORT}", open_timeout=3), timeout=4)
try:
# Auth handshake (mirrors connect.py connect_to_server).
await ws.send(_json.dumps({"type": "auth", "token": token or ""}, ensure_ascii=False))
await ws.send(_json.dumps({"type": "auth", "token": token}, ensure_ascii=False))
ack = _json.loads(await asyncio.wait_for(ws.recv(), timeout=10))
if ack.get("type") != "auth_ok":
await ws.close()
Expand All@@ -141,61 +149,68 @@ def stop_daemon() -> dict:
except (ConnectionClosed, OSError, asyncio.TimeoutError, _json.JSONDecodeError):
return False

if port:
if token:
ok = asyncio.run(_shutdown())
if ok:
result = {"method": "protocol-shutdown", "ok": True}
print(" [1] daemon stopped (protocol shutdown)")
return result
except Exception:
pass # degrade to pid-based stop
pass # degrade to cmdline-scan stop

# Fallback: SIGTERM via pid file (POSIX) / taskkill (Windows).
pid = None
if PID_FILE.exists():
try:
pid = int(PID_FILE.read_text(encoding="utf-8").strip())
except ValueError:
pid = None
if not pid and PORT_FILE.exists():
# port file second line is the token, not pid — skip
pass
if pid:
try:
if os.name == "nt":
subprocess.run(
["taskkill", "/PID", str(pid), "/F"],
capture_output=True, timeout=10,
)
# R121: 轮询确认进程退出(≤5s),避免 pid 文件被并发重建
for _ in range(35):
chk = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True, text=True, timeout=5,
)
if "No tasks" in chk.stdout:
break
time.sleep(0.15)
else:
os.kill(pid, signal.SIGTERM)
for _ in range(20):
try:
os.kill(pid, 0)
# Fallback: kill by command-line identity (``-m emrg.server``) — the only
# reliable marker on Windows (rant 2026-08-17T17:03:38; emrg._stop_all
# same approach). No pid file involved.
import subprocess as _sp
try:
if os.name == "nt":
out = _sp.run(
["wmic", "process", "where", "name='pythonw.exe'", "get", "processid,commandline"],
capture_output=True, text=True, timeout=10,
).stdout or ""
# PowerShell CIM fallback for Win11 24H2+ (no wmic).
if not out.strip() or "emrg.server" not in out:
ps_cmd = ("Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match "
"'emrg.server' } | ForEach-Object { $_.ProcessId }")
out = _sp.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(p) for p in re.findall(r"\d+", out) if p.strip()]
for pid in pids:
_sp.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, timeout=10)
else:
out = _sp.run(
["ps", "-axww", "-o", "pid=,command="],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(m.group(1)) for m in re.finditer(r"^\s*(\d+)\s+.*-m emrg\.server\b", out, re.M)]
for pid in pids:
try:
os.kill(pid, signal.SIGTERM)
except OSError:
pass
if pids:
# Poll ≤5s for the port to close (ground truth).
import socket as _socket
for _ in range(35):
try:
with _socket.create_connection(("127.0.0.1", EMRGD_PORT), timeout=0.3):
time.sleep(0.15)
except OSError:
break
result = {"method": "pid-sigterm", "ok": True}
print(f" [1] daemon stopped (pid {pid})")
except OSError:
break
result = {"method": "cmdline-scan", "ok": True}
print(f" [1] daemon stopped (cmdline scan, {len(pids)} pid(s))")
return result
except (OSError, subprocess.SubprocessError):
pass
# Fallback: remove stale runtime files.
for f in (PORT_FILE, PID_FILE):
try:
f.unlink()
except FileNotFoundError:
pass
print(" [1] daemon not running (or already stopped); removed stale port/pid files")
except (OSError, subprocess.SubprocessError, _sp.SubprocessError):
pass
# Token cleanup once port is confirmed closed (daemon removes it itself on
# graceful shutdown; a force-killed daemon cannot).
try:
token_path.unlink()
except OSError:
pass
print(" [1] daemon not running (or already stopped)")
return result


Expand Down
6 changes: 4 additions & 2 deletions packaging/make-installer.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -414,8 +414,10 @@ end;
// Inno CloseApplications 看不到无窗口的 pythonw daemon(emrgd.cmd → pythonw.exe
// -m emrg.server 常驻锁文件),覆盖 ~/.emrg\install 时卡在"停止已有进程"。
// PrepareToInstall 在安装开始前用 runtime 的 python 运行 bin\stop_all.py:
// ws 协议关闭 daemon → emrgd.pid 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、
// CIM 命令行过滤 TUI、install\git\ 前缀连坐强杀 bundled git、verify 残留检查;
// ws 协议关闭 daemon(固定端口 56031 + emrgd.token)→ 命令行扫描 -m emrg.server
// 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、CIM 命令行过滤 TUI、
// install\git\ 前缀连坐强杀 bundled git、verify 残留检查(emrgd.pid 已废弃,
// rant 2026-08-21T16:45:06:固定端口为存活 ground truth);
// 有残留 exit 1(脚本打印残留清单)→ 中止安装。干净安装(无旧 install)直接跳过。
// {app} 是旧版安装目录——不能依赖旧版 emrg 命令(可能无 stop 子命令),所以用
// 单文件脚本 + runtime python({app}\bin\python-dist\python.exe,R90 布局,
Expand Down
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
137 changes: 76 additions & 61 deletions bin/emrg-uninstall
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,15 +44,19 @@ from pathlib import Path
EMRG_DIR = Path.home() / ".emrg"
LOG_DIR = EMRG_DIR / "logs"
GRAVEYARD_DIR = EMRG_DIR / "graveyard"
PORT_FILE = EMRG_DIR / "emrgd.port"
PID_FILE = EMRG_DIR / "emrgd.pid"
INSTALL_DIR = EMRG_DIR / "install"

# Rant 2026-08-25T18:46(宿主):emrgd.pid / emrgd.port 已彻底废弃——不写不读。
# 固定端口(EMRGD_PORT=56031)是 daemon 存活唯一 ground truth(rant
# 2026-08-19T08:05:21);token 只由 emrgd.token 单行承载(rant 2026-08-20T14:32:52)。
# WHITELIST 里仍保留这两个文件名,仅为卸载时清理老安装的历史残留(不创建不读取)。

# R101 whitelist — known EMRG files. Anything else in ~/.emrg is user data
# and is preserved (listed in the report instead of deleted).
# R121: 补全运行时文件 — emrgd.log / emrg-gui.log / gui-window.json 为 daemon/GUI
# 运行日志与窗口状态;skills/ 是 daemon 启动时 mkdir 的运行时骨架(非用户数据),
# 用户自定义 skills 先入 graveyard 快照再删除(rant 2026-08-05T15:35:17)。
# emrgd.pid / emrgd.port:历史遗留(已废弃),仅卸载时清理。
WHITELIST = [
"install", "versions", "config.toml", "sessions", "memory", "logs",
"projects.yml", "tasks.yml", "rants.jsonl", "saturation",
Expand DownExpand Up@@ -99,33 +103,37 @@ def stop_gui() -> dict:


def stop_daemon() -> dict:
"""Step 1b — stop the daemon. Returns {method, ok}."""
"""Step 1b — stop the daemon. Returns {method, ok}.

Rant 2026-08-25T18:46(宿主):不读 emrgd.pid / emrgd.port(已废弃)。
顺序:① ws 协议关闭(固定端口 56031 + emrgd.token 单行 token)
② 命令行扫描 ``-m emrg.server`` 进程 SIGTERM/taskkill(与
emrg._stop_all.stop_daemon 同语义,仅剩的可靠身份标记)
"""
result = {"method": "none", "ok": False}
# Try protocol shutdown when websockets is importable (PYTHONPATH has lib/).
EMRGD_PORT = 56031 # 固定端口(connect.py EMRGD_PORT,rant 2026-08-19T08:05:21)
token_path = EMRG_DIR / "emrgd.token"
try:
import asyncio
import json as _json

from websockets.asyncio.client import connect # type: ignore
from websockets.exceptions import ConnectionClosed # type: ignore

port = None
token = None
if PORT_FILE.exists():
lines = PORT_FILE.read_text(encoding="utf-8").splitlines()
if lines:
port = lines[0].strip()
if len(lines) > 1:
token = lines[1].strip()
token = ""
try:
token = token_path.read_text(encoding="utf-8").strip()
except OSError:
pass

async def _shutdown() -> bool:
if not port:
if not token:
return False
try:
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{port}", open_timeout=3), timeout=4)
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{EMRGD_PORT}", open_timeout=3), timeout=4)
try:
# Auth handshake (mirrors connect.py connect_to_server).
await ws.send(_json.dumps({"type": "auth", "token": token or ""}, ensure_ascii=False))
await ws.send(_json.dumps({"type": "auth", "token": token}, ensure_ascii=False))
ack = _json.loads(await asyncio.wait_for(ws.recv(), timeout=10))
if ack.get("type") != "auth_ok":
await ws.close()
Expand All@@ -141,61 +149,68 @@ def stop_daemon() -> dict:
except (ConnectionClosed, OSError, asyncio.TimeoutError, _json.JSONDecodeError):
return False

if port:
if token:
ok = asyncio.run(_shutdown())
if ok:
result = {"method": "protocol-shutdown", "ok": True}
print(" [1] daemon stopped (protocol shutdown)")
return result
except Exception:
pass # degrade to pid-based stop
pass # degrade to cmdline-scan stop

# Fallback: SIGTERM via pid file (POSIX) / taskkill (Windows).
pid = None
if PID_FILE.exists():
try:
pid = int(PID_FILE.read_text(encoding="utf-8").strip())
except ValueError:
pid = None
if not pid and PORT_FILE.exists():
# port file second line is the token, not pid — skip
pass
if pid:
try:
if os.name == "nt":
subprocess.run(
["taskkill", "/PID", str(pid), "/F"],
capture_output=True, timeout=10,
)
# R121: 轮询确认进程退出(≤5s),避免 pid 文件被并发重建
for _ in range(35):
chk = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True, text=True, timeout=5,
)
if "No tasks" in chk.stdout:
break
time.sleep(0.15)
else:
os.kill(pid, signal.SIGTERM)
for _ in range(20):
try:
os.kill(pid, 0)
# Fallback: kill by command-line identity (``-m emrg.server``) — the only
# reliable marker on Windows (rant 2026-08-17T17:03:38; emrg._stop_all
# same approach). No pid file involved.
import subprocess as _sp
try:
if os.name == "nt":
out = _sp.run(
["wmic", "process", "where", "name='pythonw.exe'", "get", "processid,commandline"],
capture_output=True, text=True, timeout=10,
).stdout or ""
# PowerShell CIM fallback for Win11 24H2+ (no wmic).
if not out.strip() or "emrg.server" not in out:
ps_cmd = ("Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match "
"'emrg.server' } | ForEach-Object { $_.ProcessId }")
out = _sp.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(p) for p in re.findall(r"\d+", out) if p.strip()]
for pid in pids:
_sp.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, timeout=10)
else:
out = _sp.run(
["ps", "-axww", "-o", "pid=,command="],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(m.group(1)) for m in re.finditer(r"^\s*(\d+)\s+.*-m emrg\.server\b", out, re.M)]
for pid in pids:
try:
os.kill(pid, signal.SIGTERM)
except OSError:
pass
if pids:
# Poll ≤5s for the port to close (ground truth).
import socket as _socket
for _ in range(35):
try:
with _socket.create_connection(("127.0.0.1", EMRGD_PORT), timeout=0.3):
time.sleep(0.15)
except OSError:
break
result = {"method": "pid-sigterm", "ok": True}
print(f" [1] daemon stopped (pid {pid})")
except OSError:
break
result = {"method": "cmdline-scan", "ok": True}
print(f" [1] daemon stopped (cmdline scan, {len(pids)} pid(s))")
return result
except (OSError, subprocess.SubprocessError):
pass
# Fallback: remove stale runtime files.
for f in (PORT_FILE, PID_FILE):
try:
f.unlink()
except FileNotFoundError:
pass
print(" [1] daemon not running (or already stopped); removed stale port/pid files")
except (OSError, subprocess.SubprocessError, _sp.SubprocessError):
pass
# Token cleanup once port is confirmed closed (daemon removes it itself on
# graceful shutdown; a force-killed daemon cannot).
try:
token_path.unlink()
except OSError:
pass
print(" [1] daemon not running (or already stopped)")
return result


Expand Down
6 changes: 4 additions & 2 deletions packaging/make-installer.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -414,8 +414,10 @@ end;
// Inno CloseApplications 看不到无窗口的 pythonw daemon(emrgd.cmd → pythonw.exe
// -m emrg.server 常驻锁文件),覆盖 ~/.emrg\install 时卡在"停止已有进程"。
// PrepareToInstall 在安装开始前用 runtime 的 python 运行 bin\stop_all.py:
// ws 协议关闭 daemon → emrgd.pid 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、
// CIM 命令行过滤 TUI、install\git\ 前缀连坐强杀 bundled git、verify 残留检查;
// ws 协议关闭 daemon(固定端口 56031 + emrgd.token)→ 命令行扫描 -m emrg.server
// 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、CIM 命令行过滤 TUI、
// install\git\ 前缀连坐强杀 bundled git、verify 残留检查(emrgd.pid 已废弃,
// rant 2026-08-21T16:45:06:固定端口为存活 ground truth);
// 有残留 exit 1(脚本打印残留清单)→ 中止安装。干净安装(无旧 install)直接跳过。
// {app} 是旧版安装目录——不能依赖旧版 emrg 命令(可能无 stop 子命令),所以用
// 单文件脚本 + runtime python({app}\bin\python-dist\python.exe,R90 布局,
Expand Down
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
137 changes: 76 additions & 61 deletions bin/emrg-uninstall
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,15 +44,19 @@ from pathlib import Path
EMRG_DIR = Path.home() / ".emrg"
LOG_DIR = EMRG_DIR / "logs"
GRAVEYARD_DIR = EMRG_DIR / "graveyard"
PORT_FILE = EMRG_DIR / "emrgd.port"
PID_FILE = EMRG_DIR / "emrgd.pid"
INSTALL_DIR = EMRG_DIR / "install"

# Rant 2026-08-25T18:46(宿主):emrgd.pid / emrgd.port 已彻底废弃——不写不读。
# 固定端口(EMRGD_PORT=56031)是 daemon 存活唯一 ground truth(rant
# 2026-08-19T08:05:21);token 只由 emrgd.token 单行承载(rant 2026-08-20T14:32:52)。
# WHITELIST 里仍保留这两个文件名,仅为卸载时清理老安装的历史残留(不创建不读取)。

# R101 whitelist — known EMRG files. Anything else in ~/.emrg is user data
# and is preserved (listed in the report instead of deleted).
# R121: 补全运行时文件 — emrgd.log / emrg-gui.log / gui-window.json 为 daemon/GUI
# 运行日志与窗口状态;skills/ 是 daemon 启动时 mkdir 的运行时骨架(非用户数据),
# 用户自定义 skills 先入 graveyard 快照再删除(rant 2026-08-05T15:35:17)。
# emrgd.pid / emrgd.port:历史遗留(已废弃),仅卸载时清理。
WHITELIST = [
"install", "versions", "config.toml", "sessions", "memory", "logs",
"projects.yml", "tasks.yml", "rants.jsonl", "saturation",
Expand DownExpand Up@@ -99,33 +103,37 @@ def stop_gui() -> dict:


def stop_daemon() -> dict:
"""Step 1b — stop the daemon. Returns {method, ok}."""
"""Step 1b — stop the daemon. Returns {method, ok}.

Rant 2026-08-25T18:46(宿主):不读 emrgd.pid / emrgd.port(已废弃)。
顺序:① ws 协议关闭(固定端口 56031 + emrgd.token 单行 token)
② 命令行扫描 ``-m emrg.server`` 进程 SIGTERM/taskkill(与
emrg._stop_all.stop_daemon 同语义,仅剩的可靠身份标记)
"""
result = {"method": "none", "ok": False}
# Try protocol shutdown when websockets is importable (PYTHONPATH has lib/).
EMRGD_PORT = 56031 # 固定端口(connect.py EMRGD_PORT,rant 2026-08-19T08:05:21)
token_path = EMRG_DIR / "emrgd.token"
try:
import asyncio
import json as _json

from websockets.asyncio.client import connect # type: ignore
from websockets.exceptions import ConnectionClosed # type: ignore

port = None
token = None
if PORT_FILE.exists():
lines = PORT_FILE.read_text(encoding="utf-8").splitlines()
if lines:
port = lines[0].strip()
if len(lines) > 1:
token = lines[1].strip()
token = ""
try:
token = token_path.read_text(encoding="utf-8").strip()
except OSError:
pass

async def _shutdown() -> bool:
if not port:
if not token:
return False
try:
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{port}", open_timeout=3), timeout=4)
ws = await asyncio.wait_for(connect(f"ws://127.0.0.1:{EMRGD_PORT}", open_timeout=3), timeout=4)
try:
# Auth handshake (mirrors connect.py connect_to_server).
await ws.send(_json.dumps({"type": "auth", "token": token or ""}, ensure_ascii=False))
await ws.send(_json.dumps({"type": "auth", "token": token}, ensure_ascii=False))
ack = _json.loads(await asyncio.wait_for(ws.recv(), timeout=10))
if ack.get("type") != "auth_ok":
await ws.close()
Expand All@@ -141,61 +149,68 @@ def stop_daemon() -> dict:
except (ConnectionClosed, OSError, asyncio.TimeoutError, _json.JSONDecodeError):
return False

if port:
if token:
ok = asyncio.run(_shutdown())
if ok:
result = {"method": "protocol-shutdown", "ok": True}
print(" [1] daemon stopped (protocol shutdown)")
return result
except Exception:
pass # degrade to pid-based stop
pass # degrade to cmdline-scan stop

# Fallback: SIGTERM via pid file (POSIX) / taskkill (Windows).
pid = None
if PID_FILE.exists():
try:
pid = int(PID_FILE.read_text(encoding="utf-8").strip())
except ValueError:
pid = None
if not pid and PORT_FILE.exists():
# port file second line is the token, not pid — skip
pass
if pid:
try:
if os.name == "nt":
subprocess.run(
["taskkill", "/PID", str(pid), "/F"],
capture_output=True, timeout=10,
)
# R121: 轮询确认进程退出(≤5s),避免 pid 文件被并发重建
for _ in range(35):
chk = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True, text=True, timeout=5,
)
if "No tasks" in chk.stdout:
break
time.sleep(0.15)
else:
os.kill(pid, signal.SIGTERM)
for _ in range(20):
try:
os.kill(pid, 0)
# Fallback: kill by command-line identity (``-m emrg.server``) — the only
# reliable marker on Windows (rant 2026-08-17T17:03:38; emrg._stop_all
# same approach). No pid file involved.
import subprocess as _sp
try:
if os.name == "nt":
out = _sp.run(
["wmic", "process", "where", "name='pythonw.exe'", "get", "processid,commandline"],
capture_output=True, text=True, timeout=10,
).stdout or ""
# PowerShell CIM fallback for Win11 24H2+ (no wmic).
if not out.strip() or "emrg.server" not in out:
ps_cmd = ("Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match "
"'emrg.server' } | ForEach-Object { $_.ProcessId }")
out = _sp.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(p) for p in re.findall(r"\d+", out) if p.strip()]
for pid in pids:
_sp.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, timeout=10)
else:
out = _sp.run(
["ps", "-axww", "-o", "pid=,command="],
capture_output=True, text=True, timeout=10,
).stdout or ""
pids = [int(m.group(1)) for m in re.finditer(r"^\s*(\d+)\s+.*-m emrg\.server\b", out, re.M)]
for pid in pids:
try:
os.kill(pid, signal.SIGTERM)
except OSError:
pass
if pids:
# Poll ≤5s for the port to close (ground truth).
import socket as _socket
for _ in range(35):
try:
with _socket.create_connection(("127.0.0.1", EMRGD_PORT), timeout=0.3):
time.sleep(0.15)
except OSError:
break
result = {"method": "pid-sigterm", "ok": True}
print(f" [1] daemon stopped (pid {pid})")
except OSError:
break
result = {"method": "cmdline-scan", "ok": True}
print(f" [1] daemon stopped (cmdline scan, {len(pids)} pid(s))")
return result
except (OSError, subprocess.SubprocessError):
pass
# Fallback: remove stale runtime files.
for f in (PORT_FILE, PID_FILE):
try:
f.unlink()
except FileNotFoundError:
pass
print(" [1] daemon not running (or already stopped); removed stale port/pid files")
except (OSError, subprocess.SubprocessError, _sp.SubprocessError):
pass
# Token cleanup once port is confirmed closed (daemon removes it itself on
# graceful shutdown; a force-killed daemon cannot).
try:
token_path.unlink()
except OSError:
pass
print(" [1] daemon not running (or already stopped)")
return result


Expand Down
6 changes: 4 additions & 2 deletions packaging/make-installer.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -414,8 +414,10 @@ end;
// Inno CloseApplications 看不到无窗口的 pythonw daemon(emrgd.cmd → pythonw.exe
// -m emrg.server 常驻锁文件),覆盖 ~/.emrg\install 时卡在"停止已有进程"。
// PrepareToInstall 在安装开始前用 runtime 的 python 运行 bin\stop_all.py:
// ws 协议关闭 daemon → emrgd.pid 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、
// CIM 命令行过滤 TUI、install\git\ 前缀连坐强杀 bundled git、verify 残留检查;
// ws 协议关闭 daemon(固定端口 56031 + emrgd.token)→ 命令行扫描 -m emrg.server
// 兜底 → taskkill /F、taskkill EMRG.exe 优雅→/F、CIM 命令行过滤 TUI、
// install\git\ 前缀连坐强杀 bundled git、verify 残留检查(emrgd.pid 已废弃,
// rant 2026-08-21T16:45:06:固定端口为存活 ground truth);
// 有残留 exit 1(脚本打印残留清单)→ 中止安装。干净安装(无旧 install)直接跳过。
// {app} 是旧版安装目录——不能依赖旧版 emrg 命令(可能无 stop 子命令),所以用
// 单文件脚本 + runtime python({app}\bin\python-dist\python.exe,R90 布局,
Expand Down
Loading