pinokiod 8.0.69 → 8.0.71
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 +235 -16
- 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 +334 -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 +426 -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 { 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
|
|
@@ -745,6 +753,7 @@ class AutomaticScans {
|
|
|
745
753
|
}
|
|
746
754
|
this.active.cancelled = true
|
|
747
755
|
this.active.reason = reason
|
|
756
|
+
if (this.active.controller) this.active.controller.abort()
|
|
748
757
|
this.log("check-cancel-requested", { app, reason })
|
|
749
758
|
return true
|
|
750
759
|
}
|
|
@@ -755,6 +764,90 @@ class AutomaticScans {
|
|
|
755
764
|
}
|
|
756
765
|
}
|
|
757
766
|
|
|
767
|
+
automaticHashKey(entry) {
|
|
768
|
+
return [
|
|
769
|
+
this.automaticIdentityKey(entry),
|
|
770
|
+
entry.size,
|
|
771
|
+
entry.mtime,
|
|
772
|
+
entry.ctime
|
|
773
|
+
].join("\0")
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
automaticIdentityKey(entry) {
|
|
777
|
+
return entry.ino !== 0
|
|
778
|
+
? `inode:${entry.dev}:${entry.ino}`
|
|
779
|
+
: `path:${pathKey(entry.path)}`
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
logVerificationSkip(app, entry, reason, counts, error = null) {
|
|
783
|
+
if (error) counts.path_errors += 1
|
|
784
|
+
else counts.unstable_hashes += 1
|
|
785
|
+
this.log("verification-skipped", {
|
|
786
|
+
app,
|
|
787
|
+
path: entry.path,
|
|
788
|
+
kind: entry.kind,
|
|
789
|
+
reason,
|
|
790
|
+
code: error && error.code,
|
|
791
|
+
message: error && error.message
|
|
792
|
+
})
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async verifiedAutomaticHash(active, entry, memory, verifiedHashes, counts) {
|
|
796
|
+
this.checkpoint(active)
|
|
797
|
+
let current
|
|
798
|
+
try {
|
|
799
|
+
current = await fs.promises.lstat(entry.path)
|
|
800
|
+
} catch (error) {
|
|
801
|
+
if (!isPathError(error)) throw error
|
|
802
|
+
this.logVerificationSkip(active.app, entry, "unreadable", counts, error)
|
|
803
|
+
return null
|
|
804
|
+
}
|
|
805
|
+
this.checkpoint(active)
|
|
806
|
+
if (!current.isFile() || current.isSymbolicLink() ||
|
|
807
|
+
!sameSnapshot(entry, current)) {
|
|
808
|
+
this.logVerificationSkip(
|
|
809
|
+
active.app, entry, "snapshot-changed", counts)
|
|
810
|
+
return null
|
|
811
|
+
}
|
|
812
|
+
const key = this.automaticHashKey(entry)
|
|
813
|
+
const reusable = SHA256_RE.test(entry.hash || "")
|
|
814
|
+
? entry.hash
|
|
815
|
+
: memory.get(key)
|
|
816
|
+
if (reusable) {
|
|
817
|
+
counts.hash_reuses += 1
|
|
818
|
+
memory.set(key, reusable)
|
|
819
|
+
verifiedHashes.set(pathKey(entry.path), Object.assign({}, entry, {
|
|
820
|
+
hash: reusable
|
|
821
|
+
}))
|
|
822
|
+
return reusable
|
|
823
|
+
}
|
|
824
|
+
let verified
|
|
825
|
+
try {
|
|
826
|
+
verified = await this.vault.scanner.hashStable(entry, {
|
|
827
|
+
signal: active.controller && active.controller.signal
|
|
828
|
+
})
|
|
829
|
+
} catch (error) {
|
|
830
|
+
if (error && error.code === "EVAULTCANCELLED") throw error
|
|
831
|
+
if (!isPathError(error)) throw error
|
|
832
|
+
counts.hash_failures += 1
|
|
833
|
+
this.logVerificationSkip(active.app, entry, "hash-failed", counts, error)
|
|
834
|
+
return null
|
|
835
|
+
}
|
|
836
|
+
this.checkpoint(active)
|
|
837
|
+
if (!verified.stable || !SHA256_RE.test(verified.result.hash || "")) {
|
|
838
|
+
this.logVerificationSkip(
|
|
839
|
+
active.app, entry, "changed-during-hash", counts)
|
|
840
|
+
return null
|
|
841
|
+
}
|
|
842
|
+
counts.hashed += 1
|
|
843
|
+
counts.hash_bytes += Math.max(0, Number(verified.result.size) || 0)
|
|
844
|
+
memory.set(key, verified.result.hash)
|
|
845
|
+
verifiedHashes.set(pathKey(entry.path), Object.assign({}, entry, {
|
|
846
|
+
hash: verified.result.hash
|
|
847
|
+
}))
|
|
848
|
+
return verified.result.hash
|
|
849
|
+
}
|
|
850
|
+
|
|
758
851
|
async runPrecheck(active) {
|
|
759
852
|
const app = active.app
|
|
760
853
|
const root = path.resolve(this.vault.kernel.homedir, "api", app)
|
|
@@ -768,18 +861,24 @@ class AutomaticScans {
|
|
|
768
861
|
files: 0,
|
|
769
862
|
bytes: 0,
|
|
770
863
|
candidates: 0,
|
|
771
|
-
path_errors: 0
|
|
864
|
+
path_errors: 0,
|
|
865
|
+
hashed: 0,
|
|
866
|
+
hash_bytes: 0,
|
|
867
|
+
hash_reuses: 0,
|
|
868
|
+
hash_failures: 0,
|
|
869
|
+
unstable_hashes: 0
|
|
772
870
|
}
|
|
773
871
|
const threshold = await this.candidateThreshold()
|
|
774
872
|
const paths = [...new Set(active.paths || [])].filter((filePath) =>
|
|
775
873
|
typeof filePath === "string" && inside(root, filePath))
|
|
874
|
+
const verifiedHashes = new Map()
|
|
776
875
|
await this.vault.registry.beginAutomaticPrecheck(app)
|
|
777
876
|
this.log("check-started", {
|
|
778
877
|
app,
|
|
779
878
|
root,
|
|
780
879
|
threshold_bytes: threshold,
|
|
781
880
|
changed_paths: paths.length,
|
|
782
|
-
policy: "metadata-
|
|
881
|
+
policy: "metadata-prefilter-sha256"
|
|
783
882
|
})
|
|
784
883
|
try {
|
|
785
884
|
this.checkpoint(active)
|
|
@@ -817,8 +916,14 @@ class AutomaticScans {
|
|
|
817
916
|
candidates.push({
|
|
818
917
|
path: paths[index],
|
|
819
918
|
size: stat.size,
|
|
919
|
+
mtime: stat.mtimeMs,
|
|
920
|
+
ctime: stat.ctimeMs,
|
|
820
921
|
dev: stat.dev,
|
|
821
|
-
ino: stat.ino
|
|
922
|
+
ino: stat.ino,
|
|
923
|
+
nlink: stat.nlink,
|
|
924
|
+
mode: stat.mode,
|
|
925
|
+
uid: stat.uid,
|
|
926
|
+
gid: stat.gid
|
|
822
927
|
})
|
|
823
928
|
}
|
|
824
929
|
counts.candidates = candidates.length
|
|
@@ -831,15 +936,123 @@ class AutomaticScans {
|
|
|
831
936
|
error.code = "ENOENT"
|
|
832
937
|
throw error
|
|
833
938
|
}
|
|
834
|
-
const
|
|
939
|
+
const verifiedMatches = []
|
|
940
|
+
let group = null
|
|
941
|
+
const preparePeers = () => {
|
|
942
|
+
if (!group || group.unmatched) return
|
|
943
|
+
group.unmatched = new Map()
|
|
944
|
+
group.unmatchedIdentities = new Map()
|
|
945
|
+
for (const [hash, value] of group.candidatesByHash) {
|
|
946
|
+
if (value.identities.size > 1) {
|
|
947
|
+
for (const candidate of value.candidates) {
|
|
948
|
+
group.matched.add(pathKey(candidate.path))
|
|
949
|
+
}
|
|
950
|
+
} else {
|
|
951
|
+
const identity = value.identities.values().next().value
|
|
952
|
+
group.unmatched.set(hash, {
|
|
953
|
+
identity,
|
|
954
|
+
candidates: value.candidates
|
|
955
|
+
})
|
|
956
|
+
group.unmatchedIdentities.set(
|
|
957
|
+
identity,
|
|
958
|
+
(group.unmatchedIdentities.get(identity) || 0) + 1)
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const finishGroup = () => {
|
|
963
|
+
if (!group) return
|
|
964
|
+
preparePeers()
|
|
965
|
+
for (const [hash, value] of group.candidatesByHash) {
|
|
966
|
+
for (const candidate of value.candidates) {
|
|
967
|
+
if (!group.matched.has(pathKey(candidate.path))) continue
|
|
968
|
+
verifiedMatches.push({ path: candidate.path, hash })
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
group = null
|
|
972
|
+
}
|
|
973
|
+
let cursor = null
|
|
974
|
+
do {
|
|
975
|
+
const page = await this.vault.registry.automaticPrecheckEntries(
|
|
976
|
+
app, cursor, ENTRY_BATCH_SIZE)
|
|
977
|
+
this.checkpoint(active)
|
|
978
|
+
for (const entry of page.entries) {
|
|
979
|
+
const key = `${entry.dev}\0${entry.size}`
|
|
980
|
+
if (!group || group.key !== key) {
|
|
981
|
+
finishGroup()
|
|
982
|
+
group = {
|
|
983
|
+
key,
|
|
984
|
+
candidatesByHash: new Map(),
|
|
985
|
+
matched: new Set(),
|
|
986
|
+
unmatched: null,
|
|
987
|
+
unmatchedIdentities: null,
|
|
988
|
+
memory: new Map()
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (entry.kind === "changed") {
|
|
992
|
+
const candidate = entry
|
|
993
|
+
this.checkpoint(active)
|
|
994
|
+
const hash = await this.verifiedAutomaticHash(
|
|
995
|
+
active, candidate, group.memory, verifiedHashes, counts)
|
|
996
|
+
if (!hash) continue
|
|
997
|
+
let value = group.candidatesByHash.get(hash)
|
|
998
|
+
if (!value) {
|
|
999
|
+
value = { candidates: [], identities: new Set() }
|
|
1000
|
+
group.candidatesByHash.set(hash, value)
|
|
1001
|
+
}
|
|
1002
|
+
value.candidates.push(candidate)
|
|
1003
|
+
value.identities.add(this.automaticIdentityKey(candidate))
|
|
1004
|
+
continue
|
|
1005
|
+
}
|
|
1006
|
+
preparePeers()
|
|
1007
|
+
if (!group.unmatched.size) continue
|
|
1008
|
+
this.checkpoint(active)
|
|
1009
|
+
const identity = this.automaticIdentityKey(entry)
|
|
1010
|
+
if (group.unmatchedIdentities.size === 1 &&
|
|
1011
|
+
group.unmatchedIdentities.has(identity)) continue
|
|
1012
|
+
const hash = await this.verifiedAutomaticHash(
|
|
1013
|
+
active, entry, group.memory, verifiedHashes, counts)
|
|
1014
|
+
const wanted = hash && group.unmatched.get(hash)
|
|
1015
|
+
if (!wanted || wanted.identity === identity) {
|
|
1016
|
+
continue
|
|
1017
|
+
}
|
|
1018
|
+
for (const candidate of wanted.candidates) {
|
|
1019
|
+
group.matched.add(pathKey(candidate.path))
|
|
1020
|
+
}
|
|
1021
|
+
group.unmatched.delete(hash)
|
|
1022
|
+
const remaining = group.unmatchedIdentities.get(wanted.identity) - 1
|
|
1023
|
+
if (remaining) {
|
|
1024
|
+
group.unmatchedIdentities.set(wanted.identity, remaining)
|
|
1025
|
+
} else {
|
|
1026
|
+
group.unmatchedIdentities.delete(wanted.identity)
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (verifiedHashes.size >= ENTRY_BATCH_SIZE) {
|
|
1030
|
+
await this.vault.registry.rememberHashCache(
|
|
1031
|
+
[...verifiedHashes.values()])
|
|
1032
|
+
verifiedHashes.clear()
|
|
1033
|
+
this.checkpoint(active)
|
|
1034
|
+
}
|
|
1035
|
+
cursor = page.next_cursor
|
|
1036
|
+
} while (cursor)
|
|
1037
|
+
finishGroup()
|
|
1038
|
+
this.checkpoint(active)
|
|
1039
|
+
const result = await this.vault.registry.automaticPrecheckResult(
|
|
1040
|
+
app, verifiedMatches)
|
|
835
1041
|
this.checkpoint(active)
|
|
836
1042
|
return Object.assign({}, counts, {
|
|
837
1043
|
signature: result.signature,
|
|
838
|
-
|
|
1044
|
+
verified_files: Math.max(0, Number(result.files) || 0),
|
|
839
1045
|
duration_ms: Date.now() - startedAt
|
|
840
1046
|
})
|
|
841
1047
|
} finally {
|
|
842
|
-
|
|
1048
|
+
try {
|
|
1049
|
+
if (verifiedHashes.size) {
|
|
1050
|
+
await this.vault.registry.rememberHashCache(
|
|
1051
|
+
[...verifiedHashes.values()])
|
|
1052
|
+
}
|
|
1053
|
+
} finally {
|
|
1054
|
+
await this.vault.registry.abortAutomaticPrecheck(app)
|
|
1055
|
+
}
|
|
843
1056
|
}
|
|
844
1057
|
}
|
|
845
1058
|
|
|
@@ -894,6 +1107,7 @@ class AutomaticScans {
|
|
|
894
1107
|
paths: [...new Set(entry.paths || [])],
|
|
895
1108
|
cancelled: false,
|
|
896
1109
|
reason: null,
|
|
1110
|
+
controller: new AbortController(),
|
|
897
1111
|
promise: null
|
|
898
1112
|
}
|
|
899
1113
|
this.active = active
|
|
@@ -933,7 +1147,12 @@ class AutomaticScans {
|
|
|
933
1147
|
files: result && result.files,
|
|
934
1148
|
bytes: result && result.bytes,
|
|
935
1149
|
candidates: result && result.candidates,
|
|
936
|
-
|
|
1150
|
+
verified_files: result && result.verified_files,
|
|
1151
|
+
hashed: result && result.hashed,
|
|
1152
|
+
hash_bytes: result && result.hash_bytes,
|
|
1153
|
+
hash_reuses: result && result.hash_reuses,
|
|
1154
|
+
hash_failures: result && result.hash_failures,
|
|
1155
|
+
unstable_hashes: result && result.unstable_hashes,
|
|
937
1156
|
path_errors: result && result.path_errors,
|
|
938
1157
|
error: error && error.message
|
|
939
1158
|
})
|
|
@@ -1037,8 +1256,8 @@ class AutomaticScans {
|
|
|
1037
1256
|
}
|
|
1038
1257
|
|
|
1039
1258
|
async publishResultNow(app, result = {}) {
|
|
1040
|
-
const
|
|
1041
|
-
if (
|
|
1259
|
+
const verifiedFiles = Math.max(0, Number(result.verified_files) || 0)
|
|
1260
|
+
if (verifiedFiles > 0 && result.signature) {
|
|
1042
1261
|
const acknowledged = (this.settings.get(app) || {})
|
|
1043
1262
|
.acknowledged_signature
|
|
1044
1263
|
const options = { signature: result.signature }
|
|
@@ -1059,9 +1278,9 @@ class AutomaticScans {
|
|
|
1059
1278
|
previous: null,
|
|
1060
1279
|
hidden: acknowledged === result.signature
|
|
1061
1280
|
})
|
|
1062
|
-
this.log("
|
|
1281
|
+
this.log("verified-matches", {
|
|
1063
1282
|
app,
|
|
1064
|
-
|
|
1283
|
+
verified_files: verifiedFiles,
|
|
1065
1284
|
acknowledged: acknowledged === result.signature
|
|
1066
1285
|
})
|
|
1067
1286
|
return "result"
|
|
@@ -1076,7 +1295,7 @@ class AutomaticScans {
|
|
|
1076
1295
|
await this.vault.registry.setAutomaticAppScanState(app, null)
|
|
1077
1296
|
}
|
|
1078
1297
|
this.entries.delete(app)
|
|
1079
|
-
this.log("no-
|
|
1298
|
+
this.log("no-verified-matches", { app })
|
|
1080
1299
|
return "empty"
|
|
1081
1300
|
}
|
|
1082
1301
|
|
|
@@ -1246,17 +1465,17 @@ class AutomaticScans {
|
|
|
1246
1465
|
|
|
1247
1466
|
async acknowledge(app, signature) {
|
|
1248
1467
|
if (this.disposed) {
|
|
1249
|
-
return { error: "That
|
|
1468
|
+
return { error: "That automatic duplicate result is no longer available." }
|
|
1250
1469
|
}
|
|
1251
1470
|
return this.withAppTransition(app, async () => {
|
|
1252
1471
|
await this.hydrate()
|
|
1253
1472
|
if (this.disposed) {
|
|
1254
|
-
return { error: "That
|
|
1473
|
+
return { error: "That automatic duplicate result is no longer available." }
|
|
1255
1474
|
}
|
|
1256
1475
|
const current = this.entries.get(app)
|
|
1257
1476
|
const entry = this.publicResult(current)
|
|
1258
1477
|
if (!entry) {
|
|
1259
|
-
return { error: "That
|
|
1478
|
+
return { error: "That automatic duplicate result is no longer available." }
|
|
1260
1479
|
}
|
|
1261
1480
|
if (typeof signature !== "string" || signature !== entry.signature) {
|
|
1262
1481
|
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
|
})
|
package/kernel/vault/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const Sweeper = require("./sweeper")
|
|
|
9
9
|
const FolderFinder = require("./folder_finder")
|
|
10
10
|
const AutomaticScans = require("./automatic_scans")
|
|
11
11
|
const { fileSnapshot, sameSnapshot, sameContentState } = require("./snapshot")
|
|
12
|
+
const { cancelledError } = require("./operation_errors")
|
|
12
13
|
const {
|
|
13
14
|
SIZE_THRESHOLD,
|
|
14
15
|
CANDIDATE_SIZE_OPTIONS,
|
|
@@ -230,6 +231,7 @@ class Vault {
|
|
|
230
231
|
this._anchorStoresByDevice = new Map()
|
|
231
232
|
this.operationTail = Promise.resolve()
|
|
232
233
|
this.registryInitializationPromise = null
|
|
234
|
+
this.registryRestartPromise = null
|
|
233
235
|
this.initializationPromise = null
|
|
234
236
|
this.scanPromise = null
|
|
235
237
|
this.scanCompletionPromise = null
|
|
@@ -246,6 +248,42 @@ class Vault {
|
|
|
246
248
|
this.automaticScans = new AutomaticScans(this)
|
|
247
249
|
}
|
|
248
250
|
|
|
251
|
+
async dispose() {
|
|
252
|
+
const failures = []
|
|
253
|
+
const settle = async (promise) => {
|
|
254
|
+
if (!promise) return
|
|
255
|
+
try {
|
|
256
|
+
await promise
|
|
257
|
+
} catch (error) {
|
|
258
|
+
failures.push(error)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await settle(this.automaticScans.dispose())
|
|
263
|
+
if (this.scanPromise) this.cancelScan()
|
|
264
|
+
if (this.folderDiscoveryPromise) this.cancelFolderDiscovery()
|
|
265
|
+
if (this.fileActionProgress) this.cancelFileAction()
|
|
266
|
+
await settle(this.operationTail)
|
|
267
|
+
await settle(this.scanCompletionPromise)
|
|
268
|
+
await settle(this.folderDiscoveryCommitPromise)
|
|
269
|
+
await settle(this.registryRestartPromise)
|
|
270
|
+
|
|
271
|
+
if (this.worker) {
|
|
272
|
+
const worker = this.worker
|
|
273
|
+
this.failHashWorker(worker, cancelledError("Vault disposed."))
|
|
274
|
+
await settle(worker.terminate())
|
|
275
|
+
}
|
|
276
|
+
if (this.registry) {
|
|
277
|
+
const registry = this.registry
|
|
278
|
+
this.registry = null
|
|
279
|
+
await settle(registry.close())
|
|
280
|
+
}
|
|
281
|
+
this.initialized = false
|
|
282
|
+
this.sweeper = null
|
|
283
|
+
this.folderFinder = null
|
|
284
|
+
if (failures.length) throw failures[0]
|
|
285
|
+
}
|
|
286
|
+
|
|
249
287
|
get root() {
|
|
250
288
|
return path.resolve(this.kernel.homedir, "vault")
|
|
251
289
|
}
|
|
@@ -614,6 +652,9 @@ class Vault {
|
|
|
614
652
|
}
|
|
615
653
|
|
|
616
654
|
ensureRegistryInitialized() {
|
|
655
|
+
if (this.registryRestartPromise) {
|
|
656
|
+
return this.registryRestartPromise.then(() => ({ enabled: true }))
|
|
657
|
+
}
|
|
617
658
|
if (this.registry) return Promise.resolve({ enabled: true })
|
|
618
659
|
if (!this.registryInitializationPromise) {
|
|
619
660
|
this.registryInitializationPromise = this.initializeRegistryStorage()
|
|
@@ -640,6 +681,7 @@ class Vault {
|
|
|
640
681
|
|
|
641
682
|
async ensureInitialized() {
|
|
642
683
|
if (!this.enabled) return { enabled: false }
|
|
684
|
+
await this.ensureRegistryInitialized()
|
|
643
685
|
if (this.initialized) return { enabled: true, mode: this.mode }
|
|
644
686
|
if (!this.initializationPromise) {
|
|
645
687
|
this.initializationPromise = this.initializeStorage().finally(() => {
|
|
@@ -651,6 +693,7 @@ class Vault {
|
|
|
651
693
|
|
|
652
694
|
async openWorkspace() {
|
|
653
695
|
if (!this.initialized) return this.ensureInitialized()
|
|
696
|
+
await this.ensureRegistryInitialized()
|
|
654
697
|
await this.refreshAnchorStores()
|
|
655
698
|
await this.refreshSources()
|
|
656
699
|
return { enabled: true, mode: this.mode }
|
|
@@ -936,7 +979,7 @@ class Vault {
|
|
|
936
979
|
}
|
|
937
980
|
for (const [id, job] of [...this.workerJobs]) {
|
|
938
981
|
if (job.worker !== worker) continue
|
|
939
|
-
|
|
982
|
+
job.cleanup()
|
|
940
983
|
this.workerJobs.delete(id)
|
|
941
984
|
job.reject(error)
|
|
942
985
|
}
|
|
@@ -944,6 +987,10 @@ class Vault {
|
|
|
944
987
|
}
|
|
945
988
|
|
|
946
989
|
async hashFile(filePath, options = {}) {
|
|
990
|
+
const signal = options.signal
|
|
991
|
+
if (signal && signal.aborted) {
|
|
992
|
+
throw cancelledError("Hashing cancelled.")
|
|
993
|
+
}
|
|
947
994
|
if (this.workerIdleTimer) {
|
|
948
995
|
clearTimeout(this.workerIdleTimer)
|
|
949
996
|
this.workerIdleTimer = null
|
|
@@ -962,7 +1009,7 @@ class Vault {
|
|
|
962
1009
|
job.reportProgress(bytesRead)
|
|
963
1010
|
return
|
|
964
1011
|
}
|
|
965
|
-
|
|
1012
|
+
job.cleanup()
|
|
966
1013
|
this.workerJobs.delete(id)
|
|
967
1014
|
if (error) {
|
|
968
1015
|
const failure = new Error(error)
|
|
@@ -1016,7 +1063,22 @@ class Vault {
|
|
|
1016
1063
|
} catch (error) {}
|
|
1017
1064
|
},
|
|
1018
1065
|
inactivityTimer: null,
|
|
1019
|
-
resetInactivity: null
|
|
1066
|
+
resetInactivity: null,
|
|
1067
|
+
cancel: null,
|
|
1068
|
+
cleanup: null
|
|
1069
|
+
}
|
|
1070
|
+
job.cancel = () => {
|
|
1071
|
+
if (!this.workerJobs.has(id)) return
|
|
1072
|
+
try {
|
|
1073
|
+
worker.postMessage({ id, cancel: true })
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
const failure = cancelledError("Hashing cancelled.")
|
|
1076
|
+
this.failHashWorker(worker, failure, true)
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
job.cleanup = () => {
|
|
1080
|
+
clearTimeout(job.inactivityTimer)
|
|
1081
|
+
if (signal) signal.removeEventListener("abort", job.cancel)
|
|
1020
1082
|
}
|
|
1021
1083
|
job.resetInactivity = () => {
|
|
1022
1084
|
clearTimeout(job.inactivityTimer)
|
|
@@ -1031,9 +1093,11 @@ class Vault {
|
|
|
1031
1093
|
if (job.inactivityTimer.unref) job.inactivityTimer.unref()
|
|
1032
1094
|
}
|
|
1033
1095
|
this.workerJobs.set(id, job)
|
|
1096
|
+
if (signal) signal.addEventListener("abort", job.cancel, { once: true })
|
|
1034
1097
|
job.resetInactivity()
|
|
1035
1098
|
try {
|
|
1036
1099
|
worker.postMessage({ id, filePath })
|
|
1100
|
+
if (signal && signal.aborted) job.cancel()
|
|
1037
1101
|
} catch (error) {
|
|
1038
1102
|
const failure = new Error(error && error.message
|
|
1039
1103
|
? error.message
|
|
@@ -1696,6 +1760,9 @@ class Vault {
|
|
|
1696
1760
|
this.scanPromise = tracked
|
|
1697
1761
|
const completion = tracked.then(async () => {
|
|
1698
1762
|
try {
|
|
1763
|
+
if (this.registryRestartPromise) {
|
|
1764
|
+
await this.registryRestartPromise
|
|
1765
|
+
}
|
|
1699
1766
|
await this.automaticScans.scanFinished({
|
|
1700
1767
|
scopeId,
|
|
1701
1768
|
result: scanResult,
|
|
@@ -1719,17 +1786,47 @@ class Vault {
|
|
|
1719
1786
|
if (!this.scanPromise || !this.sweeper) {
|
|
1720
1787
|
return { cancel_requested: false }
|
|
1721
1788
|
}
|
|
1789
|
+
// Publication is one atomic commit. Once it starts, let it finish so a
|
|
1790
|
+
// committed result can never be reported as cancelled.
|
|
1791
|
+
if (this.sweeper.state.active &&
|
|
1792
|
+
this.sweeper.state.phase === "publishing") {
|
|
1793
|
+
return { cancel_requested: false }
|
|
1794
|
+
}
|
|
1722
1795
|
this.scanCancelRequested = true
|
|
1796
|
+
const error = new Error("Scan cancelled.")
|
|
1797
|
+
error.code = "EVAULTCANCELLED"
|
|
1798
|
+
const cancelRequested = this.sweeper.state.active
|
|
1799
|
+
? this.sweeper.cancel()
|
|
1800
|
+
: true
|
|
1723
1801
|
if (this.sweeper.state.active && this.sweeper.currentHash &&
|
|
1724
1802
|
this.worker) {
|
|
1725
|
-
const error = new Error("Scan cancelled.")
|
|
1726
|
-
error.code = "EVAULTCANCELLED"
|
|
1727
1803
|
this.failHashWorker(this.worker, error, true)
|
|
1728
1804
|
}
|
|
1805
|
+
if (this.sweeper.state.active && this.registry &&
|
|
1806
|
+
!this.registryRestartPromise) {
|
|
1807
|
+
const registry = this.registry
|
|
1808
|
+
const restart = registry.restart(error).catch((restartError) => {
|
|
1809
|
+
if (this.registry === registry) {
|
|
1810
|
+
this.registry = null
|
|
1811
|
+
this.initialized = false
|
|
1812
|
+
this.sweeper = null
|
|
1813
|
+
}
|
|
1814
|
+
this.scanError = restartError && restartError.message
|
|
1815
|
+
? restartError.message
|
|
1816
|
+
: String(restartError)
|
|
1817
|
+
throw restartError
|
|
1818
|
+
})
|
|
1819
|
+
const tracked = restart.finally(() => {
|
|
1820
|
+
if (this.registryRestartPromise === tracked) {
|
|
1821
|
+
this.registryRestartPromise = null
|
|
1822
|
+
}
|
|
1823
|
+
})
|
|
1824
|
+
this.registryRestartPromise = tracked
|
|
1825
|
+
// The action returns immediately; subsequent work awaits this promise.
|
|
1826
|
+
tracked.catch(() => {})
|
|
1827
|
+
}
|
|
1729
1828
|
return {
|
|
1730
|
-
cancel_requested:
|
|
1731
|
-
? this.sweeper.cancel()
|
|
1732
|
-
: true
|
|
1829
|
+
cancel_requested: cancelRequested
|
|
1733
1830
|
}
|
|
1734
1831
|
}
|
|
1735
1832
|
|