pinokiod 8.0.71 → 8.0.73
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.
|
@@ -7,7 +7,7 @@ const {
|
|
|
7
7
|
ENTRY_BATCH_SIZE
|
|
8
8
|
} = require("./constants")
|
|
9
9
|
const { statMany } = require("./walker")
|
|
10
|
-
const { sameSnapshot } = require("./snapshot")
|
|
10
|
+
const { fileSnapshot, sameSnapshot } = require("./snapshot")
|
|
11
11
|
const {
|
|
12
12
|
cancelledError,
|
|
13
13
|
isPathError
|
|
@@ -52,10 +52,12 @@ class AutomaticScans {
|
|
|
52
52
|
this.manualDepth = 0
|
|
53
53
|
this.observedApps = new Set()
|
|
54
54
|
this.changedPaths = new Map()
|
|
55
|
+
this.changedPathIndex = new Map()
|
|
55
56
|
this.pendingStops = new Map()
|
|
56
57
|
this.lifecycleWork = new Set()
|
|
57
58
|
this.stopSettleMs = STOP_SETTLE_MS
|
|
58
59
|
this.drainQueued = false
|
|
60
|
+
this.drainRequested = false
|
|
59
61
|
this.waitingFor = null
|
|
60
62
|
this.listeners = new Set()
|
|
61
63
|
this.appTransitions = new Map()
|
|
@@ -136,6 +138,7 @@ class AutomaticScans {
|
|
|
136
138
|
}
|
|
137
139
|
this.observedApps.clear()
|
|
138
140
|
this.changedPaths.clear()
|
|
141
|
+
this.changedPathIndex.clear()
|
|
139
142
|
if (watcherError) throw watcherError
|
|
140
143
|
}
|
|
141
144
|
|
|
@@ -152,7 +155,8 @@ class AutomaticScans {
|
|
|
152
155
|
if (!event || !["create", "update", "delete"].includes(event.type)) {
|
|
153
156
|
continue
|
|
154
157
|
}
|
|
155
|
-
this.
|
|
158
|
+
if (event.type === "delete") this.removeChangedPath(event.path)
|
|
159
|
+
else this.recordChangedPath(event.path)
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
|
|
@@ -171,25 +175,105 @@ class AutomaticScans {
|
|
|
171
175
|
paths = new Set()
|
|
172
176
|
this.changedPaths.set(app, paths)
|
|
173
177
|
}
|
|
178
|
+
let index = this.changedPathIndex.get(app)
|
|
179
|
+
if (!index) {
|
|
180
|
+
index = { exact: new Map(), descendants: new Map() }
|
|
181
|
+
this.changedPathIndex.set(app, index)
|
|
182
|
+
}
|
|
183
|
+
const key = pathKey(resolved)
|
|
184
|
+
if (index.exact.has(key)) return true
|
|
174
185
|
paths.add(resolved)
|
|
186
|
+
index.exact.set(key, resolved)
|
|
187
|
+
this.addChangedPathToIndex(index.descendants, appRoot, resolved)
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
changedPathParentKeys(appRoot, filePath) {
|
|
192
|
+
const rootKey = pathKey(appRoot)
|
|
193
|
+
const keys = []
|
|
194
|
+
let current = path.dirname(path.resolve(filePath))
|
|
195
|
+
while (true) {
|
|
196
|
+
const key = pathKey(current)
|
|
197
|
+
keys.push(key)
|
|
198
|
+
if (key === rootKey) return keys
|
|
199
|
+
current = path.dirname(current)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
addChangedPathToIndex(index, appRoot, filePath) {
|
|
204
|
+
for (const key of this.changedPathParentKeys(appRoot, filePath)) {
|
|
205
|
+
let descendants = index.get(key)
|
|
206
|
+
if (!descendants) {
|
|
207
|
+
descendants = new Set()
|
|
208
|
+
index.set(key, descendants)
|
|
209
|
+
}
|
|
210
|
+
descendants.add(filePath)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
removeChangedPathFromIndex(index, appRoot, filePath) {
|
|
215
|
+
index.exact.delete(pathKey(filePath))
|
|
216
|
+
for (const key of this.changedPathParentKeys(appRoot, filePath)) {
|
|
217
|
+
const descendants = index.descendants.get(key)
|
|
218
|
+
if (!descendants) continue
|
|
219
|
+
descendants.delete(filePath)
|
|
220
|
+
if (!descendants.size) index.descendants.delete(key)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
removeChangedPath(filePath) {
|
|
225
|
+
if (this.disposed || !this.supported ||
|
|
226
|
+
typeof filePath !== "string" || !filePath) {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
const app = this.appForLaunchPath(filePath)
|
|
230
|
+
if (!app || !this.observedApps.has(app)) return false
|
|
231
|
+
const paths = this.changedPaths.get(app)
|
|
232
|
+
if (!paths) return false
|
|
233
|
+
const resolved = path.resolve(filePath)
|
|
234
|
+
const appRoot = path.resolve(this.vault.kernel.homedir, "api", app)
|
|
235
|
+
const index = this.changedPathIndex.get(app)
|
|
236
|
+
if (!index) return false
|
|
237
|
+
const key = pathKey(resolved)
|
|
238
|
+
const exact = index.exact.get(key)
|
|
239
|
+
const descendants = index.descendants.get(key)
|
|
240
|
+
if (!exact && !descendants) return false
|
|
241
|
+
const matches = new Set(descendants || [])
|
|
242
|
+
if (exact) matches.add(exact)
|
|
243
|
+
for (const candidate of matches) {
|
|
244
|
+
paths.delete(candidate)
|
|
245
|
+
this.removeChangedPathFromIndex(index, appRoot, candidate)
|
|
246
|
+
}
|
|
247
|
+
if (!paths.size) this.discardChangedPaths(app)
|
|
175
248
|
return true
|
|
176
249
|
}
|
|
177
250
|
|
|
178
251
|
discardChangedPaths(app) {
|
|
179
252
|
this.changedPaths.delete(app)
|
|
253
|
+
this.changedPathIndex.delete(app)
|
|
180
254
|
}
|
|
181
255
|
|
|
182
256
|
consumeChangedPaths(app, paths) {
|
|
183
257
|
const collected = this.changedPaths.get(app)
|
|
184
258
|
if (!collected) return
|
|
185
|
-
|
|
186
|
-
|
|
259
|
+
const appRoot = path.resolve(this.vault.kernel.homedir, "api", app)
|
|
260
|
+
const index = this.changedPathIndex.get(app)
|
|
261
|
+
for (const filePath of paths || []) {
|
|
262
|
+
const resolved = path.resolve(filePath)
|
|
263
|
+
if (!collected.delete(resolved) || !index) continue
|
|
264
|
+
this.removeChangedPathFromIndex(index, appRoot, resolved)
|
|
265
|
+
}
|
|
266
|
+
if (!collected.size) this.discardChangedPaths(app)
|
|
187
267
|
}
|
|
188
268
|
|
|
189
269
|
async candidateThreshold() {
|
|
190
|
-
const
|
|
191
|
-
|
|
270
|
+
const cached = this.vault.lastScanCache &&
|
|
271
|
+
typeof this.vault.lastScanCache.get === "function"
|
|
272
|
+
? this.vault.lastScanCache.get("")
|
|
192
273
|
: null
|
|
274
|
+
const scan = cached || (this.vault.registry
|
|
275
|
+
? await this.vault.registry.scanFor()
|
|
276
|
+
: null)
|
|
193
277
|
const threshold = scan ? Number(scan.candidate_min_bytes) : NaN
|
|
194
278
|
return Number.isFinite(threshold) && threshold >= 0
|
|
195
279
|
? threshold
|
|
@@ -726,24 +810,38 @@ class AutomaticScans {
|
|
|
726
810
|
}
|
|
727
811
|
|
|
728
812
|
schedule() {
|
|
729
|
-
if (this.disposed
|
|
813
|
+
if (this.disposed) return
|
|
814
|
+
if (this.drainQueued) {
|
|
815
|
+
this.drainRequested = true
|
|
816
|
+
return
|
|
817
|
+
}
|
|
730
818
|
this.drainQueued = true
|
|
731
819
|
queueMicrotask(() => {
|
|
732
|
-
|
|
733
|
-
|
|
820
|
+
const pending = (async () => {
|
|
821
|
+
do {
|
|
822
|
+
this.drainRequested = false
|
|
823
|
+
await this.drain()
|
|
824
|
+
} while (!this.disposed && this.drainRequested)
|
|
825
|
+
})()
|
|
734
826
|
this.lifecycleWork.add(pending)
|
|
735
827
|
pending.then(
|
|
736
|
-
() =>
|
|
737
|
-
|
|
828
|
+
() => {
|
|
829
|
+
this.lifecycleWork.delete(pending)
|
|
830
|
+
this.drainQueued = false
|
|
831
|
+
if (this.drainRequested) this.schedule()
|
|
832
|
+
},
|
|
833
|
+
(error) => {
|
|
834
|
+
this.lifecycleWork.delete(pending)
|
|
835
|
+
this.drainQueued = false
|
|
836
|
+
this.log("error", {
|
|
837
|
+
stage: "queue",
|
|
838
|
+
message: error && error.message ? error.message : String(error)
|
|
839
|
+
})
|
|
840
|
+
console.warn("Automatic Disk Saver check failed:",
|
|
841
|
+
error && error.message ? error.message : error)
|
|
842
|
+
if (this.drainRequested) this.schedule()
|
|
843
|
+
}
|
|
738
844
|
)
|
|
739
|
-
pending.catch((error) => {
|
|
740
|
-
this.log("error", {
|
|
741
|
-
stage: "queue",
|
|
742
|
-
message: error && error.message ? error.message : String(error)
|
|
743
|
-
})
|
|
744
|
-
console.warn("Automatic Disk Saver check failed:",
|
|
745
|
-
error && error.message ? error.message : error)
|
|
746
|
-
})
|
|
747
845
|
})
|
|
748
846
|
}
|
|
749
847
|
|
|
@@ -799,37 +897,58 @@ class AutomaticScans {
|
|
|
799
897
|
current = await fs.promises.lstat(entry.path)
|
|
800
898
|
} catch (error) {
|
|
801
899
|
if (!isPathError(error)) throw error
|
|
900
|
+
if (entry.kind === "changed" && isMissing(error)) {
|
|
901
|
+
counts.path_errors += 1
|
|
902
|
+
return null
|
|
903
|
+
}
|
|
802
904
|
this.logVerificationSkip(active.app, entry, "unreadable", counts, error)
|
|
803
905
|
return null
|
|
804
906
|
}
|
|
805
907
|
this.checkpoint(active)
|
|
806
908
|
if (!current.isFile() || current.isSymbolicLink() ||
|
|
807
|
-
|
|
909
|
+
current.dev !== entry.dev || current.size !== entry.size) {
|
|
808
910
|
this.logVerificationSkip(
|
|
809
911
|
active.app, entry, "snapshot-changed", counts)
|
|
810
912
|
return null
|
|
811
913
|
}
|
|
812
|
-
const
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
:
|
|
914
|
+
const currentEntry = Object.assign({}, entry, fileSnapshot(current))
|
|
915
|
+
const key = this.automaticHashKey(currentEntry)
|
|
916
|
+
const cachedSnapshot = {
|
|
917
|
+
size: entry.cached_size,
|
|
918
|
+
mtime: entry.cached_mtime,
|
|
919
|
+
ctime: entry.cached_ctime,
|
|
920
|
+
dev: entry.cached_dev,
|
|
921
|
+
ino: entry.cached_ino
|
|
922
|
+
}
|
|
923
|
+
const reusable = memory.get(key) ||
|
|
924
|
+
(sameSnapshot(entry, current) && SHA256_RE.test(entry.hash || "")
|
|
925
|
+
? entry.hash
|
|
926
|
+
: null) ||
|
|
927
|
+
(sameSnapshot(cachedSnapshot, current) &&
|
|
928
|
+
SHA256_RE.test(entry.cached_hash || "")
|
|
929
|
+
? entry.cached_hash
|
|
930
|
+
: null)
|
|
816
931
|
if (reusable) {
|
|
817
932
|
counts.hash_reuses += 1
|
|
818
933
|
memory.set(key, reusable)
|
|
819
|
-
verifiedHashes.set(pathKey(entry.path), Object.assign({},
|
|
934
|
+
verifiedHashes.set(pathKey(entry.path), Object.assign({}, currentEntry, {
|
|
820
935
|
hash: reusable
|
|
821
936
|
}))
|
|
822
937
|
return reusable
|
|
823
938
|
}
|
|
824
939
|
let verified
|
|
825
940
|
try {
|
|
826
|
-
verified = await this.vault.scanner.hashStable(
|
|
941
|
+
verified = await this.vault.scanner.hashStable(currentEntry, {
|
|
827
942
|
signal: active.controller && active.controller.signal
|
|
828
943
|
})
|
|
829
944
|
} catch (error) {
|
|
830
945
|
if (error && error.code === "EVAULTCANCELLED") throw error
|
|
831
946
|
if (!isPathError(error)) throw error
|
|
832
947
|
counts.hash_failures += 1
|
|
948
|
+
if (entry.kind === "changed" && isMissing(error)) {
|
|
949
|
+
counts.path_errors += 1
|
|
950
|
+
return null
|
|
951
|
+
}
|
|
833
952
|
this.logVerificationSkip(active.app, entry, "hash-failed", counts, error)
|
|
834
953
|
return null
|
|
835
954
|
}
|
|
@@ -842,7 +961,7 @@ class AutomaticScans {
|
|
|
842
961
|
counts.hashed += 1
|
|
843
962
|
counts.hash_bytes += Math.max(0, Number(verified.result.size) || 0)
|
|
844
963
|
memory.set(key, verified.result.hash)
|
|
845
|
-
verifiedHashes.set(pathKey(entry.path), Object.assign({},
|
|
964
|
+
verifiedHashes.set(pathKey(entry.path), Object.assign({}, currentEntry, {
|
|
846
965
|
hash: verified.result.hash
|
|
847
966
|
}))
|
|
848
967
|
return verified.result.hash
|
|
@@ -872,7 +991,6 @@ class AutomaticScans {
|
|
|
872
991
|
const paths = [...new Set(active.paths || [])].filter((filePath) =>
|
|
873
992
|
typeof filePath === "string" && inside(root, filePath))
|
|
874
993
|
const verifiedHashes = new Map()
|
|
875
|
-
await this.vault.registry.beginAutomaticPrecheck(app)
|
|
876
994
|
this.log("check-started", {
|
|
877
995
|
app,
|
|
878
996
|
root,
|
|
@@ -880,6 +998,7 @@ class AutomaticScans {
|
|
|
880
998
|
changed_paths: paths.length,
|
|
881
999
|
policy: "metadata-prefilter-sha256"
|
|
882
1000
|
})
|
|
1001
|
+
await this.vault.registry.beginAutomaticPrecheck(app)
|
|
883
1002
|
try {
|
|
884
1003
|
this.checkpoint(active)
|
|
885
1004
|
const metadataOptions = {
|
|
@@ -888,6 +1007,7 @@ class AutomaticScans {
|
|
|
888
1007
|
onError: (error, filePath) => {
|
|
889
1008
|
if (!isPathError(error)) return false
|
|
890
1009
|
counts.path_errors += 1
|
|
1010
|
+
if (isMissing(error)) return true
|
|
891
1011
|
this.log("path-skipped", {
|
|
892
1012
|
app,
|
|
893
1013
|
path: filePath,
|
|
@@ -1063,7 +1183,19 @@ class AutomaticScans {
|
|
|
1063
1183
|
item.state === "checking")
|
|
1064
1184
|
if (!entry) return
|
|
1065
1185
|
await this.refreshGlobalScanReady()
|
|
1066
|
-
if (this.disposed
|
|
1186
|
+
if (this.disposed) return
|
|
1187
|
+
if (!this.globalScanReady) {
|
|
1188
|
+
if (this.entries.get(entry.app) === entry) {
|
|
1189
|
+
this.restorePrevious(entry.app)
|
|
1190
|
+
this.discardChangedPaths(entry.app)
|
|
1191
|
+
this.log("check-skipped", {
|
|
1192
|
+
app: entry.app,
|
|
1193
|
+
reason: "global-scan-required"
|
|
1194
|
+
})
|
|
1195
|
+
}
|
|
1196
|
+
this.schedule()
|
|
1197
|
+
return
|
|
1198
|
+
}
|
|
1067
1199
|
if (this.manualDepth > 0 || this.currentBusyPromise() ||
|
|
1068
1200
|
this.vault.fileActionProgress) {
|
|
1069
1201
|
this.waitForBusyWork()
|
|
@@ -2867,23 +2867,30 @@ class RegistryCore {
|
|
|
2867
2867
|
rows = this.database.prepare(`
|
|
2868
2868
|
SELECT
|
|
2869
2869
|
'anchor' AS kind,
|
|
2870
|
-
path,
|
|
2871
|
-
size,
|
|
2872
|
-
mtime,
|
|
2873
|
-
ctime,
|
|
2874
|
-
dev,
|
|
2875
|
-
ino,
|
|
2876
|
-
nlink,
|
|
2877
|
-
mode,
|
|
2878
|
-
uid,
|
|
2879
|
-
gid,
|
|
2880
|
-
hash
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2870
|
+
anchor.path,
|
|
2871
|
+
anchor.size,
|
|
2872
|
+
anchor.mtime,
|
|
2873
|
+
anchor.ctime,
|
|
2874
|
+
anchor.dev,
|
|
2875
|
+
anchor.ino,
|
|
2876
|
+
anchor.nlink,
|
|
2877
|
+
anchor.mode,
|
|
2878
|
+
anchor.uid,
|
|
2879
|
+
anchor.gid,
|
|
2880
|
+
anchor.hash,
|
|
2881
|
+
cached.hash AS cached_hash,
|
|
2882
|
+
cached.size AS cached_size,
|
|
2883
|
+
cached.mtime AS cached_mtime,
|
|
2884
|
+
cached.ctime AS cached_ctime,
|
|
2885
|
+
cached.dev AS cached_dev,
|
|
2886
|
+
cached.ino AS cached_ino
|
|
2887
|
+
FROM anchors anchor
|
|
2888
|
+
LEFT JOIN hash_cache cached ON cached.path = anchor.path
|
|
2889
|
+
WHERE anchor.dev = @dev
|
|
2890
|
+
AND anchor.size = @size
|
|
2891
|
+
AND anchor.path > @path
|
|
2892
|
+
AND anchor.verified_at IS NOT NULL
|
|
2893
|
+
ORDER BY anchor.path
|
|
2887
2894
|
LIMIT @limit
|
|
2888
2895
|
`).all({ dev, size, path: afterPath, limit: pageSize })
|
|
2889
2896
|
} else {
|
|
@@ -2900,15 +2907,15 @@ class RegistryCore {
|
|
|
2900
2907
|
peer.mode,
|
|
2901
2908
|
peer.uid,
|
|
2902
2909
|
peer.gid,
|
|
2903
|
-
|
|
2910
|
+
peer.hash,
|
|
2911
|
+
cached.hash AS cached_hash,
|
|
2912
|
+
cached.size AS cached_size,
|
|
2913
|
+
cached.mtime AS cached_mtime,
|
|
2914
|
+
cached.ctime AS cached_ctime,
|
|
2915
|
+
cached.dev AS cached_dev,
|
|
2916
|
+
cached.ino AS cached_ino
|
|
2904
2917
|
FROM files peer
|
|
2905
|
-
LEFT JOIN hash_cache cached
|
|
2906
|
-
ON cached.path = peer.path
|
|
2907
|
-
AND cached.size = peer.size
|
|
2908
|
-
AND cached.mtime = peer.mtime
|
|
2909
|
-
AND cached.ctime = peer.ctime
|
|
2910
|
-
AND cached.dev = peer.dev
|
|
2911
|
-
AND cached.ino = peer.ino
|
|
2918
|
+
LEFT JOIN hash_cache cached ON cached.path = peer.path
|
|
2912
2919
|
WHERE peer.dev = @dev
|
|
2913
2920
|
AND peer.size = @size
|
|
2914
2921
|
AND peer.path > @path
|
package/package.json
CHANGED
|
@@ -226,6 +226,45 @@ describe("automatic app checks", () => {
|
|
|
226
226
|
await close(vault)
|
|
227
227
|
})
|
|
228
228
|
|
|
229
|
+
test("Windows watcher deletes remove retained paths and descendants", async () => {
|
|
230
|
+
const home = await makeHome()
|
|
231
|
+
const appRoot = path.join(home, "api", "windows-app")
|
|
232
|
+
const scriptPath = path.join(appRoot, "start.js")
|
|
233
|
+
const first = path.join(appRoot, "first.bin")
|
|
234
|
+
const nestedRoot = path.join(appRoot, "temporary")
|
|
235
|
+
const nested = path.join(nestedRoot, "nested.bin")
|
|
236
|
+
await fs.promises.mkdir(appRoot)
|
|
237
|
+
let callback = null
|
|
238
|
+
const vault = await makeVault(home, {
|
|
239
|
+
deferStorage: true,
|
|
240
|
+
platform: "win32",
|
|
241
|
+
automaticWatcher: {
|
|
242
|
+
subscribe: async (_root, listener) => {
|
|
243
|
+
callback = listener
|
|
244
|
+
return { unsubscribe: async () => {} }
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
assert.equal(await vault.automaticScans.startWatcher(), true)
|
|
250
|
+
vault.automaticScans.handleStarted(scriptPath)
|
|
251
|
+
callback(null, [
|
|
252
|
+
{ type: "create", path: first },
|
|
253
|
+
{ type: "update", path: nestedRoot },
|
|
254
|
+
{ type: "update", path: nested }
|
|
255
|
+
])
|
|
256
|
+
assert.deepEqual([...vault.automaticScans.changedPaths.get(
|
|
257
|
+
"windows-app")].sort(), [first, nestedRoot, nested].sort())
|
|
258
|
+
|
|
259
|
+
callback(null, [{ type: "delete", path: nestedRoot }])
|
|
260
|
+
assert.deepEqual([...vault.automaticScans.changedPaths.get(
|
|
261
|
+
"windows-app")], [first])
|
|
262
|
+
callback(null, [{ type: "delete", path: first }])
|
|
263
|
+
assert.equal(vault.automaticScans.changedPaths.has("windows-app"), false)
|
|
264
|
+
assert.equal(vault.registry, null)
|
|
265
|
+
await close(vault)
|
|
266
|
+
})
|
|
267
|
+
|
|
229
268
|
test("disposing automatic checks releases the watcher and pending work", async () => {
|
|
230
269
|
const home = await makeHome()
|
|
231
270
|
const appRoot = path.join(home, "api", "disposed-app")
|
|
@@ -366,6 +405,71 @@ describe("automatic app checks", () => {
|
|
|
366
405
|
assert.equal(checksStarted, 0)
|
|
367
406
|
})
|
|
368
407
|
|
|
408
|
+
test("scheduler coalesces wakeups while dispatch is in progress", async () => {
|
|
409
|
+
let releaseReadiness
|
|
410
|
+
let readinessCalls = 0
|
|
411
|
+
let dispatches = 0
|
|
412
|
+
const automatic = makeAutomatic({
|
|
413
|
+
enabled: true,
|
|
414
|
+
registry: {},
|
|
415
|
+
runExclusive: () => {
|
|
416
|
+
dispatches += 1
|
|
417
|
+
return new Promise(() => {})
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
automatic.entries.set("demo", {
|
|
421
|
+
app: "demo",
|
|
422
|
+
state: "checking",
|
|
423
|
+
paths: []
|
|
424
|
+
})
|
|
425
|
+
automatic.globalScanReady = true
|
|
426
|
+
automatic.refreshGlobalScanReady = async () => {
|
|
427
|
+
readinessCalls += 1
|
|
428
|
+
if (readinessCalls === 1) {
|
|
429
|
+
await new Promise((resolve) => { releaseReadiness = resolve })
|
|
430
|
+
}
|
|
431
|
+
automatic.globalScanReady = true
|
|
432
|
+
}
|
|
433
|
+
automatic.appRootIsAvailable = async () => true
|
|
434
|
+
automatic.appIsRunning = () => false
|
|
435
|
+
|
|
436
|
+
automatic.schedule()
|
|
437
|
+
await waitFor(() => typeof releaseReadiness === "function",
|
|
438
|
+
"scheduler readiness pause")
|
|
439
|
+
automatic.schedule()
|
|
440
|
+
releaseReadiness()
|
|
441
|
+
await waitFor(() => dispatches > 0, "automatic dispatch")
|
|
442
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
443
|
+
|
|
444
|
+
assert.equal(dispatches, 1)
|
|
445
|
+
assert.equal(readinessCalls, 1)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
test("queued checks are cleared if global readiness disappears", async () => {
|
|
449
|
+
const automatic = makeAutomatic({
|
|
450
|
+
enabled: true,
|
|
451
|
+
registry: {}
|
|
452
|
+
})
|
|
453
|
+
automatic.entries.set("demo", {
|
|
454
|
+
app: "demo",
|
|
455
|
+
state: "checking",
|
|
456
|
+
paths: ["/pinokio/api/demo/model.bin"]
|
|
457
|
+
})
|
|
458
|
+
automatic.changedPaths.set("demo", new Set([
|
|
459
|
+
"/pinokio/api/demo/model.bin"
|
|
460
|
+
]))
|
|
461
|
+
automatic.refreshGlobalScanReady = async () => {
|
|
462
|
+
automatic.globalScanReady = false
|
|
463
|
+
}
|
|
464
|
+
automatic.log = () => {}
|
|
465
|
+
|
|
466
|
+
automatic.schedule()
|
|
467
|
+
await waitFor(() => !automatic.drainQueued, "automatic queue cleanup")
|
|
468
|
+
|
|
469
|
+
assert.equal(automatic.entries.has("demo"), false)
|
|
470
|
+
assert.equal(automatic.changedPaths.has("demo"), false)
|
|
471
|
+
})
|
|
472
|
+
|
|
369
473
|
test("Linux starts no watcher and ignores automatic lifecycle work", async () => {
|
|
370
474
|
const home = await makeHome()
|
|
371
475
|
const appRoot = path.join(home, "api", "linux-app")
|
|
@@ -1150,6 +1254,31 @@ describe("automatic app checks", () => {
|
|
|
1150
1254
|
await close(vault)
|
|
1151
1255
|
})
|
|
1152
1256
|
|
|
1257
|
+
test("transient missing watcher paths do not emit per-path logs", async () => {
|
|
1258
|
+
const home = await makeHome()
|
|
1259
|
+
const app = "temporary-path-app"
|
|
1260
|
+
const appRoot = path.join(home, "api", app)
|
|
1261
|
+
const missing = [
|
|
1262
|
+
path.join(appRoot, "temporary-1.bin"),
|
|
1263
|
+
path.join(appRoot, "temporary-2.bin")
|
|
1264
|
+
]
|
|
1265
|
+
await fs.promises.mkdir(appRoot)
|
|
1266
|
+
const vault = await makeVault(home)
|
|
1267
|
+
const logs = []
|
|
1268
|
+
vault.automaticScans.log = (event, details) => {
|
|
1269
|
+
logs.push({ event, details })
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
vault.automaticScans.queueApp(app, missing)
|
|
1273
|
+
await waitFor(() => !vault.automaticScans.active &&
|
|
1274
|
+
!vault.automaticScans.entries.has(app))
|
|
1275
|
+
|
|
1276
|
+
assert.equal(logs.some((record) =>
|
|
1277
|
+
["path-skipped", "verification-skipped"].includes(record.event) &&
|
|
1278
|
+
missing.includes(record.details.path)), false)
|
|
1279
|
+
await close(vault)
|
|
1280
|
+
})
|
|
1281
|
+
|
|
1153
1282
|
test("stable published hashes are reused without rereading contents", async () => {
|
|
1154
1283
|
const home = await makeHome()
|
|
1155
1284
|
const app = "published-hash-app"
|
|
@@ -1203,6 +1332,102 @@ describe("automatic app checks", () => {
|
|
|
1203
1332
|
await close(vault)
|
|
1204
1333
|
})
|
|
1205
1334
|
|
|
1335
|
+
test("a metadata-changed Windows peer is freshly hashed and cached", async () => {
|
|
1336
|
+
const home = await makeHome()
|
|
1337
|
+
const app = "windows-peer-app"
|
|
1338
|
+
const appRoot = path.join(home, "api", app)
|
|
1339
|
+
const candidate = path.join(appRoot, "candidate.bin")
|
|
1340
|
+
const peer = path.join(home, "published-peer.bin")
|
|
1341
|
+
await fs.promises.mkdir(appRoot)
|
|
1342
|
+
await fs.promises.writeFile(candidate, "same-content")
|
|
1343
|
+
await fs.promises.writeFile(peer, "same-content")
|
|
1344
|
+
const vault = await makeVault(home, {
|
|
1345
|
+
deferStorage: true,
|
|
1346
|
+
platform: "win32"
|
|
1347
|
+
})
|
|
1348
|
+
await vault.ensureRegistryInitialized()
|
|
1349
|
+
vault.automaticScans.candidateThreshold = async () => 1
|
|
1350
|
+
const peerStat = await fs.promises.lstat(peer)
|
|
1351
|
+
await vault.registry.upsertFile({
|
|
1352
|
+
path: peer,
|
|
1353
|
+
hash: crypto.createHash("sha256").update("old-content!").digest("hex"),
|
|
1354
|
+
size: peerStat.size,
|
|
1355
|
+
mtime: peerStat.mtimeMs - 1,
|
|
1356
|
+
ctime: peerStat.ctimeMs - 1,
|
|
1357
|
+
dev: peerStat.dev,
|
|
1358
|
+
ino: peerStat.ino,
|
|
1359
|
+
mode: peerStat.mode,
|
|
1360
|
+
uid: peerStat.uid,
|
|
1361
|
+
gid: peerStat.gid,
|
|
1362
|
+
source_id: "app:peer",
|
|
1363
|
+
app: "peer",
|
|
1364
|
+
status: "reference",
|
|
1365
|
+
unavailable_reason: null,
|
|
1366
|
+
updated_at: Date.now()
|
|
1367
|
+
})
|
|
1368
|
+
const hashedPaths = []
|
|
1369
|
+
const originalHashFile = vault.hashFile.bind(vault)
|
|
1370
|
+
vault.hashFile = async (filePath, options) => {
|
|
1371
|
+
hashedPaths.push(filePath)
|
|
1372
|
+
return originalHashFile(filePath, options)
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
vault.automaticScans.queueApp(app, [candidate])
|
|
1376
|
+
await waitFor(() => !vault.automaticScans.active &&
|
|
1377
|
+
vault.automaticScans.entries.get(app)?.state === "result")
|
|
1378
|
+
assert.deepEqual(hashedPaths.sort(), [candidate, peer].sort())
|
|
1379
|
+
|
|
1380
|
+
await vault.registry.setAutomaticAppScanState(app, null)
|
|
1381
|
+
vault.automaticScans.entries.delete(app)
|
|
1382
|
+
vault.hashFile = async () => {
|
|
1383
|
+
throw new Error("Fresh automatic hashes must be reused.")
|
|
1384
|
+
}
|
|
1385
|
+
vault.automaticScans.queueApp(app, [candidate])
|
|
1386
|
+
await waitFor(() => !vault.automaticScans.active &&
|
|
1387
|
+
vault.automaticScans.entries.get(app)?.state === "result")
|
|
1388
|
+
|
|
1389
|
+
await close(vault)
|
|
1390
|
+
})
|
|
1391
|
+
|
|
1392
|
+
test("a stale peer hash cannot match different equal-size content", async () => {
|
|
1393
|
+
const home = await makeHome()
|
|
1394
|
+
const app = "stale-peer-hash-app"
|
|
1395
|
+
const appRoot = path.join(home, "api", app)
|
|
1396
|
+
const candidate = path.join(appRoot, "candidate.bin")
|
|
1397
|
+
const peer = path.join(home, "stale-peer.bin")
|
|
1398
|
+
await fs.promises.mkdir(appRoot)
|
|
1399
|
+
await fs.promises.writeFile(candidate, "same-content")
|
|
1400
|
+
await fs.promises.writeFile(peer, "other-bytes!")
|
|
1401
|
+
const vault = await makeVault(home, { deferStorage: true })
|
|
1402
|
+
await vault.ensureRegistryInitialized()
|
|
1403
|
+
vault.automaticScans.candidateThreshold = async () => 1
|
|
1404
|
+
const peerStat = await fs.promises.lstat(peer)
|
|
1405
|
+
await vault.registry.upsertFile({
|
|
1406
|
+
path: peer,
|
|
1407
|
+
hash: crypto.createHash("sha256").update("same-content").digest("hex"),
|
|
1408
|
+
size: peerStat.size,
|
|
1409
|
+
mtime: peerStat.mtimeMs - 1,
|
|
1410
|
+
ctime: peerStat.ctimeMs - 1,
|
|
1411
|
+
dev: peerStat.dev,
|
|
1412
|
+
ino: peerStat.ino,
|
|
1413
|
+
mode: peerStat.mode,
|
|
1414
|
+
uid: peerStat.uid,
|
|
1415
|
+
gid: peerStat.gid,
|
|
1416
|
+
source_id: "app:peer",
|
|
1417
|
+
app: "peer",
|
|
1418
|
+
status: "reference",
|
|
1419
|
+
unavailable_reason: null,
|
|
1420
|
+
updated_at: Date.now()
|
|
1421
|
+
})
|
|
1422
|
+
|
|
1423
|
+
vault.automaticScans.queueApp(app, [candidate])
|
|
1424
|
+
await waitFor(() => !vault.automaticScans.active &&
|
|
1425
|
+
!vault.automaticScans.entries.has(app))
|
|
1426
|
+
|
|
1427
|
+
assert.deepEqual(vault.automaticScans.snapshot().rows, [])
|
|
1428
|
+
await close(vault)
|
|
1429
|
+
})
|
|
1430
|
+
|
|
1206
1431
|
test("stale file rows and known identical inodes are not peers", async () => {
|
|
1207
1432
|
const home = await makeHome()
|
|
1208
1433
|
const appRoot = path.join(home, "api", "no-peer-app")
|