@kzheart_/mc-pilot 0.3.3 → 0.4.0
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/data/variants.json +6 -6
- package/dist/commands/client.js +6 -2
- package/dist/commands/project.js +16 -7
- package/dist/download/VersionMatrix.js +7 -7
- package/dist/download/client/ClientDownloader.js +1 -1
- package/dist/instance/ClientInstanceManager.d.ts +20 -1
- package/dist/instance/ClientInstanceManager.js +72 -8
- package/dist/instance/ServerInstanceManager.d.ts +2 -0
- package/dist/instance/ServerInstanceManager.js +8 -6
- package/package.json +1 -1
- package/scripts/launch-fabric-client.mjs +1 -1
package/data/variants.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"support": "ready",
|
|
11
11
|
"validation": "verified",
|
|
12
12
|
"modVersion": "0.1.1",
|
|
13
|
-
"fabricLoaderVersion": "0.16.
|
|
13
|
+
"fabricLoaderVersion": "0.16.14",
|
|
14
14
|
"yarnMappings": "1.18.2+build.4",
|
|
15
15
|
"javaVersion": 17,
|
|
16
16
|
"gradleModule": "version-1.18.2"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"support": "ready",
|
|
23
23
|
"validation": "verified",
|
|
24
24
|
"modVersion": "0.1.1",
|
|
25
|
-
"fabricLoaderVersion": "0.16.
|
|
25
|
+
"fabricLoaderVersion": "0.16.14",
|
|
26
26
|
"yarnMappings": "1.20.1+build.10",
|
|
27
27
|
"javaVersion": 17,
|
|
28
28
|
"gradleModule": "version-1.20.1"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"support": "ready",
|
|
45
45
|
"validation": "verified",
|
|
46
46
|
"modVersion": "0.1.1",
|
|
47
|
-
"fabricLoaderVersion": "0.16.
|
|
47
|
+
"fabricLoaderVersion": "0.16.14",
|
|
48
48
|
"yarnMappings": "1.20.2+build.4",
|
|
49
49
|
"javaVersion": 17,
|
|
50
50
|
"gradleModule": "version-1.20.2"
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"support": "ready",
|
|
67
67
|
"validation": "verified",
|
|
68
68
|
"modVersion": "0.1.1",
|
|
69
|
-
"fabricLoaderVersion": "0.16.
|
|
69
|
+
"fabricLoaderVersion": "0.16.14",
|
|
70
70
|
"yarnMappings": "1.20.4+build.3",
|
|
71
71
|
"javaVersion": 17,
|
|
72
72
|
"gradleModule": "version-1.20.4"
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"support": "ready",
|
|
99
99
|
"validation": "verified",
|
|
100
100
|
"modVersion": "0.1.1",
|
|
101
|
-
"fabricLoaderVersion": "0.16.
|
|
101
|
+
"fabricLoaderVersion": "0.16.14",
|
|
102
102
|
"yarnMappings": "1.21.1+build.3",
|
|
103
103
|
"javaVersion": 21,
|
|
104
104
|
"gradleModule": "version-1.21.1"
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"support": "ready",
|
|
121
121
|
"validation": "verified",
|
|
122
122
|
"modVersion": "0.1.1",
|
|
123
|
-
"fabricLoaderVersion": "0.16.
|
|
123
|
+
"fabricLoaderVersion": "0.16.14",
|
|
124
124
|
"yarnMappings": "1.21.4+build.1",
|
|
125
125
|
"javaVersion": 21,
|
|
126
126
|
"gradleModule": "version-1.21.4"
|
package/dist/commands/client.js
CHANGED
|
@@ -129,6 +129,7 @@ export function createClientCommand() {
|
|
|
129
129
|
.option("--account <account>", "Offline username or account identifier")
|
|
130
130
|
.option("--ws-port <port>", "WebSocket port override", Number)
|
|
131
131
|
.option("--headless", "Launch in headless mode")
|
|
132
|
+
.option("--force", "Kill any existing client with the same name before launching")
|
|
132
133
|
.action(wrapCommand(async (context, { args, options }) => {
|
|
133
134
|
const clientName = args[0] ?? context.activeProfile?.clients[0];
|
|
134
135
|
if (!clientName) {
|
|
@@ -154,16 +155,19 @@ export function createClientCommand() {
|
|
|
154
155
|
}));
|
|
155
156
|
command
|
|
156
157
|
.command("wait-ready")
|
|
157
|
-
.description("Wait until client WebSocket is connected")
|
|
158
|
+
.description("Wait until client WebSocket is connected AND the player has joined a world")
|
|
158
159
|
.argument("[name]", "Client instance name (default: from active profile)")
|
|
159
160
|
.option("--timeout <seconds>", "Timeout in seconds", Number)
|
|
161
|
+
.option("--no-world-check", "Only wait for WebSocket connection, skip the in-world check")
|
|
160
162
|
.action(wrapCommand(async (context, { args, options }) => {
|
|
161
163
|
const clientName = args[0] ?? context.activeProfile?.clients[0];
|
|
162
164
|
if (!clientName) {
|
|
163
165
|
throw new MctError({ code: "INVALID_PARAMS", message: "Client name is required" }, 4);
|
|
164
166
|
}
|
|
165
167
|
const manager = new ClientInstanceManager(context.globalState);
|
|
166
|
-
return manager.waitReady(clientName, options.timeout ?? context.timeout("clientReady")
|
|
168
|
+
return manager.waitReady(clientName, options.timeout ?? context.timeout("clientReady"), {
|
|
169
|
+
requireWorld: options.worldCheck !== false
|
|
170
|
+
});
|
|
167
171
|
}));
|
|
168
172
|
command
|
|
169
173
|
.command("reconnect")
|
package/dist/commands/project.js
CHANGED
|
@@ -82,16 +82,21 @@ export function createUpCommand() {
|
|
|
82
82
|
// 3. Wait for server
|
|
83
83
|
const serverMeta = await serverManager.loadMeta(profile.server);
|
|
84
84
|
await serverManager.waitReady(profile.server, context.timeout("serverReady"));
|
|
85
|
-
// 4. Launch clients
|
|
85
|
+
// 4. Launch clients (reuse running clients via reconnect)
|
|
86
|
+
const serverAddress = `localhost:${serverMeta.port}`;
|
|
86
87
|
const clientResults = [];
|
|
87
88
|
for (const clientName of profile.clients) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
if (await clientManager.isAlreadyRunning(clientName)) {
|
|
90
|
+
const reconnected = await clientManager.reconnect(clientName, serverAddress);
|
|
91
|
+
clientResults.push(reconnected);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const result = await clientManager.launch(clientName, { server: serverAddress });
|
|
95
|
+
clientResults.push(result);
|
|
96
|
+
}
|
|
92
97
|
}
|
|
93
98
|
results.clients = clientResults;
|
|
94
|
-
// 5. Wait for clients
|
|
99
|
+
// 5. Wait for clients (WS connected + in-world)
|
|
95
100
|
for (const clientName of profile.clients) {
|
|
96
101
|
await clientManager.waitReady(clientName, context.timeout("clientReady"));
|
|
97
102
|
}
|
|
@@ -123,7 +128,11 @@ export function createDownCommand() {
|
|
|
123
128
|
}
|
|
124
129
|
results.clients = clientResults;
|
|
125
130
|
// Stop server
|
|
126
|
-
|
|
131
|
+
const serverResult = await serverManager.stop(profile.server);
|
|
132
|
+
results.server = serverResult;
|
|
133
|
+
const everythingAccountedFor = clientResults.every((r) => r.stopped || r.alreadyStopped) &&
|
|
134
|
+
(serverResult.stopped || serverResult.alreadyStopped);
|
|
135
|
+
results.allClean = Boolean(everythingAccountedFor);
|
|
127
136
|
return results;
|
|
128
137
|
}));
|
|
129
138
|
}
|
|
@@ -10,7 +10,7 @@ const VERSION_MATRIX = [
|
|
|
10
10
|
spigot: { supported: true, requiresBuildTools: true }
|
|
11
11
|
},
|
|
12
12
|
clients: {
|
|
13
|
-
fabric: { supported: true, loaderVersion: "0.16.
|
|
13
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.1.1", validation: "verified" },
|
|
14
14
|
forge: { supported: false, notes: "不支持此版本" },
|
|
15
15
|
neoforge: { supported: true, loaderVersion: "21.4.x", modVersion: "0.1.1" }
|
|
16
16
|
}
|
|
@@ -25,7 +25,7 @@ const VERSION_MATRIX = [
|
|
|
25
25
|
spigot: { supported: true, requiresBuildTools: true }
|
|
26
26
|
},
|
|
27
27
|
clients: {
|
|
28
|
-
fabric: { supported: true, loaderVersion: "0.16.
|
|
28
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.1.1", validation: "verified" },
|
|
29
29
|
forge: { supported: false, notes: "不支持此版本" },
|
|
30
30
|
neoforge: { supported: true, loaderVersion: "21.1.x", modVersion: "0.1.1" }
|
|
31
31
|
}
|
|
@@ -40,7 +40,7 @@ const VERSION_MATRIX = [
|
|
|
40
40
|
spigot: { supported: true, requiresBuildTools: true }
|
|
41
41
|
},
|
|
42
42
|
clients: {
|
|
43
|
-
fabric: { supported: true, loaderVersion: "0.16.
|
|
43
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.1.1" },
|
|
44
44
|
forge: { supported: true, loaderVersion: "49.0.49", modVersion: "0.1.1" },
|
|
45
45
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
46
46
|
}
|
|
@@ -55,7 +55,7 @@ const VERSION_MATRIX = [
|
|
|
55
55
|
spigot: { supported: true, requiresBuildTools: true }
|
|
56
56
|
},
|
|
57
57
|
clients: {
|
|
58
|
-
fabric: { supported: true, loaderVersion: "0.16.
|
|
58
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.1.1", validation: "verified" },
|
|
59
59
|
forge: { supported: false, notes: "当前未接入此 loader" },
|
|
60
60
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
61
61
|
}
|
|
@@ -70,7 +70,7 @@ const VERSION_MATRIX = [
|
|
|
70
70
|
spigot: { supported: true, requiresBuildTools: true }
|
|
71
71
|
},
|
|
72
72
|
clients: {
|
|
73
|
-
fabric: { supported: true, loaderVersion: "0.16.
|
|
73
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.1.1" },
|
|
74
74
|
forge: { supported: false, notes: "当前未接入此 loader" },
|
|
75
75
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
76
76
|
}
|
|
@@ -85,7 +85,7 @@ const VERSION_MATRIX = [
|
|
|
85
85
|
spigot: { supported: true, requiresBuildTools: true }
|
|
86
86
|
},
|
|
87
87
|
clients: {
|
|
88
|
-
fabric: { supported: true, loaderVersion: "0.16.
|
|
88
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.1.1" },
|
|
89
89
|
forge: { supported: true, loaderVersion: "47.x", modVersion: "0.1.1" },
|
|
90
90
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
91
91
|
}
|
|
@@ -100,7 +100,7 @@ const VERSION_MATRIX = [
|
|
|
100
100
|
spigot: { supported: true, requiresBuildTools: true }
|
|
101
101
|
},
|
|
102
102
|
clients: {
|
|
103
|
-
fabric: { supported: true, loaderVersion: "0.16.
|
|
103
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.1.1", validation: "verified" },
|
|
104
104
|
forge: { supported: true, loaderVersion: "40.x", modVersion: "0.1.1" },
|
|
105
105
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
106
106
|
}
|
|
@@ -127,7 +127,7 @@ function buildLaunchArgs(runtimePaths, variant) {
|
|
|
127
127
|
"--minecraft-version",
|
|
128
128
|
variant.minecraftVersion,
|
|
129
129
|
"--fabric-loader-version",
|
|
130
|
-
variant.fabricLoaderVersion ?? "0.16.
|
|
130
|
+
variant.fabricLoaderVersion ?? "0.16.14"
|
|
131
131
|
];
|
|
132
132
|
}
|
|
133
133
|
function buildManagedLaunchArgs(runtimeRootDir, versionId, gameDir) {
|
|
@@ -15,6 +15,10 @@ export interface LaunchClientOptions {
|
|
|
15
15
|
account?: string;
|
|
16
16
|
wsPort?: number;
|
|
17
17
|
headless?: boolean;
|
|
18
|
+
force?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface WaitReadyOptions {
|
|
21
|
+
requireWorld?: boolean;
|
|
18
22
|
}
|
|
19
23
|
export declare class ClientInstanceManager {
|
|
20
24
|
private readonly globalState;
|
|
@@ -23,12 +27,20 @@ export declare class ClientInstanceManager {
|
|
|
23
27
|
launch(clientName: string, options?: LaunchClientOptions): Promise<ClientRuntimeEntry>;
|
|
24
28
|
stop(clientName: string): Promise<{
|
|
25
29
|
stopped: boolean;
|
|
30
|
+
alreadyStopped: boolean;
|
|
26
31
|
name: string;
|
|
27
32
|
pid?: undefined;
|
|
28
33
|
} | {
|
|
29
34
|
stopped: boolean;
|
|
30
35
|
name: string;
|
|
31
36
|
pid: number;
|
|
37
|
+
alreadyStopped?: undefined;
|
|
38
|
+
}>;
|
|
39
|
+
isAlreadyRunning(clientName: string): Promise<boolean>;
|
|
40
|
+
reconnect(clientName: string, address: string): Promise<{
|
|
41
|
+
reconnected: boolean;
|
|
42
|
+
name: string;
|
|
43
|
+
address: string;
|
|
32
44
|
}>;
|
|
33
45
|
list(): Promise<{
|
|
34
46
|
defaultClient: string | undefined;
|
|
@@ -37,9 +49,16 @@ export declare class ClientInstanceManager {
|
|
|
37
49
|
pid?: number;
|
|
38
50
|
})[];
|
|
39
51
|
}>;
|
|
40
|
-
waitReady(clientName: string, timeoutSeconds: number): Promise<{
|
|
52
|
+
waitReady(clientName: string, timeoutSeconds: number, options?: WaitReadyOptions): Promise<{
|
|
53
|
+
connected: boolean;
|
|
54
|
+
url: string;
|
|
55
|
+
inWorld?: undefined;
|
|
56
|
+
position?: undefined;
|
|
57
|
+
} | {
|
|
41
58
|
connected: boolean;
|
|
42
59
|
url: string;
|
|
60
|
+
inWorld: boolean;
|
|
61
|
+
position: unknown;
|
|
43
62
|
}>;
|
|
44
63
|
getClient(name?: string): Promise<ClientRuntimeEntry>;
|
|
45
64
|
loadMeta(clientName: string): Promise<ClientInstanceMeta>;
|
|
@@ -3,7 +3,7 @@ import { mkdirSync, openSync } from "node:fs";
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { resolveClientInstanceDir, resolveClientsDir } from "../util/paths.js";
|
|
6
|
+
import { resolveMctHome, resolveClientInstanceDir, resolveClientsDir } from "../util/paths.js";
|
|
7
7
|
import { MctError } from "../util/errors.js";
|
|
8
8
|
import { getListeningPids, isProcessRunning, killProcessTree } from "../util/process.js";
|
|
9
9
|
import { WebSocketClient } from "../client/WebSocketClient.js";
|
|
@@ -37,10 +37,20 @@ export class ClientInstanceManager {
|
|
|
37
37
|
return meta;
|
|
38
38
|
}
|
|
39
39
|
async launch(clientName, options = {}) {
|
|
40
|
-
|
|
40
|
+
let state = await this.globalState.readClientState();
|
|
41
41
|
const existing = state.clients[clientName];
|
|
42
42
|
if (existing && isProcessRunning(existing.pid)) {
|
|
43
|
-
|
|
43
|
+
if (options.force) {
|
|
44
|
+
await this.stop(clientName);
|
|
45
|
+
state = await this.globalState.readClientState();
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
throw new MctError({
|
|
49
|
+
code: "CLIENT_ALREADY_RUNNING",
|
|
50
|
+
message: `Client ${clientName} is already running. Pass --force to kill and relaunch.`,
|
|
51
|
+
details: existing
|
|
52
|
+
}, 3);
|
|
53
|
+
}
|
|
44
54
|
}
|
|
45
55
|
const meta = await this.loadMeta(clientName);
|
|
46
56
|
const instanceDir = resolveClientInstanceDir(clientName);
|
|
@@ -64,7 +74,7 @@ export class ClientInstanceManager {
|
|
|
64
74
|
}
|
|
65
75
|
const launchCommand = [process.execPath, getLaunchScriptPath(), ...meta.launchArgs];
|
|
66
76
|
const minecraftDir = path.join(instanceDir, "minecraft");
|
|
67
|
-
const logsDir = path.join(
|
|
77
|
+
const logsDir = path.join(resolveMctHome(), "logs");
|
|
68
78
|
mkdirSync(logsDir, { recursive: true });
|
|
69
79
|
const logPath = path.join(logsDir, `client-${clientName}.log`);
|
|
70
80
|
const stdout = openSync(logPath, "a");
|
|
@@ -101,7 +111,7 @@ export class ClientInstanceManager {
|
|
|
101
111
|
const state = await this.globalState.readClientState();
|
|
102
112
|
const entry = state.clients[clientName];
|
|
103
113
|
if (!entry) {
|
|
104
|
-
return { stopped: false, name: clientName };
|
|
114
|
+
return { stopped: false, alreadyStopped: true, name: clientName };
|
|
105
115
|
}
|
|
106
116
|
if (isProcessRunning(entry.pid)) {
|
|
107
117
|
killProcessTree(entry.pid);
|
|
@@ -118,6 +128,27 @@ export class ClientInstanceManager {
|
|
|
118
128
|
await this.globalState.writeClientState(state);
|
|
119
129
|
return { stopped: true, name: clientName, pid: entry.pid };
|
|
120
130
|
}
|
|
131
|
+
async isAlreadyRunning(clientName) {
|
|
132
|
+
const state = await this.globalState.readClientState();
|
|
133
|
+
const entry = state.clients[clientName];
|
|
134
|
+
return Boolean(entry && isProcessRunning(entry.pid));
|
|
135
|
+
}
|
|
136
|
+
async reconnect(clientName, address) {
|
|
137
|
+
const state = await this.globalState.readClientState();
|
|
138
|
+
const entry = state.clients[clientName];
|
|
139
|
+
if (!entry) {
|
|
140
|
+
throw new MctError({ code: "CLIENT_NOT_FOUND", message: `Client ${clientName} is not running` }, 3);
|
|
141
|
+
}
|
|
142
|
+
const ws = new WebSocketClient(`ws://127.0.0.1:${entry.wsPort}`);
|
|
143
|
+
const response = (await ws.send("client.reconnect", { address }, 5));
|
|
144
|
+
if (response.error) {
|
|
145
|
+
throw new MctError({
|
|
146
|
+
code: response.error.code || "INTERNAL_ERROR",
|
|
147
|
+
message: response.error.message || `Reconnect failed for ${clientName}`
|
|
148
|
+
}, 3);
|
|
149
|
+
}
|
|
150
|
+
return { reconnected: true, name: clientName, address };
|
|
151
|
+
}
|
|
121
152
|
async list() {
|
|
122
153
|
const state = await this.globalState.readClientState();
|
|
123
154
|
const runningClients = state.clients;
|
|
@@ -148,7 +179,7 @@ export class ClientInstanceManager {
|
|
|
148
179
|
clients: instances
|
|
149
180
|
};
|
|
150
181
|
}
|
|
151
|
-
async waitReady(clientName, timeoutSeconds) {
|
|
182
|
+
async waitReady(clientName, timeoutSeconds, options = {}) {
|
|
152
183
|
const state = await this.globalState.readClientState();
|
|
153
184
|
const entry = state.clients[clientName];
|
|
154
185
|
if (!entry) {
|
|
@@ -156,16 +187,49 @@ export class ClientInstanceManager {
|
|
|
156
187
|
}
|
|
157
188
|
const wsUrl = `ws://127.0.0.1:${entry.wsPort}`;
|
|
158
189
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
190
|
+
const requireWorld = options.requireWorld ?? true;
|
|
191
|
+
// 阶段 A:等 WS 连通
|
|
192
|
+
let connected = false;
|
|
159
193
|
while (Date.now() < deadline) {
|
|
160
194
|
try {
|
|
161
195
|
const ws = new WebSocketClient(wsUrl);
|
|
162
|
-
|
|
196
|
+
await ws.ping(1);
|
|
197
|
+
connected = true;
|
|
198
|
+
break;
|
|
163
199
|
}
|
|
164
200
|
catch {
|
|
165
201
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
166
202
|
}
|
|
167
203
|
}
|
|
168
|
-
|
|
204
|
+
if (!connected) {
|
|
205
|
+
throw new MctError({ code: "TIMEOUT", message: `Timed out connecting to client ${clientName} on ${wsUrl}` }, 2);
|
|
206
|
+
}
|
|
207
|
+
if (!requireWorld) {
|
|
208
|
+
return { connected: true, url: wsUrl };
|
|
209
|
+
}
|
|
210
|
+
// 阶段 B:轮询 position.get 直到不再 NOT_IN_WORLD
|
|
211
|
+
let lastErrorCode = "NOT_IN_WORLD";
|
|
212
|
+
while (Date.now() < deadline) {
|
|
213
|
+
try {
|
|
214
|
+
const ws = new WebSocketClient(wsUrl);
|
|
215
|
+
const response = (await ws.send("position.get", {}, 1));
|
|
216
|
+
if (!response.error) {
|
|
217
|
+
return { connected: true, url: wsUrl, inWorld: true, position: response.data };
|
|
218
|
+
}
|
|
219
|
+
lastErrorCode = response.error.code || lastErrorCode;
|
|
220
|
+
if (lastErrorCode !== "NOT_IN_WORLD") {
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// 连接瞬断等偶发错误,继续轮询
|
|
226
|
+
}
|
|
227
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
228
|
+
}
|
|
229
|
+
throw new MctError({
|
|
230
|
+
code: "TIMEOUT",
|
|
231
|
+
message: `Client ${clientName} connected (${wsUrl}) but did not join a world (last status: ${lastErrorCode}). Tip: try \`mct client reconnect --address <server>\` or \`mct client launch --force\`.`
|
|
232
|
+
}, 2);
|
|
169
233
|
}
|
|
170
234
|
async getClient(name) {
|
|
171
235
|
const state = await this.globalState.readClientState();
|
|
@@ -25,11 +25,13 @@ export declare class ServerInstanceManager {
|
|
|
25
25
|
stop(serverName: string): Promise<{
|
|
26
26
|
running: boolean;
|
|
27
27
|
stopped: boolean;
|
|
28
|
+
alreadyStopped: boolean;
|
|
28
29
|
pid?: undefined;
|
|
29
30
|
} | {
|
|
30
31
|
running: boolean;
|
|
31
32
|
stopped: boolean;
|
|
32
33
|
pid: number;
|
|
34
|
+
alreadyStopped?: undefined;
|
|
33
35
|
}>;
|
|
34
36
|
status(serverName?: string): Promise<{
|
|
35
37
|
[key: string]: unknown;
|
|
@@ -2,7 +2,7 @@ import { copyFile, mkdir, readdir, readFile, writeFile, unlink } from "node:fs/p
|
|
|
2
2
|
import { mkdirSync, openSync } from "node:fs";
|
|
3
3
|
import { spawn, execSync } from "node:child_process";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { resolveProjectDir, resolveServerInstanceDir } from "../util/paths.js";
|
|
5
|
+
import { resolveMctHome, resolveProjectDir, resolveServerInstanceDir } from "../util/paths.js";
|
|
6
6
|
import { MctError } from "../util/errors.js";
|
|
7
7
|
import { waitForTcpPort } from "../util/net.js";
|
|
8
8
|
import { isProcessRunning, killProcessTree } from "../util/process.js";
|
|
@@ -56,8 +56,9 @@ export class ServerInstanceManager {
|
|
|
56
56
|
if (options.eula) {
|
|
57
57
|
await writeFile(path.join(instanceDir, "eula.txt"), "eula=true\n", "utf8");
|
|
58
58
|
}
|
|
59
|
-
const
|
|
60
|
-
const
|
|
59
|
+
const mctHome = resolveMctHome();
|
|
60
|
+
const logsDir = path.join(mctHome, "logs");
|
|
61
|
+
const stateDir = path.join(mctHome, "state");
|
|
61
62
|
mkdirSync(logsDir, { recursive: true });
|
|
62
63
|
mkdirSync(stateDir, { recursive: true });
|
|
63
64
|
const logPath = path.join(logsDir, `server-${this.project}-${serverName}.log`);
|
|
@@ -70,11 +71,12 @@ export class ServerInstanceManager {
|
|
|
70
71
|
}
|
|
71
72
|
catch { /* ignore */ }
|
|
72
73
|
execSync(`mkfifo "${stdinPipe}"`);
|
|
73
|
-
// Use bash wrapper: hold FIFO write
|
|
74
|
+
// Use bash wrapper: hold FIFO open in read-write mode (fd 3 <>) to prevent EOF
|
|
75
|
+
// without blocking (write-only > would block until a reader opens the other end),
|
|
74
76
|
// then exec java with stdin reading from the FIFO
|
|
75
77
|
const child = spawn("bash", [
|
|
76
78
|
"-c",
|
|
77
|
-
'exec 3
|
|
79
|
+
'exec 3<>"$MCT_STDIN_PIPE"; exec java "$@" <"$MCT_STDIN_PIPE"',
|
|
78
80
|
"mct-server",
|
|
79
81
|
...jvmArgs, "-jar", jarFile, "nogui"
|
|
80
82
|
], {
|
|
@@ -107,7 +109,7 @@ export class ServerInstanceManager {
|
|
|
107
109
|
const state = await this.globalState.readServerState();
|
|
108
110
|
const entry = state.servers[stateKey];
|
|
109
111
|
if (!entry) {
|
|
110
|
-
return { running: false, stopped: false };
|
|
112
|
+
return { running: false, stopped: false, alreadyStopped: true };
|
|
111
113
|
}
|
|
112
114
|
if (isProcessRunning(entry.pid)) {
|
|
113
115
|
killProcessTree(entry.pid);
|
package/package.json
CHANGED
|
@@ -198,7 +198,7 @@ async function buildLaunchSpec(options) {
|
|
|
198
198
|
const minecraftVersion = process.env.MCT_CLIENT_VERSION || options["minecraft-version"] || defaultVariant.minecraftVersion;
|
|
199
199
|
const modVariantId = process.env.MCT_CLIENT_MOD_VARIANT || options["mod-variant"] || `${minecraftVersion}-fabric`;
|
|
200
200
|
const selectedVariant = getVariantById(modVariantId) ?? defaultVariant;
|
|
201
|
-
const fabricLoaderVersion = options["fabric-loader-version"] || selectedVariant.fabricLoaderVersion || "0.16.
|
|
201
|
+
const fabricLoaderVersion = options["fabric-loader-version"] || selectedVariant.fabricLoaderVersion || "0.16.14";
|
|
202
202
|
const instanceRoot = options["instance-dir"];
|
|
203
203
|
const metaRoot = options["meta-dir"];
|
|
204
204
|
const librariesRoot = options["libraries-dir"];
|