pinokiod 8.0.69 → 8.0.72
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/index.js +5 -5
- package/kernel/shell.js +1 -0
- package/kernel/vault/automatic_scans.js +339 -19
- package/kernel/vault/hash_worker.js +17 -2
- package/kernel/vault/index.js +105 -8
- package/kernel/vault/registry.js +51 -11
- package/kernel/vault/registry_core.js +341 -58
- package/kernel/vault/registry_worker.js +16 -5
- package/kernel/vault/scanner.js +2 -1
- package/kernel/vault/sweeper.js +37 -13
- package/package.json +1 -1
- package/server/public/app-vault-mode.js +8 -5
- package/server/public/vault.css +110 -0
- package/server/public/vault.js +104 -16
- package/server/views/app.ejs +8 -2
- package/server/views/partials/vault_workspace.ejs +15 -2
- package/test/fixtures/vault-registry-blocking-worker.js +41 -0
- package/test/shell-run-template.test.js +22 -0
- package/test/vault-automatic-scan-ui.test.js +85 -11
- package/test/vault-automatic-scans.test.js +586 -41
- package/test/vault-engine.test.js +153 -3
- package/test/vault-sweep.test.js +28 -0
- package/test/vault-ui.test.js +113 -7
package/kernel/index.js
CHANGED
|
@@ -1062,15 +1062,15 @@ class Kernel {
|
|
|
1062
1062
|
kill() {
|
|
1063
1063
|
process.kill(process.pid, "SIGTERM")
|
|
1064
1064
|
}
|
|
1065
|
-
async
|
|
1065
|
+
async disposeVault() {
|
|
1066
1066
|
const vault = this.vault
|
|
1067
|
-
if (!vault
|
|
1067
|
+
if (!vault) return false
|
|
1068
1068
|
if (vault.ready) await Promise.resolve(vault.ready).catch(() => {})
|
|
1069
1069
|
try {
|
|
1070
|
-
await vault.
|
|
1070
|
+
await vault.dispose()
|
|
1071
1071
|
return true
|
|
1072
1072
|
} catch (error) {
|
|
1073
|
-
console.warn("Vault
|
|
1073
|
+
console.warn("Vault cleanup error:",
|
|
1074
1074
|
error && error.message ? error.message : error)
|
|
1075
1075
|
return false
|
|
1076
1076
|
}
|
|
@@ -1092,7 +1092,7 @@ class Kernel {
|
|
|
1092
1092
|
})
|
|
1093
1093
|
|
|
1094
1094
|
let home = this.store.get("home") || process.env.PINOKIO_HOME
|
|
1095
|
-
await this.
|
|
1095
|
+
await this.disposeVault()
|
|
1096
1096
|
this.vault = null
|
|
1097
1097
|
this.homedir = home
|
|
1098
1098
|
|
package/kernel/shell.js
CHANGED
|
@@ -290,6 +290,7 @@ class Shell {
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
setDefaultEnvValue(this.env, "UV_HTTP_TIMEOUT", "60")
|
|
293
294
|
setDefaultEnvValue(this.env, "HF_HUB_DISABLE_UPDATE_CHECK", "1")
|
|
294
295
|
setDefaultEnvValue(this.env, "HF_TOKEN_PATH", this.kernel.envs?.HF_TOKEN_PATH || path.resolve(this.kernel.homedir, "cache", "HF_AUTH", "token"))
|
|
295
296
|
|
|
@@ -2,9 +2,12 @@ const fs = require("fs")
|
|
|
2
2
|
const path = require("path")
|
|
3
3
|
const {
|
|
4
4
|
SIZE_THRESHOLD,
|
|
5
|
-
isCandidateFileSize
|
|
5
|
+
isCandidateFileSize,
|
|
6
|
+
SHA256_RE,
|
|
7
|
+
ENTRY_BATCH_SIZE
|
|
6
8
|
} = require("./constants")
|
|
7
9
|
const { statMany } = require("./walker")
|
|
10
|
+
const { fileSnapshot, sameSnapshot } = require("./snapshot")
|
|
8
11
|
const {
|
|
9
12
|
cancelledError,
|
|
10
13
|
isPathError
|
|
@@ -24,6 +27,11 @@ const inside = (root, candidate) => {
|
|
|
24
27
|
)
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
const pathKey = (value) => {
|
|
31
|
+
const resolved = path.resolve(value)
|
|
32
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
class AutomaticScans {
|
|
28
36
|
constructor(vault) {
|
|
29
37
|
this.vault = vault
|
|
@@ -44,6 +52,7 @@ class AutomaticScans {
|
|
|
44
52
|
this.manualDepth = 0
|
|
45
53
|
this.observedApps = new Set()
|
|
46
54
|
this.changedPaths = new Map()
|
|
55
|
+
this.changedPathIndex = new Map()
|
|
47
56
|
this.pendingStops = new Map()
|
|
48
57
|
this.lifecycleWork = new Set()
|
|
49
58
|
this.stopSettleMs = STOP_SETTLE_MS
|
|
@@ -128,6 +137,7 @@ class AutomaticScans {
|
|
|
128
137
|
}
|
|
129
138
|
this.observedApps.clear()
|
|
130
139
|
this.changedPaths.clear()
|
|
140
|
+
this.changedPathIndex.clear()
|
|
131
141
|
if (watcherError) throw watcherError
|
|
132
142
|
}
|
|
133
143
|
|
|
@@ -144,7 +154,8 @@ class AutomaticScans {
|
|
|
144
154
|
if (!event || !["create", "update", "delete"].includes(event.type)) {
|
|
145
155
|
continue
|
|
146
156
|
}
|
|
147
|
-
this.
|
|
157
|
+
if (event.type === "delete") this.removeChangedPath(event.path)
|
|
158
|
+
else this.recordChangedPath(event.path)
|
|
148
159
|
}
|
|
149
160
|
}
|
|
150
161
|
|
|
@@ -163,19 +174,95 @@ class AutomaticScans {
|
|
|
163
174
|
paths = new Set()
|
|
164
175
|
this.changedPaths.set(app, paths)
|
|
165
176
|
}
|
|
177
|
+
let index = this.changedPathIndex.get(app)
|
|
178
|
+
if (!index) {
|
|
179
|
+
index = { exact: new Map(), descendants: new Map() }
|
|
180
|
+
this.changedPathIndex.set(app, index)
|
|
181
|
+
}
|
|
182
|
+
const key = pathKey(resolved)
|
|
183
|
+
if (index.exact.has(key)) return true
|
|
166
184
|
paths.add(resolved)
|
|
185
|
+
index.exact.set(key, resolved)
|
|
186
|
+
this.addChangedPathToIndex(index.descendants, appRoot, resolved)
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
changedPathParentKeys(appRoot, filePath) {
|
|
191
|
+
const rootKey = pathKey(appRoot)
|
|
192
|
+
const keys = []
|
|
193
|
+
let current = path.dirname(path.resolve(filePath))
|
|
194
|
+
while (true) {
|
|
195
|
+
const key = pathKey(current)
|
|
196
|
+
keys.push(key)
|
|
197
|
+
if (key === rootKey) return keys
|
|
198
|
+
current = path.dirname(current)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
addChangedPathToIndex(index, appRoot, filePath) {
|
|
203
|
+
for (const key of this.changedPathParentKeys(appRoot, filePath)) {
|
|
204
|
+
let descendants = index.get(key)
|
|
205
|
+
if (!descendants) {
|
|
206
|
+
descendants = new Set()
|
|
207
|
+
index.set(key, descendants)
|
|
208
|
+
}
|
|
209
|
+
descendants.add(filePath)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
removeChangedPathFromIndex(index, appRoot, filePath) {
|
|
214
|
+
index.exact.delete(pathKey(filePath))
|
|
215
|
+
for (const key of this.changedPathParentKeys(appRoot, filePath)) {
|
|
216
|
+
const descendants = index.descendants.get(key)
|
|
217
|
+
if (!descendants) continue
|
|
218
|
+
descendants.delete(filePath)
|
|
219
|
+
if (!descendants.size) index.descendants.delete(key)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
removeChangedPath(filePath) {
|
|
224
|
+
if (this.disposed || !this.supported ||
|
|
225
|
+
typeof filePath !== "string" || !filePath) {
|
|
226
|
+
return false
|
|
227
|
+
}
|
|
228
|
+
const app = this.appForLaunchPath(filePath)
|
|
229
|
+
if (!app || !this.observedApps.has(app)) return false
|
|
230
|
+
const paths = this.changedPaths.get(app)
|
|
231
|
+
if (!paths) return false
|
|
232
|
+
const resolved = path.resolve(filePath)
|
|
233
|
+
const appRoot = path.resolve(this.vault.kernel.homedir, "api", app)
|
|
234
|
+
const index = this.changedPathIndex.get(app)
|
|
235
|
+
if (!index) return false
|
|
236
|
+
const key = pathKey(resolved)
|
|
237
|
+
const exact = index.exact.get(key)
|
|
238
|
+
const descendants = index.descendants.get(key)
|
|
239
|
+
if (!exact && !descendants) return false
|
|
240
|
+
const matches = new Set(descendants || [])
|
|
241
|
+
if (exact) matches.add(exact)
|
|
242
|
+
for (const candidate of matches) {
|
|
243
|
+
paths.delete(candidate)
|
|
244
|
+
this.removeChangedPathFromIndex(index, appRoot, candidate)
|
|
245
|
+
}
|
|
246
|
+
if (!paths.size) this.discardChangedPaths(app)
|
|
167
247
|
return true
|
|
168
248
|
}
|
|
169
249
|
|
|
170
250
|
discardChangedPaths(app) {
|
|
171
251
|
this.changedPaths.delete(app)
|
|
252
|
+
this.changedPathIndex.delete(app)
|
|
172
253
|
}
|
|
173
254
|
|
|
174
255
|
consumeChangedPaths(app, paths) {
|
|
175
256
|
const collected = this.changedPaths.get(app)
|
|
176
257
|
if (!collected) return
|
|
177
|
-
|
|
178
|
-
|
|
258
|
+
const appRoot = path.resolve(this.vault.kernel.homedir, "api", app)
|
|
259
|
+
const index = this.changedPathIndex.get(app)
|
|
260
|
+
for (const filePath of paths || []) {
|
|
261
|
+
const resolved = path.resolve(filePath)
|
|
262
|
+
if (!collected.delete(resolved) || !index) continue
|
|
263
|
+
this.removeChangedPathFromIndex(index, appRoot, resolved)
|
|
264
|
+
}
|
|
265
|
+
if (!collected.size) this.discardChangedPaths(app)
|
|
179
266
|
}
|
|
180
267
|
|
|
181
268
|
async candidateThreshold() {
|
|
@@ -745,6 +832,7 @@ class AutomaticScans {
|
|
|
745
832
|
}
|
|
746
833
|
this.active.cancelled = true
|
|
747
834
|
this.active.reason = reason
|
|
835
|
+
if (this.active.controller) this.active.controller.abort()
|
|
748
836
|
this.log("check-cancel-requested", { app, reason })
|
|
749
837
|
return true
|
|
750
838
|
}
|
|
@@ -755,6 +843,111 @@ class AutomaticScans {
|
|
|
755
843
|
}
|
|
756
844
|
}
|
|
757
845
|
|
|
846
|
+
automaticHashKey(entry) {
|
|
847
|
+
return [
|
|
848
|
+
this.automaticIdentityKey(entry),
|
|
849
|
+
entry.size,
|
|
850
|
+
entry.mtime,
|
|
851
|
+
entry.ctime
|
|
852
|
+
].join("\0")
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
automaticIdentityKey(entry) {
|
|
856
|
+
return entry.ino !== 0
|
|
857
|
+
? `inode:${entry.dev}:${entry.ino}`
|
|
858
|
+
: `path:${pathKey(entry.path)}`
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
logVerificationSkip(app, entry, reason, counts, error = null) {
|
|
862
|
+
if (error) counts.path_errors += 1
|
|
863
|
+
else counts.unstable_hashes += 1
|
|
864
|
+
this.log("verification-skipped", {
|
|
865
|
+
app,
|
|
866
|
+
path: entry.path,
|
|
867
|
+
kind: entry.kind,
|
|
868
|
+
reason,
|
|
869
|
+
code: error && error.code,
|
|
870
|
+
message: error && error.message
|
|
871
|
+
})
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async verifiedAutomaticHash(active, entry, memory, verifiedHashes, counts) {
|
|
875
|
+
this.checkpoint(active)
|
|
876
|
+
let current
|
|
877
|
+
try {
|
|
878
|
+
current = await fs.promises.lstat(entry.path)
|
|
879
|
+
} catch (error) {
|
|
880
|
+
if (!isPathError(error)) throw error
|
|
881
|
+
if (entry.kind === "changed" && isMissing(error)) {
|
|
882
|
+
counts.path_errors += 1
|
|
883
|
+
return null
|
|
884
|
+
}
|
|
885
|
+
this.logVerificationSkip(active.app, entry, "unreadable", counts, error)
|
|
886
|
+
return null
|
|
887
|
+
}
|
|
888
|
+
this.checkpoint(active)
|
|
889
|
+
if (!current.isFile() || current.isSymbolicLink() ||
|
|
890
|
+
current.dev !== entry.dev || current.size !== entry.size) {
|
|
891
|
+
this.logVerificationSkip(
|
|
892
|
+
active.app, entry, "snapshot-changed", counts)
|
|
893
|
+
return null
|
|
894
|
+
}
|
|
895
|
+
const currentEntry = Object.assign({}, entry, fileSnapshot(current))
|
|
896
|
+
const key = this.automaticHashKey(currentEntry)
|
|
897
|
+
const cachedSnapshot = {
|
|
898
|
+
size: entry.cached_size,
|
|
899
|
+
mtime: entry.cached_mtime,
|
|
900
|
+
ctime: entry.cached_ctime,
|
|
901
|
+
dev: entry.cached_dev,
|
|
902
|
+
ino: entry.cached_ino
|
|
903
|
+
}
|
|
904
|
+
const reusable = memory.get(key) ||
|
|
905
|
+
(sameSnapshot(entry, current) && SHA256_RE.test(entry.hash || "")
|
|
906
|
+
? entry.hash
|
|
907
|
+
: null) ||
|
|
908
|
+
(sameSnapshot(cachedSnapshot, current) &&
|
|
909
|
+
SHA256_RE.test(entry.cached_hash || "")
|
|
910
|
+
? entry.cached_hash
|
|
911
|
+
: null)
|
|
912
|
+
if (reusable) {
|
|
913
|
+
counts.hash_reuses += 1
|
|
914
|
+
memory.set(key, reusable)
|
|
915
|
+
verifiedHashes.set(pathKey(entry.path), Object.assign({}, currentEntry, {
|
|
916
|
+
hash: reusable
|
|
917
|
+
}))
|
|
918
|
+
return reusable
|
|
919
|
+
}
|
|
920
|
+
let verified
|
|
921
|
+
try {
|
|
922
|
+
verified = await this.vault.scanner.hashStable(currentEntry, {
|
|
923
|
+
signal: active.controller && active.controller.signal
|
|
924
|
+
})
|
|
925
|
+
} catch (error) {
|
|
926
|
+
if (error && error.code === "EVAULTCANCELLED") throw error
|
|
927
|
+
if (!isPathError(error)) throw error
|
|
928
|
+
counts.hash_failures += 1
|
|
929
|
+
if (entry.kind === "changed" && isMissing(error)) {
|
|
930
|
+
counts.path_errors += 1
|
|
931
|
+
return null
|
|
932
|
+
}
|
|
933
|
+
this.logVerificationSkip(active.app, entry, "hash-failed", counts, error)
|
|
934
|
+
return null
|
|
935
|
+
}
|
|
936
|
+
this.checkpoint(active)
|
|
937
|
+
if (!verified.stable || !SHA256_RE.test(verified.result.hash || "")) {
|
|
938
|
+
this.logVerificationSkip(
|
|
939
|
+
active.app, entry, "changed-during-hash", counts)
|
|
940
|
+
return null
|
|
941
|
+
}
|
|
942
|
+
counts.hashed += 1
|
|
943
|
+
counts.hash_bytes += Math.max(0, Number(verified.result.size) || 0)
|
|
944
|
+
memory.set(key, verified.result.hash)
|
|
945
|
+
verifiedHashes.set(pathKey(entry.path), Object.assign({}, currentEntry, {
|
|
946
|
+
hash: verified.result.hash
|
|
947
|
+
}))
|
|
948
|
+
return verified.result.hash
|
|
949
|
+
}
|
|
950
|
+
|
|
758
951
|
async runPrecheck(active) {
|
|
759
952
|
const app = active.app
|
|
760
953
|
const root = path.resolve(this.vault.kernel.homedir, "api", app)
|
|
@@ -768,18 +961,24 @@ class AutomaticScans {
|
|
|
768
961
|
files: 0,
|
|
769
962
|
bytes: 0,
|
|
770
963
|
candidates: 0,
|
|
771
|
-
path_errors: 0
|
|
964
|
+
path_errors: 0,
|
|
965
|
+
hashed: 0,
|
|
966
|
+
hash_bytes: 0,
|
|
967
|
+
hash_reuses: 0,
|
|
968
|
+
hash_failures: 0,
|
|
969
|
+
unstable_hashes: 0
|
|
772
970
|
}
|
|
773
971
|
const threshold = await this.candidateThreshold()
|
|
774
972
|
const paths = [...new Set(active.paths || [])].filter((filePath) =>
|
|
775
973
|
typeof filePath === "string" && inside(root, filePath))
|
|
974
|
+
const verifiedHashes = new Map()
|
|
776
975
|
await this.vault.registry.beginAutomaticPrecheck(app)
|
|
777
976
|
this.log("check-started", {
|
|
778
977
|
app,
|
|
779
978
|
root,
|
|
780
979
|
threshold_bytes: threshold,
|
|
781
980
|
changed_paths: paths.length,
|
|
782
|
-
policy: "metadata-
|
|
981
|
+
policy: "metadata-prefilter-sha256"
|
|
783
982
|
})
|
|
784
983
|
try {
|
|
785
984
|
this.checkpoint(active)
|
|
@@ -789,6 +988,7 @@ class AutomaticScans {
|
|
|
789
988
|
onError: (error, filePath) => {
|
|
790
989
|
if (!isPathError(error)) return false
|
|
791
990
|
counts.path_errors += 1
|
|
991
|
+
if (isMissing(error)) return true
|
|
792
992
|
this.log("path-skipped", {
|
|
793
993
|
app,
|
|
794
994
|
path: filePath,
|
|
@@ -817,8 +1017,14 @@ class AutomaticScans {
|
|
|
817
1017
|
candidates.push({
|
|
818
1018
|
path: paths[index],
|
|
819
1019
|
size: stat.size,
|
|
1020
|
+
mtime: stat.mtimeMs,
|
|
1021
|
+
ctime: stat.ctimeMs,
|
|
820
1022
|
dev: stat.dev,
|
|
821
|
-
ino: stat.ino
|
|
1023
|
+
ino: stat.ino,
|
|
1024
|
+
nlink: stat.nlink,
|
|
1025
|
+
mode: stat.mode,
|
|
1026
|
+
uid: stat.uid,
|
|
1027
|
+
gid: stat.gid
|
|
822
1028
|
})
|
|
823
1029
|
}
|
|
824
1030
|
counts.candidates = candidates.length
|
|
@@ -831,15 +1037,123 @@ class AutomaticScans {
|
|
|
831
1037
|
error.code = "ENOENT"
|
|
832
1038
|
throw error
|
|
833
1039
|
}
|
|
834
|
-
const
|
|
1040
|
+
const verifiedMatches = []
|
|
1041
|
+
let group = null
|
|
1042
|
+
const preparePeers = () => {
|
|
1043
|
+
if (!group || group.unmatched) return
|
|
1044
|
+
group.unmatched = new Map()
|
|
1045
|
+
group.unmatchedIdentities = new Map()
|
|
1046
|
+
for (const [hash, value] of group.candidatesByHash) {
|
|
1047
|
+
if (value.identities.size > 1) {
|
|
1048
|
+
for (const candidate of value.candidates) {
|
|
1049
|
+
group.matched.add(pathKey(candidate.path))
|
|
1050
|
+
}
|
|
1051
|
+
} else {
|
|
1052
|
+
const identity = value.identities.values().next().value
|
|
1053
|
+
group.unmatched.set(hash, {
|
|
1054
|
+
identity,
|
|
1055
|
+
candidates: value.candidates
|
|
1056
|
+
})
|
|
1057
|
+
group.unmatchedIdentities.set(
|
|
1058
|
+
identity,
|
|
1059
|
+
(group.unmatchedIdentities.get(identity) || 0) + 1)
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const finishGroup = () => {
|
|
1064
|
+
if (!group) return
|
|
1065
|
+
preparePeers()
|
|
1066
|
+
for (const [hash, value] of group.candidatesByHash) {
|
|
1067
|
+
for (const candidate of value.candidates) {
|
|
1068
|
+
if (!group.matched.has(pathKey(candidate.path))) continue
|
|
1069
|
+
verifiedMatches.push({ path: candidate.path, hash })
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
group = null
|
|
1073
|
+
}
|
|
1074
|
+
let cursor = null
|
|
1075
|
+
do {
|
|
1076
|
+
const page = await this.vault.registry.automaticPrecheckEntries(
|
|
1077
|
+
app, cursor, ENTRY_BATCH_SIZE)
|
|
1078
|
+
this.checkpoint(active)
|
|
1079
|
+
for (const entry of page.entries) {
|
|
1080
|
+
const key = `${entry.dev}\0${entry.size}`
|
|
1081
|
+
if (!group || group.key !== key) {
|
|
1082
|
+
finishGroup()
|
|
1083
|
+
group = {
|
|
1084
|
+
key,
|
|
1085
|
+
candidatesByHash: new Map(),
|
|
1086
|
+
matched: new Set(),
|
|
1087
|
+
unmatched: null,
|
|
1088
|
+
unmatchedIdentities: null,
|
|
1089
|
+
memory: new Map()
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (entry.kind === "changed") {
|
|
1093
|
+
const candidate = entry
|
|
1094
|
+
this.checkpoint(active)
|
|
1095
|
+
const hash = await this.verifiedAutomaticHash(
|
|
1096
|
+
active, candidate, group.memory, verifiedHashes, counts)
|
|
1097
|
+
if (!hash) continue
|
|
1098
|
+
let value = group.candidatesByHash.get(hash)
|
|
1099
|
+
if (!value) {
|
|
1100
|
+
value = { candidates: [], identities: new Set() }
|
|
1101
|
+
group.candidatesByHash.set(hash, value)
|
|
1102
|
+
}
|
|
1103
|
+
value.candidates.push(candidate)
|
|
1104
|
+
value.identities.add(this.automaticIdentityKey(candidate))
|
|
1105
|
+
continue
|
|
1106
|
+
}
|
|
1107
|
+
preparePeers()
|
|
1108
|
+
if (!group.unmatched.size) continue
|
|
1109
|
+
this.checkpoint(active)
|
|
1110
|
+
const identity = this.automaticIdentityKey(entry)
|
|
1111
|
+
if (group.unmatchedIdentities.size === 1 &&
|
|
1112
|
+
group.unmatchedIdentities.has(identity)) continue
|
|
1113
|
+
const hash = await this.verifiedAutomaticHash(
|
|
1114
|
+
active, entry, group.memory, verifiedHashes, counts)
|
|
1115
|
+
const wanted = hash && group.unmatched.get(hash)
|
|
1116
|
+
if (!wanted || wanted.identity === identity) {
|
|
1117
|
+
continue
|
|
1118
|
+
}
|
|
1119
|
+
for (const candidate of wanted.candidates) {
|
|
1120
|
+
group.matched.add(pathKey(candidate.path))
|
|
1121
|
+
}
|
|
1122
|
+
group.unmatched.delete(hash)
|
|
1123
|
+
const remaining = group.unmatchedIdentities.get(wanted.identity) - 1
|
|
1124
|
+
if (remaining) {
|
|
1125
|
+
group.unmatchedIdentities.set(wanted.identity, remaining)
|
|
1126
|
+
} else {
|
|
1127
|
+
group.unmatchedIdentities.delete(wanted.identity)
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (verifiedHashes.size >= ENTRY_BATCH_SIZE) {
|
|
1131
|
+
await this.vault.registry.rememberHashCache(
|
|
1132
|
+
[...verifiedHashes.values()])
|
|
1133
|
+
verifiedHashes.clear()
|
|
1134
|
+
this.checkpoint(active)
|
|
1135
|
+
}
|
|
1136
|
+
cursor = page.next_cursor
|
|
1137
|
+
} while (cursor)
|
|
1138
|
+
finishGroup()
|
|
1139
|
+
this.checkpoint(active)
|
|
1140
|
+
const result = await this.vault.registry.automaticPrecheckResult(
|
|
1141
|
+
app, verifiedMatches)
|
|
835
1142
|
this.checkpoint(active)
|
|
836
1143
|
return Object.assign({}, counts, {
|
|
837
1144
|
signature: result.signature,
|
|
838
|
-
|
|
1145
|
+
verified_files: Math.max(0, Number(result.files) || 0),
|
|
839
1146
|
duration_ms: Date.now() - startedAt
|
|
840
1147
|
})
|
|
841
1148
|
} finally {
|
|
842
|
-
|
|
1149
|
+
try {
|
|
1150
|
+
if (verifiedHashes.size) {
|
|
1151
|
+
await this.vault.registry.rememberHashCache(
|
|
1152
|
+
[...verifiedHashes.values()])
|
|
1153
|
+
}
|
|
1154
|
+
} finally {
|
|
1155
|
+
await this.vault.registry.abortAutomaticPrecheck(app)
|
|
1156
|
+
}
|
|
843
1157
|
}
|
|
844
1158
|
}
|
|
845
1159
|
|
|
@@ -894,6 +1208,7 @@ class AutomaticScans {
|
|
|
894
1208
|
paths: [...new Set(entry.paths || [])],
|
|
895
1209
|
cancelled: false,
|
|
896
1210
|
reason: null,
|
|
1211
|
+
controller: new AbortController(),
|
|
897
1212
|
promise: null
|
|
898
1213
|
}
|
|
899
1214
|
this.active = active
|
|
@@ -933,7 +1248,12 @@ class AutomaticScans {
|
|
|
933
1248
|
files: result && result.files,
|
|
934
1249
|
bytes: result && result.bytes,
|
|
935
1250
|
candidates: result && result.candidates,
|
|
936
|
-
|
|
1251
|
+
verified_files: result && result.verified_files,
|
|
1252
|
+
hashed: result && result.hashed,
|
|
1253
|
+
hash_bytes: result && result.hash_bytes,
|
|
1254
|
+
hash_reuses: result && result.hash_reuses,
|
|
1255
|
+
hash_failures: result && result.hash_failures,
|
|
1256
|
+
unstable_hashes: result && result.unstable_hashes,
|
|
937
1257
|
path_errors: result && result.path_errors,
|
|
938
1258
|
error: error && error.message
|
|
939
1259
|
})
|
|
@@ -1037,8 +1357,8 @@ class AutomaticScans {
|
|
|
1037
1357
|
}
|
|
1038
1358
|
|
|
1039
1359
|
async publishResultNow(app, result = {}) {
|
|
1040
|
-
const
|
|
1041
|
-
if (
|
|
1360
|
+
const verifiedFiles = Math.max(0, Number(result.verified_files) || 0)
|
|
1361
|
+
if (verifiedFiles > 0 && result.signature) {
|
|
1042
1362
|
const acknowledged = (this.settings.get(app) || {})
|
|
1043
1363
|
.acknowledged_signature
|
|
1044
1364
|
const options = { signature: result.signature }
|
|
@@ -1059,9 +1379,9 @@ class AutomaticScans {
|
|
|
1059
1379
|
previous: null,
|
|
1060
1380
|
hidden: acknowledged === result.signature
|
|
1061
1381
|
})
|
|
1062
|
-
this.log("
|
|
1382
|
+
this.log("verified-matches", {
|
|
1063
1383
|
app,
|
|
1064
|
-
|
|
1384
|
+
verified_files: verifiedFiles,
|
|
1065
1385
|
acknowledged: acknowledged === result.signature
|
|
1066
1386
|
})
|
|
1067
1387
|
return "result"
|
|
@@ -1076,7 +1396,7 @@ class AutomaticScans {
|
|
|
1076
1396
|
await this.vault.registry.setAutomaticAppScanState(app, null)
|
|
1077
1397
|
}
|
|
1078
1398
|
this.entries.delete(app)
|
|
1079
|
-
this.log("no-
|
|
1399
|
+
this.log("no-verified-matches", { app })
|
|
1080
1400
|
return "empty"
|
|
1081
1401
|
}
|
|
1082
1402
|
|
|
@@ -1246,17 +1566,17 @@ class AutomaticScans {
|
|
|
1246
1566
|
|
|
1247
1567
|
async acknowledge(app, signature) {
|
|
1248
1568
|
if (this.disposed) {
|
|
1249
|
-
return { error: "That
|
|
1569
|
+
return { error: "That automatic duplicate result is no longer available." }
|
|
1250
1570
|
}
|
|
1251
1571
|
return this.withAppTransition(app, async () => {
|
|
1252
1572
|
await this.hydrate()
|
|
1253
1573
|
if (this.disposed) {
|
|
1254
|
-
return { error: "That
|
|
1574
|
+
return { error: "That automatic duplicate result is no longer available." }
|
|
1255
1575
|
}
|
|
1256
1576
|
const current = this.entries.get(app)
|
|
1257
1577
|
const entry = this.publicResult(current)
|
|
1258
1578
|
if (!entry) {
|
|
1259
|
-
return { error: "That
|
|
1579
|
+
return { error: "That automatic duplicate result is no longer available." }
|
|
1260
1580
|
}
|
|
1261
1581
|
if (typeof signature !== "string" || signature !== entry.signature) {
|
|
1262
1582
|
return { stale: true, app }
|
|
@@ -7,14 +7,27 @@ const fs = require('fs')
|
|
|
7
7
|
// same sha256 digest, one file at a time.
|
|
8
8
|
const HASH_READ_SIZE = 1024 * 1024
|
|
9
9
|
const HASH_PROGRESS_INTERVAL_MS = 1000
|
|
10
|
+
const jobs = new Map()
|
|
10
11
|
|
|
11
|
-
parentPort.on('message', ({ id, filePath }) => {
|
|
12
|
+
parentPort.on('message', ({ id, filePath, cancel }) => {
|
|
13
|
+
if (cancel) {
|
|
14
|
+
const job = jobs.get(id)
|
|
15
|
+
if (!job) return
|
|
16
|
+
const error = new Error('Hashing cancelled.')
|
|
17
|
+
error.code = 'EVAULTCANCELLED'
|
|
18
|
+
job.stream.destroy(error)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
12
21
|
const hash = crypto.createHash('sha256')
|
|
13
22
|
let size = 0
|
|
14
23
|
let lastProgressAt = 0
|
|
15
24
|
let complete = false
|
|
16
25
|
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
|
|
17
|
-
const stream = fs.createReadStream(filePath, {
|
|
26
|
+
const stream = fs.createReadStream(filePath, {
|
|
27
|
+
flags,
|
|
28
|
+
highWaterMark: HASH_READ_SIZE
|
|
29
|
+
})
|
|
30
|
+
jobs.set(id, { stream })
|
|
18
31
|
stream.on('data', (chunk) => {
|
|
19
32
|
size += chunk.length
|
|
20
33
|
hash.update(chunk)
|
|
@@ -27,11 +40,13 @@ parentPort.on('message', ({ id, filePath }) => {
|
|
|
27
40
|
stream.on('error', (error) => {
|
|
28
41
|
if (complete) return
|
|
29
42
|
complete = true
|
|
43
|
+
jobs.delete(id)
|
|
30
44
|
parentPort.postMessage({ id, error: error.message, code: error.code })
|
|
31
45
|
})
|
|
32
46
|
stream.on('end', () => {
|
|
33
47
|
if (complete) return
|
|
34
48
|
complete = true
|
|
49
|
+
jobs.delete(id)
|
|
35
50
|
parentPort.postMessage({ id, hash: hash.digest('hex'), size })
|
|
36
51
|
})
|
|
37
52
|
})
|