pinokiod 8.0.54 → 8.0.55
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/kernel/vault/automatic_scans.js +359 -3
- package/kernel/vault/constants.js +5 -1
- package/kernel/vault/hash_worker.js +4 -3
- package/kernel/vault/index.js +7 -0
- package/kernel/vault/sweeper.js +99 -5
- package/package.json +1 -1
- package/server/views/app.ejs +0 -30
- package/test/vault-automatic-scans.test.js +100 -0
- package/test/vault-sweep.test.js +5 -0
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
const fs = require("fs")
|
|
2
|
+
const os = require("os")
|
|
2
3
|
const path = require("path")
|
|
3
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
DIR_CONCURRENCY,
|
|
6
|
+
ENTRY_BATCH_SIZE,
|
|
7
|
+
HASH_INACTIVITY_MS,
|
|
8
|
+
HASH_PROGRESS_INTERVAL_MS,
|
|
9
|
+
HASH_READ_SIZE,
|
|
10
|
+
SIZE_THRESHOLD,
|
|
11
|
+
STAT_CONCURRENCY
|
|
12
|
+
} = require("./constants")
|
|
4
13
|
|
|
5
14
|
const COMPLETE_PHASES = new Set(["complete", "completed_with_exclusions"])
|
|
6
15
|
const STOP_SETTLE_MS = 3000
|
|
16
|
+
const SCAN_PROGRESS_INTERVAL_MS = 5000
|
|
17
|
+
const SCAN_STALL_THRESHOLD_MS = 30000
|
|
7
18
|
const isMissing = (error) => !!(error &&
|
|
8
19
|
(error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
9
20
|
|
|
@@ -34,6 +45,10 @@ class AutomaticScans {
|
|
|
34
45
|
this.waitingFor = null
|
|
35
46
|
this.listeners = new Set()
|
|
36
47
|
this.appTransitions = new Map()
|
|
48
|
+
this.scanDiagnostics = null
|
|
49
|
+
this.scanProgressIntervalMs = SCAN_PROGRESS_INTERVAL_MS
|
|
50
|
+
this.scanStallThresholdMs = SCAN_STALL_THRESHOLD_MS
|
|
51
|
+
this.clock = () => Date.now()
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
log(event, details = {}) {
|
|
@@ -49,6 +64,345 @@ class AutomaticScans {
|
|
|
49
64
|
console.log(`[Vault Automatic Scan] ${JSON.stringify(record)}`)
|
|
50
65
|
}
|
|
51
66
|
|
|
67
|
+
diagnosticMarker(scan) {
|
|
68
|
+
return JSON.stringify([
|
|
69
|
+
scan.phase,
|
|
70
|
+
scan.stage,
|
|
71
|
+
scan.current_directory,
|
|
72
|
+
scan.anchor_files,
|
|
73
|
+
scan.anchor_bytes,
|
|
74
|
+
scan.walk_batches,
|
|
75
|
+
scan.dirs,
|
|
76
|
+
scan.files,
|
|
77
|
+
scan.bytes_total,
|
|
78
|
+
scan.comparison_batches,
|
|
79
|
+
scan.comparison_files_verified,
|
|
80
|
+
scan.candidates,
|
|
81
|
+
scan.hash_work_files,
|
|
82
|
+
scan.hash_work_bytes,
|
|
83
|
+
scan.hash_files_completed,
|
|
84
|
+
scan.hash_bytes_completed,
|
|
85
|
+
scan.hash_total,
|
|
86
|
+
scan.hashed,
|
|
87
|
+
scan.hash_bytes,
|
|
88
|
+
scan.current_file,
|
|
89
|
+
scan.current_file_role,
|
|
90
|
+
scan.current_file_bytes,
|
|
91
|
+
scan.current_file_size,
|
|
92
|
+
scan.inode_reuses,
|
|
93
|
+
scan.unstable_hashes,
|
|
94
|
+
scan.hash_failures,
|
|
95
|
+
Array.isArray(scan.exclusions) ? scan.exclusions.length : 0
|
|
96
|
+
])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
diagnosticCounters(scan) {
|
|
100
|
+
const currentFileBytes = Math.max(
|
|
101
|
+
0, Number(scan.current_file_bytes) || 0)
|
|
102
|
+
return {
|
|
103
|
+
files: Math.max(0, Number(scan.files) || 0),
|
|
104
|
+
discoveryBytes: Math.max(0, Number(scan.bytes_total) || 0),
|
|
105
|
+
hashBytes: Math.max(0,
|
|
106
|
+
Number(scan.hash_bytes_completed) || 0) + currentFileBytes
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
diagnosticDetails(monitor, scan, now) {
|
|
111
|
+
const started = Number(scan.started) || monitor.startedAt
|
|
112
|
+
const stageStarted = Number(scan.stage_started) || started
|
|
113
|
+
const lastProgress = Number(scan.last_progress_at) || stageStarted
|
|
114
|
+
const preview = scan.preview || {}
|
|
115
|
+
const currentFileSize = Math.max(
|
|
116
|
+
0, Number(scan.current_file_size) || 0)
|
|
117
|
+
const currentFileBytes = Math.min(currentFileSize || Infinity, Math.max(
|
|
118
|
+
0, Number(scan.current_file_bytes) || 0))
|
|
119
|
+
const hashWorkBytes = Math.max(
|
|
120
|
+
0, Number(scan.hash_work_bytes) || 0)
|
|
121
|
+
const hashProgressBytes = Math.min(hashWorkBytes || Infinity, Math.max(
|
|
122
|
+
0, Number(scan.hash_bytes_completed) || 0) + currentFileBytes)
|
|
123
|
+
const details = {
|
|
124
|
+
app: monitor.app,
|
|
125
|
+
scope_id: monitor.scopeId,
|
|
126
|
+
phase: scan.phase || "unknown",
|
|
127
|
+
stage: scan.stage || scan.phase || "unknown",
|
|
128
|
+
previous_stage: scan.previous_stage,
|
|
129
|
+
previous_stage_duration_ms: scan.previous_stage_duration_ms,
|
|
130
|
+
active: !!scan.active,
|
|
131
|
+
pending: !!scan.pending,
|
|
132
|
+
elapsed_ms: Math.max(0, now - started),
|
|
133
|
+
stage_elapsed_ms: Math.max(0, now - stageStarted),
|
|
134
|
+
progress_age_ms: Math.max(0, now - lastProgress),
|
|
135
|
+
progress_unchanged_ms: Math.max(0, now - monitor.lastChangedAt),
|
|
136
|
+
current_directory: scan.current_directory,
|
|
137
|
+
anchor_files: Math.max(0, Number(scan.anchor_files) || 0),
|
|
138
|
+
anchor_bytes: Math.max(0, Number(scan.anchor_bytes) || 0),
|
|
139
|
+
walk_batches: Math.max(0, Number(scan.walk_batches) || 0),
|
|
140
|
+
dirs: Math.max(0, Number(scan.dirs) || 0),
|
|
141
|
+
files: Math.max(0, Number(scan.files) || 0),
|
|
142
|
+
bytes_total: Math.max(0, Number(scan.bytes_total) || 0),
|
|
143
|
+
source_files: scan.source_files || {},
|
|
144
|
+
source_bytes: scan.source_bytes || {},
|
|
145
|
+
comparison_batches:
|
|
146
|
+
Math.max(0, Number(scan.comparison_batches) || 0),
|
|
147
|
+
comparison_files_verified:
|
|
148
|
+
Math.max(0, Number(scan.comparison_files_verified) || 0),
|
|
149
|
+
candidates: Math.max(0, Number(scan.candidates) || 0),
|
|
150
|
+
hash_work_files:
|
|
151
|
+
Math.max(0, Number(scan.hash_work_files) || 0),
|
|
152
|
+
hash_work_bytes: hashWorkBytes,
|
|
153
|
+
hash_files_completed:
|
|
154
|
+
Math.max(0, Number(scan.hash_files_completed) || 0),
|
|
155
|
+
hash_bytes_completed:
|
|
156
|
+
Math.max(0, Number(scan.hash_bytes_completed) || 0),
|
|
157
|
+
hash_progress_bytes: hashProgressBytes,
|
|
158
|
+
hash_progress_percent: hashWorkBytes
|
|
159
|
+
? Math.floor((hashProgressBytes / hashWorkBytes) * 100)
|
|
160
|
+
: 0,
|
|
161
|
+
hash_total: Math.max(0, Number(scan.hash_total) || 0),
|
|
162
|
+
hashed: Math.max(0, Number(scan.hashed) || 0),
|
|
163
|
+
hash_bytes: Math.max(0, Number(scan.hash_bytes) || 0),
|
|
164
|
+
current_file: scan.current_file,
|
|
165
|
+
current_file_role: scan.current_file_role,
|
|
166
|
+
current_file_source_id: scan.current_file_source_id,
|
|
167
|
+
current_file_bytes: Number.isFinite(currentFileBytes)
|
|
168
|
+
? currentFileBytes
|
|
169
|
+
: 0,
|
|
170
|
+
current_file_size: currentFileSize,
|
|
171
|
+
inode_reuses: Math.max(0, Number(scan.inode_reuses) || 0),
|
|
172
|
+
unstable_hashes: Math.max(0, Number(scan.unstable_hashes) || 0),
|
|
173
|
+
hash_failures: Math.max(0, Number(scan.hash_failures) || 0),
|
|
174
|
+
source_hash_failures: scan.source_hash_failures || {},
|
|
175
|
+
exclusions_count: Array.isArray(scan.exclusions)
|
|
176
|
+
? scan.exclusions.length
|
|
177
|
+
: 0,
|
|
178
|
+
preview_duplicate_files:
|
|
179
|
+
Math.max(0, Number(preview.duplicate_files) || 0),
|
|
180
|
+
preview_savings_bytes: Math.max(0, Number(preview.bytes) || 0),
|
|
181
|
+
source_refresh_duration_ms: scan.source_refresh_duration_ms,
|
|
182
|
+
anchor_walk_duration_ms: scan.anchor_walk_duration_ms,
|
|
183
|
+
walk_duration_ms: scan.walk_duration_ms,
|
|
184
|
+
comparison_duration_ms: scan.comparison_duration_ms,
|
|
185
|
+
hash_wait_duration_ms: scan.hash_wait_duration_ms,
|
|
186
|
+
hash_duration_ms: scan.hash_duration_ms,
|
|
187
|
+
anchor_verify_duration_ms: scan.anchor_verify_duration_ms,
|
|
188
|
+
publish_duration_ms: scan.publish_duration_ms,
|
|
189
|
+
error: scan.error
|
|
190
|
+
}
|
|
191
|
+
const counters = this.diagnosticCounters(scan)
|
|
192
|
+
if (monitor.lastCounters && monitor.lastSampleAt < now) {
|
|
193
|
+
const sampleMs = now - monitor.lastSampleAt
|
|
194
|
+
details.sample_ms = sampleMs
|
|
195
|
+
details.files_per_second = Number((Math.max(
|
|
196
|
+
0, counters.files - monitor.lastCounters.files
|
|
197
|
+
) * 1000 / sampleMs).toFixed(2))
|
|
198
|
+
details.discovery_bytes_per_second = Math.round(Math.max(
|
|
199
|
+
0, counters.discoveryBytes - monitor.lastCounters.discoveryBytes
|
|
200
|
+
) * 1000 / sampleMs)
|
|
201
|
+
details.hash_bytes_per_second = Math.round(Math.max(
|
|
202
|
+
0, counters.hashBytes - monitor.lastCounters.hashBytes
|
|
203
|
+
) * 1000 / sampleMs)
|
|
204
|
+
}
|
|
205
|
+
monitor.lastCounters = counters
|
|
206
|
+
monitor.lastSampleAt = now
|
|
207
|
+
try {
|
|
208
|
+
const cpu = process.cpuUsage()
|
|
209
|
+
if (monitor.cpuUsage && Number(details.sample_ms) > 0) {
|
|
210
|
+
const user = Math.max(0, cpu.user - monitor.cpuUsage.user)
|
|
211
|
+
const system = Math.max(0, cpu.system - monitor.cpuUsage.system)
|
|
212
|
+
details.process_cpu_user_delta_ms =
|
|
213
|
+
Number((user / 1000).toFixed(2))
|
|
214
|
+
details.process_cpu_system_delta_ms =
|
|
215
|
+
Number((system / 1000).toFixed(2))
|
|
216
|
+
const sampleMs = Math.max(1, Number(details.sample_ms) ||
|
|
217
|
+
now - monitor.startedAt)
|
|
218
|
+
details.process_cpu_percent =
|
|
219
|
+
Number((((user + system) / 1000) * 100 / sampleMs).toFixed(2))
|
|
220
|
+
}
|
|
221
|
+
monitor.cpuUsage = cpu
|
|
222
|
+
const memory = process.memoryUsage()
|
|
223
|
+
details.process_rss_bytes = memory.rss
|
|
224
|
+
details.process_heap_used_bytes = memory.heapUsed
|
|
225
|
+
} catch (_) {}
|
|
226
|
+
return details
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
sampleScanDiagnostics(options = {}) {
|
|
230
|
+
const monitor = this.scanDiagnostics
|
|
231
|
+
if (!monitor) return null
|
|
232
|
+
let scan
|
|
233
|
+
try {
|
|
234
|
+
scan = typeof this.vault.scanStatus === "function"
|
|
235
|
+
? this.vault.scanStatus()
|
|
236
|
+
: null
|
|
237
|
+
} catch (error) {
|
|
238
|
+
this.log("scan-progress-error", {
|
|
239
|
+
app: monitor.app,
|
|
240
|
+
scope_id: monitor.scopeId,
|
|
241
|
+
message: error && error.message ? error.message : String(error)
|
|
242
|
+
})
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
if (!scan) return null
|
|
246
|
+
const now = this.clock()
|
|
247
|
+
const marker = this.diagnosticMarker(scan)
|
|
248
|
+
const changed = marker !== monitor.lastMarker
|
|
249
|
+
const previousUnchanged = now - monitor.lastChangedAt
|
|
250
|
+
if (changed) {
|
|
251
|
+
if (monitor.stalled) {
|
|
252
|
+
this.log("scan-progress-resumed", {
|
|
253
|
+
app: monitor.app,
|
|
254
|
+
scope_id: monitor.scopeId,
|
|
255
|
+
phase: scan.phase,
|
|
256
|
+
stage: scan.stage,
|
|
257
|
+
stalled_ms: Math.max(0, previousUnchanged)
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
monitor.lastMarker = marker
|
|
261
|
+
monitor.lastChangedAt = now
|
|
262
|
+
monitor.stalled = false
|
|
263
|
+
}
|
|
264
|
+
const stage = scan.stage || scan.phase || "unknown"
|
|
265
|
+
if (monitor.lastStage && stage !== monitor.lastStage) {
|
|
266
|
+
this.log("scan-stage-changed", {
|
|
267
|
+
app: monitor.app,
|
|
268
|
+
scope_id: monitor.scopeId,
|
|
269
|
+
from: monitor.lastStage,
|
|
270
|
+
to: stage,
|
|
271
|
+
previous_stage_observed_ms: Math.max(
|
|
272
|
+
0, now - monitor.lastStageObservedAt)
|
|
273
|
+
})
|
|
274
|
+
monitor.lastStageObservedAt = now
|
|
275
|
+
}
|
|
276
|
+
monitor.lastStage = stage
|
|
277
|
+
const details = this.diagnosticDetails(monitor, scan, now)
|
|
278
|
+
if (!options.final) this.log("scan-progress", details)
|
|
279
|
+
if (!monitor.stalled && details.progress_unchanged_ms >=
|
|
280
|
+
this.scanStallThresholdMs && (scan.active || scan.pending)) {
|
|
281
|
+
monitor.stalled = true
|
|
282
|
+
this.log("scan-progress-stalled", details)
|
|
283
|
+
}
|
|
284
|
+
return details
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
startScanDiagnostics(app, scopeId) {
|
|
288
|
+
this.stopScanDiagnostics()
|
|
289
|
+
const now = this.clock()
|
|
290
|
+
let cpuUsage = null
|
|
291
|
+
try { cpuUsage = process.cpuUsage() } catch (_) {}
|
|
292
|
+
this.scanDiagnostics = {
|
|
293
|
+
app,
|
|
294
|
+
scopeId,
|
|
295
|
+
startedAt: now,
|
|
296
|
+
lastChangedAt: now,
|
|
297
|
+
lastMarker: null,
|
|
298
|
+
lastStage: null,
|
|
299
|
+
lastStageObservedAt: now,
|
|
300
|
+
lastCounters: null,
|
|
301
|
+
lastSampleAt: now,
|
|
302
|
+
cpuUsage,
|
|
303
|
+
stalled: false,
|
|
304
|
+
timer: null
|
|
305
|
+
}
|
|
306
|
+
const version = this.vault.kernel && this.vault.kernel.version
|
|
307
|
+
let cpuCount = null
|
|
308
|
+
let cpuModel = null
|
|
309
|
+
let totalMemory = null
|
|
310
|
+
let osRelease = null
|
|
311
|
+
try {
|
|
312
|
+
const cpus = os.cpus()
|
|
313
|
+
cpuCount = cpus.length
|
|
314
|
+
cpuModel = cpus[0] && cpus[0].model
|
|
315
|
+
totalMemory = os.totalmem()
|
|
316
|
+
osRelease = os.release()
|
|
317
|
+
} catch (_) {}
|
|
318
|
+
this.log("scan-diagnostics-started", {
|
|
319
|
+
app,
|
|
320
|
+
scope_id: scopeId,
|
|
321
|
+
progress_interval_ms: this.scanProgressIntervalMs,
|
|
322
|
+
stall_threshold_ms: this.scanStallThresholdMs,
|
|
323
|
+
platform: this.vault.kernel && this.vault.kernel.platform ||
|
|
324
|
+
process.platform,
|
|
325
|
+
arch: process.arch,
|
|
326
|
+
os_release: osRelease,
|
|
327
|
+
cpu_count: cpuCount,
|
|
328
|
+
cpu_model: cpuModel,
|
|
329
|
+
total_memory_bytes: totalMemory,
|
|
330
|
+
node_version: process.versions && process.versions.node,
|
|
331
|
+
electron_version: process.versions && process.versions.electron,
|
|
332
|
+
pinokiod_version: version && version.pinokiod,
|
|
333
|
+
process_pid: process.pid,
|
|
334
|
+
threshold_bytes: this.sizeThreshold,
|
|
335
|
+
entry_batch_size: ENTRY_BATCH_SIZE,
|
|
336
|
+
directory_concurrency: this.vault.scanner
|
|
337
|
+
? this.vault.scanner.dirConcurrency
|
|
338
|
+
: DIR_CONCURRENCY,
|
|
339
|
+
stat_concurrency: this.vault.scanner
|
|
340
|
+
? this.vault.scanner.statConcurrency
|
|
341
|
+
: STAT_CONCURRENCY,
|
|
342
|
+
hash_read_size: HASH_READ_SIZE,
|
|
343
|
+
hash_progress_interval_ms: HASH_PROGRESS_INTERVAL_MS,
|
|
344
|
+
hash_inactivity_timeout_ms:
|
|
345
|
+
Number(this.vault.hashInactivityMs) || HASH_INACTIVITY_MS
|
|
346
|
+
})
|
|
347
|
+
this.logFilesystemDiagnostics(app, scopeId)
|
|
348
|
+
this.sampleScanDiagnostics()
|
|
349
|
+
const timer = setInterval(() => {
|
|
350
|
+
this.sampleScanDiagnostics()
|
|
351
|
+
}, this.scanProgressIntervalMs)
|
|
352
|
+
if (typeof timer.unref === "function") timer.unref()
|
|
353
|
+
this.scanDiagnostics.timer = timer
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
finishScanDiagnostics(app) {
|
|
357
|
+
const monitor = this.scanDiagnostics
|
|
358
|
+
if (!monitor || monitor.app !== app) return {}
|
|
359
|
+
if (monitor.timer) clearInterval(monitor.timer)
|
|
360
|
+
const details = this.sampleScanDiagnostics({ final: true }) || {}
|
|
361
|
+
this.scanDiagnostics = null
|
|
362
|
+
return details
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
stopScanDiagnostics() {
|
|
366
|
+
const monitor = this.scanDiagnostics
|
|
367
|
+
if (monitor && monitor.timer) clearInterval(monitor.timer)
|
|
368
|
+
this.scanDiagnostics = null
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
logFilesystemDiagnostics(app, scopeId) {
|
|
372
|
+
if (typeof fs.promises.statfs !== "function") return
|
|
373
|
+
let source
|
|
374
|
+
try {
|
|
375
|
+
source = this.sourceForApp(app)
|
|
376
|
+
} catch (_) {
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
if (!source || !source.root) return
|
|
380
|
+
fs.promises.statfs(source.root).then((stat) => {
|
|
381
|
+
const blockSize = Math.max(0, Number(stat.bsize) || 0)
|
|
382
|
+
const bytes = (blocks) => Math.max(
|
|
383
|
+
0, Number(blocks) || 0) * blockSize
|
|
384
|
+
this.log("scan-filesystem", {
|
|
385
|
+
app,
|
|
386
|
+
scope_id: scopeId,
|
|
387
|
+
source_device: source.dev,
|
|
388
|
+
filesystem_type: stat.type,
|
|
389
|
+
block_size: blockSize,
|
|
390
|
+
total_bytes: bytes(stat.blocks),
|
|
391
|
+
free_bytes: bytes(stat.bfree),
|
|
392
|
+
available_bytes: bytes(stat.bavail),
|
|
393
|
+
total_file_nodes: stat.files,
|
|
394
|
+
free_file_nodes: stat.ffree
|
|
395
|
+
})
|
|
396
|
+
}).catch((error) => {
|
|
397
|
+
this.log("scan-filesystem-error", {
|
|
398
|
+
app,
|
|
399
|
+
scope_id: scopeId,
|
|
400
|
+
code: error && error.code,
|
|
401
|
+
error_name: error && error.name
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
52
406
|
appForLaunchPath(launchPath) {
|
|
53
407
|
if (typeof launchPath !== "string" || !launchPath) return null
|
|
54
408
|
const apiRoot = path.resolve(this.vault.kernel.homedir, "api")
|
|
@@ -515,6 +869,7 @@ class AutomaticScans {
|
|
|
515
869
|
this.waitForBusyWork()
|
|
516
870
|
} else {
|
|
517
871
|
this.log("scan-started", { app: entry.app, scope_id: scopeId })
|
|
872
|
+
this.startScanDiagnostics(entry.app, scopeId)
|
|
518
873
|
}
|
|
519
874
|
}
|
|
520
875
|
|
|
@@ -743,13 +1098,14 @@ class AutomaticScans {
|
|
|
743
1098
|
if (this.active && this.active.app === app) this.active = null
|
|
744
1099
|
const reason = this.cancelReasons.get(app)
|
|
745
1100
|
this.cancelReasons.delete(app)
|
|
746
|
-
this.
|
|
1101
|
+
const diagnostics = this.finishScanDiagnostics(app)
|
|
1102
|
+
this.log("scan-finished", Object.assign({}, diagnostics, {
|
|
747
1103
|
app,
|
|
748
1104
|
scope_id: scopeId,
|
|
749
1105
|
outcome: result && result.outcome,
|
|
750
1106
|
cancel_reason: reason,
|
|
751
1107
|
error: error && error.message ? error.message : error
|
|
752
|
-
})
|
|
1108
|
+
}))
|
|
753
1109
|
try {
|
|
754
1110
|
const entry = this.entries.get(app)
|
|
755
1111
|
if (reason === "paused" || reason === "manual-mode" ||
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const CANDIDATE_SIZE_BASE = process.platform === "win32" ? 1024 : 1000
|
|
2
2
|
const SIZE_THRESHOLD = 100 * CANDIDATE_SIZE_BASE ** 2
|
|
3
|
+
const HASH_READ_SIZE = 1024 * 1024
|
|
4
|
+
const HASH_PROGRESS_INTERVAL_MS = 1000
|
|
3
5
|
const CANDIDATE_SIZE_OPTIONS = [0]
|
|
4
6
|
.concat([1, 10, 50, 100, 500].map((value) => value * CANDIDATE_SIZE_BASE ** 2))
|
|
5
7
|
.concat(CANDIDATE_SIZE_BASE ** 3)
|
|
@@ -14,5 +16,7 @@ module.exports = {
|
|
|
14
16
|
ENTRY_BATCH_SIZE: 256,
|
|
15
17
|
DIR_CONCURRENCY: 8,
|
|
16
18
|
STAT_CONCURRENCY: 32,
|
|
17
|
-
HASH_INACTIVITY_MS: 120 * 1000
|
|
19
|
+
HASH_INACTIVITY_MS: 120 * 1000,
|
|
20
|
+
HASH_READ_SIZE,
|
|
21
|
+
HASH_PROGRESS_INTERVAL_MS
|
|
18
22
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
const { parentPort } = require('worker_threads')
|
|
2
2
|
const crypto = require('crypto')
|
|
3
3
|
const fs = require('fs')
|
|
4
|
+
const {
|
|
5
|
+
HASH_READ_SIZE,
|
|
6
|
+
HASH_PROGRESS_INTERVAL_MS
|
|
7
|
+
} = require('./constants')
|
|
4
8
|
|
|
5
9
|
// Large model files otherwise generate a very high number of 64 KiB stream
|
|
6
10
|
// events. This changes only the read granularity; every byte still feeds the
|
|
7
11
|
// same sha256 digest, one file at a time.
|
|
8
|
-
const HASH_READ_SIZE = 1024 * 1024
|
|
9
|
-
const HASH_PROGRESS_INTERVAL_MS = 1000
|
|
10
|
-
|
|
11
12
|
parentPort.on('message', ({ id, filePath }) => {
|
|
12
13
|
const hash = crypto.createHash('sha256')
|
|
13
14
|
let size = 0
|
package/kernel/vault/index.js
CHANGED
|
@@ -3372,6 +3372,7 @@ class Vault {
|
|
|
3372
3372
|
const state = pending
|
|
3373
3373
|
? Object.assign(this.sweeper.idleState(), {
|
|
3374
3374
|
phase: "queued",
|
|
3375
|
+
stage: "queued",
|
|
3375
3376
|
scope_id: this.scanScopeId
|
|
3376
3377
|
})
|
|
3377
3378
|
: this.sweeper.state
|
|
@@ -3385,6 +3386,12 @@ class Vault {
|
|
|
3385
3386
|
current_file_size: this.sweeper.currentHash
|
|
3386
3387
|
? this.sweeper.currentHash.size
|
|
3387
3388
|
: null,
|
|
3389
|
+
current_file_source_id: this.sweeper.currentHash
|
|
3390
|
+
? this.sweeper.currentHash.source_id
|
|
3391
|
+
: null,
|
|
3392
|
+
current_file_role: this.sweeper.currentHash
|
|
3393
|
+
? this.sweeper.currentHash.role
|
|
3394
|
+
: null,
|
|
3388
3395
|
pending,
|
|
3389
3396
|
error: this.scanError
|
|
3390
3397
|
})
|
package/kernel/vault/sweeper.js
CHANGED
|
@@ -22,9 +22,20 @@ class Sweeper {
|
|
|
22
22
|
return {
|
|
23
23
|
active: false,
|
|
24
24
|
phase: "idle",
|
|
25
|
+
stage: "idle",
|
|
26
|
+
stage_started: null,
|
|
27
|
+
previous_stage: null,
|
|
28
|
+
previous_stage_duration_ms: null,
|
|
29
|
+
last_progress_at: null,
|
|
30
|
+
current_directory: null,
|
|
31
|
+
anchor_files: 0,
|
|
32
|
+
anchor_bytes: 0,
|
|
25
33
|
dirs: 0,
|
|
26
34
|
files: 0,
|
|
27
35
|
bytes_total: 0,
|
|
36
|
+
walk_batches: 0,
|
|
37
|
+
comparison_batches: 0,
|
|
38
|
+
comparison_files_verified: 0,
|
|
28
39
|
source_bytes: {},
|
|
29
40
|
source_files: {},
|
|
30
41
|
source_hash_failures: {},
|
|
@@ -50,12 +61,34 @@ class Sweeper {
|
|
|
50
61
|
},
|
|
51
62
|
started: null,
|
|
52
63
|
duration_ms: null,
|
|
64
|
+
source_refresh_duration_ms: null,
|
|
65
|
+
anchor_walk_duration_ms: null,
|
|
53
66
|
walk_duration_ms: null,
|
|
67
|
+
comparison_duration_ms: null,
|
|
54
68
|
hash_wait_duration_ms: null,
|
|
55
|
-
hash_duration_ms: 0
|
|
69
|
+
hash_duration_ms: 0,
|
|
70
|
+
anchor_verify_duration_ms: null,
|
|
71
|
+
publish_duration_ms: null
|
|
56
72
|
}
|
|
57
73
|
}
|
|
58
74
|
|
|
75
|
+
setStage(stage) {
|
|
76
|
+
const now = Date.now()
|
|
77
|
+
if (this.state.stage && this.state.stage !== stage &&
|
|
78
|
+
Number.isFinite(this.state.stage_started)) {
|
|
79
|
+
this.state.previous_stage = this.state.stage
|
|
80
|
+
this.state.previous_stage_duration_ms = Math.max(
|
|
81
|
+
0, now - this.state.stage_started)
|
|
82
|
+
}
|
|
83
|
+
this.state.stage = stage
|
|
84
|
+
this.state.stage_started = now
|
|
85
|
+
this.state.last_progress_at = now
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
markProgress() {
|
|
89
|
+
this.state.last_progress_at = Date.now()
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
cancel() {
|
|
60
93
|
if (!this.state.active) return false
|
|
61
94
|
this.cancelRequested = true
|
|
@@ -89,6 +122,7 @@ class Sweeper {
|
|
|
89
122
|
started: Date.now(),
|
|
90
123
|
scope_id: scopeId
|
|
91
124
|
})
|
|
125
|
+
this.setStage("starting")
|
|
92
126
|
|
|
93
127
|
const registry = this.vault.registry
|
|
94
128
|
const runId = await registry.beginScan(scopeId)
|
|
@@ -96,7 +130,12 @@ class Sweeper {
|
|
|
96
130
|
let fatalError = null
|
|
97
131
|
let affectedApps = []
|
|
98
132
|
try {
|
|
133
|
+
this.setStage("refreshing_sources")
|
|
134
|
+
const sourceRefreshStarted = Date.now()
|
|
99
135
|
await this.vault.refreshSources()
|
|
136
|
+
this.state.source_refresh_duration_ms =
|
|
137
|
+
Date.now() - sourceRefreshStarted
|
|
138
|
+
this.markProgress()
|
|
100
139
|
if (!scopeId) {
|
|
101
140
|
const unavailable = this.vault.sources().find((source) =>
|
|
102
141
|
source.kind === "external" &&
|
|
@@ -113,9 +152,13 @@ class Sweeper {
|
|
|
113
152
|
}
|
|
114
153
|
|
|
115
154
|
const anchorStores = this.anchorStoresForScope(scopeId)
|
|
155
|
+
this.setStage("scanning_anchors")
|
|
156
|
+
const anchorWalkStarted = Date.now()
|
|
116
157
|
await this.stageAnchors(runId, anchorStores)
|
|
158
|
+
this.state.anchor_walk_duration_ms = Date.now() - anchorWalkStarted
|
|
117
159
|
this.checkpoint()
|
|
118
160
|
|
|
161
|
+
this.setStage("discovering_files")
|
|
119
162
|
const walkStarted = Date.now()
|
|
120
163
|
for (const source of scanRoots) {
|
|
121
164
|
if (!Object.prototype.hasOwnProperty.call(
|
|
@@ -127,20 +170,34 @@ class Sweeper {
|
|
|
127
170
|
await this.walk(source.root, runId, source.source_id)
|
|
128
171
|
this.checkpoint()
|
|
129
172
|
}
|
|
173
|
+
this.state.current_directory = null
|
|
130
174
|
this.state.walk_duration_ms = Date.now() - walkStarted
|
|
175
|
+
this.setStage("staging_discovery_results")
|
|
131
176
|
await registry.stageExclusions(runId, this.exclusionList())
|
|
132
177
|
if (scopeId) {
|
|
178
|
+
const comparisonStarted = Date.now()
|
|
179
|
+
this.setStage("staging_comparisons")
|
|
133
180
|
const comparisons = await registry.stageComparisonFiles(
|
|
134
181
|
runId, this.publicationSourceIds(scopeId))
|
|
135
182
|
this.applyHashWork(comparisons && comparisons.work)
|
|
183
|
+
this.setStage("verifying_comparisons")
|
|
136
184
|
await this.verifyComparisonFiles(runId)
|
|
185
|
+
this.state.comparison_duration_ms =
|
|
186
|
+
Date.now() - comparisonStarted
|
|
137
187
|
}
|
|
138
188
|
|
|
139
189
|
this.state.phase = "hashing"
|
|
190
|
+
this.setStage("hashing_candidates")
|
|
140
191
|
const hashStarted = Date.now()
|
|
141
192
|
await this.hashCandidates(runId)
|
|
193
|
+
this.setStage("staging_hash_results")
|
|
142
194
|
await registry.stageExclusions(runId, this.exclusionList())
|
|
195
|
+
this.setStage("verifying_anchors")
|
|
196
|
+
const anchorVerifyStarted = Date.now()
|
|
143
197
|
await this.verifyCandidateAnchors(runId)
|
|
198
|
+
this.state.anchor_verify_duration_ms =
|
|
199
|
+
Date.now() - anchorVerifyStarted
|
|
200
|
+
this.setStage("staging_verification_results")
|
|
144
201
|
await registry.stageExclusions(runId, this.exclusionList())
|
|
145
202
|
this.state.hash_wait_duration_ms = Date.now() - hashStarted
|
|
146
203
|
this.checkpoint()
|
|
@@ -149,6 +206,7 @@ class Sweeper {
|
|
|
149
206
|
? "completed_with_exclusions"
|
|
150
207
|
: "complete"
|
|
151
208
|
this.state.phase = "publishing"
|
|
209
|
+
this.setStage("publishing")
|
|
152
210
|
this.state.duration_ms = Date.now() - this.state.started
|
|
153
211
|
const metadata = this.scanMetadata(scopeId, outcome)
|
|
154
212
|
const stores = anchorStores
|
|
@@ -160,12 +218,15 @@ class Sweeper {
|
|
|
160
218
|
can_link: store.mode !== "copy",
|
|
161
219
|
root: store.root
|
|
162
220
|
}))
|
|
221
|
+
const publishStarted = Date.now()
|
|
163
222
|
const publication = await registry.publishScan(
|
|
164
223
|
runId,
|
|
165
224
|
this.publicationSourceIds(scopeId),
|
|
166
225
|
metadata,
|
|
167
226
|
stores
|
|
168
227
|
)
|
|
228
|
+
this.state.publish_duration_ms = Date.now() - publishStarted
|
|
229
|
+
this.markProgress()
|
|
169
230
|
affectedApps = publication && Array.isArray(publication.affected_apps)
|
|
170
231
|
? publication.affected_apps
|
|
171
232
|
: []
|
|
@@ -178,9 +239,11 @@ class Sweeper {
|
|
|
178
239
|
}
|
|
179
240
|
} finally {
|
|
180
241
|
this.currentHash = null
|
|
242
|
+
this.state.current_directory = null
|
|
181
243
|
this.state.active = false
|
|
182
244
|
this.state.phase = outcome
|
|
183
245
|
this.state.duration_ms = Date.now() - this.state.started
|
|
246
|
+
this.setStage(outcome)
|
|
184
247
|
this.clearPreview()
|
|
185
248
|
}
|
|
186
249
|
|
|
@@ -223,9 +286,15 @@ class Sweeper {
|
|
|
223
286
|
hash_failures: this.state.hash_failures,
|
|
224
287
|
candidate_min_bytes: this.vault.sizeThreshold,
|
|
225
288
|
duration_ms: Date.now() - this.state.started,
|
|
289
|
+
source_refresh_duration_ms:
|
|
290
|
+
this.state.source_refresh_duration_ms || 0,
|
|
291
|
+
anchor_walk_duration_ms: this.state.anchor_walk_duration_ms || 0,
|
|
226
292
|
walk_duration_ms: this.state.walk_duration_ms || 0,
|
|
293
|
+
comparison_duration_ms: this.state.comparison_duration_ms || 0,
|
|
227
294
|
hash_wait_duration_ms: this.state.hash_wait_duration_ms || 0,
|
|
228
|
-
hash_duration_ms: this.state.hash_duration_ms || 0
|
|
295
|
+
hash_duration_ms: this.state.hash_duration_ms || 0,
|
|
296
|
+
anchor_verify_duration_ms:
|
|
297
|
+
this.state.anchor_verify_duration_ms || 0
|
|
229
298
|
}
|
|
230
299
|
}
|
|
231
300
|
|
|
@@ -334,9 +403,14 @@ class Sweeper {
|
|
|
334
403
|
onError: (error, filePath, reason = null) =>
|
|
335
404
|
this.recordExclusion(error, filePath, null, reason),
|
|
336
405
|
onEntries: async (anchors) => {
|
|
406
|
+
this.state.anchor_files += anchors.length
|
|
407
|
+
this.state.anchor_bytes += anchors.reduce((total, anchor) =>
|
|
408
|
+
total + (Math.max(0, Number(anchor.size) || 0)), 0)
|
|
409
|
+
this.markProgress()
|
|
337
410
|
const staged = await this.vault.registry.stageAnchors(
|
|
338
411
|
runId, anchors)
|
|
339
412
|
this.applyHashWork(staged && staged.work)
|
|
413
|
+
this.markProgress()
|
|
340
414
|
}
|
|
341
415
|
})
|
|
342
416
|
}
|
|
@@ -352,6 +426,9 @@ class Sweeper {
|
|
|
352
426
|
}
|
|
353
427
|
const observations = await this.vault.scanner.validateSnapshots(entries)
|
|
354
428
|
await this.vault.registry.resolveComparisonFiles(runId, observations)
|
|
429
|
+
this.state.comparison_batches += 1
|
|
430
|
+
this.state.comparison_files_verified += entries.length
|
|
431
|
+
this.markProgress()
|
|
355
432
|
}
|
|
356
433
|
}
|
|
357
434
|
|
|
@@ -361,7 +438,12 @@ class Sweeper {
|
|
|
361
438
|
skipDirectory: (full) => this.vault.isStorageRoot(full),
|
|
362
439
|
onError: (error, filePath) =>
|
|
363
440
|
this.recordExclusion(error, filePath, preferredSourceId),
|
|
364
|
-
onBatch: async ({ files, directories }) => {
|
|
441
|
+
onBatch: async ({ files, directories, currentDirectory }) => {
|
|
442
|
+
this.state.current_directory = currentDirectory
|
|
443
|
+
? path.relative(root, currentDirectory) || "."
|
|
444
|
+
: this.state.current_directory
|
|
445
|
+
this.markProgress()
|
|
446
|
+
this.state.walk_batches += 1
|
|
365
447
|
this.state.dirs += directories
|
|
366
448
|
const entries = files.map((file) =>
|
|
367
449
|
this.considerStat(file.path, file.stat, preferredSourceId))
|
|
@@ -371,6 +453,7 @@ class Sweeper {
|
|
|
371
453
|
this.state.candidates += Number(staged && staged.changes) || 0
|
|
372
454
|
this.applyPreview(staged && staged.preview)
|
|
373
455
|
this.applyHashWork(staged && staged.work)
|
|
456
|
+
this.markProgress()
|
|
374
457
|
}
|
|
375
458
|
})
|
|
376
459
|
}
|
|
@@ -421,6 +504,7 @@ class Sweeper {
|
|
|
421
504
|
runId, candidate.dev, candidate.ino, candidate.reusable_hash)
|
|
422
505
|
this.state.inode_reuses += updated.changes
|
|
423
506
|
this.applyPreview(updated.preview)
|
|
507
|
+
this.markProgress()
|
|
424
508
|
continue
|
|
425
509
|
}
|
|
426
510
|
await this.hashCandidateRoutes(runId, candidate)
|
|
@@ -439,14 +523,18 @@ class Sweeper {
|
|
|
439
523
|
this.currentHash = {
|
|
440
524
|
path: candidate.path,
|
|
441
525
|
size: candidate.size,
|
|
442
|
-
bytes: 0
|
|
526
|
+
bytes: 0,
|
|
527
|
+
source_id: candidate.source_id || null,
|
|
528
|
+
role: candidate.comparison_only ? "comparison" : "candidate"
|
|
443
529
|
}
|
|
530
|
+
this.markProgress()
|
|
444
531
|
try {
|
|
445
532
|
const verified = await this.vault.scanner.hashStable(candidate, {
|
|
446
533
|
onProgress: (bytes) => {
|
|
447
534
|
if (this.currentHash &&
|
|
448
535
|
this.currentHash.path === candidate.path) {
|
|
449
536
|
this.currentHash.bytes = bytes
|
|
537
|
+
this.markProgress()
|
|
450
538
|
}
|
|
451
539
|
}
|
|
452
540
|
})
|
|
@@ -476,6 +564,7 @@ class Sweeper {
|
|
|
476
564
|
this.state.hash_bytes += verified.result.size
|
|
477
565
|
this.state.inode_reuses += Math.max(0, updated.changes - 1)
|
|
478
566
|
this.applyPreview(updated.preview)
|
|
567
|
+
this.markProgress()
|
|
479
568
|
candidate = null
|
|
480
569
|
deferCompletion = false
|
|
481
570
|
} catch (error) {
|
|
@@ -513,14 +602,18 @@ class Sweeper {
|
|
|
513
602
|
this.currentHash = {
|
|
514
603
|
path: anchor.path,
|
|
515
604
|
size: anchor.size,
|
|
516
|
-
bytes: 0
|
|
605
|
+
bytes: 0,
|
|
606
|
+
source_id: null,
|
|
607
|
+
role: "anchor"
|
|
517
608
|
}
|
|
609
|
+
this.markProgress()
|
|
518
610
|
try {
|
|
519
611
|
const verified = await this.vault.scanner.hashStable(anchor, {
|
|
520
612
|
onProgress: (bytes) => {
|
|
521
613
|
if (this.currentHash &&
|
|
522
614
|
this.currentHash.path === anchor.path) {
|
|
523
615
|
this.currentHash.bytes = bytes
|
|
616
|
+
this.markProgress()
|
|
524
617
|
}
|
|
525
618
|
}
|
|
526
619
|
})
|
|
@@ -528,6 +621,7 @@ class Sweeper {
|
|
|
528
621
|
await this.vault.registry.markAnchorChecked(
|
|
529
622
|
runId, anchor, verified.result.hash)
|
|
530
623
|
completeWork = true
|
|
624
|
+
this.markProgress()
|
|
531
625
|
} else {
|
|
532
626
|
const retry =
|
|
533
627
|
await this.vault.registry.markAnchorVerificationFailed(
|
package/package.json
CHANGED
package/server/views/app.ejs
CHANGED
|
@@ -12131,11 +12131,6 @@ const rerenderMenuSection = (container, html) => {
|
|
|
12131
12131
|
let shell = target.closest("[data-shell]")
|
|
12132
12132
|
if (shell) {
|
|
12133
12133
|
let shell_id = shell.getAttribute("data-shell")
|
|
12134
|
-
n.Noty({
|
|
12135
|
-
text: `stopping shell`,
|
|
12136
|
-
silent: true,
|
|
12137
|
-
timeout: 2000
|
|
12138
|
-
})
|
|
12139
12134
|
let socket = new Socket()
|
|
12140
12135
|
socket.run({
|
|
12141
12136
|
method: "kernel.bin.shell_kill",
|
|
@@ -12146,11 +12141,6 @@ const rerenderMenuSection = (container, html) => {
|
|
|
12146
12141
|
console.log("packet", packet)
|
|
12147
12142
|
if (packet.type === "result") {
|
|
12148
12143
|
socket.close()
|
|
12149
|
-
n.Noty({
|
|
12150
|
-
timeout: 2000,
|
|
12151
|
-
text: `stopped`,
|
|
12152
|
-
silent: true
|
|
12153
|
-
})
|
|
12154
12144
|
console.log("Refresh 4")
|
|
12155
12145
|
refresh(true)
|
|
12156
12146
|
}
|
|
@@ -12163,11 +12153,6 @@ const rerenderMenuSection = (container, html) => {
|
|
|
12163
12153
|
target.querySelector("i").className = "fa-solid fa-check"
|
|
12164
12154
|
} catch (e) {
|
|
12165
12155
|
}
|
|
12166
|
-
n.Noty({
|
|
12167
|
-
text: `stopping script`,
|
|
12168
|
-
silent: true,
|
|
12169
|
-
timeout: 2000
|
|
12170
|
-
})
|
|
12171
12156
|
|
|
12172
12157
|
let socket = new Socket()
|
|
12173
12158
|
socket.run({
|
|
@@ -12179,11 +12164,6 @@ const rerenderMenuSection = (container, html) => {
|
|
|
12179
12164
|
if (packet.type === "result") {
|
|
12180
12165
|
console.log("is result")
|
|
12181
12166
|
socket.close()
|
|
12182
|
-
n.Noty({
|
|
12183
|
-
timeout: 2000,
|
|
12184
|
-
text: `stopped`,
|
|
12185
|
-
silent: true
|
|
12186
|
-
})
|
|
12187
12167
|
console.log("Refresh 4")
|
|
12188
12168
|
refresh(true)
|
|
12189
12169
|
}
|
|
@@ -12196,11 +12176,6 @@ const rerenderMenuSection = (container, html) => {
|
|
|
12196
12176
|
target.querySelector("i").className = "fa-solid fa-check"
|
|
12197
12177
|
} catch (e) {
|
|
12198
12178
|
}
|
|
12199
|
-
n.Noty({
|
|
12200
|
-
text: `stopping ${src}`,
|
|
12201
|
-
silent: true,
|
|
12202
|
-
timeout: 2000
|
|
12203
|
-
})
|
|
12204
12179
|
console.log("src", src)
|
|
12205
12180
|
|
|
12206
12181
|
let socket = new Socket()
|
|
@@ -12212,11 +12187,6 @@ const rerenderMenuSection = (container, html) => {
|
|
|
12212
12187
|
}, (packet) => {
|
|
12213
12188
|
if (packet.type === "result") {
|
|
12214
12189
|
socket.close()
|
|
12215
|
-
n.Noty({
|
|
12216
|
-
timeout: 2000,
|
|
12217
|
-
text: `stopped`,
|
|
12218
|
-
silent: true
|
|
12219
|
-
})
|
|
12220
12190
|
console.log("Refresh 4")
|
|
12221
12191
|
refresh(true)
|
|
12222
12192
|
}
|
|
@@ -1698,6 +1698,106 @@ describe("automatic app scans", () => {
|
|
|
1698
1698
|
await close(vault)
|
|
1699
1699
|
})
|
|
1700
1700
|
|
|
1701
|
+
test("diagnostics report progress, stalls, resumed work, and final state", () => {
|
|
1702
|
+
let now = 1000
|
|
1703
|
+
let scan = {
|
|
1704
|
+
active: true,
|
|
1705
|
+
pending: false,
|
|
1706
|
+
phase: "discovering",
|
|
1707
|
+
stage: "discovering_files",
|
|
1708
|
+
stage_started: now,
|
|
1709
|
+
last_progress_at: now,
|
|
1710
|
+
started: now,
|
|
1711
|
+
scope_id: "app:demo",
|
|
1712
|
+
dirs: 1,
|
|
1713
|
+
files: 2,
|
|
1714
|
+
bytes_total: 2048,
|
|
1715
|
+
source_files: { "app:demo": 2 },
|
|
1716
|
+
source_bytes: { "app:demo": 2048 },
|
|
1717
|
+
exclusions: [],
|
|
1718
|
+
preview: { duplicate_files: 0, bytes: 0 }
|
|
1719
|
+
}
|
|
1720
|
+
const automatic = new AutomaticScans({
|
|
1721
|
+
kernel: { homedir: "/pinokio", platform: "win32" },
|
|
1722
|
+
scanner: { dirConcurrency: 8, statConcurrency: 32 },
|
|
1723
|
+
hashInactivityMs: 120000,
|
|
1724
|
+
scanStatus: () => scan
|
|
1725
|
+
})
|
|
1726
|
+
automatic.clock = () => now
|
|
1727
|
+
automatic.scanProgressIntervalMs = 60000
|
|
1728
|
+
automatic.scanStallThresholdMs = 100
|
|
1729
|
+
const events = []
|
|
1730
|
+
automatic.log = (event, details) => events.push({ event, details })
|
|
1731
|
+
|
|
1732
|
+
automatic.startScanDiagnostics("demo", "app:demo")
|
|
1733
|
+
now += 50
|
|
1734
|
+
scan = Object.assign({}, scan, {
|
|
1735
|
+
dirs: 4,
|
|
1736
|
+
files: 10,
|
|
1737
|
+
bytes_total: 10240,
|
|
1738
|
+
last_progress_at: now
|
|
1739
|
+
})
|
|
1740
|
+
automatic.sampleScanDiagnostics()
|
|
1741
|
+
now += 100
|
|
1742
|
+
automatic.sampleScanDiagnostics()
|
|
1743
|
+
|
|
1744
|
+
const stalled = events.find((entry) =>
|
|
1745
|
+
entry.event === "scan-progress-stalled")
|
|
1746
|
+
assert.ok(stalled)
|
|
1747
|
+
assert.equal(stalled.details.stage, "discovering_files")
|
|
1748
|
+
assert.equal(stalled.details.files, 10)
|
|
1749
|
+
assert.equal(stalled.details.progress_unchanged_ms, 100)
|
|
1750
|
+
|
|
1751
|
+
now += 10
|
|
1752
|
+
scan = Object.assign({}, scan, {
|
|
1753
|
+
phase: "hashing",
|
|
1754
|
+
stage: "hashing_candidates",
|
|
1755
|
+
stage_started: now,
|
|
1756
|
+
last_progress_at: now,
|
|
1757
|
+
hash_work_files: 2,
|
|
1758
|
+
hash_work_bytes: 8192,
|
|
1759
|
+
current_file: "model.bin",
|
|
1760
|
+
current_file_role: "candidate",
|
|
1761
|
+
current_file_source_id: "app:demo",
|
|
1762
|
+
current_file_bytes: 1024,
|
|
1763
|
+
current_file_size: 4096
|
|
1764
|
+
})
|
|
1765
|
+
automatic.sampleScanDiagnostics()
|
|
1766
|
+
|
|
1767
|
+
assert.ok(events.some((entry) =>
|
|
1768
|
+
entry.event === "scan-progress-resumed"))
|
|
1769
|
+
assert.ok(events.some((entry) =>
|
|
1770
|
+
entry.event === "scan-stage-changed" &&
|
|
1771
|
+
entry.details.from === "discovering_files" &&
|
|
1772
|
+
entry.details.to === "hashing_candidates"))
|
|
1773
|
+
const hashing = events.filter((entry) =>
|
|
1774
|
+
entry.event === "scan-progress").at(-1).details
|
|
1775
|
+
assert.equal(hashing.hash_progress_bytes, 1024)
|
|
1776
|
+
assert.equal(hashing.hash_progress_percent, 12)
|
|
1777
|
+
assert.equal(hashing.current_file, "model.bin")
|
|
1778
|
+
assert.ok(hashing.hash_bytes_per_second > 0)
|
|
1779
|
+
|
|
1780
|
+
now += 20
|
|
1781
|
+
scan = Object.assign({}, scan, {
|
|
1782
|
+
active: false,
|
|
1783
|
+
phase: "cancelled",
|
|
1784
|
+
stage: "cancelled",
|
|
1785
|
+
previous_stage: "hashing_candidates",
|
|
1786
|
+
previous_stage_duration_ms: 20,
|
|
1787
|
+
current_file: null,
|
|
1788
|
+
current_file_role: null,
|
|
1789
|
+
current_file_source_id: null,
|
|
1790
|
+
current_file_bytes: null,
|
|
1791
|
+
current_file_size: null
|
|
1792
|
+
})
|
|
1793
|
+
const final = automatic.finishScanDiagnostics("demo")
|
|
1794
|
+
|
|
1795
|
+
assert.equal(final.phase, "cancelled")
|
|
1796
|
+
assert.equal(final.previous_stage, "hashing_candidates")
|
|
1797
|
+
assert.equal(final.previous_stage_duration_ms, 20)
|
|
1798
|
+
assert.equal(automatic.scanDiagnostics, null)
|
|
1799
|
+
})
|
|
1800
|
+
|
|
1701
1801
|
test("existing lifecycle results are unchanged while Vault observes them", () => {
|
|
1702
1802
|
const calls = []
|
|
1703
1803
|
const fakeKernel = {
|
package/test/vault-sweep.test.js
CHANGED
|
@@ -513,12 +513,15 @@ describe("Save Space scans", () => {
|
|
|
513
513
|
|
|
514
514
|
try {
|
|
515
515
|
assert.equal(progress.phase, "hashing")
|
|
516
|
+
assert.equal(progress.stage, "hashing_candidates")
|
|
516
517
|
assert.equal(progress.hash_work_files, 2)
|
|
517
518
|
assert.equal(progress.hash_work_bytes, contents.length * 2)
|
|
518
519
|
assert.equal(progress.hash_files_completed, 0)
|
|
519
520
|
assert.equal(progress.hash_bytes_completed, 0)
|
|
520
521
|
assert.equal(progress.current_file_bytes, 1024)
|
|
521
522
|
assert.equal(progress.current_file_size, contents.length)
|
|
523
|
+
assert.equal(progress.current_file_role, "candidate")
|
|
524
|
+
assert.match(progress.current_file_source_id, /^app:/)
|
|
522
525
|
} finally {
|
|
523
526
|
release()
|
|
524
527
|
}
|
|
@@ -528,6 +531,8 @@ describe("Save Space scans", () => {
|
|
|
528
531
|
vault.scanStatus().hash_bytes_completed,
|
|
529
532
|
contents.length * 2
|
|
530
533
|
)
|
|
534
|
+
assert.equal(vault.scanStatus().previous_stage, "publishing")
|
|
535
|
+
assert.ok(vault.scanStatus().publish_duration_ms >= 0)
|
|
531
536
|
})
|
|
532
537
|
|
|
533
538
|
test("an anchor sharing a scanned inode is not counted as a second read", async () => {
|