@nightpainters/dsh-ling 1.5.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACCESS-DESIGN.md +6 -4
- package/CHANGELOG.md +65 -0
- package/DESIGN.md +141 -14
- package/README.md +55 -31
- package/lib/audit/access-log.js +135 -13
- package/lib/audit/index.js +43 -1
- package/lib/client.js +902 -57
- package/lib/host/api.js +129 -31
- package/lib/host/genesis.js +27 -5
- package/lib/host/guard.js +1 -1
- package/lib/host/inject.js +49 -12
- package/lib/host/l1.js +11 -4
- package/lib/host/memory.js +263 -20
- package/lib/host/persona.js +38 -7
- package/lib/host/session-line.js +648 -0
- package/lib/host/snapshot.js +48 -7
- package/lib/host/summarizer.js +1 -1
- package/lib/index.js +5 -0
- package/package.json +51 -4
package/lib/audit/access-log.js
CHANGED
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
// 写失败 ⇒ 自我禁用 + 首次立刻告警(带 errno)⇒ 之后按节流**继续**告警并累计"丢了多少行",
|
|
25
25
|
// 同时把可机读的健康面常驻在 `info().health`(`/health` 读它即可)。旧实现只喊一行就永久静默,
|
|
26
26
|
// 而"日志悄悄不记了"恰恰是本模块最该被发现的故障。
|
|
27
|
+
// **C-07b(2026-09-27)**:C-07 之后"降级"仍然是**永久**的(全文件只有初始化与失败两处给状态赋值),
|
|
28
|
+
// 现场实测:10:26:49 之后再 41.6 分钟文件零增长 —— 进程健在、补丁在位、请求照发、**增长 0 字节**,
|
|
29
|
+
// 只有重启才恢复。现在改成**带退避的自愈**:写失败后进入退避窗口(默认 1s→2s→…→30s,±20% 抖动),
|
|
30
|
+
// 窗口内只计数;窗口一过,**下一次请求自己本来就要做的那次写盘**就是探针 —— 写成功即恢复
|
|
31
|
+
// (喊一次恢复告警,带上丢了总量与故障时长),写失败就再排一轮退避。
|
|
32
|
+
// **零新增 IO、零定时器**(没有请求就没有东西要记):探针不是额外动作,是复用本次写盘。
|
|
33
|
+
// 为什么不做 errno 分类:现成故障注入打出的 errno 是 `EEXIST`(教科书式的"永久码"),
|
|
34
|
+
// 但它的条件是**瞬时**的(占位文件删掉就好)—— 一分类就刚好把它判死,所以 v1 一律重试。
|
|
27
35
|
// 3) **只观察、不判定**:本模块不调用鉴权、不改响应。状态码与拒绝原因全部来自
|
|
28
36
|
// 「已经发生的响应」,判据与 guard 保持单一来源(任务书 §3.5)。
|
|
29
37
|
|
|
@@ -67,6 +75,17 @@ export const ACCESS_LOG_DEFAULTS = {
|
|
|
67
75
|
riskPatterns: ['settingRestart/restart', '/dsh-ling/'],
|
|
68
76
|
/** C-07:降级(写失败)后**重复告警**的最小间隔(毫秒);0 = 每次丢行都告警(测试用)。 */
|
|
69
77
|
degradeWarnMs: 60_000,
|
|
78
|
+
/**
|
|
79
|
+
* C-07b:降级后"下一次探针"的**退避**(毫秒)。第 n 次尝试等
|
|
80
|
+
* `min(retryBaseMs * retryFactor^(n-1), retryCapMs)`,再乘 ±retryJitter 抖动;到 cap 之后
|
|
81
|
+
* 就是"每 cap 一次",**不限次数**。探针不产生新 IO —— 用的就是下一次请求本来就要做的那次写盘。
|
|
82
|
+
* ⚠️ `retryBaseMs` 别设 0:现有单测在毫秒内连发多次请求,0 会让它们在测试里就重试、断言失真。
|
|
83
|
+
*/
|
|
84
|
+
retryBaseMs: 1000,
|
|
85
|
+
retryFactor: 2,
|
|
86
|
+
retryCapMs: 30_000,
|
|
87
|
+
/** 抖动比例(±):免得所有实例在同一毫秒齐步重试;0 = 不抖。 */
|
|
88
|
+
retryJitter: 0.2,
|
|
70
89
|
};
|
|
71
90
|
|
|
72
91
|
/** 本地时间 ISO(带毫秒与偏移):既能排序、又能直接肉眼对齐现场时刻。 */
|
|
@@ -234,10 +253,30 @@ export function createAccessLog(options = {}) {
|
|
|
234
253
|
const degradeWarnMs = Number.isFinite(Number(options.degradeWarnMs))
|
|
235
254
|
? Math.max(0, Number(options.degradeWarnMs))
|
|
236
255
|
: ACCESS_LOG_DEFAULTS.degradeWarnMs;
|
|
256
|
+
// C-07b 的退避参数(照旧逐个解析 + 夹取,缺省取 ACCESS_LOG_DEFAULTS)。
|
|
257
|
+
const retryBaseMs = Number.isFinite(Number(options.retryBaseMs))
|
|
258
|
+
? Math.max(0, Number(options.retryBaseMs))
|
|
259
|
+
: ACCESS_LOG_DEFAULTS.retryBaseMs;
|
|
260
|
+
const retryFactor = Number.isFinite(Number(options.retryFactor))
|
|
261
|
+
? Math.max(0, Number(options.retryFactor))
|
|
262
|
+
: ACCESS_LOG_DEFAULTS.retryFactor;
|
|
263
|
+
const retryCapMs = Number.isFinite(Number(options.retryCapMs))
|
|
264
|
+
? Math.max(0, Number(options.retryCapMs))
|
|
265
|
+
: ACCESS_LOG_DEFAULTS.retryCapMs;
|
|
266
|
+
// 抖动夹到 [0,1]:大于 1 会让等待时间变负(等于"永远立刻重试",正好破坏时间闸门)。
|
|
267
|
+
const retryJitter = Number.isFinite(Number(options.retryJitter))
|
|
268
|
+
? Math.min(1, Math.max(0, Number(options.retryJitter)))
|
|
269
|
+
: ACCESS_LOG_DEFAULTS.retryJitter;
|
|
237
270
|
|
|
238
271
|
let day = null;
|
|
239
272
|
let writes = 0;
|
|
240
|
-
|
|
273
|
+
// C-07b:把"为什么写不了"拆成两种**互不混淆**的状态。旧的单个 `disabled` 把"配置关闭"与
|
|
274
|
+
// "写失败"混成一个布尔,而后者必须能自己好起来 —— 这正是 C-07b 的成因。
|
|
275
|
+
// configOff —— 配置关闭(enabled:false 或无 dir):**永久**,行为与旧实现完全一致
|
|
276
|
+
// degraded —— 写失败降级:**可自愈**(旧实现一旦置位就再没有清除点)
|
|
277
|
+
// `info().disabled` 仍按 `configOff || degraded` 返回,向后兼容不变。
|
|
278
|
+
const configOff = options.enabled === false || !dir;
|
|
279
|
+
let degraded = false;
|
|
241
280
|
// C-07 的观测面(原实现只有一个 warned 布尔 ⇒ 喊一行之后永久静默):
|
|
242
281
|
// failures —— 写调用抛异常的次数(一次性的 vs 持续坏掉)
|
|
243
282
|
// lost —— 因此**丢掉的行数**:降级后唯一还能增长的量,告警节流就挂在它上面
|
|
@@ -247,6 +286,17 @@ export function createAccessLog(options = {}) {
|
|
|
247
286
|
let lost = 0;
|
|
248
287
|
let lastError = null;
|
|
249
288
|
let warnAt = 0;
|
|
289
|
+
// C-07b 的重试面:
|
|
290
|
+
// lostSinceAt —— **本次**故障的起点(epoch 毫秒;从未失败过 = 0 ⇒ health.lostSince 为 null)
|
|
291
|
+
// retryAttempts —— 连续失败次数(自愈时归零):退避的指数就挂在它上面
|
|
292
|
+
// nextRetryAt —— 下次允许**碰磁盘**的时刻;它之前只计数不写盘 —— 这就是"不每请求重试"的闸门
|
|
293
|
+
// recoveredAt —— 最近一次自愈的时刻(本地 ISO)
|
|
294
|
+
// retrySucceeded —— 自愈成功过几次(0 = 没坏过,或坏了还没好)
|
|
295
|
+
let lostSinceAt = 0;
|
|
296
|
+
let retryAttempts = 0;
|
|
297
|
+
let nextRetryAt = 0;
|
|
298
|
+
let recoveredAt = null;
|
|
299
|
+
let retrySucceeded = 0;
|
|
250
300
|
|
|
251
301
|
/**
|
|
252
302
|
* 该不该喊:首次必喊;之后**只按时间节流**(默认 60s 一次)。
|
|
@@ -254,7 +304,9 @@ export function createAccessLog(options = {}) {
|
|
|
254
304
|
* 而"到底丢了多少"正是这条告警唯一的硬信息 —— 时间节流既不刷屏、计数又永远是最新的。
|
|
255
305
|
*/
|
|
256
306
|
const shouldWarn = () => {
|
|
257
|
-
|
|
307
|
+
// C-07b:判据从"有 lastError"换成**当前是否降级** —— lastError 现在会作为历史证据保留到恢复之后,
|
|
308
|
+
// 拿它兼作状态会让"已经好了"的姿态也照喊。
|
|
309
|
+
if (!degraded) return false;
|
|
258
310
|
if (warnAt === 0) return true;
|
|
259
311
|
return Date.now() - warnAt >= degradeWarnMs;
|
|
260
312
|
};
|
|
@@ -274,28 +326,70 @@ export function createAccessLog(options = {}) {
|
|
|
274
326
|
};
|
|
275
327
|
|
|
276
328
|
/**
|
|
277
|
-
* 记一次"丢了行"
|
|
329
|
+
* 记一次"丢了行"。**降级时**才计数并考虑告警 ——
|
|
278
330
|
* `enabled:false` 的"配置关闭"是有意为之,不是故障,既不计丢失也不喊。
|
|
331
|
+
* C-07b:守卫从 `!lastError` 换成 `!degraded` —— lastError 恢复后仍留着(历史证据),
|
|
332
|
+
* 继续拿它当判据会让"配置关闭"这种场景错误计数。
|
|
279
333
|
*/
|
|
280
334
|
function noteLost() {
|
|
281
|
-
if (!
|
|
335
|
+
if (!degraded) return;
|
|
282
336
|
lost += 1;
|
|
283
337
|
warn();
|
|
284
338
|
}
|
|
285
339
|
|
|
286
|
-
/**
|
|
340
|
+
/** 第 n 次重试要等多久(毫秒):指数退避到 cap,再乘 ±jitter。 */
|
|
341
|
+
function backoffOf(attempt) {
|
|
342
|
+
const n = Math.max(1, attempt);
|
|
343
|
+
const grow = retryFactor > 0 ? retryFactor ** (n - 1) : 1;
|
|
344
|
+
const raw = retryBaseMs * grow;
|
|
345
|
+
const capped = Math.min(Number.isFinite(raw) ? raw : retryCapMs, retryCapMs);
|
|
346
|
+
if (!(retryJitter > 0)) return Math.max(0, capped);
|
|
347
|
+
return Math.max(0, capped * (1 + (Math.random() * 2 - 1) * retryJitter));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** 写失败 ⇒ 降级:记下证据 + 排定下一次探针时刻(stderr、info().health 两处同时可见)。 */
|
|
287
351
|
function degrade(err) {
|
|
288
352
|
failures += 1;
|
|
289
353
|
lost += 1; // 触发失败的那一行本身也丢了
|
|
354
|
+
const now = Date.now();
|
|
355
|
+
// 本次故障的起点:只在"从正常跌进降级"那一刻记一次(同一次故障里的后续失败不覆盖它)——
|
|
356
|
+
// 恢复告警要拿它算故障时长;恢复后也**不清空**,它是"上一次丢行始于何时"的证据。
|
|
357
|
+
if (!degraded) lostSinceAt = now;
|
|
290
358
|
lastError = {
|
|
291
359
|
code: err && err.code ? String(err.code) : null,
|
|
292
360
|
message: (err && err.message) || String(err),
|
|
293
361
|
at: localIso(),
|
|
294
362
|
};
|
|
295
|
-
|
|
363
|
+
degraded = true;
|
|
364
|
+
retryAttempts += 1;
|
|
365
|
+
// **只在状态里排时刻,不做任何 IO**(本函数跑在请求热路径上、且是同步的)。
|
|
366
|
+
nextRetryAt = now + backoffOf(retryAttempts);
|
|
296
367
|
warn();
|
|
297
368
|
}
|
|
298
369
|
|
|
370
|
+
/**
|
|
371
|
+
* 写成功 ⇒ 自愈(C-07b)。**只有真的写进去了才会走到这里** —— 探针就是"这一次本来就要做的写盘"。
|
|
372
|
+
* 四件事,一件都不能少:清降级 / 退避归零 / 留下恢复痕迹 / **重置告警节流**。
|
|
373
|
+
* 最后一件最容易漏:不重置的话,恢复后紧接着再坏一轮,那一次的首次告警会被上一轮的 `warnAt`
|
|
374
|
+
* 节流吃掉 —— 而"刚坏"恰恰是最该立刻喊的那一次。
|
|
375
|
+
* `lastError` 故意**不清空**:它是历史证据,恢复不抹掉"曾经坏过、坏在哪"这一事实。
|
|
376
|
+
*/
|
|
377
|
+
function recover() {
|
|
378
|
+
const downMs = lostSinceAt ? Date.now() - lostSinceAt : 0;
|
|
379
|
+
degraded = false;
|
|
380
|
+
retryAttempts = 0;
|
|
381
|
+
nextRetryAt = 0;
|
|
382
|
+
recoveredAt = localIso();
|
|
383
|
+
retrySucceeded += 1;
|
|
384
|
+
warnAt = 0;
|
|
385
|
+
try {
|
|
386
|
+
// "它什么时候好的"必须留痕,否则只有"坏"的痕迹、没有"好"的痕迹。
|
|
387
|
+
console.error(
|
|
388
|
+
`[dsh-ling] access log recovered: write ok again after ${downMs}ms down, ${lost} line(s) lost so far; dir=${dir}`,
|
|
389
|
+
);
|
|
390
|
+
} catch {}
|
|
391
|
+
}
|
|
392
|
+
|
|
299
393
|
const fileNameOf = (d) => `${FILE_PREFIX}${d}${FILE_SUFFIX}`;
|
|
300
394
|
|
|
301
395
|
/** 超限删最旧(按文件名=时间序),当天文件永不删。 */
|
|
@@ -340,8 +434,14 @@ export function createAccessLog(options = {}) {
|
|
|
340
434
|
}
|
|
341
435
|
|
|
342
436
|
function append(entry) {
|
|
343
|
-
|
|
344
|
-
|
|
437
|
+
// C-07:降级后每来一行都如实计入"丢掉的行数",并按节流继续告警 —— 不再永久静默。
|
|
438
|
+
// C-07b:改成两段判定 —— ① 配置关闭是**永久**的(有意为之,noteLost 会直接返回);
|
|
439
|
+
// ② 降级只在**退避窗口内**才放弃写盘,窗口一过就走正常路径(写成功即自愈)。
|
|
440
|
+
if (configOff) {
|
|
441
|
+
noteLost();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (degraded && Date.now() < nextRetryAt) {
|
|
345
445
|
noteLost();
|
|
346
446
|
return;
|
|
347
447
|
}
|
|
@@ -354,6 +454,9 @@ export function createAccessLog(options = {}) {
|
|
|
354
454
|
try {
|
|
355
455
|
appendFileSync(join(dir, ensureDay()), line);
|
|
356
456
|
if (++writes % 256 === 0) enforceCap();
|
|
457
|
+
// C-07b:写成功就是探针的回执 —— 降级态下的这一次成功即自愈(appends 是唯一的落盘点,
|
|
458
|
+
// 所以恢复判定放在这里就足够,不必再问一次磁盘)。
|
|
459
|
+
if (degraded) recover();
|
|
357
460
|
} catch (err) {
|
|
358
461
|
degrade(err);
|
|
359
462
|
}
|
|
@@ -443,8 +546,13 @@ export function createAccessLog(options = {}) {
|
|
|
443
546
|
*/
|
|
444
547
|
function observe(req, res) {
|
|
445
548
|
if (!req || !res) return;
|
|
446
|
-
|
|
447
|
-
|
|
549
|
+
// C-07:降级后不再试着写盘,但请求还在来 —— 每来一次都是一次**可见的丢失**(计数 + 节流告警)。
|
|
550
|
+
// C-07b:只有在**退避窗口内**才这么做;窗口一过,这次请求就当成探针走完整路径。
|
|
551
|
+
if (configOff) {
|
|
552
|
+
noteLost();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (degraded && Date.now() < nextRetryAt) {
|
|
448
556
|
noteLost();
|
|
449
557
|
return;
|
|
450
558
|
}
|
|
@@ -494,7 +602,12 @@ export function createAccessLog(options = {}) {
|
|
|
494
602
|
*/
|
|
495
603
|
function observeUpgrade(req, rejection) {
|
|
496
604
|
if (!req) return;
|
|
497
|
-
|
|
605
|
+
// C-07b:与 observe/append 同一套两段判定 —— 三个闸门漏改任何一个,那条通道就还是"永久死"。
|
|
606
|
+
if (configOff) {
|
|
607
|
+
noteLost();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (degraded && Date.now() < nextRetryAt) {
|
|
498
611
|
noteLost();
|
|
499
612
|
return;
|
|
500
613
|
}
|
|
@@ -518,15 +631,24 @@ export function createAccessLog(options = {}) {
|
|
|
518
631
|
maxTotalBytes,
|
|
519
632
|
deviceCookie,
|
|
520
633
|
riskPatterns,
|
|
521
|
-
|
|
634
|
+
// C-07b:向后兼容字段,语义不变(旧消费者只问"还能不能写")—— 现在它由两个状态合成。
|
|
635
|
+
disabled: configOff || degraded,
|
|
522
636
|
// C-07:可机读的健康面 —— `/health`(或界面)读这一个字段即可,不必去猜 stderr 那一行。
|
|
523
637
|
health: {
|
|
524
|
-
|
|
638
|
+
// 顺序不能反:先判 degraded(可自愈的故障),再判 configOff(有意关闭)。
|
|
639
|
+
state: degraded ? 'degraded' : configOff ? 'off' : 'ok',
|
|
525
640
|
writes,
|
|
526
641
|
failures,
|
|
527
642
|
lost,
|
|
643
|
+
// 本次故障的起点;从未失败过就是 null(lastError 保留为历史证据,恢复后不清空)。
|
|
644
|
+
lostSince: lostSinceAt ? localIso(new Date(lostSinceAt)) : null,
|
|
528
645
|
lastError,
|
|
529
646
|
warnedAt: warnAt ? new Date(warnAt).toISOString() : null,
|
|
647
|
+
// C-07b 的重试面:自愈的痕迹(恢复时刻 / 已连续失败几次 / 下次探针时刻 / 自愈成功次数)。
|
|
648
|
+
recoveredAt,
|
|
649
|
+
retryAttempts,
|
|
650
|
+
nextRetryAt,
|
|
651
|
+
retrySucceeded,
|
|
530
652
|
},
|
|
531
653
|
}),
|
|
532
654
|
};
|
package/lib/audit/index.js
CHANGED
|
@@ -28,6 +28,45 @@ export function accessLogHealth() {
|
|
|
28
28
|
return active ? active.info() : null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** 有意关闭(config.accessLog.enabled=false)时由安装器留痕 —— 见 accessLogStatus() 的原因 ①。 */
|
|
32
|
+
let offByConfig = false;
|
|
33
|
+
|
|
34
|
+
/** 补丁状态文件说"两条都 applied 了" ⇒ 补丁在位(缺文件 / 认不出标记都算没在)。 */
|
|
35
|
+
function patchApplied(home) {
|
|
36
|
+
const st = readPatchState(home);
|
|
37
|
+
if (!st || st.marker !== PATCH_MARKER) return false;
|
|
38
|
+
if (!Array.isArray(st.targets) || st.targets.length === 0) return false;
|
|
39
|
+
return st.targets.every((t) => t && t.state === 'applied');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* C-07c(2026-09-27):访问日志的**三态出口** —— 未装时也要说清"为什么没装"。
|
|
44
|
+
*
|
|
45
|
+
* 背景:`audit === null` 曾经同时代表四种情况(老后端 / 有意关闭 / 补丁没打 / 没挂载),
|
|
46
|
+
* 界面上全是空白,分不出"应为但未装"(真·静默失败)与"有意关闭"。本函数把它们分开。
|
|
47
|
+
*
|
|
48
|
+
* @param {{enabled?: boolean, home?: string}} [opts]
|
|
49
|
+
* `enabled`:宿主侧真实配置 `config.accessLog.enabled`(读不到就别传 —— 安装器在**同一进程**里
|
|
50
|
+
* 已经记过 `offByConfig`,跨进程/重启后仍建议由调用方把配置传进来,那才是不靠猜的判据)。
|
|
51
|
+
* `home`:补丁状态文件所在的 $DSH_HOME,测试用;默认 dshHome()。
|
|
52
|
+
* @returns {object} 装上了 ⇒ `{installed:true, ...health 的 11 个字段}`;
|
|
53
|
+
* 没装上 ⇒ `{installed:false, reason}`。
|
|
54
|
+
*
|
|
55
|
+
* 能拿到的三种 reason(按判据强弱排序):
|
|
56
|
+
* ① 'disabled-by-config' —— 安装器记过 offByConfig,或调用方传入 enabled===false。**确定性判据**。
|
|
57
|
+
* ② 'patch-missing' —— 补丁状态文件缺失 / 标记对不上 / 有目标不是 applied。**确定性判据**
|
|
58
|
+
* (补丁真的没打 —— 此时无论插件在不在,请求都不会落盘)。
|
|
59
|
+
* ③ 'not-mounted' —— 补丁在位但日志器不在(没装 / 已卸载 / apply 早退 / 同进程别处覆盖)。
|
|
60
|
+
* 这是**兜底分类**,不是精确诊断:模块内无法把"apply 没跑到"与
|
|
61
|
+
* "apply 跑了但安装器抛了"分开(两者都不留痕)。
|
|
62
|
+
*/
|
|
63
|
+
export function accessLogStatus(opts = {}) {
|
|
64
|
+
if (active) return { installed: true, ...active.info().health };
|
|
65
|
+
if (offByConfig || opts.enabled === false) return { installed: false, reason: 'disabled-by-config' };
|
|
66
|
+
if (!patchApplied(opts.home)) return { installed: false, reason: 'patch-missing' };
|
|
67
|
+
return { installed: false, reason: 'not-mounted' };
|
|
68
|
+
}
|
|
69
|
+
|
|
31
70
|
/** 读补丁状态文件(补丁脚本写的),拿不到就返回 undefined —— 只用于提示,不影响功能。 */
|
|
32
71
|
export function readPatchState(home = dshHome()) {
|
|
33
72
|
const file = join(home, 'logs', PATCH_STATE_FILE);
|
|
@@ -49,9 +88,12 @@ export function installAccessLog(ctx, cfg = {}) {
|
|
|
49
88
|
const options = { ...(cfg || {}) };
|
|
50
89
|
if (!options.dir) options.dir = join(dshHome(), 'logs');
|
|
51
90
|
if (options.enabled === false) {
|
|
91
|
+
offByConfig = true; // C-07c:留痕,好让 accessLogStatus() 说出"有意关闭"而不是"没装上"
|
|
52
92
|
console.info('[dsh-ling] access log off (config.accessLog.enabled=false)');
|
|
53
|
-
|
|
93
|
+
// 摘除后不再声称"按配置关闭" —— 否则同一进程里卸载完仍会报 disabled-by-config(会骗人)。
|
|
94
|
+
return () => { offByConfig = false; };
|
|
54
95
|
}
|
|
96
|
+
offByConfig = false;
|
|
55
97
|
|
|
56
98
|
const log = createAccessLog(options);
|
|
57
99
|
active = log;
|