#!/usr/bin/env python3 # -*- coding: utf-8 -*- """session-log-guard.py —— 「会话诊断日志 · 事前叫停」钩子(WorkBuddy / CodeBuddy) ═══ 为什么需要它(2026-10-01 取证 · 结论见 接续包_日志事前叫停_20261001.md)═══ 宿主把每次工具调用写成 ≈145 帧 = ≈37 KB 诊断日志(零去重零节流,**其中 97.8% 是零信息唤醒行**)⇒ `10 MiB ÷ 37 KB ≈ 280 次调用` 就是一个会话的**物理寿命**;撞上 10 MiB 上限即 **拒写 ⇒ 界面静默哑掉、用户零感知**。而全平台**只做了事后拦截**(`collabd.py::_deaf_sids()` 管"哑了不许再投"), **没有任何机制在"快哑了"时叫停** —— 本钩子=补这道**事前**闸。 🔴 但请注意**病根不在"每次调用贵",而在"调用次数多"**:2026-10-01 会话 `ee3c2d82` 自证 —— 8 分钟内跑了 **166 次调用(≈20 次/分,是参考会话 10.5 次/分的 2 倍)** ⇒ 日志 8 分钟就 5.9 MB; 而其后**空闲 34 分钟写 0 行**(坐实"增长只与工具在跑成正比、与挂钟时间无关")。 ⚠️ 本钩子**只叫停、不降速**:真正降速要**压调用次数** + 上报宿主(那 97.8% 本就不该写)。 ═══ 它只做一件事 ═══ 只读**本会话诊断日志的字节数**(`os.path.getsize`,⛔ **绝不 grep 日志内容** —— 否则判定本身拖慢会话)。 达档 ⇒ 通过 `hookSpecificOutput.additionalContext` 注入一段提醒;未达档 ⇒ **零输出**。 🟡 软档 ≥ 5 MiB ⇒ 开始写交接材料 🔴 硬档 ≥ 8 MiB ⇒ **先试一次「就地回收」**;成功 ⇒ **不必停手**(会话继续跑) |失败 ⇒ 退回原口径:立刻停手 + 按 `CODEBUDDY.md §G` 建接续会话 🔴 **「就地回收」是什么、依据什么**(2026-10-01 实测,推翻了当天更早的相反结论): 把**本会话自己的** `.log` 改名为 `.log.recycled-<时间戳>`(⛔ 改名、**不删**)⇒ 宿主立刻**重建**同名新文件并**继续写入**(实测:改名后紧邻的下一次读数里原文件名已回来, 文件冻结在旧尺寸)⇒ 会话因此**不必再因撞 10 MiB 而停手**。 ⚠️ 对照:当天更早的 scratch 复现(子进程持续 append、父进程在写入进行中改名 ⇒ `WinError 32`) 只证明了**"正在被写的那一瞬"**不可改名;**本次实测在真实会话里一次成功** ⇒ 宿主并非"句柄常开", 而是**每次日志批写开-写-关** ⇒ 工具调用的间隙里句柄是**关着的**。 ⇒ 判语:**能回收就回收(会话不被截断),回收不了才叫停(fail-safe 方向=退回旧行为)**。 ⛔ 只试**一次**,失败立即退回叫停口径 —— ⛔ **不做重试式硬撞**。 ⛔ 只动**本会话自己那一份**(按 `session_id` 精确拼名),⛔ 绝不碰别人的日志。 开关:`DSH_SLG_NO_RECYCLE=1` ⇒ 只关回收、**保留**软/硬档叫停。 ⛔ **不重复造轮子**:上下文 token / 工具调用次数的分级+去重+注入**已由 `stop-dialog-guard.py` 覆盖** (其 `session_budget()` / `budget_note()`)。本脚本的**真实增量只有"日志字节"这一档**,⛔ 不要再抄一份。 ═══ 挂点 ═══ `PostToolUse`(每次工具调用后)+ `UserPromptSubmit`(每次用户消息到达时)。 ⚠️ 为什么要两个:日志既随工具调用涨,也随每次模型输出涨 —— 会话闲下来只剩后者,单挂 PostToolUse 会漏。 ═══ 硬纪律(都不许省)═══ 1. 🔴 **输出必须走 `sys.stdout.buffer.write(bytes)`**。本机 python 在 hook 环境下 stdin/stdout 可能是 **cp936**(`-E` 会屏蔽 `PYTHONUTF8`)⇒ 文案里的 `⛔` / `🟡` 会 `UnicodeEncodeError` ⇒ **stdout 为空 ⇒ 宿主当"无输出"静默放行**(=钩子白装,且看不出坏在哪里)。 2. 🔴 **去抖**:同一会话**同一档位只报一次**(升级才再报)。 3. 🔴 **状态落盘用「临时文件 + `os.replace`」原子替换,⛔ 不用文件锁** —— 2026-09-30 本机实测:文件锁在隔离目录里全绿,接进生产后 `--once` 与 6 个并发 `--report` **全部 rc=124 卡死**(`session-mechanism/scripts/collabd.py` 注释里有完整登记)。 本脚本用**乐观写法**:读→改→写临时文件→`os.replace`;并发下最坏后果=多报一条,**不会卡死**。 4. 🔴 **性能 < 50 ms**:只做 `getsize` + 一个小 JSON 的读改写;⛔ 不读转录、⛔ 不读 settings.json、 ⛔ 不 grep 任何大文件。本脚本被**每次工具调用**触发 ⇒ 任何 O(日志大小) 的动作都不可接受。 5. **fail-open**:任何异常 ⇒ 静默 exit 0(钩子绝不能因自身故障干扰会话)。 6. **急停**:env `DSH_SLG_OFF=1`,或 `<工作区>/.workbuddy/session-log-guard.disabled` 存在。 7. **自证**:命中才写一行 `<工作区>/.workbuddy/session-log-guard.log`;排查期可设 `DSH_SLG_LOG_ALL=1` 让**每次调用**都留痕(用来回答"宿主到底调没调我")。 退出码始终 0;决策只通过 stdout 的 JSON 表达。 ═══ 安装(`/settings.json` 的 `hooks` 段)═══ "PostToolUse": [{ "hooks": [{ "type": "command", "command": "\"\" -S \"<此脚本>\"", "timeout": 10 }] }], "UserPromptSubmit": [ …既有条目… , { "hooks": [{ "type":"command", "command": "\"\" -S \"<此脚本>\"", "timeout": 10 }] }] 🔴 **可以加 `-S`**(跳过 site 初始化 ⇒ 单次调用实测 84 ms → 52 ms;本脚本**只用标准库** ⇒ 安全)。 🔴 **⛔ 绝不可加 `-E` / `-I`** —— 它们会忽略 `PYTHONUTF8` / `PYTHONIOENCODING` ⇒ stdin/stdout 回退 **cp936** (`stop-dialog-guard.py` docstring 里有完整事故记录:`-S -E` 曾让钩子"看起来从未被调用"整整一天)。 ⚠️ 本机实测:`settings.json` 的 hooks **下一次调用即生效**(⛔ 无需重启宿主,2026-10-01 复核)。 ⚠️ `07-scripts/` 改名 / 改名后必须**同批**改宿主 hooks 入口 —— 见库内 `CODEBUDDY.md` (原写「5 条 hook 入口」,2026-10-01 起为 **7 条**:lock-guard ×2 / bash-output-guard / skill-load-guard / stop-dialog-guard / **session-log-guard ×2**)。 """ # [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导) def _sm_load_roots(): import os as _os _here = _os.path.dirname(_os.path.abspath(__file__)) for _up in range(4): _p = _os.path.join(_here, *([".."] * _up), "roots.env") _p = _os.path.normpath(_p) if _os.path.isfile(_p): try: with open(_p, encoding="utf-8") as _f: for _ln in _f: _ln = _ln.strip() if _ln and not _ln.startswith("#") and "=" in _ln: _k, _v = _ln.split("=", 1) _os.environ.setdefault(_k.strip(), _v.strip()) except Exception: pass return _sm_load_roots() import io import json import os import re import sys import time # 🔴 2026-10-02:环境定位/体检收敛到 `_env`(同目录优先)——⛔ 不再用 `~` 直拼 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) try: import _env except Exception: sys.stderr.write('[env] 无法导入 _env.py ⇒ 环境定位不可信\n') raise # ── 阈值(口径来源:`CODEBUDDY.md §G` 档 2 · 2026-10-01 用户拍板「B方案」)────────── SOFT_BYTES = 5 * 1024 * 1024 # 5 MiB ⇒ 开始写交接材料 HARD_BYTES = 8 * 1024 * 1024 # 8 MiB ⇒ 就地回收;回收不成才停手 + 建接续会话 # 🔴 为什么不是 10 MiB:宿主上限即 10 MiB,且实测**一次交接本身烧 ≈1.9 MiB** # ⇒ 8 MiB 已是**下限**,⛔ 不得再往上调(调到 9 以上 = 交接材料写不完就撞顶)。详见 §G。 # ♻️ 回收阈值=硬档(**同一个点**):到这一点"要么回收、要么叫停",⛔ 不另设第三个阈值 # (多一个阈值就多一条要维护的口径;且 8 MiB 已实测是"还来得及"的下限)。 RECYCLE_MIN_BYTES = HARD_BYTES # 🔴 **验收通道**(⛔ 日常**没有这个文件** ⇒ 行为与上面两个常量完全一致) # 为什么必须有:本钩子的真实触发条件是"日志 ≥5 MiB",而正常会话远达不到 ⇒ # 在 **⛔ 不动生产日志一字节** 的前提下,若不开口子就**无法在真实会话里验收** # (只能本地跑 ⇒ 按交付门禁**不算交付**:宿主到底调没调我、注入通道通不通,都还是未知)。 # 形态:`<工作区>/tmp/.session-log-guard.verify.json`,例 # {"sid": "ee3c2d82", "soft_mib": 0.3, "hard_mib": 0.6} # · `sid` ⇒ **把口子收窄到单个会话**(前缀匹配)⇒ 验收期间其它会话**零影响**,不会误伤别人。 # · 用完**立刻删**(本机 `tmp/` 收口即清);只要文件在,每次触发都会往日志写 `VERIFY-ON` 自证。 # ⚠️ 取不到 / 解析失败 / 无此文件 ⇒ 一律回落到真阈值(fail-safe 方向=按真阈值办事)。 VERIFY_REL = os.path.join('tmp', '.session-log-guard.verify.json') def _verify(root): try: with io.open(os.path.join(root, VERIFY_REL), encoding='utf-8') as f: o = json.loads(f.read() or '{}') if isinstance(o, dict): return o except Exception: pass return {} def _mib_env(o, key, default): try: v = float(o.get(key) or 0) return int(v * 1024 * 1024) if v > 0 else default except Exception: return default STATE_REL = os.path.join('tmp', '.session-log-guard.level.json') LOG_REL = os.path.join('.workbuddy', 'session-log-guard.log') MAX_STATE_KEYS = 40 # 状态文件只留最近 N 个会话(防无限膨胀) # 本脚本位于 <工作区>/dsh-server-docs/07-scripts/ ⇒ 往上三级即工作区(宿主没给 cwd 时的兜底) WS_FALLBACK = os.environ.get("DSH_WS_ROOT") or os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) def _norm_path(p): """MSYS / Git-Bash 风格路径 → Windows 风格(`/e/foo` → `E:/foo`)。 ⛔ 同源铁律:**Windows 原生 python 会把 `/e/...` 解释成「当前盘根 + 相对路径」** ⇒ 落到 `E:\\e\\...`。本工作区已有实证(E 盘根长出 `E:\\e\\` 影子目录)。 """ try: m = re.match(r'^/([A-Za-z])(/.*)?$', str(p or '')) if m: return m.group(1).upper() + ':' + (m.group(2) or '/') except Exception: pass return p def _read_stdin_text(): """显式按 UTF-8 读 stdin(⛔ 不要用 `sys.stdin.read()`:cp936 下中文 payload 直接抛错)。""" try: return sys.stdin.buffer.read().decode('utf-8', 'replace') except Exception: try: return sys.stdin.read() except Exception: return '' def _emit(obj): """显式按 UTF-8 写 stdout(见模块 docstring 硬纪律 1)。""" data = json.dumps(obj, ensure_ascii=False).encode('utf-8') try: sys.stdout.buffer.write(data) sys.stdout.buffer.flush() except Exception: try: sys.stdout.write(data.decode('utf-8', 'replace')) sys.stdout.flush() except Exception: pass def _root(payload): return (_norm_path(os.environ.get('CODEBUDDY_PROJECT_DIR') or os.environ.get('DSH_WORKSPACE') or (payload or {}).get('cwd') or WS_FALLBACK)) def _config_dir(): """🔴 2026-10-02:共用 `_env.config_dir()` —— ⛔ 不再直接拼 `~` (Windows 上 `~` 通常不是真配置目录;真值顺序=env → roots.env)。""" return _norm_path(_env.config_dir()) def _log_path(sid): """本会话诊断日志的绝对路径(返回 [] 表示还没落盘 / 取不到)。 真实形态(2026-10-01 实测): /logs//sdk/conversations/.log ⚠️ 跨零点:会话可能昨天开的 ⇒ 今天那份不存在时**回看昨天**(只多一次 `getsize`,可忽略)。 """ base = os.path.join(_config_dir(), 'logs') out = [] now = time.time() for back in (0, 1): d = time.strftime('%Y-%m-%d', time.localtime(now - back * 86400)) p = os.path.join(base, d, 'sdk', 'conversations', '%s.log' % sid) try: os.path.getsize(p) # 🔴 只取**字节数**;⛔ 绝不打开/搜内容 except OSError: continue out.append(p) return out def _level_of(size, soft=SOFT_BYTES, hard=HARD_BYTES): if size >= hard: return 2 if size >= soft: return 1 return 0 def _read_state(root): try: with io.open(os.path.join(root, STATE_REL), encoding='utf-8') as f: d = json.loads(f.read() or '{}') return d if isinstance(d, dict) else {} except Exception: return {} def _write_state(root, st): """临时文件 + `os.replace` 原子替换(⛔ 不用文件锁 ⇒ 见 docstring 硬纪律 3)。""" p = os.path.join(root, STATE_REL) d = os.path.dirname(p) try: os.makedirs(d, exist_ok=True) tmp = '%s.tmp.%d' % (p, os.getpid()) with io.open(tmp, 'w', encoding='utf-8', newline='\n') as f: f.write(json.dumps(st, ensure_ascii=False)) os.replace(tmp, p) except Exception: pass def _log(root, detail): try: p = os.path.join(_norm_path(root), LOG_REL) os.makedirs(os.path.dirname(p), exist_ok=True) with io.open(p, 'a', encoding='utf-8') as f: f.write('%s\t%s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), detail)) lines = io.open(p, encoding='utf-8').read().split('\n') # 上限 300 行,超出截半 if len(lines) > 300: io.open(p, 'w', encoding='utf-8', newline='\n').write('\n'.join(lines[-150:])) except Exception: pass def _mib(n): return '%.2f MiB' % (n / 1048576.0) def _recycle(paths, min_bytes=RECYCLE_MIN_BYTES): """♻️ 就地回收**本会话自己**的日志:改名挪开,让宿主重建。 返回 `(源路径, 目标路径, 错误串)`;未达到回收条件或失败 ⇒ 源路径为 `None`。 🔴 只试**一次**:`os.rename` 抛 `OSError`(典型 `WinError 32`=文件被占用)就**立刻返回**, ⛔ 不 sleep、⛔ 不重试、⛔ 不降级成"删/截断"(那会毁掉唯一的取证载体)。 ⚠️ 后缀**刻意不以 `.log` 结尾** ⇒ 与 `wb-logcap-sweep.py` 的 `*.log` 匹配**天然不重叠** (清扫器不会二次搬运本文件)。 """ best, bestsz = None, -1 for p in paths: # `_log_path` 已按 sid 精确拼名 ⇒ 这里只会是本会话自己的 try: s = os.path.getsize(p) except OSError: continue if s > bestsz: best, bestsz = p, s if not best or bestsz < int(min_bytes): return None, None, 'below-threshold' dst = '%s.recycled-%s' % (best, time.strftime('%Y%m%d-%H%M%S')) try: os.rename(best, dst) except OSError as e: return None, None, '%s(errno=%s, winerror=%s)' % (type(e).__name__, e.errno, getattr(e, 'winerror', None)) return best, dst, '' def _recycle_note(size, src, dst, hard=HARD_BYTES): """回收**成功**时注入的提醒(⛔ 与"叫停"是两套口径:这套说的是"可以继续")。 ⚠️ `hard` 取**实际生效值**(⛔ 不写死常量)⇒ 验收通道把阈值调小时,提示里的数字跟着对。 """ return ('♻️【日志就地回收 · 已复位】本会话诊断日志已达 **%s**(硬档 %s)⇒ 已**当场回收**:' '把 `%s` 改名为 `%s`(⛔ 只改名、**没删**,取证内容仍在同目录),' '宿主会**立刻重建**同名文件并**继续写入** ⇒ **本会话可继续,⛔ 不必停手、' '⛔ 不必建接续会话**(2026-10-01 本机实测:改名后紧邻读数里原文件名已回来、旧件冻结)。' # ⚠️ 下面有**字面量百分号** ⇒ 必须写成 `%%`(本串走 `%` 格式化;漏了会抛 # `TypeError: not enough arguments for format string` ⇒ 提示发不出去) '\n⚠️ 但**病根没除**:日志增长只与"工具在跑"成正比(≈37 KB/次调用,其中 97.8%% 是' '零信息唤醒行)⇒ 仍请压住频率:① 合并命令、一次取多个读数;② 大输出先落盘再只读关键行' '(⛔ 不 `cat` 大文件);③ 让脚本内部聚合、只 print 摘要。' '\n(下一次再涨到硬档会再回收一次;⛔ 若某次回收失败,本钩子会退回"停手+建接续"口径。)' % (_mib(size), _mib(hard), os.path.basename(src), os.path.basename(dst))) def _note(level, size, soft=SOFT_BYTES, hard=HARD_BYTES): pct = int(size * 100 / (10 * 1024 * 1024)) # 占宿主 10 MiB 上限的比例 soft_s, hard_s = _mib(soft), _mib(hard) head = ('🟡【日志事前叫停 · 软档】本会话诊断日志已达 **%s**(10 MiB 上限的 %d%%,软档 %s)。' % (_mib(size), pct, soft_s)) if level == 2: head = ('🔴【日志事前叫停 · 硬档】本会话诊断日志已达 **%s**(10 MiB 上限的 %d%%,硬档 %s)。' % (_mib(size), pct, hard_s)) why = ('为什么必须现在处理:宿主把**每次工具调用**写成 ≈145 帧 = **≈37 KB** 诊断日志' '(零去重零节流,⛔ 其中约 **97.8% 是零信息唤醒行** —— 每行 249 B,只有 `requestId` 与四个' '恒为 `false` 的布尔位在变)⇒ `10 MiB ÷ 37 KB ≈ 280 次调用` 就是本会话的**物理寿命**' '(2026-10-01 实测:某会话 8 分钟跑了 166 次调用 ⇒ 日志 5.9 MB,≈20 次/分);' '撞顶即**拒写 ⇒ 界面静默哑掉、用户零感知**,而常驻程序的投递仍会记 `ok:true`(假绿)。' '🔴 **病根是"调用次数多",不是"每次调用贵"**;且**增长只与"工具在跑"成正比、与挂钟时间无关**' '(实测:活跃 630 KB/分 ↔ 空闲 34 分钟 0 行)。') if level == 1: act = ('处置(软档):**现在就开始写交接材料**(接续包落工作区根),并立刻压住增长 —— ' '① 合并命令,一次取多个读数;② 大输出先落盘再只读关键行(⛔ 不 `cat` 大文件);' '③ 让脚本内部聚合、只 print 摘要。⛔ 不要再开新任务。') else: act = ('处置(硬档 · 顺序固定):① **立刻停手**,本轮不再开新任务;② 写接续包(含「不要重做」清单);' '③ **建接续会话**(走一次性 automation —— 自动化是**唯一**能开新会话的通道);' '④ 在**给用户的最后一条回复里用陈述句**告知「已登记自动接续,约 N 分钟后自动开新会话,不需要你操作」。' '⚠️ **钩子开不了新会话** ⇒ 这四步必须由**你自己**做完;本钩子只负责"叫"。') return head + '\n' + why + '\n' + act def main(): raw = _read_stdin_text() payload = None if raw.strip(): try: payload = json.loads(raw) except ValueError: payload = None if not isinstance(payload, dict): return root = _root(payload) ev = str(payload.get('hook_event_name') or '') sid = str(payload.get('session_id') or '') force_log = bool(os.environ.get('DSH_SLG_LOG_ALL')) if force_log: _log(root, 'invoked|event=%s|sid=%s' % (ev or '(none)', sid[-8:] or '-')) if not sid: return vf = _verify(root) if vf: _log(root, 'VERIFY-ON|sid_gate=%s|soft=%s|hard=%s' % (vf.get('sid') or '-', vf.get('soft_mib'), vf.get('hard_mib'))) gate = str(vf.get('sid') or '') if gate and not sid.startswith(gate): return # 验收通道:只认指定会话 soft = _mib_env(vf, 'soft_mib', SOFT_BYTES) hard = _mib_env(vf, 'hard_mib', HARD_BYTES) if os.environ.get('DSH_SLG_OFF'): # 急停(env) return try: if os.path.exists(os.path.join(root, '.workbuddy', 'session-log-guard.disabled')): return # 急停(闸刀文件) except Exception: pass paths = _log_path(sid) if not paths: return # 还没落盘 ⇒ 静默 size = 0 for p in paths: try: size = max(size, os.path.getsize(p)) except OSError: pass lv = _level_of(size, soft, hard) if not lv: return # 🔴 未达阈值 ⇒ **零输出** st = _read_state(root) try: prev = int((st.get(sid) or [0])[0] or 0) except Exception: prev = 0 if lv <= prev: # 同档只报一次(升级才再报) return # ♻️ 硬档 ⇒ **先试一次**「就地回收」(依据见模块 docstring;2026-10-01 本机实测可行) # · 放在去重之后 ⇒ **每个档位只试一次** ⇒ ⛔ 天然不会"每次调用都硬撞" # · 成功 ⇒ 复位本会话档位(不复位的话后半程再也报不出来)+ 注入"可以继续"的提醒,**直接结束** # · 失败 ⇒ 一个字都不改,继续往下走原来的叫停口径(fail-safe 方向) if lv >= 2 and not os.environ.get('DSH_SLG_NO_RECYCLE'): # ⚠️ 传 `hard`(而不是模块常量)⇒ 验收通道把阈值调小时,**回收分支也跟着走** # (否则验收通道只能验"叫停"、验不到"回收" ⇒ 按交付门禁=没验) _src, _dst, _err = _recycle(paths, hard) if _src: st[sid] = [0, time.time()] if len(st) > MAX_STATE_KEYS: for k in sorted(st, key=lambda k: (st[k] or [0, 0])[-1])[:-MAX_STATE_KEYS]: st.pop(k, None) _write_state(root, st) _log(root, 'RECYCLE|size=%d B(%s)|%s -> %s' % (size, _mib(size), os.path.basename(_src), os.path.basename(_dst))) _emit({'hookSpecificOutput': {'hookEventName': ev or 'PostToolUse', 'additionalContext': _recycle_note(size, _src, _dst, hard)}}) return _log(root, 'RECYCLE-FAIL|size=%d B|%s ⇒ 退回叫停口径' % (size, _err)) now = time.time() st[sid] = [lv, now] if len(st) > MAX_STATE_KEYS: # 只留最近 N 个会话 for k in sorted(st, key=lambda k: (st[k] or [0, 0])[-1])[:-MAX_STATE_KEYS]: st.pop(k, None) _write_state(root, st) _log(root, 'EMIT|level=%d|size=%d B(%s)|event=%s' % (lv, size, _mib(size), ev or '-')) _emit({'hookSpecificOutput': {'hookEventName': ev or 'PostToolUse', 'additionalContext': _note(lv, size, soft, hard)}}) if __name__ == '__main__': t0 = time.time() try: main() except Exception: try: import traceback _r = os.environ.get('CODEBUDDY_PROJECT_DIR') or os.environ.get('DSH_WORKSPACE') or WS_FALLBACK _log(_r, 'EXCEPTION|%s' % traceback.format_exc().strip().split('\n')[-1][:120]) except Exception: pass # 性能自证(仅排查期):DSH_SLG_LOG_ALL=1 时把耗时也落一行 if os.environ.get('DSH_SLG_LOG_ALL'): try: _log(os.environ.get('CODEBUDDY_PROJECT_DIR') or os.environ.get('DSH_WORKSPACE') or WS_FALLBACK, 'elapsed=%.1f ms' % ((time.time() - t0) * 1000)) except Exception: pass sys.exit(0)