@adhisang/minecraft-modding-mcp 7.0.0-rc.2 → 7.0.0-rc.3
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/CHANGELOG.md +37 -3
- package/README.md +1 -1
- package/dist/artifact-identity.d.ts +130 -0
- package/dist/artifact-identity.js +142 -0
- package/dist/config.js +2 -2
- package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
- package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
- package/dist/json-rpc-framing.js +77 -18
- package/dist/minecraft-explorer-service.js +2 -2
- package/dist/path-resolver.d.ts +13 -2
- package/dist/path-resolver.js +12 -1
- package/dist/repo-downloader.d.ts +19 -7
- package/dist/repo-downloader.js +110 -1
- package/dist/source/artifact-resolver.js +14 -6
- package/dist/source-resolver.d.ts +0 -1
- package/dist/source-resolver.js +25 -94
- package/dist/stdio-supervisor.d.ts +150 -4
- package/dist/stdio-supervisor.js +528 -45
- package/dist/tool-schemas.d.ts +55 -55
- package/dist/types.d.ts +18 -1
- package/docs/tool-reference.md +24 -12
- package/package.json +1 -1
package/dist/path-resolver.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { JarStatStamp } from "./types.js";
|
|
2
2
|
export interface ResolvedJarInfo {
|
|
3
3
|
originalPath: string;
|
|
4
4
|
resolvedPath: string;
|
|
@@ -9,7 +9,18 @@ export declare function buildJarSignature(stats: {
|
|
|
9
9
|
mtimeMs: number;
|
|
10
10
|
size: number;
|
|
11
11
|
}): string;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Read a jar's stat stamp: it stats the file, it does not read a byte of it.
|
|
14
|
+
*
|
|
15
|
+
* The name says stat because a stat is all this is. Artifact identity is
|
|
16
|
+
* derived and composed in `src/artifact-identity.ts`; nothing this returns is
|
|
17
|
+
* an artifactId, and nothing keys an artifact by it.
|
|
18
|
+
*
|
|
19
|
+
* Its one production caller is the `jarSignature` field in
|
|
20
|
+
* `src/minecraft-explorer-service.ts`, which publishes this stat-based value to
|
|
21
|
+
* callers of `get-project-context`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function readJarStatStamp(jarPath: string): JarStatStamp;
|
|
13
24
|
export declare function isSecureJarEntryPath(entryPath: string): boolean;
|
|
14
25
|
/**
|
|
15
26
|
* Validate and normalize a user-supplied jar path input.
|
package/dist/path-resolver.js
CHANGED
|
@@ -72,7 +72,18 @@ export function resolveJarPathWithSymlinkCheck(jarPath) {
|
|
|
72
72
|
export function buildJarSignature(stats) {
|
|
73
73
|
return `${Math.trunc(stats.mtimeMs)}:${stats.size}`;
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Read a jar's stat stamp: it stats the file, it does not read a byte of it.
|
|
77
|
+
*
|
|
78
|
+
* The name says stat because a stat is all this is. Artifact identity is
|
|
79
|
+
* derived and composed in `src/artifact-identity.ts`; nothing this returns is
|
|
80
|
+
* an artifactId, and nothing keys an artifact by it.
|
|
81
|
+
*
|
|
82
|
+
* Its one production caller is the `jarSignature` field in
|
|
83
|
+
* `src/minecraft-explorer-service.ts`, which publishes this stat-based value to
|
|
84
|
+
* callers of `get-project-context`.
|
|
85
|
+
*/
|
|
86
|
+
export function readJarStatStamp(jarPath) {
|
|
76
87
|
const resolvedPath = resolveJarPathWithSymlinkCheck(jarPath).resolvedPath;
|
|
77
88
|
const stats = statSync(resolvedPath);
|
|
78
89
|
return {
|
|
@@ -11,13 +11,16 @@
|
|
|
11
11
|
* That claim is scoped to this entry point, not to the `downloads/` directory.
|
|
12
12
|
* Callers that reach for raw {@link downloadToCache} - the version service's
|
|
13
13
|
* Mojang jars, the mapping service's mapping archives - park files under the
|
|
14
|
-
* same root
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* bytes
|
|
20
|
-
* {@link digestFile} to get
|
|
14
|
+
* same root and write no sidecar: a Mojang artifact already carries an upstream
|
|
15
|
+
* SHA-1 contract. They are a different cache layer, not a migration this module
|
|
16
|
+
* is waiting on.
|
|
17
|
+
*
|
|
18
|
+
* Every artifactId `src/artifact-identity.ts` composes, in both of its id
|
|
19
|
+
* spaces, is built on a sha256 of the bytes of the file it names - see
|
|
20
|
+
* `contentSignature` there, which reuses {@link digestFile} to get it. That
|
|
21
|
+
* covers artifacts on local disk outside the cache entirely (`~/.m2`, the
|
|
22
|
+
* Gradle module cache) and a jar or a Minecraft version the caller names
|
|
23
|
+
* directly.
|
|
21
24
|
*/
|
|
22
25
|
export interface DownloadResult {
|
|
23
26
|
ok: boolean;
|
|
@@ -109,6 +112,15 @@ export interface CachedDownloadFailure {
|
|
|
109
112
|
* for a fresh download.
|
|
110
113
|
*/
|
|
111
114
|
export type CachedDownloadResult = CachedDownloadSuccess | CachedDownloadFailure;
|
|
115
|
+
/**
|
|
116
|
+
* Drop every remembered rejection.
|
|
117
|
+
*
|
|
118
|
+
* The records are process-local and expire on their own, so this exists for a
|
|
119
|
+
* caller that wants the next resolve to ask the repositories again without
|
|
120
|
+
* waiting out {@link REMEMBERED_REJECTION_TTL_MS} - a test isolating itself from
|
|
121
|
+
* the record another test left behind, above all.
|
|
122
|
+
*/
|
|
123
|
+
export declare function clearRememberedRejections(): void;
|
|
112
124
|
/** The persisted identity and freshness record for one cached download. */
|
|
113
125
|
export interface DownloadSidecar {
|
|
114
126
|
version: number;
|
package/dist/repo-downloader.js
CHANGED
|
@@ -40,6 +40,96 @@ const DEFINITIVE_REJECTION_STATUS_CODES = new Set([403, 404, 410]);
|
|
|
40
40
|
function isDefinitiveRejection(statusCode) {
|
|
41
41
|
return statusCode !== undefined && DEFINITIVE_REJECTION_STATUS_CODES.has(statusCode);
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* How long a definitive rejection is remembered, and how many such records are
|
|
45
|
+
* kept at once.
|
|
46
|
+
*
|
|
47
|
+
* Nothing about a FAILED download is recorded anywhere else: only bytes land in
|
|
48
|
+
* the download cache, so a coordinate whose sources jar does not exist re-issues
|
|
49
|
+
* the same request to every configured repository on every resolve. That is what
|
|
50
|
+
* makes a repeated `target`-addressed resolve expensive - the artifact index it
|
|
51
|
+
* built is reused, but the repository sweep that preceded it is paid again - and
|
|
52
|
+
* a caller whose first call timed out pays the identical sweep on its retry.
|
|
53
|
+
*
|
|
54
|
+
* Only {@link DEFINITIVE_REJECTION_STATUS_CODES} are eligible, because those are
|
|
55
|
+
* the statuses this module already treats as an answer about the artifact rather
|
|
56
|
+
* than about the repository's health; the doc comment there is the reasoning.
|
|
57
|
+
* A 5xx, a 429, a 401, a timeout and a thrown network error are all ineligible,
|
|
58
|
+
* so a passing fault costs one extra request on the next call rather than
|
|
59
|
+
* locking the artifact out of reach.
|
|
60
|
+
*
|
|
61
|
+
* The window bounds even the eligible ones. Records live in memory only, so the
|
|
62
|
+
* whole set dies with the process, and an artifact published after its rejection
|
|
63
|
+
* was recorded is reachable again once the record expires.
|
|
64
|
+
*/
|
|
65
|
+
const REMEMBERED_REJECTION_TTL_MS = 5 * 60_000;
|
|
66
|
+
const REMEMBERED_REJECTION_MAX_ENTRIES = 1024;
|
|
67
|
+
/**
|
|
68
|
+
* Definitive rejections, keyed by the cache slot AND the url that was refused.
|
|
69
|
+
*
|
|
70
|
+
* The slot is in the key because it names the cache directory: two configs
|
|
71
|
+
* pointed at different cache directories are different caches and must not
|
|
72
|
+
* inherit each other's verdicts. The url is in the key as well because
|
|
73
|
+
* `destinationPath` is the caller's to choose, so nothing here may assume the
|
|
74
|
+
* slot was derived from the url it is being asked about.
|
|
75
|
+
*/
|
|
76
|
+
const rememberedRejections = new Map();
|
|
77
|
+
function rememberedRejectionKey(url, destinationPath) {
|
|
78
|
+
return `${destinationPath}\u0000${url}`;
|
|
79
|
+
}
|
|
80
|
+
/** The unexpired rejection recorded for this url in this slot, if there is one. */
|
|
81
|
+
function readRememberedRejection(url, destinationPath, now) {
|
|
82
|
+
const key = rememberedRejectionKey(url, destinationPath);
|
|
83
|
+
const remembered = rememberedRejections.get(key);
|
|
84
|
+
if (remembered === undefined) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (remembered.expiresAt <= now) {
|
|
88
|
+
rememberedRejections.delete(key);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
// A copy, so a caller that mutates what it was handed cannot edit the record
|
|
92
|
+
// every later call is answered from.
|
|
93
|
+
return { ...remembered.result };
|
|
94
|
+
}
|
|
95
|
+
function rememberRejection(url, destinationPath, result, now) {
|
|
96
|
+
const key = rememberedRejectionKey(url, destinationPath);
|
|
97
|
+
// Delete before set, so re-recording a key moves it to the back of the Map's
|
|
98
|
+
// insertion order - which is the order the eviction below reads as recency.
|
|
99
|
+
rememberedRejections.delete(key);
|
|
100
|
+
rememberedRejections.set(key, {
|
|
101
|
+
expiresAt: now + REMEMBERED_REJECTION_TTL_MS,
|
|
102
|
+
result: { ...result }
|
|
103
|
+
});
|
|
104
|
+
if (rememberedRejections.size <= REMEMBERED_REJECTION_MAX_ENTRIES) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Over the cap. Expired records first - they answer nothing anyway - and then
|
|
108
|
+
// oldest-recorded first until the set fits. Deleting during iteration is
|
|
109
|
+
// defined for a Map: the iterator skips what has already been removed.
|
|
110
|
+
for (const [candidate, entry] of rememberedRejections) {
|
|
111
|
+
if (entry.expiresAt <= now) {
|
|
112
|
+
rememberedRejections.delete(candidate);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const candidate of rememberedRejections.keys()) {
|
|
116
|
+
if (rememberedRejections.size <= REMEMBERED_REJECTION_MAX_ENTRIES) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
rememberedRejections.delete(candidate);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Drop every remembered rejection.
|
|
124
|
+
*
|
|
125
|
+
* The records are process-local and expire on their own, so this exists for a
|
|
126
|
+
* caller that wants the next resolve to ask the repositories again without
|
|
127
|
+
* waiting out {@link REMEMBERED_REJECTION_TTL_MS} - a test isolating itself from
|
|
128
|
+
* the record another test left behind, above all.
|
|
129
|
+
*/
|
|
130
|
+
export function clearRememberedRejections() {
|
|
131
|
+
rememberedRejections.clear();
|
|
132
|
+
}
|
|
43
133
|
/** Location of the sidecar describing `destinationPath`. Destination-adjacent. */
|
|
44
134
|
export function downloadSidecarPath(destinationPath) {
|
|
45
135
|
return `${destinationPath}${DOWNLOAD_SIDECAR_SUFFIX}`;
|
|
@@ -737,6 +827,17 @@ export async function resolveCachedDownload(url, destinationPath, options) {
|
|
|
737
827
|
// cache miss, so fall through to the transfer below - failing here would
|
|
738
828
|
// hand the caller's repository loop an ENOENT as its reason to fail over.
|
|
739
829
|
}
|
|
830
|
+
if (freshness === "immutable") {
|
|
831
|
+
// A repository's definitive answer about this url, still inside its window.
|
|
832
|
+
// Below the cached-bytes hit above on purpose: bytes we hold outrank a
|
|
833
|
+
// remembered refusal, and the 404 leg further down deliberately keeps the
|
|
834
|
+
// bytes it found. Only immutable urls are ever recorded, so this can never
|
|
835
|
+
// answer for a -SNAPSHOT.
|
|
836
|
+
const remembered = readRememberedRejection(url, destinationPath, Date.now());
|
|
837
|
+
if (remembered !== undefined) {
|
|
838
|
+
return remembered;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
740
841
|
const conditionalHeaders = freshness === "revalidate" ? conditionalHeadersFor(sidecar) : undefined;
|
|
741
842
|
/**
|
|
742
843
|
* Run the transfer, carrying the cached copy's validators only when asked.
|
|
@@ -920,13 +1021,21 @@ export async function resolveCachedDownload(url, destinationPath, options) {
|
|
|
920
1021
|
// this module's call - and the record goes back with them, so a later
|
|
921
1022
|
// revalidation that finds the artifact restored still has its validators.
|
|
922
1023
|
restoreRetiredSidecar();
|
|
923
|
-
|
|
1024
|
+
const rejection = {
|
|
924
1025
|
ok: false,
|
|
925
1026
|
statusCode: downloaded.statusCode,
|
|
926
1027
|
etag: downloaded.etag,
|
|
927
1028
|
lastModified: downloaded.lastModified,
|
|
928
1029
|
contentLength: downloaded.contentLength
|
|
929
1030
|
};
|
|
1031
|
+
// Remember it, so the next resolve of this url inside the window answers
|
|
1032
|
+
// from here instead of re-issuing the request. Immutable urls only: a
|
|
1033
|
+
// -SNAPSHOT is republished under the same name, so its 404 can be a
|
|
1034
|
+
// publish in progress rather than an answer that will hold.
|
|
1035
|
+
if (freshness === "immutable") {
|
|
1036
|
+
rememberRejection(url, destinationPath, rejection, Date.now());
|
|
1037
|
+
}
|
|
1038
|
+
return rejection;
|
|
930
1039
|
}
|
|
931
1040
|
// The same stale-if-error reuse, for a repository that answered rather than
|
|
932
1041
|
// failed to: a 5xx, a 429, an authentication hiccup. Nothing here says the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fastGlob from "fast-glob";
|
|
2
|
+
import { DECOMPILE_SIGNATURE_QUALIFIER, jarArtifactIdentity } from "../artifact-identity.js";
|
|
2
3
|
import { buildArtifactAlias } from "../config.js";
|
|
3
4
|
import { buildSuggestedCall } from "../build-suggested-call.js";
|
|
4
5
|
import { ERROR_CODES, createError, isAppError } from "../errors.js";
|
|
@@ -7,9 +8,8 @@ import { log } from "../logger.js";
|
|
|
7
8
|
import { applyMappingPipeline } from "../mapping-pipeline-service.js";
|
|
8
9
|
import { parseCoordinate } from "../maven-resolver.js";
|
|
9
10
|
import { resolveMojangTinyFile } from "../mojang-tiny-mapping-service.js";
|
|
10
|
-
import { artifactSignatureFromFile } from "../path-resolver.js";
|
|
11
11
|
import { detectFabricLikeInputNamespace, listJavaEntries } from "../source-jar-reader.js";
|
|
12
|
-
import {
|
|
12
|
+
import { resolveSourceTarget as resolveSourceTargetInternal } from "../source-resolver.js";
|
|
13
13
|
import { resolveTinyRemapperJar } from "../tiny-remapper-resolver.js";
|
|
14
14
|
import { isUnobfuscatedVersion } from "../version-service.js";
|
|
15
15
|
import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
|
|
@@ -562,8 +562,10 @@ export async function probeMinecraftArtifact(svc, input) {
|
|
|
562
562
|
});
|
|
563
563
|
}
|
|
564
564
|
const selectedSourceJarPath = versionSourceDiscovery.selectedSourceJarPath;
|
|
565
|
-
|
|
566
|
-
|
|
565
|
+
// The same helper `resolveSourceTarget`'s jar branch uses, so the id this
|
|
566
|
+
// probe publishes and the id a full resolve mints for the same jar are
|
|
567
|
+
// derived and composed by one piece of code.
|
|
568
|
+
const { artifactId } = await jarArtifactIdentity(selectedSourceJarPath);
|
|
567
569
|
warnings.push(`Resolved source-backed artifact from Loom cache candidate: ${selectedSourceJarPath}.`);
|
|
568
570
|
if (versionSourceDiscovery.selectedHasMinecraftNamespace === false) {
|
|
569
571
|
warnings.push(`Source coverage does not include net.minecraft for ${selectedSourceJarPath}; class lookups may fall back to the binary artifact.`);
|
|
@@ -577,8 +579,14 @@ export async function probeMinecraftArtifact(svc, input) {
|
|
|
577
579
|
...(warnings.length > 0 ? { warnings } : {})
|
|
578
580
|
};
|
|
579
581
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
+
// "Lightweight" describes what this probe SKIPS: it neither decompiles nor
|
|
583
|
+
// rebuilds the index. It does read the version jar's bytes, because that is
|
|
584
|
+
// what the jar route derives an id from, so the first probe of a given jar in
|
|
585
|
+
// a process pays for one hash of it; the digest memo in
|
|
586
|
+
// `src/artifact-identity.ts` covers the ones after that.
|
|
587
|
+
const { artifactId } = await jarArtifactIdentity(versionJar.jarPath, {
|
|
588
|
+
signatureQualifier: DECOMPILE_SIGNATURE_QUALIFIER
|
|
589
|
+
});
|
|
582
590
|
return {
|
|
583
591
|
artifactId,
|
|
584
592
|
mappingApplied: effectiveMapping,
|
|
@@ -48,7 +48,6 @@ export declare function localM2CoordinateCandidatePaths(localM2Path: string, par
|
|
|
48
48
|
*/
|
|
49
49
|
export declare const BINARY_JAR_NO_CLASSES_FLAG = "binary-jar-no-classes";
|
|
50
50
|
export type { MappingVariant } from "./types.js";
|
|
51
|
-
export declare function artifactIdForJar(inputKind: string, artifactPath: string, signature: string, suffix?: string, mappingVariant?: MappingVariant): string;
|
|
52
51
|
export interface ResolveSourceTargetOptions {
|
|
53
52
|
allowDecompile: boolean;
|
|
54
53
|
preferBinaryOnly?: boolean;
|
package/dist/source-resolver.js
CHANGED
|
@@ -1,78 +1,13 @@
|
|
|
1
|
-
import { statSync } from "node:fs";
|
|
2
1
|
import { readdir } from "node:fs/promises";
|
|
3
2
|
import { basename, dirname, join, resolve as resolvePath, sep } from "node:path";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
4
|
import fastGlob from "fast-glob";
|
|
6
5
|
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
7
6
|
import { buildRemoteBinaryUrls, buildRemoteSourceUrls, groupToPath, hasExistingJar, isMutableMavenCoordinate, parseCoordinate, normalizedCoordinateValue } from "./maven-resolver.js";
|
|
8
|
-
import { defaultDownloadPath,
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
7
|
+
import { defaultDownloadPath, discardCachedDownload, resolveCachedDownload } from "./repo-downloader.js";
|
|
8
|
+
import { normalizeJarPath } from "./path-resolver.js";
|
|
9
|
+
import { composeArtifactId, contentDigestSignature, contentSignature, jarArtifactIdentity, DECOMPILE_SIGNATURE_QUALIFIER } from "./artifact-identity.js";
|
|
11
10
|
import { hasAnyJarEntry, hasJavaSourceExtension } from "./source-jar-reader.js";
|
|
12
|
-
function readStatsSignature(filePath) {
|
|
13
|
-
const stats = artifactSignatureFromFile(filePath);
|
|
14
|
-
return stats.signature;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Digests already derived from a local jar, each pinned to the stat that
|
|
18
|
-
* produced it.
|
|
19
|
-
*
|
|
20
|
-
* Keyed by the symlink-resolved path, so a jar reached through two names is
|
|
21
|
-
* hashed once. The entry is only ever *reused*, never trusted on its own: a
|
|
22
|
-
* mismatched mtime or size discards it, so the map cannot serve a digest for
|
|
23
|
-
* bytes that have since been replaced.
|
|
24
|
-
*
|
|
25
|
-
* Bounded, like the helper caches in `src/source/artifact-resolver.ts`: this map
|
|
26
|
-
* is module-level and lives as long as the process, and a long-running server
|
|
27
|
-
* walks this cascade once per target-driven tool call, so an unbounded map grows
|
|
28
|
-
* with every distinct jar path the server has ever seen. Eviction costs at most
|
|
29
|
-
* one re-hash, which is exactly what a cache miss already costs.
|
|
30
|
-
*/
|
|
31
|
-
const contentSignatureCache = new Map();
|
|
32
|
-
const MAX_CONTENT_SIGNATURE_CACHE = 512;
|
|
33
|
-
/** Insert, dropping the oldest key first when the bound is reached. */
|
|
34
|
-
function rememberContentSignature(resolvedPath, entry) {
|
|
35
|
-
if (!contentSignatureCache.has(resolvedPath) && contentSignatureCache.size >= MAX_CONTENT_SIGNATURE_CACHE) {
|
|
36
|
-
const oldestKey = contentSignatureCache.keys().next().value;
|
|
37
|
-
if (oldestKey) {
|
|
38
|
-
contentSignatureCache.delete(oldestKey);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
contentSignatureCache.set(resolvedPath, entry);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* The identity of a jar sitting on local disk: a sha256 of its bytes.
|
|
45
|
-
*
|
|
46
|
-
* `~/.m2` and the Gradle module cache move a file's mtime for reasons that have
|
|
47
|
-
* nothing to do with its contents - an eviction followed by a re-fetch of
|
|
48
|
-
* byte-identical bytes, a filesystem restore, a plain `touch`. An `mtimeMs:size`
|
|
49
|
-
* signature turns every one of those into a fresh artifactId and a fresh
|
|
50
|
-
* decompile, which is exactly the instability the download cache's
|
|
51
|
-
* content-addressed identity removed from the remote half of this cascade.
|
|
52
|
-
*
|
|
53
|
-
* Hashing is not free and this cascade is re-walked on every target-driven tool
|
|
54
|
-
* call, so the digest is memoized against the stat that produced it. The stat is
|
|
55
|
-
* taken *before* the digest on purpose: bytes replaced mid-hash are recorded
|
|
56
|
-
* against a stat they no longer have, so the entry is rejected on the next call
|
|
57
|
-
* and re-derived - a wasted hash, never a wrong identity.
|
|
58
|
-
*/
|
|
59
|
-
async function contentSignature(jarPath) {
|
|
60
|
-
// The same normalization `artifactSignatureFromFile` applied, kept so this
|
|
61
|
-
// path still refuses a vanished or non-jar file the way it always has.
|
|
62
|
-
const resolvedPath = normalizeJarPath(jarPath);
|
|
63
|
-
const stats = statSync(resolvedPath);
|
|
64
|
-
const cached = contentSignatureCache.get(resolvedPath);
|
|
65
|
-
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
66
|
-
return cached.sha256;
|
|
67
|
-
}
|
|
68
|
-
const { contentSha256 } = await digestFile(resolvedPath);
|
|
69
|
-
rememberContentSignature(resolvedPath, {
|
|
70
|
-
mtimeMs: stats.mtimeMs,
|
|
71
|
-
size: stats.size,
|
|
72
|
-
sha256: contentSha256
|
|
73
|
-
});
|
|
74
|
-
return contentSha256;
|
|
75
|
-
}
|
|
76
11
|
/**
|
|
77
12
|
* Whether a jar contains java sources, with archive errors deliberately
|
|
78
13
|
* propagating.
|
|
@@ -445,28 +380,20 @@ async function resolveGradleCacheCoordinateCandidate(coordinate) {
|
|
|
445
380
|
function resolveRemoteBinaryCandidate(coordinate, repos) {
|
|
446
381
|
return buildRemoteBinaryUrls(repos, coordinate);
|
|
447
382
|
}
|
|
448
|
-
export function artifactIdForJar(inputKind, artifactPath, signature, suffix, mappingVariant = "pass") {
|
|
449
|
-
const parts = [inputKind, artifactPath, signature, suffix ?? "source"];
|
|
450
|
-
if (mappingVariant === "mojang-remapped") {
|
|
451
|
-
parts.push("mojang-remapped");
|
|
452
|
-
}
|
|
453
|
-
return stableArtifactId(parts);
|
|
454
|
-
}
|
|
455
|
-
function artifactIdForCoordinate(coordinate, source, signature, mappingVariant = "pass") {
|
|
456
|
-
const parts = ["coord", coordinate, source, signature];
|
|
457
|
-
if (mappingVariant === "mojang-remapped") {
|
|
458
|
-
parts.push("mojang-remapped");
|
|
459
|
-
}
|
|
460
|
-
return stableArtifactId(parts);
|
|
461
|
-
}
|
|
462
383
|
function resolvedAtNow() {
|
|
463
384
|
return new Date().toISOString();
|
|
464
385
|
}
|
|
465
386
|
/** Shared shape for every artifact the coordinate cascade can return. */
|
|
466
387
|
function coordinateArtifact(spec) {
|
|
467
388
|
return {
|
|
468
|
-
artifactId:
|
|
469
|
-
|
|
389
|
+
artifactId: composeArtifactId({
|
|
390
|
+
space: "coordinate",
|
|
391
|
+
coordinate: spec.coordinate,
|
|
392
|
+
idSource: spec.idSource,
|
|
393
|
+
signature: spec.signature,
|
|
394
|
+
mappingVariant: spec.mappingVariant
|
|
395
|
+
}),
|
|
396
|
+
artifactSignature: spec.signature.value,
|
|
470
397
|
origin: spec.origin,
|
|
471
398
|
sourceJarPath: spec.sourceJarPath,
|
|
472
399
|
binaryJarPath: spec.binaryJarPath,
|
|
@@ -595,7 +522,6 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
595
522
|
};
|
|
596
523
|
if (input.kind === "jar") {
|
|
597
524
|
const resolvedJarPath = normalizeJarPath(input.value);
|
|
598
|
-
const binarySignature = readStatsSignature(resolvedJarPath);
|
|
599
525
|
const exactSourceJarPath = resolveExactJarSourceCandidate(resolvedJarPath);
|
|
600
526
|
const adjacentSourceCandidates = await listAdjacentJarSourceCandidates(resolvedJarPath);
|
|
601
527
|
const maybeAdjacentSourceCandidates = adjacentSourceCandidates.length > 0 ? adjacentSourceCandidates : undefined;
|
|
@@ -604,9 +530,10 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
604
530
|
const siblingBinaryJarPath = resolveSiblingBinaryJarCandidate(resolvedJarPath);
|
|
605
531
|
const binaryJarPath = siblingBinaryJarPath ??
|
|
606
532
|
(basename(resolvedJarPath).endsWith("-sources.jar") ? undefined : resolvedJarPath);
|
|
533
|
+
const identity = await jarArtifactIdentity(resolvedJarPath);
|
|
607
534
|
return {
|
|
608
|
-
artifactId:
|
|
609
|
-
artifactSignature:
|
|
535
|
+
artifactId: identity.artifactId,
|
|
536
|
+
artifactSignature: identity.signature,
|
|
610
537
|
origin: "local-jar",
|
|
611
538
|
binaryJarPath,
|
|
612
539
|
sourceJarPath: resolvedJarPath,
|
|
@@ -616,10 +543,10 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
616
543
|
};
|
|
617
544
|
}
|
|
618
545
|
if (!preferBinaryOnly && await candidateHasJavaSources(exactSourceJarPath)) {
|
|
619
|
-
const
|
|
546
|
+
const identity = await jarArtifactIdentity(exactSourceJarPath);
|
|
620
547
|
return {
|
|
621
|
-
artifactId:
|
|
622
|
-
artifactSignature:
|
|
548
|
+
artifactId: identity.artifactId,
|
|
549
|
+
artifactSignature: identity.signature,
|
|
623
550
|
origin: "local-jar",
|
|
624
551
|
binaryJarPath: resolvedJarPath,
|
|
625
552
|
sourceJarPath: exactSourceJarPath,
|
|
@@ -648,9 +575,13 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
648
575
|
// no `.java` entries.
|
|
649
576
|
const subjectArchive = await inspectSubjectJarArchive(resolvedJarPath);
|
|
650
577
|
const subjectQualityFlags = subjectArchive ? binaryJarQualityFlags(subjectArchive) : [];
|
|
578
|
+
const identity = await jarArtifactIdentity(resolvedJarPath, {
|
|
579
|
+
signatureQualifier: DECOMPILE_SIGNATURE_QUALIFIER,
|
|
580
|
+
mappingVariant: options.mappingVariant
|
|
581
|
+
});
|
|
651
582
|
return {
|
|
652
|
-
artifactId:
|
|
653
|
-
artifactSignature:
|
|
583
|
+
artifactId: identity.artifactId,
|
|
584
|
+
artifactSignature: identity.signature,
|
|
654
585
|
origin: "decompiled",
|
|
655
586
|
binaryJarPath: resolvedJarPath,
|
|
656
587
|
adjacentSourceCandidates: maybeAdjacentSourceCandidates,
|
|
@@ -843,7 +774,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
843
774
|
// Identity follows the bytes. The HTTP validators that used to form this
|
|
844
775
|
// signature rotate whenever a CDN or repository migration happens, even
|
|
845
776
|
// when the jar is byte-identical.
|
|
846
|
-
const signature = download.contentSha256;
|
|
777
|
+
const signature = contentDigestSignature(download.contentSha256);
|
|
847
778
|
return coordinateArtifact({
|
|
848
779
|
coordinate,
|
|
849
780
|
idSource: "remote-repo",
|
|
@@ -983,7 +914,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
983
914
|
}
|
|
984
915
|
// A file in the URL-keyed download cache is identified by its bytes, not
|
|
985
916
|
// by a stat signature that a re-download would change for free.
|
|
986
|
-
const signature = downloaded.contentSha256;
|
|
917
|
+
const signature = contentDigestSignature(downloaded.contentSha256);
|
|
987
918
|
return coordinateArtifact({
|
|
988
919
|
coordinate,
|
|
989
920
|
idSource: "decompiled",
|
|
@@ -238,6 +238,22 @@ export declare class StdioSupervisor {
|
|
|
238
238
|
*/
|
|
239
239
|
private readonly syntheticTombstones;
|
|
240
240
|
private child;
|
|
241
|
+
/**
|
|
242
|
+
* The one response `handleWorkerMessage` is part-way through settling.
|
|
243
|
+
*
|
|
244
|
+
* It removes a response's pending entry BEFORE writing the reply, so between
|
|
245
|
+
* those two statements the id is owed a response that NOTHING records: the
|
|
246
|
+
* entry is gone, no deadline is armed for anything but validate-project, and
|
|
247
|
+
* the worker considers the request answered. A fault in that window used to
|
|
248
|
+
* be indistinguishable from an id already settled, and the request stayed
|
|
249
|
+
* unanswered for the life of an otherwise healthy session.
|
|
250
|
+
*
|
|
251
|
+
* Set the moment the entry is deleted and cleared the moment the write
|
|
252
|
+
* returns, so it is defined only inside that window and only ever names one
|
|
253
|
+
* request. `answerFaultedWorkerResponse` is the sole reader, and it consumes
|
|
254
|
+
* the marker rather than merely reading it.
|
|
255
|
+
*/
|
|
256
|
+
private settlingWorkerResponse;
|
|
241
257
|
private childReady;
|
|
242
258
|
/**
|
|
243
259
|
* Monotonic timestamp of the current generation's adoption, or undefined
|
|
@@ -338,17 +354,47 @@ export declare class StdioSupervisor {
|
|
|
338
354
|
*/
|
|
339
355
|
private readonly handleFatalError;
|
|
340
356
|
/**
|
|
341
|
-
* Admission entry point, wrapped so a fault
|
|
357
|
+
* Admission entry point, wrapped so a fault cannot silently drop a request.
|
|
342
358
|
*
|
|
343
359
|
* Anything thrown while classifying, queueing or forwarding a client frame
|
|
344
360
|
* propagates out of the frame reader's `onFrame`, where the reader swallows
|
|
345
361
|
* it as a parse error — the request then gets NO reply and the client waits
|
|
346
362
|
* on that id forever (and on a modern-era request the one-way era lock has
|
|
347
|
-
* already happened).
|
|
348
|
-
*
|
|
349
|
-
*
|
|
363
|
+
* already happened). The catch below answers the id with -32603 instead.
|
|
364
|
+
*
|
|
365
|
+
* The reply is conditional, and on exactly one thing: that nothing this
|
|
366
|
+
* admission installed is still live at the id. A surviving instance is
|
|
367
|
+
* already tracked and will be settled by an ordinary path, so answering
|
|
368
|
+
* alongside it would make two terminal replies for one request; the catch
|
|
369
|
+
* reports and stands down in that case rather than answering. Id-less frames
|
|
370
|
+
* (notifications, malformed ids) have nothing to answer and are only logged.
|
|
350
371
|
*/
|
|
351
372
|
private handleClientMessage;
|
|
373
|
+
/**
|
|
374
|
+
* Runs one step of a fault-recovery path so that it cannot throw.
|
|
375
|
+
*
|
|
376
|
+
* The two frame-reader callers — the admission catch in handleClientMessage
|
|
377
|
+
* and the worker-frame catch in handleWorkerData — execute inside a
|
|
378
|
+
* {@link JsonRpcFrameReader}'s `onFrame`, where the reader deliberately
|
|
379
|
+
* converts an escaping throw into a plain parse error: the frame is then
|
|
380
|
+
* silently dropped, which for a recovery path means the request it was
|
|
381
|
+
* rescuing is lost after all. `dispatchQueuedRequest` is the third caller,
|
|
382
|
+
* and its exposure is wider rather than narrower: a drain is reached from
|
|
383
|
+
* those two `onFrame` paths, but also from timer callbacks
|
|
384
|
+
* (handleValidateProjectDeadline) and from process-event handlers
|
|
385
|
+
* (handleWorkerExit), where nothing above it is prepared to contain a throw
|
|
386
|
+
* at all.
|
|
387
|
+
*
|
|
388
|
+
* Reporting goes through `log` rather than the injected `eventWriter` so
|
|
389
|
+
* that an INJECTED writer — the collaborator most likely to have thrown — is
|
|
390
|
+
* not also the reporter. That NARROWS the failure; it does not remove it.
|
|
391
|
+
* `eventWriter` defaults to `log` (see the constructor), so in the default
|
|
392
|
+
* configuration, where nothing is injected, the reporter IS the writer that
|
|
393
|
+
* just faulted. The report therefore carries its own try/catch whose handler
|
|
394
|
+
* does nothing: past this point there is no reporting channel left, and a
|
|
395
|
+
* throw here would drop the frame this method exists to save.
|
|
396
|
+
*/
|
|
397
|
+
private runRecoveryStep;
|
|
352
398
|
/**
|
|
353
399
|
* Every request INSTANCE live at `key` right now: the forwarded entry plus any
|
|
354
400
|
* queued ones. Captured before admission runs so a rollback can tell the state
|
|
@@ -374,6 +420,20 @@ export declare class StdioSupervisor {
|
|
|
374
420
|
* Returns whether anything was rolled back.
|
|
375
421
|
*/
|
|
376
422
|
private rollbackFailedAdmission;
|
|
423
|
+
/**
|
|
424
|
+
* Removes every QUEUED instance at `key` that is absent from `preexisting`.
|
|
425
|
+
*
|
|
426
|
+
* Split out of rollbackFailedAdmission so the admission catch's finality
|
|
427
|
+
* check can repeat the removal without re-entering the method that may just
|
|
428
|
+
* have thrown. Returns whether anything was removed; calling it twice is
|
|
429
|
+
* harmless, because the second call finds nothing.
|
|
430
|
+
*/
|
|
431
|
+
private dropQueuedInstances;
|
|
432
|
+
/**
|
|
433
|
+
* Whether any request instance live at `key` is absent from `preexisting` —
|
|
434
|
+
* that is, whether the admission being rolled back still owns its id.
|
|
435
|
+
*/
|
|
436
|
+
private hasInstanceOutside;
|
|
377
437
|
private routeClientMessage;
|
|
378
438
|
/**
|
|
379
439
|
* Era-aware notification dispatch. notifications/cancelled stays
|
|
@@ -499,8 +559,94 @@ export declare class StdioSupervisor {
|
|
|
499
559
|
*/
|
|
500
560
|
private handleValidateProjectDeadline;
|
|
501
561
|
private drainQueue;
|
|
562
|
+
/**
|
|
563
|
+
* Forwards one queued entry, containing a fault in the forward itself.
|
|
564
|
+
*
|
|
565
|
+
* `forwardRequest` installs the pending entry and takes the validate
|
|
566
|
+
* slot/barrier BEFORE it writes to the worker's stdin, so a throw from the
|
|
567
|
+
* encode or the write leaves the request holding an id it never reached the
|
|
568
|
+
* worker on. Unguarded, that request is unreachable: it has been shifted out
|
|
569
|
+
* of the queue, so no later drain can dispatch it, and no worker will answer
|
|
570
|
+
* an id it never saw — and only validate-project arms a deadline, so nothing
|
|
571
|
+
* else settles it either. The one containing guard above this
|
|
572
|
+
* (`runRecoveryStep` around the recovery drain) contains the exception
|
|
573
|
+
* without recovering the request, and stops the drain on top of that,
|
|
574
|
+
* leaving everything behind it parked as well.
|
|
575
|
+
*
|
|
576
|
+
* Both are repaired here: the entry is released — which records the finality
|
|
577
|
+
* tombstone, so a worker that did somehow see the bytes cannot answer over
|
|
578
|
+
* the reply below — terminally answered, and the drain carries on to the
|
|
579
|
+
* next entry. Containment is per-entry because the fault this catches is
|
|
580
|
+
* per-message: an encode failure on one payload, or a stdin `write` that
|
|
581
|
+
* throws. A stdin that is merely destroyed never reaches this catch at all —
|
|
582
|
+
* `forwardRequest` tests for that first and takes its own no-child fallback,
|
|
583
|
+
* which does not throw.
|
|
584
|
+
*
|
|
585
|
+
* The `initialize` carve-out in `releaseForwardedRequest` is honoured: if it
|
|
586
|
+
* declines, nothing is answered here and the handshake lifecycle keeps the
|
|
587
|
+
* entry. Admission parks `initialize` in `queuedNotifications` rather than in
|
|
588
|
+
* `queuedRequests`, so the ordinary path never puts one in front of this
|
|
589
|
+
* drain; `forwardRequest`'s own no-child fallback is the one way an
|
|
590
|
+
* initialize can end up queued, and the carve-out is what covers it.
|
|
591
|
+
*
|
|
592
|
+
* Returns whether the entry reached the worker.
|
|
593
|
+
*/
|
|
594
|
+
private dispatchQueuedRequest;
|
|
502
595
|
private spawnWorker;
|
|
503
596
|
private handleWorkerData;
|
|
597
|
+
/**
|
|
598
|
+
* Rescues the request a faulted worker frame was answering.
|
|
599
|
+
*
|
|
600
|
+
* `handleWorkerMessage` runs inside the worker reader's `onFrame`, where a
|
|
601
|
+
* throw becomes a parse error and the frame is simply dropped. For a
|
|
602
|
+
* RESPONSE frame that left the pending entry live with no reply and nothing
|
|
603
|
+
* left to settle it — deadlines are armed for validate-project only, so any
|
|
604
|
+
* other tool had no rescue at all and the client waited on that id for the
|
|
605
|
+
* life of the session. The id is released and answered here instead.
|
|
606
|
+
*
|
|
607
|
+
* The queue drain is the OTHER half, and it is why the answering half lives
|
|
608
|
+
* in its own method: `handleWorkerMessage` deletes a response's pending
|
|
609
|
+
* entry before the `writeToClient` that can throw, and its `drainQueue()` is
|
|
610
|
+
* the last statement of all. So the commonest fault arrives here with the
|
|
611
|
+
* entry already gone AND the queue undrained, and every early return in the
|
|
612
|
+
* answering path is a case where the drain is the only rescue left.
|
|
613
|
+
*
|
|
614
|
+
* What this recovery is worth, stated exactly. It answers the id when this
|
|
615
|
+
* supervisor is the one entitled to (see `answerFaultedWorkerResponse`), and
|
|
616
|
+
* every id it answers is tombstoned, so the worker's own answer for that id
|
|
617
|
+
* cannot become a second reply. It does NOT make one-response-per-id a
|
|
618
|
+
* property of the whole file: an id whose entry could not be released is
|
|
619
|
+
* deliberately left unanswered here rather than answered twice, the
|
|
620
|
+
* `initialize` carve-out is settled by the handshake lifecycle instead, and
|
|
621
|
+
* tombstone retention is capped at {@link MAX_SYNTHETIC_TOMBSTONES} — past
|
|
622
|
+
* 1024 live tombstones in one worker generation the oldest is evicted, and a
|
|
623
|
+
* very old worker's late answer for an evicted id would pass through.
|
|
624
|
+
*
|
|
625
|
+
* What this deliberately does NOT do: tear the session down or touch framing
|
|
626
|
+
* state. One frame the supervisor could not handle is not evidence that the
|
|
627
|
+
* worker's stream desynchronized (that is `supervisor.worker_framing_fatal`,
|
|
628
|
+
* reported by the reader itself), and the reader's own state already
|
|
629
|
+
* describes the stream correctly. The one exception is an in-flight
|
|
630
|
+
* `initialize`, whose generation cannot finish its handshake once its answer
|
|
631
|
+
* has been lost — see `answerFaultedWorkerResponse`.
|
|
632
|
+
*/
|
|
633
|
+
private recoverFaultedWorkerMessage;
|
|
634
|
+
/**
|
|
635
|
+
* Terminally answers the client request a faulted worker RESPONSE frame was
|
|
636
|
+
* carrying the answer for, if this supervisor is still the one entitled to
|
|
637
|
+
* answer it. Returns without replying otherwise; the caller's drain runs
|
|
638
|
+
* either way.
|
|
639
|
+
*/
|
|
640
|
+
private answerFaultedWorkerResponse;
|
|
641
|
+
/**
|
|
642
|
+
* Answers a request whose pending entry `handleWorkerMessage` had already
|
|
643
|
+
* removed when it faulted, and which therefore never reached the client.
|
|
644
|
+
*
|
|
645
|
+
* Consumed once: the marker is cleared before the reply is attempted, so a
|
|
646
|
+
* fault in the reply cannot leave a stale claim on the id for a later,
|
|
647
|
+
* unrelated fault to act on.
|
|
648
|
+
*/
|
|
649
|
+
private answerUndeliveredWorkerResponse;
|
|
504
650
|
private handleWorkerStdinError;
|
|
505
651
|
/**
|
|
506
652
|
* Reassembles the worker's stderr into lines (the ready marker may be split
|