@chrrxs/robloxstudio-mcp-inspector 2.22.3 → 2.22.5
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/dist/index.js +1622 -409
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +3 -3
- package/studio-plugin/MCPPlugin.rbxmx +3 -3
package/dist/index.js
CHANGED
|
@@ -974,7 +974,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
974
974
|
serverVersion: serverConfig?.version,
|
|
975
975
|
capabilities: studioLifecycleCallable ? {
|
|
976
976
|
studioLifecycle: {
|
|
977
|
-
protocolVersion:
|
|
977
|
+
protocolVersion: 3,
|
|
978
978
|
endpoint: "/mcp/manage_instance"
|
|
979
979
|
}
|
|
980
980
|
} : {},
|
|
@@ -3024,34 +3024,124 @@ var init_roblox_cookie_client = __esm({
|
|
|
3024
3024
|
}
|
|
3025
3025
|
return response;
|
|
3026
3026
|
}
|
|
3027
|
-
async
|
|
3027
|
+
async uploadImage(options) {
|
|
3028
3028
|
if (!this.cookie) {
|
|
3029
3029
|
throw new Error("ROBLOSECURITY cookie is not set.");
|
|
3030
3030
|
}
|
|
3031
|
-
const
|
|
3032
|
-
const
|
|
3033
|
-
|
|
3034
|
-
|
|
3031
|
+
const creator = await this.resolveCreator(options.userId, options.groupId);
|
|
3032
|
+
const request = {
|
|
3033
|
+
assetType: "Image",
|
|
3034
|
+
displayName: options.displayName,
|
|
3035
|
+
description: options.description,
|
|
3036
|
+
creationContext: { creator }
|
|
3037
|
+
};
|
|
3038
|
+
const formData = new FormData();
|
|
3039
|
+
formData.append("request", JSON.stringify(request));
|
|
3040
|
+
formData.append("fileContent", new Blob([new Uint8Array(options.fileContent)], { type: this.getImageMimeType(options.fileName) }), options.fileName);
|
|
3041
|
+
const response = await this.fetchWithCsrf("https://apis.roblox.com/assets/user-auth/v1/assets", {
|
|
3035
3042
|
method: "POST",
|
|
3036
|
-
|
|
3037
|
-
"Content-Type": "application/octet-stream",
|
|
3038
|
-
"User-Agent": "RobloxStudio/WinInet",
|
|
3039
|
-
Requester: "Client"
|
|
3040
|
-
},
|
|
3041
|
-
body: new Uint8Array(fileContent)
|
|
3043
|
+
body: formData
|
|
3042
3044
|
});
|
|
3045
|
+
const operation = await this.readOperation(response, "Image upload");
|
|
3046
|
+
return { assetId: await this.completeOperation(operation) };
|
|
3047
|
+
}
|
|
3048
|
+
async resolveCreator(userId, groupId) {
|
|
3049
|
+
if (groupId)
|
|
3050
|
+
return { groupId };
|
|
3051
|
+
if (userId)
|
|
3052
|
+
return { userId };
|
|
3053
|
+
const response = await this.fetchWithCsrf("https://users.roblox.com/v1/users/authenticated");
|
|
3043
3054
|
if (!response.ok) {
|
|
3044
3055
|
const body = await response.text();
|
|
3045
|
-
throw new Error(`
|
|
3056
|
+
throw new Error(`Failed to resolve authenticated Roblox user (${response.status}): ${body}`);
|
|
3046
3057
|
}
|
|
3047
|
-
const
|
|
3048
|
-
|
|
3049
|
-
|
|
3058
|
+
const authenticatedUser = await response.json();
|
|
3059
|
+
const resolvedUserId = String(authenticatedUser.id ?? "");
|
|
3060
|
+
if (!/^\d+$/.test(resolvedUserId) || resolvedUserId === "0") {
|
|
3061
|
+
throw new Error("Authenticated Roblox user response did not include a valid user ID.");
|
|
3050
3062
|
}
|
|
3051
|
-
return {
|
|
3052
|
-
|
|
3053
|
-
|
|
3063
|
+
return { userId: resolvedUserId };
|
|
3064
|
+
}
|
|
3065
|
+
getImageMimeType(fileName) {
|
|
3066
|
+
const extension = fileName.split(".").pop()?.toLowerCase();
|
|
3067
|
+
const mimeTypes = {
|
|
3068
|
+
png: "image/png",
|
|
3069
|
+
jpg: "image/jpeg",
|
|
3070
|
+
jpeg: "image/jpeg",
|
|
3071
|
+
bmp: "image/bmp",
|
|
3072
|
+
tga: "image/tga"
|
|
3054
3073
|
};
|
|
3074
|
+
const mimeType = extension ? mimeTypes[extension] : void 0;
|
|
3075
|
+
if (mimeType)
|
|
3076
|
+
return mimeType;
|
|
3077
|
+
throw new Error(`Unsupported image format: .${extension ?? "(none)"}. Supported: png/jpg/jpeg/bmp/tga`);
|
|
3078
|
+
}
|
|
3079
|
+
async readOperation(response, action) {
|
|
3080
|
+
const body = await response.text();
|
|
3081
|
+
if (!response.ok) {
|
|
3082
|
+
throw new Error(`${action} failed (${response.status}): ${body}`);
|
|
3083
|
+
}
|
|
3084
|
+
try {
|
|
3085
|
+
return JSON.parse(body);
|
|
3086
|
+
} catch {
|
|
3087
|
+
throw new Error(`${action} returned malformed JSON: ${body}`);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
operationAssetId(operation) {
|
|
3091
|
+
const rawAssetId = operation.response?.assetId;
|
|
3092
|
+
if (rawAssetId === void 0)
|
|
3093
|
+
return null;
|
|
3094
|
+
const assetId = Number(rawAssetId);
|
|
3095
|
+
if (!Number.isSafeInteger(assetId) || assetId <= 0) {
|
|
3096
|
+
throw new Error(`Image upload returned an invalid asset ID: ${String(rawAssetId)}`);
|
|
3097
|
+
}
|
|
3098
|
+
return assetId;
|
|
3099
|
+
}
|
|
3100
|
+
operationError(operation) {
|
|
3101
|
+
if (operation.error?.message)
|
|
3102
|
+
return operation.error.message;
|
|
3103
|
+
if (operation.response?.message && operation.response.assetId === void 0) {
|
|
3104
|
+
return operation.response.message;
|
|
3105
|
+
}
|
|
3106
|
+
if (operation.message)
|
|
3107
|
+
return operation.message;
|
|
3108
|
+
return null;
|
|
3109
|
+
}
|
|
3110
|
+
async completeOperation(operation) {
|
|
3111
|
+
const initialError = this.operationError(operation);
|
|
3112
|
+
if (initialError)
|
|
3113
|
+
throw new Error(`Image upload failed: ${initialError}`);
|
|
3114
|
+
const initialAssetId = this.operationAssetId(operation);
|
|
3115
|
+
if (initialAssetId !== null)
|
|
3116
|
+
return initialAssetId;
|
|
3117
|
+
if (operation.done) {
|
|
3118
|
+
throw new Error("Image upload completed without an asset ID.");
|
|
3119
|
+
}
|
|
3120
|
+
const operationId = operation.operationId ?? operation.path?.split("/").pop();
|
|
3121
|
+
if (!operationId) {
|
|
3122
|
+
throw new Error("Image upload response did not include an operation ID.");
|
|
3123
|
+
}
|
|
3124
|
+
return this.pollOperation(operationId);
|
|
3125
|
+
}
|
|
3126
|
+
async pollOperation(operationId, maxAttempts = 30, intervalMs = 2e3) {
|
|
3127
|
+
const url = `https://apis.roblox.com/assets/user-auth/v1/operations/${encodeURIComponent(operationId)}`;
|
|
3128
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3129
|
+
const response = await this.fetchWithCsrf(url);
|
|
3130
|
+
const operation = await this.readOperation(response, "Image upload status");
|
|
3131
|
+
const operationError = this.operationError(operation);
|
|
3132
|
+
if (operationError)
|
|
3133
|
+
throw new Error(`Image upload failed: ${operationError}`);
|
|
3134
|
+
const assetId = this.operationAssetId(operation);
|
|
3135
|
+
if (assetId !== null)
|
|
3136
|
+
return assetId;
|
|
3137
|
+
if (operation.done) {
|
|
3138
|
+
throw new Error("Image upload completed without an asset ID.");
|
|
3139
|
+
}
|
|
3140
|
+
if (attempt < maxAttempts - 1) {
|
|
3141
|
+
await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
throw new Error(`Image upload timed out after ${maxAttempts * intervalMs / 1e3}s. Operation ID: ${operationId}`);
|
|
3055
3145
|
}
|
|
3056
3146
|
async getAssetDetails(assetIds) {
|
|
3057
3147
|
if (!this.cookie) {
|
|
@@ -3073,7 +3163,8 @@ var init_roblox_cookie_client = __esm({
|
|
|
3073
3163
|
});
|
|
3074
3164
|
|
|
3075
3165
|
// ../core/dist/managed-instance-registry.js
|
|
3076
|
-
import * as fs from "fs";
|
|
3166
|
+
import * as fs from "fs/promises";
|
|
3167
|
+
import { randomUUID } from "crypto";
|
|
3077
3168
|
import * as os from "os";
|
|
3078
3169
|
import * as path from "path";
|
|
3079
3170
|
function defaultManagedInstanceRegistryDir() {
|
|
@@ -3088,8 +3179,8 @@ function defaultManagedInstanceRegistryDir() {
|
|
|
3088
3179
|
}
|
|
3089
3180
|
return path.join(os.homedir(), ".local", "state", "robloxstudio-mcp", "managed-instances", "v1");
|
|
3090
3181
|
}
|
|
3091
|
-
function
|
|
3092
|
-
|
|
3182
|
+
function delay(ms) {
|
|
3183
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
3093
3184
|
}
|
|
3094
3185
|
function ymd(timestamp) {
|
|
3095
3186
|
return new Date(timestamp).toISOString().slice(0, 10);
|
|
@@ -3104,92 +3195,134 @@ function isRecord(value) {
|
|
|
3104
3195
|
const record = value;
|
|
3105
3196
|
return record.version === REGISTRY_VERSION && typeof record.recordId === "string" && typeof record.source === "string" && typeof record.exe === "string" && Array.isArray(record.args) && typeof record.launchedAt === "number" && typeof record.bootId === "string";
|
|
3106
3197
|
}
|
|
3107
|
-
|
|
3198
|
+
function isLockOwner(value) {
|
|
3199
|
+
if (!value || typeof value !== "object")
|
|
3200
|
+
return false;
|
|
3201
|
+
const owner = value;
|
|
3202
|
+
return Number.isInteger(owner.pid) && owner.pid > 0 && typeof owner.token === "string" && owner.token.length > 0 && typeof owner.createdAt === "number";
|
|
3203
|
+
}
|
|
3204
|
+
function isProcessAlive(pid) {
|
|
3205
|
+
if (pid === process.pid)
|
|
3206
|
+
return true;
|
|
3207
|
+
try {
|
|
3208
|
+
process.kill(pid, 0);
|
|
3209
|
+
return true;
|
|
3210
|
+
} catch (error) {
|
|
3211
|
+
return error.code === "EPERM";
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, TERMINAL_RECORD_RETENTION_MS, DEFAULT_CONFIRMED_EXIT_MISSES, DEFAULT_CONFIRMED_EXIT_GRACE_MS, activeLockTokens, activeLockPaths, ManagedInstanceRegistry;
|
|
3108
3215
|
var init_managed_instance_registry = __esm({
|
|
3109
3216
|
"../core/dist/managed-instance-registry.js"() {
|
|
3110
3217
|
"use strict";
|
|
3111
3218
|
REGISTRY_VERSION = 1;
|
|
3112
3219
|
LOCK_STALE_MS = 1e4;
|
|
3113
3220
|
LOCK_RETRY_MS = 25;
|
|
3114
|
-
LOCK_TIMEOUT_MS =
|
|
3221
|
+
LOCK_TIMEOUT_MS = 15e3;
|
|
3115
3222
|
EVENT_RETENTION_DAYS = 2;
|
|
3116
3223
|
TERMINAL_RECORD_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
3224
|
+
DEFAULT_CONFIRMED_EXIT_MISSES = 2;
|
|
3225
|
+
DEFAULT_CONFIRMED_EXIT_GRACE_MS = 5e3;
|
|
3226
|
+
activeLockTokens = /* @__PURE__ */ new Set();
|
|
3227
|
+
activeLockPaths = /* @__PURE__ */ new Set();
|
|
3117
3228
|
ManagedInstanceRegistry = class {
|
|
3118
3229
|
dir;
|
|
3119
3230
|
constructor(dir = defaultManagedInstanceRegistryDir()) {
|
|
3120
3231
|
this.dir = dir;
|
|
3121
3232
|
}
|
|
3122
|
-
upsert(record) {
|
|
3123
|
-
this.withLock(() => this.writeRecordUnlocked(record));
|
|
3233
|
+
async upsert(record) {
|
|
3234
|
+
await this.withLock(() => this.writeRecordUnlocked(record));
|
|
3124
3235
|
}
|
|
3125
|
-
attachInstanceId(recordId, instanceId) {
|
|
3126
|
-
this.withLock(() => {
|
|
3127
|
-
const record = this.readRecordUnlocked(recordId);
|
|
3236
|
+
async attachInstanceId(recordId, instanceId) {
|
|
3237
|
+
await this.withLock(async () => {
|
|
3238
|
+
const record = await this.readRecordUnlocked(recordId);
|
|
3128
3239
|
if (!record)
|
|
3129
3240
|
return;
|
|
3130
3241
|
record.instanceId = instanceId;
|
|
3131
3242
|
record.attachedAt = Date.now();
|
|
3132
|
-
this.writeRecordUnlocked(record);
|
|
3243
|
+
await this.writeRecordUnlocked(record);
|
|
3133
3244
|
});
|
|
3134
3245
|
}
|
|
3135
|
-
findOpenByInstanceId(instanceId, options) {
|
|
3136
|
-
return this.withLock(() => {
|
|
3137
|
-
this.sweepUnlocked(options);
|
|
3138
|
-
return this.readOpenRecordsUnlocked().find((record) => record.instanceId === instanceId);
|
|
3246
|
+
async findOpenByInstanceId(instanceId, options) {
|
|
3247
|
+
return this.withLock(async () => {
|
|
3248
|
+
await this.sweepUnlocked(options);
|
|
3249
|
+
return (await this.readOpenRecordsUnlocked()).find((record) => record.instanceId === instanceId);
|
|
3139
3250
|
});
|
|
3140
3251
|
}
|
|
3141
|
-
findAnyByInstanceId(instanceId) {
|
|
3142
|
-
return this.withLock(() => this.readRecordsUnlocked().find((record) => record.instanceId === instanceId));
|
|
3252
|
+
async findAnyByInstanceId(instanceId) {
|
|
3253
|
+
return this.withLock(async () => (await this.readRecordsUnlocked()).find((record) => record.instanceId === instanceId));
|
|
3143
3254
|
}
|
|
3144
|
-
findAnyByRecordId(recordId, options) {
|
|
3145
|
-
return this.withLock(() => {
|
|
3146
|
-
|
|
3147
|
-
|
|
3255
|
+
async findAnyByRecordId(recordId, options) {
|
|
3256
|
+
return this.withLock(async () => {
|
|
3257
|
+
if (options)
|
|
3258
|
+
await this.sweepUnlocked(options);
|
|
3259
|
+
return (await this.readRecordsUnlocked()).find((record) => record.recordId === recordId);
|
|
3148
3260
|
});
|
|
3149
3261
|
}
|
|
3150
|
-
listOpen(options) {
|
|
3151
|
-
return this.withLock(() => {
|
|
3152
|
-
this.sweepUnlocked(options);
|
|
3262
|
+
async listOpen(options) {
|
|
3263
|
+
return this.withLock(async () => {
|
|
3264
|
+
await this.sweepUnlocked(options);
|
|
3153
3265
|
return this.readOpenRecordsUnlocked();
|
|
3154
3266
|
});
|
|
3155
3267
|
}
|
|
3156
|
-
listOpenUnchecked() {
|
|
3268
|
+
async listOpenUnchecked() {
|
|
3157
3269
|
return this.withLock(() => this.readOpenRecordsUnlocked());
|
|
3158
3270
|
}
|
|
3159
|
-
markClosed(recordId, closedAt = Date.now()) {
|
|
3160
|
-
this.withLock(() => {
|
|
3161
|
-
const record = this.readRecordUnlocked(recordId);
|
|
3271
|
+
async markClosed(recordId, closedAt = Date.now()) {
|
|
3272
|
+
await this.withLock(async () => {
|
|
3273
|
+
const record = await this.readRecordUnlocked(recordId);
|
|
3162
3274
|
if (!record)
|
|
3163
3275
|
return;
|
|
3164
3276
|
record.closedAt = closedAt;
|
|
3165
|
-
this.writeRecordUnlocked(record);
|
|
3277
|
+
await this.writeRecordUnlocked(record);
|
|
3166
3278
|
});
|
|
3167
3279
|
}
|
|
3168
|
-
delete(recordId) {
|
|
3169
|
-
this.withLock(() => this.deleteRecordUnlocked(recordId));
|
|
3280
|
+
async delete(recordId) {
|
|
3281
|
+
await this.withLock(() => this.deleteRecordUnlocked(recordId));
|
|
3170
3282
|
}
|
|
3171
|
-
sweep(options) {
|
|
3172
|
-
this.withLock(() => this.sweepUnlocked(options));
|
|
3283
|
+
async sweep(options) {
|
|
3284
|
+
await this.withLock(() => this.sweepUnlocked(options));
|
|
3173
3285
|
}
|
|
3174
|
-
logEvent(event, now = Date.now()) {
|
|
3175
|
-
this.withLock(() => this.appendEventUnlocked(event, now));
|
|
3286
|
+
async logEvent(event, now = Date.now()) {
|
|
3287
|
+
await this.withLock(() => this.appendEventUnlocked(event, now));
|
|
3176
3288
|
}
|
|
3177
|
-
withLock(fn) {
|
|
3178
|
-
this.ensureDir();
|
|
3289
|
+
async withLock(fn) {
|
|
3290
|
+
await this.ensureDir();
|
|
3179
3291
|
const lockDir = path.join(this.dir, ".lock");
|
|
3180
3292
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
3181
|
-
|
|
3293
|
+
const owner = {
|
|
3294
|
+
pid: process.pid,
|
|
3295
|
+
token: randomUUID(),
|
|
3296
|
+
createdAt: Date.now()
|
|
3297
|
+
};
|
|
3298
|
+
for (; ; ) {
|
|
3182
3299
|
try {
|
|
3183
|
-
fs.
|
|
3300
|
+
await fs.mkdir(lockDir);
|
|
3301
|
+
activeLockTokens.add(owner.token);
|
|
3302
|
+
activeLockPaths.add(lockDir);
|
|
3303
|
+
try {
|
|
3304
|
+
await fs.writeFile(path.join(lockDir, "owner.json"), `${JSON.stringify(owner)}
|
|
3305
|
+
`, {
|
|
3306
|
+
encoding: "utf8",
|
|
3307
|
+
flag: "wx"
|
|
3308
|
+
});
|
|
3309
|
+
} catch (error) {
|
|
3310
|
+
activeLockTokens.delete(owner.token);
|
|
3311
|
+
activeLockPaths.delete(lockDir);
|
|
3312
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
3313
|
+
throw error;
|
|
3314
|
+
}
|
|
3184
3315
|
break;
|
|
3185
3316
|
} catch (error) {
|
|
3186
3317
|
const code = error.code;
|
|
3187
3318
|
if (code !== "EEXIST")
|
|
3188
3319
|
throw error;
|
|
3189
3320
|
try {
|
|
3190
|
-
const
|
|
3191
|
-
|
|
3192
|
-
|
|
3321
|
+
const stat2 = await fs.stat(lockDir);
|
|
3322
|
+
const currentOwner = await this.readLockOwner(lockDir, stat2.isDirectory());
|
|
3323
|
+
const ownerIsActive = currentOwner?.pid === process.pid ? activeLockTokens.has(currentOwner.token) : currentOwner ? isProcessAlive(currentOwner.pid) : void 0;
|
|
3324
|
+
if (ownerIsActive === false || currentOwner === void 0 && !activeLockPaths.has(lockDir) && Date.now() - stat2.mtimeMs > LOCK_STALE_MS) {
|
|
3325
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
3193
3326
|
continue;
|
|
3194
3327
|
}
|
|
3195
3328
|
} catch {
|
|
@@ -3198,40 +3331,59 @@ var init_managed_instance_registry = __esm({
|
|
|
3198
3331
|
if (Date.now() > deadline) {
|
|
3199
3332
|
throw new Error(`Timed out waiting for managed instance registry lock: ${lockDir}`);
|
|
3200
3333
|
}
|
|
3201
|
-
|
|
3334
|
+
await delay(LOCK_RETRY_MS);
|
|
3202
3335
|
}
|
|
3203
3336
|
}
|
|
3204
3337
|
try {
|
|
3205
|
-
return fn();
|
|
3338
|
+
return await fn();
|
|
3206
3339
|
} finally {
|
|
3207
|
-
|
|
3340
|
+
try {
|
|
3341
|
+
const stat2 = await fs.stat(lockDir);
|
|
3342
|
+
const currentOwner = await this.readLockOwner(lockDir, stat2.isDirectory());
|
|
3343
|
+
if (currentOwner?.pid === owner.pid && currentOwner.token === owner.token) {
|
|
3344
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
3345
|
+
}
|
|
3346
|
+
} catch {
|
|
3347
|
+
} finally {
|
|
3348
|
+
activeLockTokens.delete(owner.token);
|
|
3349
|
+
activeLockPaths.delete(lockDir);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
async readLockOwner(lockPath, isDirectory) {
|
|
3354
|
+
try {
|
|
3355
|
+
const contents = await fs.readFile(isDirectory ? path.join(lockPath, "owner.json") : lockPath, "utf8");
|
|
3356
|
+
const parsed = JSON.parse(contents);
|
|
3357
|
+
return isLockOwner(parsed) ? parsed : void 0;
|
|
3358
|
+
} catch {
|
|
3359
|
+
return void 0;
|
|
3208
3360
|
}
|
|
3209
3361
|
}
|
|
3210
|
-
ensureDir() {
|
|
3211
|
-
fs.
|
|
3362
|
+
async ensureDir() {
|
|
3363
|
+
await fs.mkdir(this.dir, { recursive: true });
|
|
3212
3364
|
}
|
|
3213
3365
|
recordPath(recordId) {
|
|
3214
3366
|
return path.join(this.dir, `${recordId}.json`);
|
|
3215
3367
|
}
|
|
3216
|
-
recordFilesUnlocked() {
|
|
3217
|
-
return fs.
|
|
3368
|
+
async recordFilesUnlocked() {
|
|
3369
|
+
return (await fs.readdir(this.dir)).filter((name) => name.endsWith(".json")).map((name) => path.join(this.dir, name));
|
|
3218
3370
|
}
|
|
3219
|
-
readRecordUnlocked(recordId) {
|
|
3371
|
+
async readRecordUnlocked(recordId) {
|
|
3220
3372
|
try {
|
|
3221
|
-
const parsed = JSON.parse(fs.
|
|
3373
|
+
const parsed = JSON.parse(await fs.readFile(this.recordPath(recordId), "utf8"));
|
|
3222
3374
|
return isRecord(parsed) ? parsed : void 0;
|
|
3223
3375
|
} catch {
|
|
3224
3376
|
return void 0;
|
|
3225
3377
|
}
|
|
3226
3378
|
}
|
|
3227
|
-
readOpenRecordsUnlocked() {
|
|
3228
|
-
return this.readRecordsUnlocked().filter((record) => record.closedAt === void 0);
|
|
3379
|
+
async readOpenRecordsUnlocked() {
|
|
3380
|
+
return (await this.readRecordsUnlocked()).filter((record) => record.closedAt === void 0);
|
|
3229
3381
|
}
|
|
3230
|
-
readRecordsUnlocked() {
|
|
3382
|
+
async readRecordsUnlocked() {
|
|
3231
3383
|
const records = [];
|
|
3232
|
-
for (const file of this.recordFilesUnlocked()) {
|
|
3384
|
+
for (const file of await this.recordFilesUnlocked()) {
|
|
3233
3385
|
try {
|
|
3234
|
-
const parsed = JSON.parse(fs.
|
|
3386
|
+
const parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
3235
3387
|
if (!isRecord(parsed))
|
|
3236
3388
|
continue;
|
|
3237
3389
|
records.push(parsed);
|
|
@@ -3240,48 +3392,48 @@ var init_managed_instance_registry = __esm({
|
|
|
3240
3392
|
}
|
|
3241
3393
|
return records;
|
|
3242
3394
|
}
|
|
3243
|
-
writeRecordUnlocked(record) {
|
|
3244
|
-
this.ensureDir();
|
|
3395
|
+
async writeRecordUnlocked(record) {
|
|
3396
|
+
await this.ensureDir();
|
|
3245
3397
|
const finalPath = this.recordPath(record.recordId);
|
|
3246
3398
|
const tmpPath = path.join(this.dir, `${record.recordId}.${process.pid}.${Date.now()}.tmp`);
|
|
3247
|
-
const fd = fs.
|
|
3399
|
+
const fd = await fs.open(tmpPath, "w");
|
|
3248
3400
|
try {
|
|
3249
|
-
|
|
3401
|
+
await fd.writeFile(`${JSON.stringify(record, null, 2)}
|
|
3250
3402
|
`, "utf8");
|
|
3251
|
-
|
|
3403
|
+
await fd.sync();
|
|
3252
3404
|
} finally {
|
|
3253
|
-
|
|
3405
|
+
await fd.close();
|
|
3254
3406
|
}
|
|
3255
|
-
fs.
|
|
3407
|
+
await fs.rename(tmpPath, finalPath);
|
|
3256
3408
|
}
|
|
3257
|
-
deleteRecordUnlocked(recordId) {
|
|
3258
|
-
fs.
|
|
3409
|
+
async deleteRecordUnlocked(recordId) {
|
|
3410
|
+
await fs.rm(this.recordPath(recordId), { force: true });
|
|
3259
3411
|
}
|
|
3260
|
-
appendEventUnlocked(event, now) {
|
|
3412
|
+
async appendEventUnlocked(event, now) {
|
|
3261
3413
|
const file = path.join(this.dir, `events-${ymd(now)}.jsonl`);
|
|
3262
|
-
fs.
|
|
3414
|
+
await fs.appendFile(file, `${JSON.stringify({
|
|
3263
3415
|
ts: new Date(now).toISOString(),
|
|
3264
3416
|
...event
|
|
3265
3417
|
})}
|
|
3266
3418
|
`, "utf8");
|
|
3267
3419
|
}
|
|
3268
|
-
cleanupOldEventLogsUnlocked(now) {
|
|
3420
|
+
async cleanupOldEventLogsUnlocked(now) {
|
|
3269
3421
|
const cutoff = ymd(now - EVENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3);
|
|
3270
|
-
for (const name of fs.
|
|
3422
|
+
for (const name of await fs.readdir(this.dir)) {
|
|
3271
3423
|
const date = eventLogDate(name);
|
|
3272
3424
|
if (!date || date >= cutoff)
|
|
3273
3425
|
continue;
|
|
3274
3426
|
try {
|
|
3275
|
-
fs.
|
|
3427
|
+
await fs.rm(path.join(this.dir, name), { force: true });
|
|
3276
3428
|
} catch {
|
|
3277
3429
|
}
|
|
3278
3430
|
}
|
|
3279
3431
|
}
|
|
3280
|
-
cleanupRecord(options, record) {
|
|
3432
|
+
async cleanupRecord(options, record) {
|
|
3281
3433
|
try {
|
|
3282
|
-
options.cleanupRecord?.(record);
|
|
3434
|
+
await options.cleanupRecord?.(record);
|
|
3283
3435
|
} catch {
|
|
3284
|
-
this.appendEventUnlocked({
|
|
3436
|
+
await this.appendEventUnlocked({
|
|
3285
3437
|
event: "registry_cleanup_failed",
|
|
3286
3438
|
recordId: record.recordId,
|
|
3287
3439
|
instanceId: record.instanceId,
|
|
@@ -3290,16 +3442,16 @@ var init_managed_instance_registry = __esm({
|
|
|
3290
3442
|
}, options.now ?? Date.now());
|
|
3291
3443
|
}
|
|
3292
3444
|
}
|
|
3293
|
-
sweepUnlocked(options) {
|
|
3445
|
+
async sweepUnlocked(options) {
|
|
3294
3446
|
const now = options.now ?? Date.now();
|
|
3295
|
-
this.cleanupOldEventLogsUnlocked(now);
|
|
3296
|
-
for (const file of this.recordFilesUnlocked()) {
|
|
3447
|
+
await this.cleanupOldEventLogsUnlocked(now);
|
|
3448
|
+
for (const file of await this.recordFilesUnlocked()) {
|
|
3297
3449
|
let parsed;
|
|
3298
3450
|
try {
|
|
3299
|
-
parsed = JSON.parse(fs.
|
|
3451
|
+
parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
3300
3452
|
} catch {
|
|
3301
|
-
fs.
|
|
3302
|
-
this.appendEventUnlocked({
|
|
3453
|
+
await fs.rm(file, { force: true });
|
|
3454
|
+
await this.appendEventUnlocked({
|
|
3303
3455
|
event: "registry_pruned_malformed_record",
|
|
3304
3456
|
reason: "parse_error",
|
|
3305
3457
|
action: "deleted_record"
|
|
@@ -3307,8 +3459,8 @@ var init_managed_instance_registry = __esm({
|
|
|
3307
3459
|
continue;
|
|
3308
3460
|
}
|
|
3309
3461
|
if (!parsed || typeof parsed !== "object") {
|
|
3310
|
-
fs.
|
|
3311
|
-
this.appendEventUnlocked({
|
|
3462
|
+
await fs.rm(file, { force: true });
|
|
3463
|
+
await this.appendEventUnlocked({
|
|
3312
3464
|
event: "registry_pruned_malformed_record",
|
|
3313
3465
|
reason: "invalid_shape",
|
|
3314
3466
|
action: "deleted_record"
|
|
@@ -3319,8 +3471,8 @@ var init_managed_instance_registry = __esm({
|
|
|
3319
3471
|
if (typeof version === "number" && version > REGISTRY_VERSION)
|
|
3320
3472
|
continue;
|
|
3321
3473
|
if (!isRecord(parsed)) {
|
|
3322
|
-
fs.
|
|
3323
|
-
this.appendEventUnlocked({
|
|
3474
|
+
await fs.rm(file, { force: true });
|
|
3475
|
+
await this.appendEventUnlocked({
|
|
3324
3476
|
event: "registry_pruned_malformed_record",
|
|
3325
3477
|
reason: "invalid_shape",
|
|
3326
3478
|
action: "deleted_record"
|
|
@@ -3330,8 +3482,8 @@ var init_managed_instance_registry = __esm({
|
|
|
3330
3482
|
const terminalAt = parsed.closedAt ?? parsed.exitedAt;
|
|
3331
3483
|
if (terminalAt !== void 0) {
|
|
3332
3484
|
if (now - terminalAt > TERMINAL_RECORD_RETENTION_MS) {
|
|
3333
|
-
fs.
|
|
3334
|
-
this.appendEventUnlocked({
|
|
3485
|
+
await fs.rm(file, { force: true });
|
|
3486
|
+
await this.appendEventUnlocked({
|
|
3335
3487
|
event: "registry_pruned_terminal_record",
|
|
3336
3488
|
recordId: parsed.recordId,
|
|
3337
3489
|
instanceId: parsed.instanceId,
|
|
@@ -3343,13 +3495,13 @@ var init_managed_instance_registry = __esm({
|
|
|
3343
3495
|
continue;
|
|
3344
3496
|
}
|
|
3345
3497
|
if (parsed.bootId !== options.currentBootId) {
|
|
3346
|
-
this.cleanupRecord(options, parsed);
|
|
3498
|
+
await this.cleanupRecord(options, parsed);
|
|
3347
3499
|
parsed.state = parsed.state === "failed" ? "failed" : "exited";
|
|
3348
3500
|
parsed.exitedAt = now;
|
|
3349
3501
|
parsed.closedAt = now;
|
|
3350
3502
|
parsed.failureReason ??= "Studio process belongs to a previous host boot.";
|
|
3351
|
-
this.writeRecordUnlocked(parsed);
|
|
3352
|
-
this.appendEventUnlocked({
|
|
3503
|
+
await this.writeRecordUnlocked(parsed);
|
|
3504
|
+
await this.appendEventUnlocked({
|
|
3353
3505
|
event: "registry_marked_previous_boot_exited",
|
|
3354
3506
|
recordId: parsed.recordId,
|
|
3355
3507
|
instanceId: parsed.instanceId,
|
|
@@ -3359,22 +3511,56 @@ var init_managed_instance_registry = __esm({
|
|
|
3359
3511
|
}, now);
|
|
3360
3512
|
continue;
|
|
3361
3513
|
}
|
|
3362
|
-
if (options.
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
parsed.
|
|
3367
|
-
parsed.
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3514
|
+
if (!options.observeProcess || !(parsed.nativeProcessId || parsed.spawnPid))
|
|
3515
|
+
continue;
|
|
3516
|
+
const observation = await options.observeProcess(parsed);
|
|
3517
|
+
if (observation.status === "unknown") {
|
|
3518
|
+
parsed.processObservationStatus = "unknown";
|
|
3519
|
+
parsed.lastProcessObservationAt = observation.observedAt;
|
|
3520
|
+
parsed.lastProcessObservationError = observation.error;
|
|
3521
|
+
parsed.consecutiveConfirmedMisses = 0;
|
|
3522
|
+
parsed.firstConfirmedMissAt = void 0;
|
|
3523
|
+
await this.writeRecordUnlocked(parsed);
|
|
3524
|
+
continue;
|
|
3525
|
+
}
|
|
3526
|
+
const previousObservationAt = parsed.lastProcessObservationAt;
|
|
3527
|
+
parsed.lastSuccessfulProcessObservationAt = observation.observedAt;
|
|
3528
|
+
parsed.lastProcessObservationAt = observation.observedAt;
|
|
3529
|
+
parsed.lastProcessObservationError = void 0;
|
|
3530
|
+
if (observation.status === "running") {
|
|
3531
|
+
parsed.processObservationStatus = "running";
|
|
3532
|
+
parsed.consecutiveConfirmedMisses = 0;
|
|
3533
|
+
parsed.firstConfirmedMissAt = void 0;
|
|
3534
|
+
await this.writeRecordUnlocked(parsed);
|
|
3535
|
+
continue;
|
|
3536
|
+
}
|
|
3537
|
+
const isNewObservation = previousObservationAt !== observation.observedAt;
|
|
3538
|
+
parsed.processObservationStatus = "not_running";
|
|
3539
|
+
if (previousObservationAt !== observation.observedAt) {
|
|
3540
|
+
parsed.consecutiveConfirmedMisses = (parsed.consecutiveConfirmedMisses ?? 0) + 1;
|
|
3541
|
+
parsed.firstConfirmedMissAt ??= observation.observedAt;
|
|
3542
|
+
}
|
|
3543
|
+
const requiredMisses = options.confirmedExitMisses ?? DEFAULT_CONFIRMED_EXIT_MISSES;
|
|
3544
|
+
const graceMs = options.confirmedExitGraceMs ?? DEFAULT_CONFIRMED_EXIT_GRACE_MS;
|
|
3545
|
+
const confirmedAbsent = observation.reason === "identity_mismatch" || isNewObservation && (parsed.consecutiveConfirmedMisses ?? 0) >= requiredMisses && observation.observedAt - (parsed.firstConfirmedMissAt ?? observation.observedAt) >= graceMs;
|
|
3546
|
+
if (!confirmedAbsent) {
|
|
3547
|
+
await this.writeRecordUnlocked(parsed);
|
|
3548
|
+
continue;
|
|
3377
3549
|
}
|
|
3550
|
+
await this.cleanupRecord(options, parsed);
|
|
3551
|
+
parsed.state = parsed.state === "failed" ? "failed" : "exited";
|
|
3552
|
+
parsed.exitedAt = observation.observedAt;
|
|
3553
|
+
parsed.closedAt = observation.observedAt;
|
|
3554
|
+
parsed.failureReason = observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : parsed.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.";
|
|
3555
|
+
await this.writeRecordUnlocked(parsed);
|
|
3556
|
+
await this.appendEventUnlocked({
|
|
3557
|
+
event: "registry_marked_process_exited",
|
|
3558
|
+
recordId: parsed.recordId,
|
|
3559
|
+
instanceId: parsed.instanceId,
|
|
3560
|
+
source: parsed.source,
|
|
3561
|
+
reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
|
|
3562
|
+
action: "marked_exited_and_cleaned_baseplate"
|
|
3563
|
+
}, now);
|
|
3378
3564
|
}
|
|
3379
3565
|
}
|
|
3380
3566
|
};
|
|
@@ -3382,11 +3568,12 @@ var init_managed_instance_registry = __esm({
|
|
|
3382
3568
|
});
|
|
3383
3569
|
|
|
3384
3570
|
// ../core/dist/studio-instance-manager.js
|
|
3385
|
-
import { execFileSync, spawn } from "child_process";
|
|
3386
|
-
import { copyFileSync, existsSync as existsSync2, mkdirSync as
|
|
3387
|
-
import { randomUUID } from "crypto";
|
|
3571
|
+
import { execFile, execFileSync, spawn } from "child_process";
|
|
3572
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, realpathSync, rmSync, statSync } from "fs";
|
|
3573
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3388
3574
|
import * as os2 from "os";
|
|
3389
3575
|
import * as path2 from "path";
|
|
3576
|
+
import { promisify } from "util";
|
|
3390
3577
|
function run(command, args, options = {}) {
|
|
3391
3578
|
return execFileSync(command, args, {
|
|
3392
3579
|
encoding: "utf8",
|
|
@@ -3394,17 +3581,29 @@ function run(command, args, options = {}) {
|
|
|
3394
3581
|
...options
|
|
3395
3582
|
}).trim();
|
|
3396
3583
|
}
|
|
3584
|
+
async function runAsync(command, args, options = {}) {
|
|
3585
|
+
const result = await execFileAsync(command, args, {
|
|
3586
|
+
encoding: "utf8",
|
|
3587
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
3588
|
+
timeout: 15e3,
|
|
3589
|
+
killSignal: "SIGKILL",
|
|
3590
|
+
...options
|
|
3591
|
+
});
|
|
3592
|
+
return `${result.stdout}`.trim();
|
|
3593
|
+
}
|
|
3397
3594
|
function isWsl() {
|
|
3398
3595
|
if (process.platform !== "linux")
|
|
3399
3596
|
return false;
|
|
3597
|
+
if (!process.env.WSL_INTEROP && !process.env.WSL_DISTRO_NAME)
|
|
3598
|
+
return false;
|
|
3400
3599
|
try {
|
|
3401
|
-
return /microsoft|wsl/i.test(
|
|
3600
|
+
return /microsoft|wsl/i.test(readFileSync2("/proc/version", "utf8"));
|
|
3402
3601
|
} catch {
|
|
3403
3602
|
return false;
|
|
3404
3603
|
}
|
|
3405
3604
|
}
|
|
3406
|
-
function
|
|
3407
|
-
return
|
|
3605
|
+
async function powershellAsync(script) {
|
|
3606
|
+
return runAsync("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
3408
3607
|
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
3409
3608
|
});
|
|
3410
3609
|
}
|
|
@@ -3421,15 +3620,33 @@ function windowsLocalAppData() {
|
|
|
3421
3620
|
return void 0;
|
|
3422
3621
|
}
|
|
3423
3622
|
}
|
|
3623
|
+
async function windowsLocalAppDataAsync() {
|
|
3624
|
+
if (process.platform === "win32")
|
|
3625
|
+
return process.env.LOCALAPPDATA;
|
|
3626
|
+
if (!isWsl())
|
|
3627
|
+
return void 0;
|
|
3628
|
+
try {
|
|
3629
|
+
return await runAsync("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
|
|
3630
|
+
cwd: existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
3631
|
+
});
|
|
3632
|
+
} catch {
|
|
3633
|
+
return void 0;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3424
3636
|
function toWslPath(windowsPath) {
|
|
3425
3637
|
if (!isWsl())
|
|
3426
3638
|
return windowsPath;
|
|
3427
3639
|
return run("wslpath", ["-u", windowsPath]);
|
|
3428
3640
|
}
|
|
3429
|
-
function
|
|
3641
|
+
async function toWslPathAsync(windowsPath) {
|
|
3642
|
+
if (!isWsl())
|
|
3643
|
+
return windowsPath;
|
|
3644
|
+
return runAsync("wslpath", ["-u", windowsPath]);
|
|
3645
|
+
}
|
|
3646
|
+
async function toStudioLaunchArgAsync(arg) {
|
|
3430
3647
|
if (!isWsl() || !path2.isAbsolute(arg) || !existsSync2(arg))
|
|
3431
3648
|
return arg;
|
|
3432
|
-
return
|
|
3649
|
+
return runAsync("wslpath", ["-w", arg]);
|
|
3433
3650
|
}
|
|
3434
3651
|
function powershellStringLiteral(value) {
|
|
3435
3652
|
return `'${value.replace(/'/g, "''")}'`;
|
|
@@ -3513,41 +3730,556 @@ function quoteWindowsCommandLineArg(value) {
|
|
|
3513
3730
|
}
|
|
3514
3731
|
return `${quoted}${"\\".repeat(backslashes * 2)}"`;
|
|
3515
3732
|
}
|
|
3516
|
-
function
|
|
3733
|
+
function buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment) {
|
|
3517
3734
|
const environmentPatch = parseStudioProcessEnvironmentPatch(processEnvironment);
|
|
3518
|
-
const
|
|
3519
|
-
const commandLine = args.map(quoteWindowsCommandLineArg).join(" ");
|
|
3735
|
+
const commandLine = [windowsExe, ...args].map(quoteWindowsCommandLineArg).join(" ");
|
|
3520
3736
|
return [
|
|
3521
3737
|
...environmentPatch ? powershellEnvironmentPatchStatements(environmentPatch) : [],
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3738
|
+
`Add-Type -TypeDefinition @'
|
|
3739
|
+
using System;
|
|
3740
|
+
using System.ComponentModel;
|
|
3741
|
+
using System.Runtime.InteropServices;
|
|
3742
|
+
using System.Text;
|
|
3743
|
+
|
|
3744
|
+
public sealed class McpSuspendedStudio : IDisposable
|
|
3745
|
+
{
|
|
3746
|
+
private const uint CREATE_SUSPENDED = 0x00000004;
|
|
3747
|
+
private const uint CREATE_BREAKAWAY_FROM_JOB = 0x01000000;
|
|
3748
|
+
private const uint RESUME_FAILED = 0xFFFFFFFF;
|
|
3749
|
+
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
|
3750
|
+
private const int JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9;
|
|
3751
|
+
private const uint WAIT_OBJECT_0 = 0x00000000;
|
|
3752
|
+
private const uint WAIT_TIMEOUT = 0x00000102;
|
|
3753
|
+
private const uint WAIT_FAILED = 0xFFFFFFFF;
|
|
3754
|
+
|
|
3755
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
3756
|
+
private struct StartupInfo
|
|
3757
|
+
{
|
|
3758
|
+
public int cb;
|
|
3759
|
+
public string lpReserved;
|
|
3760
|
+
public string lpDesktop;
|
|
3761
|
+
public string lpTitle;
|
|
3762
|
+
public uint dwX;
|
|
3763
|
+
public uint dwY;
|
|
3764
|
+
public uint dwXSize;
|
|
3765
|
+
public uint dwYSize;
|
|
3766
|
+
public uint dwXCountChars;
|
|
3767
|
+
public uint dwYCountChars;
|
|
3768
|
+
public uint dwFillAttribute;
|
|
3769
|
+
public uint dwFlags;
|
|
3770
|
+
public short wShowWindow;
|
|
3771
|
+
public short cbReserved2;
|
|
3772
|
+
public IntPtr lpReserved2;
|
|
3773
|
+
public IntPtr hStdInput;
|
|
3774
|
+
public IntPtr hStdOutput;
|
|
3775
|
+
public IntPtr hStdError;
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
3779
|
+
private struct ProcessInformation
|
|
3780
|
+
{
|
|
3781
|
+
public IntPtr hProcess;
|
|
3782
|
+
public IntPtr hThread;
|
|
3783
|
+
public uint dwProcessId;
|
|
3784
|
+
public uint dwThreadId;
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
3788
|
+
private struct FileTime
|
|
3789
|
+
{
|
|
3790
|
+
public uint Low;
|
|
3791
|
+
public uint High;
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
3795
|
+
private struct BasicLimitInformation
|
|
3796
|
+
{
|
|
3797
|
+
public long PerProcessUserTimeLimit;
|
|
3798
|
+
public long PerJobUserTimeLimit;
|
|
3799
|
+
public uint LimitFlags;
|
|
3800
|
+
public UIntPtr MinimumWorkingSetSize;
|
|
3801
|
+
public UIntPtr MaximumWorkingSetSize;
|
|
3802
|
+
public uint ActiveProcessLimit;
|
|
3803
|
+
public UIntPtr Affinity;
|
|
3804
|
+
public uint PriorityClass;
|
|
3805
|
+
public uint SchedulingClass;
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
3809
|
+
private struct IoCounters
|
|
3810
|
+
{
|
|
3811
|
+
public ulong ReadOperationCount;
|
|
3812
|
+
public ulong WriteOperationCount;
|
|
3813
|
+
public ulong OtherOperationCount;
|
|
3814
|
+
public ulong ReadTransferCount;
|
|
3815
|
+
public ulong WriteTransferCount;
|
|
3816
|
+
public ulong OtherTransferCount;
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
3820
|
+
private struct ExtendedLimitInformation
|
|
3821
|
+
{
|
|
3822
|
+
public BasicLimitInformation BasicLimitInformation;
|
|
3823
|
+
public IoCounters IoInfo;
|
|
3824
|
+
public UIntPtr ProcessMemoryLimit;
|
|
3825
|
+
public UIntPtr JobMemoryLimit;
|
|
3826
|
+
public UIntPtr PeakProcessMemoryUsed;
|
|
3827
|
+
public UIntPtr PeakJobMemoryUsed;
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
3831
|
+
private static extern bool CreateProcessW(
|
|
3832
|
+
string applicationName,
|
|
3833
|
+
StringBuilder commandLine,
|
|
3834
|
+
IntPtr processAttributes,
|
|
3835
|
+
IntPtr threadAttributes,
|
|
3836
|
+
bool inheritHandles,
|
|
3837
|
+
uint creationFlags,
|
|
3838
|
+
IntPtr environment,
|
|
3839
|
+
string currentDirectory,
|
|
3840
|
+
ref StartupInfo startupInfo,
|
|
3841
|
+
out ProcessInformation processInformation);
|
|
3842
|
+
|
|
3843
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
3844
|
+
private static extern IntPtr CreateJobObjectW(IntPtr jobAttributes, string name);
|
|
3845
|
+
|
|
3846
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3847
|
+
private static extern bool SetInformationJobObject(
|
|
3848
|
+
IntPtr job,
|
|
3849
|
+
int informationClass,
|
|
3850
|
+
IntPtr information,
|
|
3851
|
+
uint informationLength);
|
|
3852
|
+
|
|
3853
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3854
|
+
private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
|
3855
|
+
|
|
3856
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3857
|
+
private static extern bool GetProcessTimes(
|
|
3858
|
+
IntPtr process,
|
|
3859
|
+
out FileTime creation,
|
|
3860
|
+
out FileTime exit,
|
|
3861
|
+
out FileTime kernel,
|
|
3862
|
+
out FileTime user);
|
|
3863
|
+
|
|
3864
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3865
|
+
private static extern uint ResumeThread(IntPtr thread);
|
|
3866
|
+
|
|
3867
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3868
|
+
private static extern bool TerminateProcess(IntPtr process, uint exitCode);
|
|
3869
|
+
|
|
3870
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
3871
|
+
private static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
|
|
3872
|
+
|
|
3873
|
+
[DllImport("kernel32.dll")]
|
|
3874
|
+
private static extern bool CloseHandle(IntPtr handle);
|
|
3875
|
+
|
|
3876
|
+
private IntPtr process;
|
|
3877
|
+
private IntPtr thread;
|
|
3878
|
+
|
|
3879
|
+
private IntPtr job;
|
|
3880
|
+
public uint ProcessId { get; private set; }
|
|
3881
|
+
public ulong StartedAtFileTime { get; private set; }
|
|
3882
|
+
|
|
3883
|
+
private McpSuspendedStudio(ProcessInformation processInformation, IntPtr jobHandle)
|
|
3884
|
+
{
|
|
3885
|
+
process = processInformation.hProcess;
|
|
3886
|
+
thread = processInformation.hThread;
|
|
3887
|
+
job = jobHandle;
|
|
3888
|
+
ProcessId = processInformation.dwProcessId;
|
|
3889
|
+
FileTime creation;
|
|
3890
|
+
FileTime exit;
|
|
3891
|
+
FileTime kernel;
|
|
3892
|
+
FileTime user;
|
|
3893
|
+
if (!GetProcessTimes(process, out creation, out exit, out kernel, out user))
|
|
3894
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "GetProcessTimes failed");
|
|
3895
|
+
StartedAtFileTime = ((ulong)creation.High << 32) | creation.Low;
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
private static void ConfigureKillOnClose(IntPtr jobHandle, bool enabled)
|
|
3899
|
+
{
|
|
3900
|
+
var information = new ExtendedLimitInformation();
|
|
3901
|
+
information.BasicLimitInformation.LimitFlags = enabled ? JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE : 0;
|
|
3902
|
+
int length = Marshal.SizeOf(typeof(ExtendedLimitInformation));
|
|
3903
|
+
IntPtr buffer = Marshal.AllocHGlobal(length);
|
|
3904
|
+
try
|
|
3905
|
+
{
|
|
3906
|
+
Marshal.StructureToPtr(information, buffer, false);
|
|
3907
|
+
if (!SetInformationJobObject(jobHandle, JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, buffer, (uint)length))
|
|
3908
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "SetInformationJobObject failed");
|
|
3909
|
+
}
|
|
3910
|
+
finally
|
|
3911
|
+
{
|
|
3912
|
+
Marshal.FreeHGlobal(buffer);
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
private static void TerminateAndWait(IntPtr processHandle)
|
|
3917
|
+
{
|
|
3918
|
+
if (!TerminateProcess(processHandle, 1))
|
|
3919
|
+
{
|
|
3920
|
+
int error = Marshal.GetLastWin32Error();
|
|
3921
|
+
if (error != 5)
|
|
3922
|
+
throw new Win32Exception(error, "TerminateProcess failed");
|
|
3923
|
+
}
|
|
3924
|
+
uint wait = WaitForSingleObject(processHandle, 15000);
|
|
3925
|
+
if (wait == WAIT_TIMEOUT)
|
|
3926
|
+
throw new TimeoutException("Timed out waiting for the terminated Studio process");
|
|
3927
|
+
if (wait == WAIT_FAILED)
|
|
3928
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "WaitForSingleObject failed");
|
|
3929
|
+
if (wait != WAIT_OBJECT_0)
|
|
3930
|
+
throw new InvalidOperationException("Unexpected Studio process wait result: " + wait);
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
public static McpSuspendedStudio Start(string application, string commandLine)
|
|
3934
|
+
{
|
|
3935
|
+
IntPtr jobHandle = CreateJobObjectW(IntPtr.Zero, null);
|
|
3936
|
+
if (jobHandle == IntPtr.Zero)
|
|
3937
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateJobObjectW failed");
|
|
3938
|
+
try
|
|
3939
|
+
{
|
|
3940
|
+
ConfigureKillOnClose(jobHandle, true);
|
|
3941
|
+
var startup = new StartupInfo();
|
|
3942
|
+
startup.cb = Marshal.SizeOf(startup);
|
|
3943
|
+
ProcessInformation created;
|
|
3944
|
+
uint flags = CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB;
|
|
3945
|
+
bool started = CreateProcessW(application, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, flags, IntPtr.Zero, null, ref startup, out created);
|
|
3946
|
+
if (!started && Marshal.GetLastWin32Error() == 5)
|
|
3947
|
+
started = CreateProcessW(application, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, CREATE_SUSPENDED, IntPtr.Zero, null, ref startup, out created);
|
|
3948
|
+
if (!started)
|
|
3949
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateProcessW failed");
|
|
3950
|
+
try
|
|
3951
|
+
{
|
|
3952
|
+
if (!AssignProcessToJobObject(jobHandle, created.hProcess))
|
|
3953
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "AssignProcessToJobObject failed");
|
|
3954
|
+
return new McpSuspendedStudio(created, jobHandle);
|
|
3955
|
+
}
|
|
3956
|
+
catch
|
|
3957
|
+
{
|
|
3958
|
+
try
|
|
3959
|
+
{
|
|
3960
|
+
TerminateAndWait(created.hProcess);
|
|
3961
|
+
}
|
|
3962
|
+
finally
|
|
3963
|
+
{
|
|
3964
|
+
CloseHandle(created.hThread);
|
|
3965
|
+
CloseHandle(created.hProcess);
|
|
3966
|
+
}
|
|
3967
|
+
throw;
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
catch
|
|
3971
|
+
{
|
|
3972
|
+
CloseHandle(jobHandle);
|
|
3973
|
+
throw;
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
public void Resume()
|
|
3978
|
+
{
|
|
3979
|
+
if (thread == IntPtr.Zero)
|
|
3980
|
+
throw new InvalidOperationException("Studio launch was already resumed or disposed");
|
|
3981
|
+
if (ResumeThread(thread) == RESUME_FAILED)
|
|
3982
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "ResumeThread failed");
|
|
3983
|
+
CloseHandle(thread);
|
|
3984
|
+
thread = IntPtr.Zero;
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
public void Release()
|
|
3988
|
+
{
|
|
3989
|
+
if (thread != IntPtr.Zero)
|
|
3990
|
+
throw new InvalidOperationException("Studio launch cannot be released before it is resumed");
|
|
3991
|
+
if (job == IntPtr.Zero)
|
|
3992
|
+
throw new InvalidOperationException("Studio launch was already released or disposed");
|
|
3993
|
+
ConfigureKillOnClose(job, false);
|
|
3994
|
+
CloseHandle(job);
|
|
3995
|
+
job = IntPtr.Zero;
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
public void Abort()
|
|
3999
|
+
{
|
|
4000
|
+
if (process != IntPtr.Zero)
|
|
4001
|
+
TerminateAndWait(process);
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
public void Dispose()
|
|
4005
|
+
{
|
|
4006
|
+
if (thread != IntPtr.Zero)
|
|
4007
|
+
{
|
|
4008
|
+
CloseHandle(thread);
|
|
4009
|
+
thread = IntPtr.Zero;
|
|
4010
|
+
}
|
|
4011
|
+
if (job != IntPtr.Zero)
|
|
4012
|
+
{
|
|
4013
|
+
CloseHandle(job);
|
|
4014
|
+
job = IntPtr.Zero;
|
|
4015
|
+
}
|
|
4016
|
+
if (process != IntPtr.Zero)
|
|
4017
|
+
{
|
|
4018
|
+
CloseHandle(process);
|
|
4019
|
+
process = IntPtr.Zero;
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
'@`,
|
|
4024
|
+
`$launch = [McpSuspendedStudio]::Start(${powershellStringLiteral(windowsExe)}, ${powershellStringLiteral(commandLine)})`,
|
|
4025
|
+
"try {",
|
|
4026
|
+
"[Console]::Out.WriteLine((ConvertTo-Json @{ pid = $launch.ProcessId; started = [string]$launch.StartedAtFileTime } -Compress)); [Console]::Out.Flush()",
|
|
4027
|
+
"$accepted = $false",
|
|
4028
|
+
"$command = [Console]::In.ReadLine()",
|
|
4029
|
+
'if ($command -eq "MCP_STUDIO_LAUNCH_ACCEPT") {',
|
|
4030
|
+
"$launch.Resume()",
|
|
4031
|
+
'[Console]::Out.WriteLine("MCP_STUDIO_LAUNCH_RESUMED"); [Console]::Out.Flush()',
|
|
4032
|
+
"$command = [Console]::In.ReadLine()",
|
|
4033
|
+
'if ($command -eq "MCP_STUDIO_LAUNCH_COMPLETE") { $launch.Release(); $accepted = $true }',
|
|
4034
|
+
'elseif ($command -eq "MCP_STUDIO_LAUNCH_ABORT") { $launch.Abort(); $accepted = $true }',
|
|
4035
|
+
'else { throw "Resumed Studio launch was not completed or aborted." }',
|
|
4036
|
+
'} elseif ($command -eq "MCP_STUDIO_LAUNCH_ABORT") { $launch.Abort(); $accepted = $true }',
|
|
4037
|
+
'else { throw "Studio launch identity was not accepted." }',
|
|
4038
|
+
"} finally {",
|
|
4039
|
+
"try { if (-not $accepted) { $launch.Abort() } } finally { $launch.Dispose() }",
|
|
4040
|
+
"}"
|
|
4041
|
+
].join("\n");
|
|
4042
|
+
}
|
|
4043
|
+
function buildWindowsStudioStopScript(processId, startedAt) {
|
|
4044
|
+
if (!Number.isSafeInteger(processId) || processId <= 0)
|
|
4045
|
+
throw new Error("processId must be a positive integer.");
|
|
4046
|
+
if (!/^[1-9]\d*$/u.test(startedAt))
|
|
4047
|
+
throw new Error("startedAt must be a positive FILETIME string.");
|
|
4048
|
+
return [
|
|
4049
|
+
`$processId = [int]${processId}`,
|
|
4050
|
+
`$expectedStartedAt = [long]${startedAt}`,
|
|
4051
|
+
"$studio = $null",
|
|
4052
|
+
"try { $studio = [System.Diagnostics.Process]::GetProcessById($processId) } catch [System.ArgumentException] { return }",
|
|
4053
|
+
"try {",
|
|
4054
|
+
"$actualStartedAt = $studio.StartTime.ToUniversalTime().ToFileTimeUtc()",
|
|
4055
|
+
"if ($actualStartedAt -ne $expectedStartedAt) { return }",
|
|
4056
|
+
"if (-not $studio.HasExited) {",
|
|
4057
|
+
"try { $studio.Kill() } catch { if (-not $studio.HasExited) { throw } }",
|
|
4058
|
+
"$studio.WaitForExit()",
|
|
4059
|
+
"}",
|
|
4060
|
+
"} finally { if ($null -ne $studio) { $studio.Dispose() } }"
|
|
3533
4061
|
].join("; ");
|
|
3534
4062
|
}
|
|
3535
|
-
function
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
const
|
|
3540
|
-
const
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
4063
|
+
async function stopWindowsStudio(processId, startedAt) {
|
|
4064
|
+
await powershellAsync(buildWindowsStudioStopScript(processId, startedAt));
|
|
4065
|
+
}
|
|
4066
|
+
async function spawnWindowsStudio(exe, args, processEnvironment) {
|
|
4067
|
+
const windowsExe = await toStudioLaunchArgAsync(exe);
|
|
4068
|
+
const script = buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment);
|
|
4069
|
+
const launcher = spawn("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
4070
|
+
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
4071
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4072
|
+
windowsHide: true
|
|
4073
|
+
});
|
|
4074
|
+
return new Promise((resolve5, reject) => {
|
|
4075
|
+
let stdout = "";
|
|
4076
|
+
let controlOutput = "";
|
|
4077
|
+
let stderr = "";
|
|
4078
|
+
let identity;
|
|
4079
|
+
let initialFailure;
|
|
4080
|
+
let identitySettled = false;
|
|
4081
|
+
let controlState = "pending";
|
|
4082
|
+
let resumeSettled = false;
|
|
4083
|
+
let completeResume;
|
|
4084
|
+
const resumeAcknowledgment = new Promise((complete) => {
|
|
4085
|
+
completeResume = complete;
|
|
4086
|
+
});
|
|
4087
|
+
const signalResume = (error) => {
|
|
4088
|
+
if (resumeSettled)
|
|
4089
|
+
return;
|
|
4090
|
+
resumeSettled = true;
|
|
4091
|
+
completeResume(error);
|
|
4092
|
+
};
|
|
4093
|
+
let completeLauncher;
|
|
4094
|
+
const launcherCompletion = new Promise((complete) => {
|
|
4095
|
+
completeLauncher = complete;
|
|
4096
|
+
});
|
|
4097
|
+
const waitForLauncherCompletion = (timeoutMessage) => new Promise((complete) => {
|
|
4098
|
+
let settled = false;
|
|
4099
|
+
const forceTimeout = setTimeout(() => {
|
|
4100
|
+
if (settled)
|
|
4101
|
+
return;
|
|
4102
|
+
settled = true;
|
|
4103
|
+
const error = new Error(timeoutMessage);
|
|
4104
|
+
launcher.kill("SIGKILL");
|
|
4105
|
+
signalResume(error);
|
|
4106
|
+
completeLauncher(error);
|
|
4107
|
+
complete(error);
|
|
4108
|
+
}, 1e4);
|
|
4109
|
+
void launcherCompletion.then((error) => {
|
|
4110
|
+
if (settled)
|
|
4111
|
+
return;
|
|
4112
|
+
settled = true;
|
|
4113
|
+
clearTimeout(forceTimeout);
|
|
4114
|
+
complete(error);
|
|
4115
|
+
});
|
|
4116
|
+
});
|
|
4117
|
+
const waitForResumeAcknowledgment = () => new Promise((complete) => {
|
|
4118
|
+
const forceTimeout = setTimeout(() => {
|
|
4119
|
+
const error = new Error("Timed out resuming the suspended Studio process.");
|
|
4120
|
+
launcher.kill("SIGKILL");
|
|
4121
|
+
signalResume(error);
|
|
4122
|
+
completeLauncher(error);
|
|
4123
|
+
}, 1e4);
|
|
4124
|
+
void resumeAcknowledgment.then((error) => {
|
|
4125
|
+
clearTimeout(forceTimeout);
|
|
4126
|
+
complete(error);
|
|
4127
|
+
});
|
|
4128
|
+
});
|
|
4129
|
+
const throwWithExactCleanup = async (error) => {
|
|
4130
|
+
if (!identity)
|
|
4131
|
+
throw error;
|
|
4132
|
+
try {
|
|
4133
|
+
await stopWindowsStudio(identity.pid, identity.startedAt);
|
|
4134
|
+
} catch (cleanupError) {
|
|
4135
|
+
throw new Error(`${error.message} Exact Studio cleanup also failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
4136
|
+
}
|
|
4137
|
+
throw error;
|
|
4138
|
+
};
|
|
4139
|
+
const authorize = async () => {
|
|
4140
|
+
if (controlState === "resumed")
|
|
4141
|
+
return;
|
|
4142
|
+
if (controlState !== "pending")
|
|
4143
|
+
throw new Error(`Studio launch cannot be authorized from ${controlState}.`);
|
|
4144
|
+
controlState = "accepting";
|
|
4145
|
+
launcher.stdin.write("MCP_STUDIO_LAUNCH_ACCEPT\n");
|
|
4146
|
+
const error = await waitForResumeAcknowledgment();
|
|
4147
|
+
if (error)
|
|
4148
|
+
await throwWithExactCleanup(error);
|
|
4149
|
+
controlState = "resumed";
|
|
4150
|
+
};
|
|
4151
|
+
const release = async () => {
|
|
4152
|
+
if (controlState === "released")
|
|
4153
|
+
return;
|
|
4154
|
+
if (controlState !== "resumed")
|
|
4155
|
+
throw new Error(`Studio launch cannot be released from ${controlState}.`);
|
|
4156
|
+
controlState = "releasing";
|
|
4157
|
+
launcher.stdin.end("MCP_STUDIO_LAUNCH_COMPLETE\n");
|
|
4158
|
+
const error = await waitForLauncherCompletion("Timed out releasing the authorized Studio process.");
|
|
4159
|
+
if (error)
|
|
4160
|
+
await throwWithExactCleanup(error);
|
|
4161
|
+
controlState = "released";
|
|
4162
|
+
};
|
|
4163
|
+
const abort = async () => {
|
|
4164
|
+
if (controlState === "released")
|
|
4165
|
+
return;
|
|
4166
|
+
if (controlState !== "aborting") {
|
|
4167
|
+
controlState = "aborting";
|
|
4168
|
+
launcher.stdin.end("MCP_STUDIO_LAUNCH_ABORT\n");
|
|
4169
|
+
}
|
|
4170
|
+
const error = await waitForLauncherCompletion("Timed out aborting the owned Studio process.");
|
|
4171
|
+
if (error && identity) {
|
|
4172
|
+
await stopWindowsStudio(identity.pid, identity.startedAt);
|
|
4173
|
+
} else if (error) {
|
|
4174
|
+
throw error;
|
|
4175
|
+
}
|
|
4176
|
+
};
|
|
4177
|
+
const timeout = setTimeout(() => {
|
|
4178
|
+
if (identitySettled)
|
|
4179
|
+
return;
|
|
4180
|
+
identitySettled = true;
|
|
4181
|
+
const failure = initialFailure = new Error("Timed out while capturing the exact Studio process identity.");
|
|
4182
|
+
controlState = "aborting";
|
|
4183
|
+
launcher.stdin.end("MCP_STUDIO_LAUNCH_ABORT\n");
|
|
4184
|
+
void waitForLauncherCompletion("Timed out aborting the Studio identity launcher.").then((completionError) => {
|
|
4185
|
+
if (!identity)
|
|
4186
|
+
reject(completionError ?? failure);
|
|
4187
|
+
});
|
|
4188
|
+
}, 15e3);
|
|
4189
|
+
launcher.stdout.on("data", (chunk) => {
|
|
4190
|
+
if (identitySettled) {
|
|
4191
|
+
controlOutput = (controlOutput + chunk.toString("utf8")).slice(-1024);
|
|
4192
|
+
if (controlOutput.includes("MCP_STUDIO_LAUNCH_RESUMED"))
|
|
4193
|
+
signalResume();
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
stdout += chunk.toString("utf8");
|
|
4197
|
+
if (stdout.length > 1024 * 1024) {
|
|
4198
|
+
const failure = initialFailure = new Error("Studio launcher identity output exceeded 1 MiB.");
|
|
4199
|
+
identitySettled = true;
|
|
4200
|
+
controlState = "aborting";
|
|
4201
|
+
launcher.stdin.end("MCP_STUDIO_LAUNCH_ABORT\n");
|
|
4202
|
+
void waitForLauncherCompletion("Timed out aborting the oversized Studio identity response.").then((completionError) => {
|
|
4203
|
+
if (!identity)
|
|
4204
|
+
reject(completionError ?? failure);
|
|
4205
|
+
});
|
|
4206
|
+
return;
|
|
4207
|
+
}
|
|
4208
|
+
const newline = stdout.indexOf("\n");
|
|
4209
|
+
if (newline < 0)
|
|
4210
|
+
return;
|
|
4211
|
+
identitySettled = true;
|
|
4212
|
+
clearTimeout(timeout);
|
|
4213
|
+
try {
|
|
4214
|
+
const parsed = JSON.parse(stdout.slice(0, newline).trim());
|
|
4215
|
+
const nativePid = Number(parsed.pid);
|
|
4216
|
+
const nativeStartedAt = typeof parsed.started === "string" && /^[1-9]\d*$/u.test(parsed.started) ? parsed.started : void 0;
|
|
4217
|
+
if (!Number.isSafeInteger(nativePid) || nativePid <= 0 || nativeStartedAt === void 0) {
|
|
4218
|
+
throw new Error("PowerShell returned an invalid native Studio process identity.");
|
|
4219
|
+
}
|
|
4220
|
+
identity = { pid: nativePid, startedAt: nativeStartedAt };
|
|
4221
|
+
resolve5({
|
|
4222
|
+
pid: nativePid,
|
|
4223
|
+
nativePid,
|
|
4224
|
+
nativeStartedAt,
|
|
4225
|
+
unref: () => {
|
|
4226
|
+
},
|
|
4227
|
+
authorize,
|
|
4228
|
+
release,
|
|
4229
|
+
abort
|
|
4230
|
+
});
|
|
4231
|
+
} catch (error) {
|
|
4232
|
+
const failure = initialFailure = error instanceof Error ? error : new Error(String(error));
|
|
4233
|
+
controlState = "aborting";
|
|
4234
|
+
launcher.stdin.end("MCP_STUDIO_LAUNCH_ABORT\n");
|
|
4235
|
+
void waitForLauncherCompletion("Timed out aborting the invalid Studio identity response.").then((completionError) => {
|
|
4236
|
+
if (!identity)
|
|
4237
|
+
reject(completionError ?? failure);
|
|
4238
|
+
});
|
|
4239
|
+
}
|
|
4240
|
+
});
|
|
4241
|
+
launcher.stdin.on("error", (error) => {
|
|
4242
|
+
if (!initialFailure)
|
|
4243
|
+
initialFailure = error;
|
|
4244
|
+
});
|
|
4245
|
+
launcher.stderr.on("data", (chunk) => {
|
|
4246
|
+
if (stderr.length <= 1024 * 1024)
|
|
4247
|
+
stderr += chunk.toString("utf8");
|
|
4248
|
+
});
|
|
4249
|
+
launcher.once("error", (error) => {
|
|
4250
|
+
clearTimeout(timeout);
|
|
4251
|
+
signalResume(error);
|
|
4252
|
+
completeLauncher(error);
|
|
4253
|
+
if (!identity)
|
|
4254
|
+
reject(error);
|
|
4255
|
+
});
|
|
4256
|
+
launcher.once("exit", (code) => {
|
|
4257
|
+
clearTimeout(timeout);
|
|
4258
|
+
void (async () => {
|
|
4259
|
+
if (code === 0 && identity && !initialFailure) {
|
|
4260
|
+
if (!resumeSettled)
|
|
4261
|
+
signalResume(new Error("Studio launcher exited before resume acknowledgment."));
|
|
4262
|
+
completeLauncher();
|
|
4263
|
+
return;
|
|
4264
|
+
}
|
|
4265
|
+
let cleanupError;
|
|
4266
|
+
if (identity) {
|
|
4267
|
+
try {
|
|
4268
|
+
await stopWindowsStudio(identity.pid, identity.startedAt);
|
|
4269
|
+
} catch (error2) {
|
|
4270
|
+
cleanupError = error2;
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
const detail = initialFailure?.message ?? (stderr.trim() || `PowerShell launcher exited with code ${code}.`);
|
|
4274
|
+
const cleanupDetail = cleanupError ? ` Exact Studio cleanup also failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}` : "";
|
|
4275
|
+
const error = new Error(`${detail}${cleanupDetail}`);
|
|
4276
|
+
signalResume(error);
|
|
4277
|
+
completeLauncher(error);
|
|
4278
|
+
if (!identity)
|
|
4279
|
+
reject(error);
|
|
4280
|
+
})();
|
|
4281
|
+
});
|
|
4282
|
+
});
|
|
3551
4283
|
}
|
|
3552
4284
|
function resolveEntrypointDir() {
|
|
3553
4285
|
const entrypoint = process.argv[1];
|
|
@@ -3576,7 +4308,7 @@ function resolveBaseplateTemplatePath() {
|
|
|
3576
4308
|
}
|
|
3577
4309
|
throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
|
|
3578
4310
|
}
|
|
3579
|
-
function
|
|
4311
|
+
function isProcessAlive2(pid) {
|
|
3580
4312
|
try {
|
|
3581
4313
|
process.kill(pid, 0);
|
|
3582
4314
|
return true;
|
|
@@ -3587,7 +4319,7 @@ function isProcessAlive(pid) {
|
|
|
3587
4319
|
function sweepStaleBaseplateFiles() {
|
|
3588
4320
|
let entries;
|
|
3589
4321
|
try {
|
|
3590
|
-
entries =
|
|
4322
|
+
entries = readdirSync(BASEPLATE_TEMP_DIR);
|
|
3591
4323
|
} catch {
|
|
3592
4324
|
return;
|
|
3593
4325
|
}
|
|
@@ -3596,18 +4328,18 @@ function sweepStaleBaseplateFiles() {
|
|
|
3596
4328
|
const match = BASEPLATE_TEMP_SWEEP_NAME.exec(entry);
|
|
3597
4329
|
if (!match)
|
|
3598
4330
|
continue;
|
|
3599
|
-
if (Number(match[1]) !== process.pid &&
|
|
4331
|
+
if (Number(match[1]) !== process.pid && isProcessAlive2(Number(match[1])))
|
|
3600
4332
|
continue;
|
|
3601
4333
|
const file = path2.join(BASEPLATE_TEMP_DIR, entry);
|
|
3602
4334
|
try {
|
|
3603
|
-
if (
|
|
3604
|
-
|
|
4335
|
+
if (statSync(file).mtimeMs < cutoff)
|
|
4336
|
+
rmSync(file, { force: true });
|
|
3605
4337
|
} catch {
|
|
3606
4338
|
}
|
|
3607
4339
|
}
|
|
3608
4340
|
}
|
|
3609
4341
|
function createBaseplatePlaceFile() {
|
|
3610
|
-
|
|
4342
|
+
mkdirSync2(BASEPLATE_TEMP_DIR, { recursive: true });
|
|
3611
4343
|
sweepStaleBaseplateFiles();
|
|
3612
4344
|
const file = path2.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
|
|
3613
4345
|
copyFileSync(resolveBaseplateTemplatePath(), file);
|
|
@@ -3624,7 +4356,7 @@ function cleanupManagedBaseplateFiles(record) {
|
|
|
3624
4356
|
return;
|
|
3625
4357
|
for (const file of [record.localPlaceFile, `${record.localPlaceFile}.lock`]) {
|
|
3626
4358
|
try {
|
|
3627
|
-
|
|
4359
|
+
rmSync(file, { force: true });
|
|
3628
4360
|
} catch {
|
|
3629
4361
|
}
|
|
3630
4362
|
}
|
|
@@ -3651,54 +4383,83 @@ function resolveStudioExe() {
|
|
|
3651
4383
|
if (!existsSync2(root)) {
|
|
3652
4384
|
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
3653
4385
|
}
|
|
3654
|
-
const candidates =
|
|
4386
|
+
const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
3655
4387
|
if (candidates.length === 0) {
|
|
3656
4388
|
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
3657
4389
|
}
|
|
3658
4390
|
return candidates[0];
|
|
3659
4391
|
}
|
|
3660
|
-
function
|
|
4392
|
+
async function resolveStudioExeAsync() {
|
|
4393
|
+
if (process.env.ROBLOX_STUDIO_EXE)
|
|
4394
|
+
return process.env.ROBLOX_STUDIO_EXE;
|
|
3661
4395
|
if (process.platform === "darwin") {
|
|
3662
|
-
|
|
3663
|
-
try {
|
|
3664
|
-
out2 = run("pgrep", ["-fl", "RobloxStudio"]);
|
|
3665
|
-
} catch {
|
|
3666
|
-
return [];
|
|
3667
|
-
}
|
|
3668
|
-
return out2.split("\n").filter(Boolean).map((line) => {
|
|
3669
|
-
const [pid, ...rest] = line.trim().split(/\s+/);
|
|
3670
|
-
return { Id: Number(pid), Name: "RobloxStudio", Path: rest.join(" "), MainWindowTitle: "" };
|
|
3671
|
-
}).filter((proc) => Number.isFinite(proc.Id));
|
|
4396
|
+
return "/Applications/RobloxStudio.app/Contents/MacOS/RobloxStudio";
|
|
3672
4397
|
}
|
|
3673
|
-
if (process.platform !== "win32" && !isWsl())
|
|
3674
|
-
|
|
3675
|
-
|
|
4398
|
+
if (process.platform !== "win32" && !isWsl()) {
|
|
4399
|
+
throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
|
|
4400
|
+
}
|
|
4401
|
+
const localAppData = await windowsLocalAppDataAsync();
|
|
4402
|
+
const root = localAppData ? path2.join(await toWslPathAsync(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
4403
|
+
if (!existsSync2(root)) {
|
|
4404
|
+
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4405
|
+
}
|
|
4406
|
+
const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
4407
|
+
if (candidates.length === 0) {
|
|
4408
|
+
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4409
|
+
}
|
|
4410
|
+
return candidates[0];
|
|
4411
|
+
}
|
|
4412
|
+
async function observeStudioProcesses() {
|
|
4413
|
+
const observedAt = Date.now();
|
|
3676
4414
|
try {
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
4415
|
+
if (process.platform === "darwin") {
|
|
4416
|
+
let out2 = "";
|
|
4417
|
+
try {
|
|
4418
|
+
out2 = await runAsync("pgrep", ["-fl", "RobloxStudio"]);
|
|
4419
|
+
} catch (error) {
|
|
4420
|
+
const code = error.code;
|
|
4421
|
+
if (Number(code) === 1)
|
|
4422
|
+
return { status: "ok", observedAt, processes: [] };
|
|
4423
|
+
throw error;
|
|
4424
|
+
}
|
|
4425
|
+
const processes = out2.split("\n").filter(Boolean).map((line) => {
|
|
4426
|
+
const [pid, ...rest] = line.trim().split(/\s+/);
|
|
4427
|
+
return { Id: Number(pid), Name: "RobloxStudio", Path: rest.join(" "), MainWindowTitle: "" };
|
|
4428
|
+
}).filter((proc) => Number.isFinite(proc.Id));
|
|
4429
|
+
return { status: "ok", observedAt, processes };
|
|
4430
|
+
}
|
|
4431
|
+
if (process.platform !== "win32" && !isWsl()) {
|
|
4432
|
+
return { status: "ok", observedAt, processes: [] };
|
|
4433
|
+
}
|
|
4434
|
+
const out = await powershellAsync("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | ForEach-Object { [PSCustomObject]@{ Id = $_.Id; Name = $_.Name; Path = $_.Path; MainWindowTitle = $_.MainWindowTitle; StartTimeUtcFileTime = $_.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } } | ConvertTo-Json -Compress");
|
|
4435
|
+
if (!out)
|
|
4436
|
+
return { status: "ok", observedAt, processes: [] };
|
|
4437
|
+
const parsed = JSON.parse(out);
|
|
4438
|
+
return { status: "ok", observedAt, processes: Array.isArray(parsed) ? parsed : [parsed] };
|
|
4439
|
+
} catch (error) {
|
|
4440
|
+
return {
|
|
4441
|
+
status: "error",
|
|
4442
|
+
observedAt,
|
|
4443
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4444
|
+
};
|
|
3680
4445
|
}
|
|
3681
|
-
if (!out)
|
|
3682
|
-
return [];
|
|
3683
|
-
const parsed = JSON.parse(out);
|
|
3684
|
-
return Array.isArray(parsed) ? parsed : [parsed];
|
|
3685
4446
|
}
|
|
3686
|
-
function
|
|
4447
|
+
async function currentBootIdAsync() {
|
|
3687
4448
|
if (process.platform === "linux") {
|
|
3688
4449
|
try {
|
|
3689
|
-
return
|
|
4450
|
+
return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
3690
4451
|
} catch {
|
|
3691
4452
|
}
|
|
3692
4453
|
}
|
|
3693
4454
|
if (process.platform === "win32" || isWsl()) {
|
|
3694
4455
|
try {
|
|
3695
|
-
return
|
|
4456
|
+
return await powershellAsync('(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString("o")');
|
|
3696
4457
|
} catch {
|
|
3697
4458
|
}
|
|
3698
4459
|
}
|
|
3699
4460
|
if (process.platform === "darwin") {
|
|
3700
4461
|
try {
|
|
3701
|
-
return
|
|
4462
|
+
return await runAsync("sysctl", ["-n", "kern.boottime"]);
|
|
3702
4463
|
} catch {
|
|
3703
4464
|
}
|
|
3704
4465
|
}
|
|
@@ -3737,13 +4498,13 @@ function buildStudioLaunchArgs(options) {
|
|
|
3737
4498
|
];
|
|
3738
4499
|
}
|
|
3739
4500
|
}
|
|
3740
|
-
function
|
|
4501
|
+
function delay2(ms) {
|
|
3741
4502
|
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
3742
4503
|
}
|
|
3743
4504
|
function basenameAny(filePath) {
|
|
3744
4505
|
return path2.basename(filePath.replace(/\\/g, "/"));
|
|
3745
4506
|
}
|
|
3746
|
-
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, ENVIRONMENT_VARIABLE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
|
|
4507
|
+
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, retainedLaunchControls, execFileAsync, ENVIRONMENT_VARIABLE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, LAUNCH_COMPLETION_TIMEOUT_MS, StudioInstanceManager;
|
|
3747
4508
|
var init_studio_instance_manager = __esm({
|
|
3748
4509
|
"../core/dist/studio-instance-manager.js"() {
|
|
3749
4510
|
"use strict";
|
|
@@ -3751,27 +4512,45 @@ var init_studio_instance_manager = __esm({
|
|
|
3751
4512
|
BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
|
|
3752
4513
|
BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
|
|
3753
4514
|
BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
|
|
4515
|
+
retainedLaunchControls = /* @__PURE__ */ new Map();
|
|
4516
|
+
execFileAsync = promisify(execFile);
|
|
3754
4517
|
ENVIRONMENT_VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
3755
4518
|
STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
3756
4519
|
BASEPLATE_TEMP_SWEEP_NAME = /^Baseplate-(\d+)-\d+\.rbxl(\.lock)?$/;
|
|
4520
|
+
LAUNCH_COMPLETION_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
3757
4521
|
StudioInstanceManager = class {
|
|
3758
4522
|
managedByInstanceId = /* @__PURE__ */ new Map();
|
|
3759
4523
|
pending = /* @__PURE__ */ new Set();
|
|
3760
|
-
monitors = /* @__PURE__ */ new Map();
|
|
3761
4524
|
connectionTimers = /* @__PURE__ */ new Map();
|
|
4525
|
+
launchCompletionTimers = /* @__PURE__ */ new Map();
|
|
4526
|
+
launchControls = retainedLaunchControls;
|
|
3762
4527
|
registry;
|
|
3763
4528
|
processAdapter;
|
|
4529
|
+
confirmedExitMisses;
|
|
4530
|
+
confirmedExitGraceMs;
|
|
4531
|
+
snapshotCacheMs;
|
|
4532
|
+
launchCompletionTimeoutMs;
|
|
4533
|
+
coordinatorTimer;
|
|
4534
|
+
coordinatorRefresh;
|
|
4535
|
+
cachedSnapshot;
|
|
4536
|
+
snapshotInFlight;
|
|
4537
|
+
launchQueue = Promise.resolve();
|
|
3764
4538
|
constructor(options = {}) {
|
|
3765
4539
|
this.registry = options.registry ?? new ManagedInstanceRegistry(options.registryDir);
|
|
3766
4540
|
this.processAdapter = options.processAdapter ?? {};
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
this.
|
|
4541
|
+
this.confirmedExitMisses = options.confirmedExitMisses ?? 2;
|
|
4542
|
+
this.confirmedExitGraceMs = options.confirmedExitGraceMs ?? 5e3;
|
|
4543
|
+
this.snapshotCacheMs = options.snapshotCacheMs ?? 0;
|
|
4544
|
+
this.launchCompletionTimeoutMs = options.launchCompletionTimeoutMs ?? LAUNCH_COMPLETION_TIMEOUT_MS;
|
|
4545
|
+
}
|
|
4546
|
+
async list() {
|
|
4547
|
+
const snapshot = await this.getProcessSnapshot();
|
|
4548
|
+
await this.sweepRegistry(snapshot);
|
|
3770
4549
|
for (const record of [...this.managedByInstanceId.values(), ...this.pending]) {
|
|
3771
|
-
this.refresh(record);
|
|
4550
|
+
await this.refresh(record, snapshot);
|
|
3772
4551
|
}
|
|
3773
4552
|
const records = [...this.managedByInstanceId.values(), ...this.pending];
|
|
3774
|
-
for (const registryRecord of this.registry.
|
|
4553
|
+
for (const registryRecord of await this.registry.listOpenUnchecked()) {
|
|
3775
4554
|
const record = this.fromRegistryRecord(registryRecord);
|
|
3776
4555
|
if (records.some((existing) => record.recordId && existing.recordId === record.recordId || record.instanceId && existing.instanceId === record.instanceId)) {
|
|
3777
4556
|
continue;
|
|
@@ -3780,27 +4559,86 @@ var init_studio_instance_manager = __esm({
|
|
|
3780
4559
|
}
|
|
3781
4560
|
return records.filter((record) => record.closedAt === void 0).filter((instance, index, all) => all.indexOf(instance) === index);
|
|
3782
4561
|
}
|
|
3783
|
-
get(instanceId) {
|
|
3784
|
-
this.
|
|
4562
|
+
async get(instanceId) {
|
|
4563
|
+
const snapshot = await this.getProcessSnapshot();
|
|
4564
|
+
await this.sweepRegistry(snapshot);
|
|
3785
4565
|
const memoryRecord = this.managedByInstanceId.get(instanceId);
|
|
3786
4566
|
if (memoryRecord)
|
|
3787
|
-
return this.refresh(memoryRecord);
|
|
3788
|
-
const registryRecord = this.registry.findAnyByInstanceId(instanceId);
|
|
3789
|
-
return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
|
|
4567
|
+
return this.refresh(memoryRecord, snapshot);
|
|
4568
|
+
const registryRecord = await this.registry.findAnyByInstanceId(instanceId);
|
|
4569
|
+
return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord), snapshot) : void 0;
|
|
3790
4570
|
}
|
|
3791
|
-
getByLaunchId(launchId) {
|
|
3792
|
-
this.
|
|
4571
|
+
async getByLaunchId(launchId) {
|
|
4572
|
+
const snapshot = await this.getProcessSnapshot();
|
|
4573
|
+
await this.sweepRegistry(snapshot);
|
|
3793
4574
|
const memoryRecord = [...this.managedByInstanceId.values(), ...this.pending].find((record) => record.recordId === launchId);
|
|
3794
4575
|
if (memoryRecord)
|
|
3795
|
-
return this.refresh(memoryRecord);
|
|
3796
|
-
const registryRecord = this.registry.findAnyByRecordId(launchId
|
|
3797
|
-
return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
|
|
4576
|
+
return this.refresh(memoryRecord, snapshot);
|
|
4577
|
+
const registryRecord = await this.registry.findAnyByRecordId(launchId);
|
|
4578
|
+
return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord), snapshot) : void 0;
|
|
4579
|
+
}
|
|
4580
|
+
peekByLaunchId(launchId) {
|
|
4581
|
+
return [...this.managedByInstanceId.values(), ...this.pending].find((record) => record.recordId === launchId);
|
|
4582
|
+
}
|
|
4583
|
+
async authorizeByLaunchId(launchId) {
|
|
4584
|
+
const record = this.peekByLaunchId(launchId);
|
|
4585
|
+
if (!record)
|
|
4586
|
+
throw new Error(`Launch ${launchId} is not retained by this broker process.`);
|
|
4587
|
+
if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited") {
|
|
4588
|
+
throw new Error(`Launch ${launchId} is no longer eligible for process authorization.`);
|
|
4589
|
+
}
|
|
4590
|
+
if (record.processAuthorizationState === "authorized")
|
|
4591
|
+
return record;
|
|
4592
|
+
const control = this.launchControls.get(launchId);
|
|
4593
|
+
if (record.processAuthorizationState !== "pending" || !control) {
|
|
4594
|
+
throw new Error(`Launch ${launchId} has no retained process authorization handle.`);
|
|
4595
|
+
}
|
|
4596
|
+
try {
|
|
4597
|
+
this.armLaunchCompletionTimer(record);
|
|
4598
|
+
await control.authorize();
|
|
4599
|
+
record.processAuthorizationState = "authorized";
|
|
4600
|
+
await this.persist(record);
|
|
4601
|
+
this.armLaunchCompletionTimer(record);
|
|
4602
|
+
return record;
|
|
4603
|
+
} catch (error) {
|
|
4604
|
+
record.processAuthorizationState = "pending";
|
|
4605
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4606
|
+
await this.markFailed(record, `Studio process authorization failed: ${detail}`);
|
|
4607
|
+
throw error;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
async completeByLaunchId(launchId) {
|
|
4611
|
+
const record = this.peekByLaunchId(launchId);
|
|
4612
|
+
if (!record)
|
|
4613
|
+
throw new Error(`Launch ${launchId} is not retained by this broker process.`);
|
|
4614
|
+
if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited") {
|
|
4615
|
+
throw new Error(`Launch ${launchId} is no longer eligible for ownership release.`);
|
|
4616
|
+
}
|
|
4617
|
+
if (record.processAuthorizationState === "released")
|
|
4618
|
+
return record;
|
|
4619
|
+
const control = this.launchControls.get(launchId);
|
|
4620
|
+
if (record.processAuthorizationState !== "authorized" || !control) {
|
|
4621
|
+
throw new Error(`Launch ${launchId} has no authorized ownership handle to release.`);
|
|
4622
|
+
}
|
|
4623
|
+
try {
|
|
4624
|
+
this.clearLaunchCompletionTimer(record);
|
|
4625
|
+
await control.release();
|
|
4626
|
+
record.processAuthorizationState = "released";
|
|
4627
|
+
this.launchControls.delete(launchId);
|
|
4628
|
+
await this.persist(record);
|
|
4629
|
+
return record;
|
|
4630
|
+
} catch (error) {
|
|
4631
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4632
|
+
await this.markFailed(record, `Studio process ownership release failed: ${detail}`);
|
|
4633
|
+
throw error;
|
|
4634
|
+
}
|
|
3798
4635
|
}
|
|
3799
|
-
pendingLaunches() {
|
|
4636
|
+
async pendingLaunches() {
|
|
3800
4637
|
const now = Date.now();
|
|
4638
|
+
const bootId = await this.getCurrentBootId();
|
|
3801
4639
|
const records = [...this.pending];
|
|
3802
|
-
for (const registryRecord of this.registry.listOpenUnchecked()) {
|
|
3803
|
-
if (registryRecord.bootId !==
|
|
4640
|
+
for (const registryRecord of await this.registry.listOpenUnchecked()) {
|
|
4641
|
+
if (registryRecord.bootId !== bootId)
|
|
3804
4642
|
continue;
|
|
3805
4643
|
if (records.some((record) => record.recordId === registryRecord.recordId))
|
|
3806
4644
|
continue;
|
|
@@ -3808,8 +4646,9 @@ var init_studio_instance_manager = __esm({
|
|
|
3808
4646
|
}
|
|
3809
4647
|
return records.filter((record) => record.instanceId === void 0).filter((record) => record.state === "launching").filter((record) => record.connectionDeadlineAt === void 0 || record.connectionDeadlineAt > now);
|
|
3810
4648
|
}
|
|
3811
|
-
attachInstanceId(record, instanceId) {
|
|
3812
|
-
this.
|
|
4649
|
+
async attachInstanceId(record, instanceId) {
|
|
4650
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4651
|
+
await this.reconcileFromPositiveEvidence(record, snapshot);
|
|
3813
4652
|
if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited")
|
|
3814
4653
|
return;
|
|
3815
4654
|
if (record.instanceId && record.instanceId !== instanceId)
|
|
@@ -3820,47 +4659,75 @@ var init_studio_instance_manager = __esm({
|
|
|
3820
4659
|
this.clearConnectionTimer(record);
|
|
3821
4660
|
this.pending.delete(record);
|
|
3822
4661
|
this.managedByInstanceId.set(instanceId, record);
|
|
3823
|
-
this.persist(record);
|
|
4662
|
+
await this.persist(record);
|
|
3824
4663
|
}
|
|
3825
|
-
markFailed(record, reason) {
|
|
3826
|
-
|
|
3827
|
-
if (record.closedAt !== void 0 || record.state !== "launching")
|
|
4664
|
+
async markFailed(record, reason) {
|
|
4665
|
+
if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited")
|
|
3828
4666
|
return record;
|
|
3829
4667
|
record.state = "failed";
|
|
3830
4668
|
record.failedAt = Date.now();
|
|
3831
4669
|
record.failureReason = reason;
|
|
3832
4670
|
this.clearConnectionTimer(record);
|
|
3833
|
-
this.
|
|
4671
|
+
this.clearLaunchCompletionTimer(record);
|
|
4672
|
+
const control = record.recordId ? this.launchControls.get(record.recordId) : void 0;
|
|
4673
|
+
if (record.processAuthorizationState !== "released" && control) {
|
|
4674
|
+
try {
|
|
4675
|
+
await control.abort();
|
|
4676
|
+
const exitedAt = Date.now();
|
|
4677
|
+
record.exitedAt = exitedAt;
|
|
4678
|
+
record.closedAt = exitedAt;
|
|
4679
|
+
record.processObservationStatus = "not_running";
|
|
4680
|
+
record.lastProcessObservationAt = exitedAt;
|
|
4681
|
+
record.lastSuccessfulProcessObservationAt = exitedAt;
|
|
4682
|
+
this.cleanupManagedRecord(record);
|
|
4683
|
+
this.markClosedInMemory(record);
|
|
4684
|
+
} catch (error) {
|
|
4685
|
+
record.failureReason += ` Exact suspended-process cleanup also failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
await this.persist(record);
|
|
3834
4689
|
return record;
|
|
3835
4690
|
}
|
|
3836
|
-
refresh(record) {
|
|
4691
|
+
async refresh(record, providedSnapshot) {
|
|
4692
|
+
if (record.closedAt !== void 0)
|
|
4693
|
+
return record;
|
|
4694
|
+
const snapshot = providedSnapshot ?? await this.getProcessSnapshot();
|
|
4695
|
+
await this.applyProcessObservation(record, this.observeRecord(record, snapshot));
|
|
3837
4696
|
if (record.closedAt !== void 0)
|
|
3838
4697
|
return record;
|
|
3839
|
-
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
3840
|
-
const studioProcess = processId ? this.findProcessById(processId) : void 0;
|
|
3841
|
-
if (processId && (!studioProcess || !this.verifyProcessForRecord(record, studioProcess))) {
|
|
3842
|
-
return this.markProcessExited(record, void 0, studioProcess ? "Studio process identity changed; the retained PID was not reused." : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
|
|
3843
|
-
}
|
|
3844
4698
|
if (record.state === "launching" && record.connectionDeadlineAt !== void 0 && Date.now() >= record.connectionDeadlineAt) {
|
|
3845
|
-
record
|
|
3846
|
-
record.failedAt = Date.now();
|
|
3847
|
-
record.failureReason = "Studio launched, but the MCP plugin did not connect before timeout.";
|
|
3848
|
-
this.clearConnectionTimer(record);
|
|
3849
|
-
this.persist(record);
|
|
4699
|
+
return this.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
|
|
3850
4700
|
}
|
|
3851
4701
|
return record;
|
|
3852
4702
|
}
|
|
3853
4703
|
async launch(options) {
|
|
3854
|
-
this.
|
|
4704
|
+
const previous = this.launchQueue;
|
|
4705
|
+
let release;
|
|
4706
|
+
this.launchQueue = new Promise((resolve5) => {
|
|
4707
|
+
release = resolve5;
|
|
4708
|
+
});
|
|
4709
|
+
await previous;
|
|
4710
|
+
try {
|
|
4711
|
+
return await this.launchSerialized(options);
|
|
4712
|
+
} finally {
|
|
4713
|
+
release();
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
async launchSerialized(options) {
|
|
4717
|
+
const initialSnapshot = await this.getProcessSnapshot(true);
|
|
4718
|
+
await this.sweepRegistry(initialSnapshot);
|
|
3855
4719
|
const processEnvironment = parseStudioProcessEnvironmentPatch(options.processEnvironment);
|
|
3856
4720
|
if (options.studioExecutable !== void 0 && (typeof options.studioExecutable !== "string" || options.studioExecutable.length === 0 || options.studioExecutable.includes("\0"))) {
|
|
3857
4721
|
throw new Error("studio_executable must be a non-empty string without null characters.");
|
|
3858
4722
|
}
|
|
4723
|
+
if (options.requireProcessIdentity && !this.processAdapter.spawnStudio && process.platform !== "win32" && !isWsl()) {
|
|
4724
|
+
throw new Error("require_process_identity is supported only by the identity-retaining Windows launcher or a custom process adapter.");
|
|
4725
|
+
}
|
|
3859
4726
|
const preparedOptions = prepareStudioLaunchOptions(options);
|
|
3860
|
-
const bootId = this.getCurrentBootId();
|
|
3861
|
-
const before = new Set(
|
|
3862
|
-
const exe = preparedOptions.studioExecutable ?? this.processAdapter.resolveStudioExe
|
|
3863
|
-
const args = buildStudioLaunchArgs(preparedOptions).map(
|
|
4727
|
+
const bootId = await this.getCurrentBootId();
|
|
4728
|
+
const before = new Set(initialSnapshot.status === "ok" ? initialSnapshot.processes.map((proc2) => proc2.Id) : []);
|
|
4729
|
+
const exe = preparedOptions.studioExecutable ?? (this.processAdapter.resolveStudioExe ? await this.processAdapter.resolveStudioExe() : await resolveStudioExeAsync());
|
|
4730
|
+
const args = await Promise.all(buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArgAsync));
|
|
3864
4731
|
const spawnOptions = {
|
|
3865
4732
|
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
3866
4733
|
detached: true,
|
|
@@ -3870,9 +4737,9 @@ var init_studio_instance_manager = __esm({
|
|
|
3870
4737
|
let proc;
|
|
3871
4738
|
try {
|
|
3872
4739
|
if (this.processAdapter.spawnStudio) {
|
|
3873
|
-
proc = this.processAdapter.spawnStudio(exe, args, spawnOptions);
|
|
3874
|
-
} else if (isWsl()) {
|
|
3875
|
-
proc =
|
|
4740
|
+
proc = await this.processAdapter.spawnStudio(exe, args, spawnOptions);
|
|
4741
|
+
} else if (process.platform === "win32" || isWsl()) {
|
|
4742
|
+
proc = await spawnWindowsStudio(exe, args, processEnvironment);
|
|
3876
4743
|
} else {
|
|
3877
4744
|
const child = spawn(exe, args, spawnOptions);
|
|
3878
4745
|
proc = {
|
|
@@ -3891,8 +4758,40 @@ var init_studio_instance_manager = __esm({
|
|
|
3891
4758
|
cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
|
|
3892
4759
|
throw error;
|
|
3893
4760
|
}
|
|
4761
|
+
if (options.requireProcessIdentity && (!Number.isSafeInteger(proc.nativePid) || (proc.nativePid ?? 0) <= 0 || proc.nativeStartedAt === void 0 || !/^[1-9]\d*$/u.test(proc.nativeStartedAt) || !proc.authorize || !proc.release || !proc.abort)) {
|
|
4762
|
+
const reason = "Studio launcher did not return an exact creation identity and suspended-process control handles.";
|
|
4763
|
+
let abortError;
|
|
4764
|
+
try {
|
|
4765
|
+
if (proc.abort) {
|
|
4766
|
+
await proc.abort();
|
|
4767
|
+
} else if (proc.nativePid && proc.nativeStartedAt) {
|
|
4768
|
+
await this.closeProcess(proc.nativePid, proc.nativeStartedAt);
|
|
4769
|
+
} else {
|
|
4770
|
+
throw new Error("the process adapter did not retain an abort handle or exact process identity");
|
|
4771
|
+
}
|
|
4772
|
+
} catch (error) {
|
|
4773
|
+
abortError = error;
|
|
4774
|
+
}
|
|
4775
|
+
cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
|
|
4776
|
+
const abortDetail = abortError ? ` Exact child cleanup also failed: ${abortError instanceof Error ? abortError.message : String(abortError)}` : " The owned process was stopped.";
|
|
4777
|
+
throw new Error(`${reason}${abortDetail}`);
|
|
4778
|
+
}
|
|
4779
|
+
if (!options.requireProcessIdentity && proc.authorize) {
|
|
4780
|
+
try {
|
|
4781
|
+
await proc.authorize();
|
|
4782
|
+
await proc.release?.();
|
|
4783
|
+
} catch (error) {
|
|
4784
|
+
try {
|
|
4785
|
+
await proc.abort?.();
|
|
4786
|
+
} finally {
|
|
4787
|
+
cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
|
|
4788
|
+
}
|
|
4789
|
+
throw error;
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
const launchedAt = Date.now();
|
|
3894
4793
|
const record = {
|
|
3895
|
-
recordId:
|
|
4794
|
+
recordId: randomUUID2(),
|
|
3896
4795
|
source: options.source,
|
|
3897
4796
|
nativeProcessId: proc.nativePid,
|
|
3898
4797
|
nativeProcessStartedAt: proc.nativeStartedAt,
|
|
@@ -3903,26 +4802,41 @@ var init_studio_instance_manager = __esm({
|
|
|
3903
4802
|
universeId: preparedOptions.universeId,
|
|
3904
4803
|
placeVersion: preparedOptions.placeVersion,
|
|
3905
4804
|
localPlaceFile: preparedOptions.localPlaceFile,
|
|
3906
|
-
launchedAt
|
|
3907
|
-
connectionDeadlineAt:
|
|
4805
|
+
launchedAt,
|
|
4806
|
+
connectionDeadlineAt: options.requireProcessIdentity ? void 0 : launchedAt + (options.connectionTimeoutMs ?? 12e4),
|
|
3908
4807
|
state: "launching",
|
|
3909
4808
|
ownerPid: process.pid,
|
|
3910
4809
|
bootId,
|
|
3911
|
-
deleteLocalPlaceFileOnClose: options.source === "baseplate"
|
|
4810
|
+
deleteLocalPlaceFileOnClose: options.source === "baseplate",
|
|
4811
|
+
processAuthorizationState: options.requireProcessIdentity && proc.authorize && proc.release ? "pending" : "released",
|
|
4812
|
+
processObservationStatus: "running",
|
|
4813
|
+
lastProcessObservationAt: launchedAt,
|
|
4814
|
+
lastSuccessfulProcessObservationAt: launchedAt,
|
|
4815
|
+
consecutiveConfirmedMisses: 0
|
|
3912
4816
|
};
|
|
3913
4817
|
this.pending.add(record);
|
|
3914
4818
|
try {
|
|
3915
|
-
this.persist(record);
|
|
4819
|
+
await this.persist(record);
|
|
4820
|
+
if (record.recordId && record.processAuthorizationState === "pending" && proc.authorize && proc.release && proc.abort) {
|
|
4821
|
+
this.launchControls.set(record.recordId, {
|
|
4822
|
+
authorize: proc.authorize,
|
|
4823
|
+
release: proc.release,
|
|
4824
|
+
abort: proc.abort
|
|
4825
|
+
});
|
|
4826
|
+
this.armLaunchCompletionTimer(record);
|
|
4827
|
+
}
|
|
3916
4828
|
} catch (error) {
|
|
3917
4829
|
this.pending.delete(record);
|
|
3918
4830
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
3919
4831
|
let stopError;
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
}
|
|
3924
|
-
|
|
4832
|
+
try {
|
|
4833
|
+
if (proc.abort) {
|
|
4834
|
+
await proc.abort();
|
|
4835
|
+
} else if (processId) {
|
|
4836
|
+
await this.closeProcess(processId, record.nativeProcessStartedAt);
|
|
3925
4837
|
}
|
|
4838
|
+
} catch (caught) {
|
|
4839
|
+
stopError = caught;
|
|
3926
4840
|
}
|
|
3927
4841
|
cleanupManagedBaseplateFiles(record);
|
|
3928
4842
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -3931,38 +4845,40 @@ var init_studio_instance_manager = __esm({
|
|
|
3931
4845
|
}
|
|
3932
4846
|
proc.unref();
|
|
3933
4847
|
proc.onExit?.((code, signal) => {
|
|
3934
|
-
this.markProcessExited(record, code ?? void 0, signal ? `Studio process exited from signal ${signal}.` : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
|
|
4848
|
+
this.runInBackground("persisting a Studio process exit", this.markProcessExited(record, code ?? void 0, signal ? `Studio process exited from signal ${signal}.` : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected."));
|
|
3935
4849
|
});
|
|
3936
4850
|
proc.onError?.((error) => {
|
|
3937
|
-
this.markFailed(record, `Studio process failed to start: ${error.message}`);
|
|
4851
|
+
this.runInBackground("persisting a Studio process launch failure", this.markFailed(record, `Studio process failed to start: ${error.message}`));
|
|
3938
4852
|
});
|
|
3939
4853
|
const deadline = Date.now() + 5e3;
|
|
3940
4854
|
while (Date.now() < deadline && record.nativeProcessId === void 0) {
|
|
3941
|
-
const
|
|
4855
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4856
|
+
const created = snapshot.status === "ok" ? snapshot.processes.find((candidate) => !before.has(candidate.Id)) : void 0;
|
|
3942
4857
|
if (created) {
|
|
3943
4858
|
record.nativeProcessId = created.Id;
|
|
3944
4859
|
record.nativeProcessStartedAt = created.StartTimeUtcFileTime;
|
|
3945
|
-
this.persist(record);
|
|
4860
|
+
await this.persist(record);
|
|
3946
4861
|
break;
|
|
3947
4862
|
}
|
|
3948
|
-
await
|
|
4863
|
+
await delay2(250);
|
|
3949
4864
|
}
|
|
3950
4865
|
if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
|
|
3951
4866
|
record.nativeProcessId = proc.pid;
|
|
3952
|
-
this.persist(record);
|
|
4867
|
+
await this.persist(record);
|
|
3953
4868
|
}
|
|
3954
4869
|
if (record.nativeProcessId !== void 0 && record.nativeProcessStartedAt === void 0) {
|
|
3955
|
-
const
|
|
4870
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4871
|
+
const nativeProcess = snapshot.status === "ok" ? snapshot.processes.find((candidate) => candidate.Id === record.nativeProcessId) : void 0;
|
|
3956
4872
|
if (nativeProcess?.StartTimeUtcFileTime !== void 0) {
|
|
3957
4873
|
record.nativeProcessStartedAt = nativeProcess.StartTimeUtcFileTime;
|
|
3958
|
-
this.persist(record);
|
|
4874
|
+
await this.persist(record);
|
|
3959
4875
|
}
|
|
3960
4876
|
}
|
|
3961
|
-
this.
|
|
4877
|
+
this.startCoordinator(record);
|
|
3962
4878
|
return record;
|
|
3963
4879
|
}
|
|
3964
|
-
closeByLaunchId(launchId) {
|
|
3965
|
-
const record = this.getByLaunchId(launchId);
|
|
4880
|
+
async closeByLaunchId(launchId) {
|
|
4881
|
+
const record = await this.getByLaunchId(launchId);
|
|
3966
4882
|
if (!record)
|
|
3967
4883
|
return { status: "not_found", launchId };
|
|
3968
4884
|
if (record.closedAt !== void 0) {
|
|
@@ -3970,19 +4886,19 @@ var init_studio_instance_manager = __esm({
|
|
|
3970
4886
|
}
|
|
3971
4887
|
return this.close(record);
|
|
3972
4888
|
}
|
|
3973
|
-
closeByInstanceId(instanceId) {
|
|
3974
|
-
this.
|
|
4889
|
+
async closeByInstanceId(instanceId) {
|
|
4890
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4891
|
+
await this.sweepRegistry(snapshot);
|
|
3975
4892
|
const memoryRecord = this.managedByInstanceId.get(instanceId);
|
|
3976
4893
|
if (memoryRecord)
|
|
3977
4894
|
return this.close(memoryRecord);
|
|
3978
|
-
const registryRecord = this.registry.findAnyByInstanceId(instanceId);
|
|
4895
|
+
const registryRecord = await this.registry.findAnyByInstanceId(instanceId);
|
|
3979
4896
|
if (!registryRecord) {
|
|
3980
|
-
this.sweepRegistry();
|
|
3981
4897
|
return { status: "not_found", instanceId };
|
|
3982
4898
|
}
|
|
3983
4899
|
if (registryRecord.closedAt !== void 0) {
|
|
3984
4900
|
this.cleanupManagedRecord(registryRecord);
|
|
3985
|
-
this.registry.logEvent({
|
|
4901
|
+
await this.registry.logEvent({
|
|
3986
4902
|
event: "registry_close_already_stopped",
|
|
3987
4903
|
recordId: registryRecord.recordId,
|
|
3988
4904
|
instanceId: registryRecord.instanceId,
|
|
@@ -3994,46 +4910,69 @@ var init_studio_instance_manager = __esm({
|
|
|
3994
4910
|
}
|
|
3995
4911
|
return this.close(this.fromRegistryRecord(registryRecord));
|
|
3996
4912
|
}
|
|
3997
|
-
close(record) {
|
|
3998
|
-
this.stopMonitor(record);
|
|
3999
|
-
this.refresh(record);
|
|
4913
|
+
async close(record) {
|
|
4000
4914
|
if (record.closedAt !== void 0) {
|
|
4001
|
-
return {
|
|
4915
|
+
return {
|
|
4916
|
+
status: "already_closed",
|
|
4917
|
+
launchId: record.recordId,
|
|
4918
|
+
instanceId: record.instanceId
|
|
4919
|
+
};
|
|
4002
4920
|
}
|
|
4003
4921
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
4004
4922
|
if (!processId) {
|
|
4005
4923
|
throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
|
|
4006
4924
|
}
|
|
4007
|
-
const
|
|
4008
|
-
if (
|
|
4925
|
+
const control = record.recordId ? this.launchControls.get(record.recordId) : void 0;
|
|
4926
|
+
if (record.processAuthorizationState !== "released" && control) {
|
|
4927
|
+
await control.abort();
|
|
4928
|
+
const closedAt2 = Date.now();
|
|
4929
|
+
record.closedAt = closedAt2;
|
|
4930
|
+
record.exitedAt = closedAt2;
|
|
4931
|
+
if (record.state !== "failed")
|
|
4932
|
+
record.state = "exited";
|
|
4933
|
+
record.processObservationStatus = "not_running";
|
|
4934
|
+
record.lastProcessObservationAt = closedAt2;
|
|
4935
|
+
record.lastSuccessfulProcessObservationAt = closedAt2;
|
|
4936
|
+
record.lastProcessObservationError = void 0;
|
|
4009
4937
|
this.cleanupManagedRecord(record);
|
|
4010
|
-
this.
|
|
4011
|
-
this.
|
|
4938
|
+
this.markClosedInMemory(record);
|
|
4939
|
+
await this.persist(record);
|
|
4940
|
+
return {
|
|
4941
|
+
status: "closed",
|
|
4942
|
+
launchId: record.recordId,
|
|
4943
|
+
instanceId: record.instanceId
|
|
4944
|
+
};
|
|
4945
|
+
}
|
|
4946
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4947
|
+
const observation = this.observeRecord(record, snapshot);
|
|
4948
|
+
if (observation.status === "unknown") {
|
|
4949
|
+
await this.applyProcessObservation(record, observation);
|
|
4950
|
+
throw new Error(`Cannot verify the managed Studio process because process observation failed: ${observation.error}`);
|
|
4951
|
+
}
|
|
4952
|
+
if (observation.status === "not_running") {
|
|
4953
|
+
await this.markProcessExited(record, void 0, observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : record.failureReason);
|
|
4954
|
+
await this.registry.logEvent({
|
|
4012
4955
|
event: "registry_close_already_stopped",
|
|
4013
4956
|
recordId: record.recordId,
|
|
4014
4957
|
instanceId: record.instanceId,
|
|
4015
4958
|
source: record.source,
|
|
4016
|
-
reason: "pid_not_running",
|
|
4959
|
+
reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
|
|
4017
4960
|
action: "marked_closed_and_cleaned_baseplate"
|
|
4018
4961
|
});
|
|
4019
|
-
return {
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
recordId: record.recordId,
|
|
4025
|
-
instanceId: record.instanceId,
|
|
4026
|
-
source: record.source,
|
|
4027
|
-
reason: "identity_mismatch"
|
|
4028
|
-
});
|
|
4029
|
-
throw new Error("Managed Studio process identity could not be verified.");
|
|
4962
|
+
return {
|
|
4963
|
+
status: "already_closed",
|
|
4964
|
+
launchId: record.recordId,
|
|
4965
|
+
instanceId: record.instanceId
|
|
4966
|
+
};
|
|
4030
4967
|
}
|
|
4031
4968
|
try {
|
|
4032
|
-
this.closeProcess(processId);
|
|
4969
|
+
await this.closeProcess(processId, record.nativeProcessStartedAt);
|
|
4033
4970
|
} catch (error) {
|
|
4034
|
-
|
|
4971
|
+
const retry = await this.getProcessSnapshot(true);
|
|
4972
|
+
const retryObservation = this.observeRecord(record, retry);
|
|
4973
|
+
if (retryObservation.status === "running" || retryObservation.status === "unknown")
|
|
4035
4974
|
throw error;
|
|
4036
|
-
this.registry.logEvent({
|
|
4975
|
+
await this.registry.logEvent({
|
|
4037
4976
|
event: "registry_close_already_stopped",
|
|
4038
4977
|
recordId: record.recordId,
|
|
4039
4978
|
instanceId: record.instanceId,
|
|
@@ -4041,34 +4980,52 @@ var init_studio_instance_manager = __esm({
|
|
|
4041
4980
|
reason: "stop_raced_with_exit",
|
|
4042
4981
|
action: "marked_closed_and_cleaned_baseplate"
|
|
4043
4982
|
});
|
|
4044
|
-
this.
|
|
4045
|
-
|
|
4046
|
-
|
|
4983
|
+
await this.markProcessExited(record, void 0, record.failureReason);
|
|
4984
|
+
return {
|
|
4985
|
+
status: "already_closed",
|
|
4986
|
+
launchId: record.recordId,
|
|
4987
|
+
instanceId: record.instanceId
|
|
4988
|
+
};
|
|
4047
4989
|
}
|
|
4048
4990
|
const closedAt = Date.now();
|
|
4049
4991
|
record.closedAt = closedAt;
|
|
4050
4992
|
record.exitedAt = record.exitedAt ?? closedAt;
|
|
4051
4993
|
if (record.state !== "failed")
|
|
4052
4994
|
record.state = "exited";
|
|
4995
|
+
record.processObservationStatus = "not_running";
|
|
4996
|
+
record.lastProcessObservationAt = closedAt;
|
|
4997
|
+
record.lastSuccessfulProcessObservationAt = closedAt;
|
|
4998
|
+
record.lastProcessObservationError = void 0;
|
|
4053
4999
|
this.cleanupManagedRecord(record);
|
|
4054
5000
|
this.markClosedInMemory(record);
|
|
4055
|
-
this.persist(record);
|
|
4056
|
-
return {
|
|
5001
|
+
await this.persist(record);
|
|
5002
|
+
return {
|
|
5003
|
+
status: "closed",
|
|
5004
|
+
launchId: record.recordId,
|
|
5005
|
+
instanceId: record.instanceId
|
|
5006
|
+
};
|
|
4057
5007
|
}
|
|
4058
|
-
closeConnectedInstance(instance) {
|
|
4059
|
-
const
|
|
5008
|
+
async closeConnectedInstance(instance) {
|
|
5009
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
5010
|
+
if (snapshot.status === "error") {
|
|
5011
|
+
throw new Error(`Could not enumerate Studio processes: ${snapshot.error}`);
|
|
5012
|
+
}
|
|
5013
|
+
const process2 = this.findProcessForConnectedInstance(instance, snapshot.processes);
|
|
4060
5014
|
if (!process2) {
|
|
4061
5015
|
throw new Error(`Could not find a Studio process for connected instance "${instance.instanceId}".`);
|
|
4062
5016
|
}
|
|
4063
|
-
this.closeProcess(process2.Id);
|
|
5017
|
+
await this.closeProcess(process2.Id, process2.StartTimeUtcFileTime);
|
|
4064
5018
|
}
|
|
4065
|
-
closeProcess(processId) {
|
|
5019
|
+
async closeProcess(processId, startedAt) {
|
|
4066
5020
|
if (this.processAdapter.stopProcess) {
|
|
4067
|
-
this.processAdapter.stopProcess(processId);
|
|
5021
|
+
await this.processAdapter.stopProcess(processId, startedAt);
|
|
4068
5022
|
return;
|
|
4069
5023
|
}
|
|
4070
5024
|
if (process.platform === "win32" || isWsl()) {
|
|
4071
|
-
|
|
5025
|
+
if (startedAt === void 0) {
|
|
5026
|
+
throw new Error("Cannot stop a Windows Studio process without its creation-time identity.");
|
|
5027
|
+
}
|
|
5028
|
+
await stopWindowsStudio(processId, startedAt);
|
|
4072
5029
|
} else {
|
|
4073
5030
|
try {
|
|
4074
5031
|
process.kill(processId, "SIGTERM");
|
|
@@ -4078,8 +5035,7 @@ var init_studio_instance_manager = __esm({
|
|
|
4078
5035
|
}
|
|
4079
5036
|
}
|
|
4080
5037
|
}
|
|
4081
|
-
findProcessForConnectedInstance(instance) {
|
|
4082
|
-
const processes = this.listStudioProcesses();
|
|
5038
|
+
findProcessForConnectedInstance(instance, processes) {
|
|
4083
5039
|
if (processes.length === 0)
|
|
4084
5040
|
return void 0;
|
|
4085
5041
|
if (processes.length === 1)
|
|
@@ -4098,31 +5054,109 @@ var init_studio_instance_manager = __esm({
|
|
|
4098
5054
|
}
|
|
4099
5055
|
return void 0;
|
|
4100
5056
|
}
|
|
4101
|
-
|
|
4102
|
-
|
|
5057
|
+
async getProcessSnapshot(force = false) {
|
|
5058
|
+
const now = Date.now();
|
|
5059
|
+
if (!force && this.snapshotCacheMs > 0 && this.cachedSnapshot && now - this.cachedSnapshot.observedAt <= this.snapshotCacheMs) {
|
|
5060
|
+
return this.cachedSnapshot;
|
|
5061
|
+
}
|
|
5062
|
+
if (this.snapshotInFlight)
|
|
5063
|
+
return this.snapshotInFlight;
|
|
5064
|
+
this.snapshotInFlight = (async () => {
|
|
5065
|
+
try {
|
|
5066
|
+
let snapshot;
|
|
5067
|
+
if (this.processAdapter.observeStudioProcesses) {
|
|
5068
|
+
snapshot = await this.processAdapter.observeStudioProcesses();
|
|
5069
|
+
} else if (this.processAdapter.listStudioProcesses) {
|
|
5070
|
+
const observedAt = Date.now();
|
|
5071
|
+
const processes = await this.processAdapter.listStudioProcesses();
|
|
5072
|
+
snapshot = { status: "ok", observedAt, processes };
|
|
5073
|
+
} else {
|
|
5074
|
+
snapshot = await observeStudioProcesses();
|
|
5075
|
+
}
|
|
5076
|
+
this.cachedSnapshot = snapshot;
|
|
5077
|
+
return snapshot;
|
|
5078
|
+
} catch (error) {
|
|
5079
|
+
const snapshot = {
|
|
5080
|
+
status: "error",
|
|
5081
|
+
observedAt: Date.now(),
|
|
5082
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5083
|
+
};
|
|
5084
|
+
this.cachedSnapshot = snapshot;
|
|
5085
|
+
return snapshot;
|
|
5086
|
+
} finally {
|
|
5087
|
+
this.snapshotInFlight = void 0;
|
|
5088
|
+
}
|
|
5089
|
+
})();
|
|
5090
|
+
return this.snapshotInFlight;
|
|
4103
5091
|
}
|
|
4104
|
-
getCurrentBootId() {
|
|
4105
|
-
return this.processAdapter.currentBootId
|
|
5092
|
+
async getCurrentBootId() {
|
|
5093
|
+
return this.processAdapter.currentBootId ? await this.processAdapter.currentBootId() : currentBootIdAsync();
|
|
4106
5094
|
}
|
|
4107
|
-
registrySweepOptions() {
|
|
5095
|
+
async registrySweepOptions(snapshot) {
|
|
4108
5096
|
return {
|
|
4109
|
-
currentBootId: this.getCurrentBootId(),
|
|
4110
|
-
|
|
4111
|
-
cleanupRecord: (record) =>
|
|
5097
|
+
currentBootId: await this.getCurrentBootId(),
|
|
5098
|
+
observeProcess: (record) => this.observeRecord(this.fromRegistryRecord(record), snapshot),
|
|
5099
|
+
cleanupRecord: (record) => {
|
|
5100
|
+
if (record.processAuthorizationState !== "released" && this.launchControls.has(record.recordId))
|
|
5101
|
+
return;
|
|
5102
|
+
this.cleanupManagedRecord(record);
|
|
5103
|
+
},
|
|
5104
|
+
confirmedExitMisses: this.confirmedExitMisses,
|
|
5105
|
+
confirmedExitGraceMs: this.confirmedExitGraceMs
|
|
4112
5106
|
};
|
|
4113
5107
|
}
|
|
4114
|
-
sweepRegistry() {
|
|
4115
|
-
this.
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
5108
|
+
async sweepRegistry(snapshot) {
|
|
5109
|
+
const sweepOptions = await this.registrySweepOptions(snapshot);
|
|
5110
|
+
await this.registry.sweep(sweepOptions);
|
|
5111
|
+
const persisted = await this.registry.listOpenUnchecked();
|
|
5112
|
+
for (const registryRecord of persisted) {
|
|
5113
|
+
const ownerPid = registryRecord.ownerPid;
|
|
5114
|
+
if (registryRecord.processAuthorizationState === "released" || this.launchControls.has(registryRecord.recordId) || registryRecord.bootId !== sweepOptions.currentBootId || ownerPid === void 0 || ownerPid !== process.pid && isProcessAlive2(ownerPid)) {
|
|
5115
|
+
continue;
|
|
5116
|
+
}
|
|
5117
|
+
const record = this.fromRegistryRecord(registryRecord);
|
|
5118
|
+
if (this.observeRecord(record, snapshot).status !== "running")
|
|
5119
|
+
continue;
|
|
5120
|
+
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
5121
|
+
if (!processId || !record.nativeProcessStartedAt) {
|
|
5122
|
+
record.state = "failed";
|
|
5123
|
+
record.failedAt = Date.now();
|
|
5124
|
+
record.failureReason = "Orphaned unreleased Studio launch has no exact process identity for cleanup.";
|
|
5125
|
+
await this.persist(record);
|
|
5126
|
+
continue;
|
|
5127
|
+
}
|
|
5128
|
+
try {
|
|
5129
|
+
await this.closeProcess(processId, record.nativeProcessStartedAt);
|
|
5130
|
+
const closedAt = Date.now();
|
|
5131
|
+
record.state = "failed";
|
|
5132
|
+
record.failedAt = closedAt;
|
|
5133
|
+
record.failureReason = "Orphaned unreleased Studio launch was stopped after its broker owner exited.";
|
|
5134
|
+
record.exitedAt = closedAt;
|
|
5135
|
+
record.closedAt = closedAt;
|
|
5136
|
+
record.processObservationStatus = "not_running";
|
|
5137
|
+
record.lastProcessObservationAt = closedAt;
|
|
5138
|
+
record.lastSuccessfulProcessObservationAt = closedAt;
|
|
5139
|
+
this.cleanupManagedRecord(record);
|
|
5140
|
+
await this.persist(record);
|
|
5141
|
+
} catch (error) {
|
|
5142
|
+
record.state = "failed";
|
|
5143
|
+
record.failedAt = Date.now();
|
|
5144
|
+
record.failureReason = `Failed to stop orphaned unreleased Studio launch: ${error instanceof Error ? error.message : String(error)}`;
|
|
5145
|
+
await this.persist(record);
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
4119
5148
|
}
|
|
4120
|
-
|
|
5149
|
+
observeRecord(record, snapshot) {
|
|
5150
|
+
if (snapshot.status === "error") {
|
|
5151
|
+
return { status: "unknown", observedAt: snapshot.observedAt, error: snapshot.error };
|
|
5152
|
+
}
|
|
4121
5153
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
4122
5154
|
if (!processId)
|
|
4123
|
-
return
|
|
4124
|
-
const studioProcess =
|
|
4125
|
-
|
|
5155
|
+
return { status: "running", observedAt: snapshot.observedAt };
|
|
5156
|
+
const studioProcess = snapshot.processes.find((candidate) => candidate.Id === processId);
|
|
5157
|
+
if (!studioProcess)
|
|
5158
|
+
return { status: "not_running", observedAt: snapshot.observedAt, reason: "missing" };
|
|
5159
|
+
return this.verifyProcessForRecord(record, studioProcess) ? { status: "running", observedAt: snapshot.observedAt } : { status: "not_running", observedAt: snapshot.observedAt, reason: "identity_mismatch" };
|
|
4126
5160
|
}
|
|
4127
5161
|
verifyProcessForRecord(record, studioProcess) {
|
|
4128
5162
|
const processName = `${studioProcess.Name ?? ""} ${studioProcess.Path ?? ""}`.toLowerCase();
|
|
@@ -4147,62 +5181,104 @@ var init_studio_instance_manager = __esm({
|
|
|
4147
5181
|
return false;
|
|
4148
5182
|
}
|
|
4149
5183
|
cleanupManagedRecord(record) {
|
|
5184
|
+
if (record.recordId)
|
|
5185
|
+
this.launchControls.delete(record.recordId);
|
|
5186
|
+
if (record.recordId)
|
|
5187
|
+
this.clearLaunchCompletionTimer(record);
|
|
4150
5188
|
if (record.source !== "baseplate")
|
|
4151
5189
|
return;
|
|
4152
|
-
cleanupManagedBaseplateFiles({
|
|
5190
|
+
cleanupManagedBaseplateFiles({
|
|
5191
|
+
source: "baseplate",
|
|
5192
|
+
localPlaceFile: record.localPlaceFile
|
|
5193
|
+
});
|
|
4153
5194
|
}
|
|
4154
5195
|
markClosedInMemory(record) {
|
|
4155
5196
|
record.closedAt = record.closedAt ?? Date.now();
|
|
4156
5197
|
if (record.instanceId)
|
|
4157
5198
|
this.managedByInstanceId.delete(record.instanceId);
|
|
4158
5199
|
this.pending.delete(record);
|
|
4159
|
-
this.
|
|
5200
|
+
this.clearConnectionTimer(record);
|
|
5201
|
+
this.clearLaunchCompletionTimer(record);
|
|
5202
|
+
}
|
|
5203
|
+
armLaunchCompletionTimer(record) {
|
|
5204
|
+
if (!record.recordId)
|
|
5205
|
+
return;
|
|
5206
|
+
this.clearLaunchCompletionTimer(record);
|
|
5207
|
+
const timeout = setTimeout(() => {
|
|
5208
|
+
this.runInBackground("aborting an uncompleted Studio launch", this.markFailed(record, "Carbon did not complete Studio launch ownership transfer before timeout."));
|
|
5209
|
+
}, this.launchCompletionTimeoutMs);
|
|
5210
|
+
if (typeof timeout === "object" && "unref" in timeout)
|
|
5211
|
+
timeout.unref();
|
|
5212
|
+
this.launchCompletionTimers.set(record.recordId, timeout);
|
|
5213
|
+
}
|
|
5214
|
+
clearLaunchCompletionTimer(record) {
|
|
5215
|
+
if (!record.recordId)
|
|
5216
|
+
return;
|
|
5217
|
+
const timer = this.launchCompletionTimers.get(record.recordId);
|
|
5218
|
+
clearTimeout(timer);
|
|
5219
|
+
this.launchCompletionTimers.delete(record.recordId);
|
|
4160
5220
|
}
|
|
4161
|
-
markProcessExited(record, exitCode, reason) {
|
|
5221
|
+
async markProcessExited(record, exitCode, reason) {
|
|
4162
5222
|
if (record.closedAt !== void 0)
|
|
4163
5223
|
return record;
|
|
5224
|
+
const control = record.recordId ? this.launchControls.get(record.recordId) : void 0;
|
|
5225
|
+
let controlFailure;
|
|
5226
|
+
if (record.processAuthorizationState !== "released" && control) {
|
|
5227
|
+
try {
|
|
5228
|
+
await control.abort();
|
|
5229
|
+
} catch (error) {
|
|
5230
|
+
controlFailure = error instanceof Error ? error.message : String(error);
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
4164
5233
|
const exitedAt = Date.now();
|
|
4165
5234
|
record.exitedAt = exitedAt;
|
|
4166
5235
|
record.closedAt = exitedAt;
|
|
4167
5236
|
if (record.state !== "failed")
|
|
4168
5237
|
record.state = "exited";
|
|
5238
|
+
record.processObservationStatus = "not_running";
|
|
5239
|
+
record.lastProcessObservationAt = exitedAt;
|
|
5240
|
+
record.lastSuccessfulProcessObservationAt = exitedAt;
|
|
5241
|
+
record.lastProcessObservationError = void 0;
|
|
4169
5242
|
if (exitCode !== void 0)
|
|
4170
5243
|
record.exitCode = exitCode;
|
|
4171
5244
|
if (reason)
|
|
4172
5245
|
record.failureReason = reason;
|
|
5246
|
+
if (controlFailure) {
|
|
5247
|
+
record.failureReason = [
|
|
5248
|
+
record.failureReason,
|
|
5249
|
+
`Launch ownership cleanup failed: ${controlFailure}`
|
|
5250
|
+
].filter(Boolean).join(" ");
|
|
5251
|
+
}
|
|
4173
5252
|
this.cleanupManagedRecord(record);
|
|
4174
5253
|
this.markClosedInMemory(record);
|
|
4175
|
-
this.persist(record);
|
|
5254
|
+
await this.persist(record);
|
|
4176
5255
|
return record;
|
|
4177
5256
|
}
|
|
4178
|
-
|
|
4179
|
-
if (!record.recordId || record.closedAt !== void 0
|
|
5257
|
+
startCoordinator(record) {
|
|
5258
|
+
if (!record.recordId || record.closedAt !== void 0)
|
|
4180
5259
|
return;
|
|
4181
5260
|
if (record.state === "launching" && record.connectionDeadlineAt !== void 0) {
|
|
4182
5261
|
const timeout = setTimeout(() => {
|
|
4183
|
-
this.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
|
|
5262
|
+
this.runInBackground("persisting a Studio plugin connection timeout", this.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout."));
|
|
4184
5263
|
}, Math.max(0, record.connectionDeadlineAt - Date.now()));
|
|
4185
5264
|
if (typeof timeout === "object" && "unref" in timeout)
|
|
4186
5265
|
timeout.unref();
|
|
4187
5266
|
this.connectionTimers.set(record.recordId, timeout);
|
|
4188
5267
|
}
|
|
4189
|
-
|
|
4190
|
-
this.refresh(record);
|
|
4191
|
-
if (record.closedAt !== void 0)
|
|
4192
|
-
this.stopMonitor(record);
|
|
4193
|
-
}, 5e3);
|
|
4194
|
-
if (typeof timer === "object" && "unref" in timer)
|
|
4195
|
-
timer.unref();
|
|
4196
|
-
this.monitors.set(record.recordId, timer);
|
|
4197
|
-
}
|
|
4198
|
-
stopMonitor(record) {
|
|
4199
|
-
if (!record.recordId)
|
|
5268
|
+
if (this.coordinatorTimer)
|
|
4200
5269
|
return;
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
5270
|
+
this.coordinatorTimer = setInterval(() => {
|
|
5271
|
+
if (this.coordinatorRefresh)
|
|
5272
|
+
return;
|
|
5273
|
+
this.coordinatorRefresh = this.refreshOwnedRecords().catch((error) => {
|
|
5274
|
+
this.reportBackgroundFailure("refreshing managed Studio records", error);
|
|
5275
|
+
}).finally(() => {
|
|
5276
|
+
this.coordinatorRefresh = void 0;
|
|
5277
|
+
});
|
|
5278
|
+
}, 5e3);
|
|
5279
|
+
if (typeof this.coordinatorTimer === "object" && "unref" in this.coordinatorTimer) {
|
|
5280
|
+
this.coordinatorTimer.unref();
|
|
5281
|
+
}
|
|
4206
5282
|
}
|
|
4207
5283
|
clearConnectionTimer(record) {
|
|
4208
5284
|
if (!record.recordId)
|
|
@@ -4212,8 +5288,80 @@ var init_studio_instance_manager = __esm({
|
|
|
4212
5288
|
clearTimeout(timer);
|
|
4213
5289
|
this.connectionTimers.delete(record.recordId);
|
|
4214
5290
|
}
|
|
4215
|
-
|
|
4216
|
-
this.
|
|
5291
|
+
async refreshOwnedRecords() {
|
|
5292
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
5293
|
+
await this.sweepRegistry(snapshot);
|
|
5294
|
+
for (const record of [...this.managedByInstanceId.values(), ...this.pending]) {
|
|
5295
|
+
await this.refresh(record, snapshot);
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
5298
|
+
runInBackground(context, operation) {
|
|
5299
|
+
void operation.catch((error) => this.reportBackgroundFailure(context, error));
|
|
5300
|
+
}
|
|
5301
|
+
reportBackgroundFailure(context, error) {
|
|
5302
|
+
console.warn(`[robloxstudio-mcp] failed while ${context}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5303
|
+
}
|
|
5304
|
+
async applyProcessObservation(record, observation) {
|
|
5305
|
+
if (record.closedAt !== void 0)
|
|
5306
|
+
return;
|
|
5307
|
+
const previousObservationAt = record.lastProcessObservationAt;
|
|
5308
|
+
record.lastProcessObservationAt = observation.observedAt;
|
|
5309
|
+
if (observation.status === "unknown") {
|
|
5310
|
+
record.processObservationStatus = "unknown";
|
|
5311
|
+
record.lastProcessObservationError = observation.error;
|
|
5312
|
+
record.consecutiveConfirmedMisses = 0;
|
|
5313
|
+
record.firstConfirmedMissAt = void 0;
|
|
5314
|
+
await this.persist(record);
|
|
5315
|
+
return;
|
|
5316
|
+
}
|
|
5317
|
+
record.lastSuccessfulProcessObservationAt = observation.observedAt;
|
|
5318
|
+
record.lastProcessObservationError = void 0;
|
|
5319
|
+
if (observation.status === "running") {
|
|
5320
|
+
record.processObservationStatus = "running";
|
|
5321
|
+
record.consecutiveConfirmedMisses = 0;
|
|
5322
|
+
record.firstConfirmedMissAt = void 0;
|
|
5323
|
+
await this.persist(record);
|
|
5324
|
+
return;
|
|
5325
|
+
}
|
|
5326
|
+
record.processObservationStatus = "not_running";
|
|
5327
|
+
if (previousObservationAt !== observation.observedAt) {
|
|
5328
|
+
record.consecutiveConfirmedMisses = (record.consecutiveConfirmedMisses ?? 0) + 1;
|
|
5329
|
+
record.firstConfirmedMissAt ??= observation.observedAt;
|
|
5330
|
+
}
|
|
5331
|
+
const confirmedAbsent = observation.reason === "identity_mismatch" || (record.consecutiveConfirmedMisses ?? 0) >= this.confirmedExitMisses && observation.observedAt - (record.firstConfirmedMissAt ?? observation.observedAt) >= this.confirmedExitGraceMs;
|
|
5332
|
+
if (!confirmedAbsent) {
|
|
5333
|
+
await this.persist(record);
|
|
5334
|
+
return;
|
|
5335
|
+
}
|
|
5336
|
+
await this.markProcessExited(record, void 0, observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
|
|
5337
|
+
}
|
|
5338
|
+
async reconcileFromPositiveEvidence(record, snapshot) {
|
|
5339
|
+
if (record.closedAt === void 0)
|
|
5340
|
+
return;
|
|
5341
|
+
if (record.failureReason !== "Studio process exited." && record.failureReason !== "Studio process exited before the MCP plugin connected.")
|
|
5342
|
+
return;
|
|
5343
|
+
if (snapshot.status !== "ok")
|
|
5344
|
+
return;
|
|
5345
|
+
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
5346
|
+
const studioProcess = processId ? snapshot.processes.find((candidate) => candidate.Id === processId) : void 0;
|
|
5347
|
+
if (!studioProcess || !this.verifyProcessForRecord(record, studioProcess))
|
|
5348
|
+
return;
|
|
5349
|
+
if (record.exitCode !== void 0)
|
|
5350
|
+
return;
|
|
5351
|
+
record.closedAt = void 0;
|
|
5352
|
+
record.exitedAt = void 0;
|
|
5353
|
+
record.failureReason = void 0;
|
|
5354
|
+
record.state = record.instanceId ? "connected" : "launching";
|
|
5355
|
+
record.processObservationStatus = "running";
|
|
5356
|
+
record.lastProcessObservationAt = snapshot.observedAt;
|
|
5357
|
+
record.lastSuccessfulProcessObservationAt = snapshot.observedAt;
|
|
5358
|
+
record.lastProcessObservationError = void 0;
|
|
5359
|
+
record.consecutiveConfirmedMisses = 0;
|
|
5360
|
+
record.firstConfirmedMissAt = void 0;
|
|
5361
|
+
await this.persist(record);
|
|
5362
|
+
}
|
|
5363
|
+
async persist(record) {
|
|
5364
|
+
await this.registry.upsert(this.toRegistryRecord(record));
|
|
4217
5365
|
}
|
|
4218
5366
|
toRegistryRecord(record) {
|
|
4219
5367
|
if (!record.recordId)
|
|
@@ -4245,7 +5393,14 @@ var init_studio_instance_manager = __esm({
|
|
|
4245
5393
|
failureReason: record.failureReason,
|
|
4246
5394
|
closedAt: record.closedAt,
|
|
4247
5395
|
ownerPid: record.ownerPid,
|
|
4248
|
-
bootId: record.bootId
|
|
5396
|
+
bootId: record.bootId,
|
|
5397
|
+
processObservationStatus: record.processObservationStatus,
|
|
5398
|
+
processAuthorizationState: record.processAuthorizationState,
|
|
5399
|
+
lastProcessObservationAt: record.lastProcessObservationAt,
|
|
5400
|
+
lastSuccessfulProcessObservationAt: record.lastSuccessfulProcessObservationAt,
|
|
5401
|
+
lastProcessObservationError: record.lastProcessObservationError,
|
|
5402
|
+
consecutiveConfirmedMisses: record.consecutiveConfirmedMisses,
|
|
5403
|
+
firstConfirmedMissAt: record.firstConfirmedMissAt
|
|
4249
5404
|
};
|
|
4250
5405
|
}
|
|
4251
5406
|
fromRegistryRecord(record) {
|
|
@@ -4264,7 +5419,7 @@ var init_studio_instance_manager = __esm({
|
|
|
4264
5419
|
placeVersion: record.placeVersion,
|
|
4265
5420
|
localPlaceFile: record.localPlaceFile,
|
|
4266
5421
|
launchedAt: record.launchedAt,
|
|
4267
|
-
connectionDeadlineAt: record.connectionDeadlineAt ?? (state === "launching" ? record.launchedAt + 12e4 : void 0),
|
|
5422
|
+
connectionDeadlineAt: record.connectionDeadlineAt ?? (state === "launching" && record.processAuthorizationState === void 0 ? record.launchedAt + 12e4 : void 0),
|
|
4268
5423
|
state,
|
|
4269
5424
|
connectedAt: record.attachedAt,
|
|
4270
5425
|
failedAt: record.failedAt,
|
|
@@ -4274,7 +5429,14 @@ var init_studio_instance_manager = __esm({
|
|
|
4274
5429
|
closedAt: record.closedAt,
|
|
4275
5430
|
ownerPid: record.ownerPid,
|
|
4276
5431
|
bootId: record.bootId,
|
|
4277
|
-
deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose
|
|
5432
|
+
deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose,
|
|
5433
|
+
processObservationStatus: record.processObservationStatus,
|
|
5434
|
+
processAuthorizationState: record.processAuthorizationState ?? "released",
|
|
5435
|
+
lastProcessObservationAt: record.lastProcessObservationAt,
|
|
5436
|
+
lastSuccessfulProcessObservationAt: record.lastSuccessfulProcessObservationAt,
|
|
5437
|
+
lastProcessObservationError: record.lastProcessObservationError,
|
|
5438
|
+
consecutiveConfirmedMisses: record.consecutiveConfirmedMisses,
|
|
5439
|
+
firstConfirmedMissAt: record.firstConfirmedMissAt
|
|
4278
5440
|
};
|
|
4279
5441
|
}
|
|
4280
5442
|
};
|
|
@@ -5085,7 +6247,7 @@ var init_esm = __esm({
|
|
|
5085
6247
|
|
|
5086
6248
|
// ../core/dist/studio-skills.js
|
|
5087
6249
|
import { createHash as createHash2 } from "crypto";
|
|
5088
|
-
import { existsSync as existsSync4, readFileSync as
|
|
6250
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
5089
6251
|
import * as path4 from "path";
|
|
5090
6252
|
function decompressLz4Block(input, outputLength) {
|
|
5091
6253
|
const output = Buffer.allocUnsafe(outputLength);
|
|
@@ -5297,7 +6459,7 @@ function discoverNamedFile(root, fileName, depth = 0) {
|
|
|
5297
6459
|
const matches = [];
|
|
5298
6460
|
let entries;
|
|
5299
6461
|
try {
|
|
5300
|
-
entries =
|
|
6462
|
+
entries = readdirSync2(root, { withFileTypes: true });
|
|
5301
6463
|
} catch {
|
|
5302
6464
|
return matches;
|
|
5303
6465
|
}
|
|
@@ -5311,7 +6473,7 @@ function discoverNamedFile(root, fileName, depth = 0) {
|
|
|
5311
6473
|
}
|
|
5312
6474
|
return matches;
|
|
5313
6475
|
}
|
|
5314
|
-
function resolveAssistantBundlePath(studioExe
|
|
6476
|
+
function resolveAssistantBundlePath(studioExe) {
|
|
5315
6477
|
const override = process.env.ROBLOX_STUDIO_ASSISTANT_BUNDLE;
|
|
5316
6478
|
if (override) {
|
|
5317
6479
|
if (!existsSync4(override)) {
|
|
@@ -5319,7 +6481,7 @@ function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
|
|
|
5319
6481
|
}
|
|
5320
6482
|
return override;
|
|
5321
6483
|
}
|
|
5322
|
-
const exeDirectory = path4.dirname(studioExe);
|
|
6484
|
+
const exeDirectory = path4.dirname(studioExe ?? resolveStudioExe());
|
|
5323
6485
|
const roots = [
|
|
5324
6486
|
path4.join(exeDirectory, "BuiltInStandalonePlugins"),
|
|
5325
6487
|
path4.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
|
|
@@ -5333,18 +6495,18 @@ function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
|
|
|
5333
6495
|
for (const candidate of discoverNamedFile(root, "Assistant.rbxm"))
|
|
5334
6496
|
candidates.add(candidate);
|
|
5335
6497
|
}
|
|
5336
|
-
const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt:
|
|
6498
|
+
const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt: statSync2(candidate).mtimeMs })).sort((left, right) => right.modifiedAt - left.modifiedAt)[0]?.candidate;
|
|
5337
6499
|
if (!newest) {
|
|
5338
6500
|
throw new Error(`Studio Assistant bundle not found for ${studioExe}. Set ROBLOX_STUDIO_ASSISTANT_BUNDLE to the installed Assistant.rbxm path.`);
|
|
5339
6501
|
}
|
|
5340
6502
|
return newest;
|
|
5341
6503
|
}
|
|
5342
6504
|
function loadBuiltInStudioSkills(bundlePath = resolveAssistantBundlePath()) {
|
|
5343
|
-
const stats =
|
|
6505
|
+
const stats = statSync2(bundlePath);
|
|
5344
6506
|
if (cachedBundle?.path === bundlePath && cachedBundle.modifiedAt === stats.mtimeMs && cachedBundle.size === stats.size) {
|
|
5345
6507
|
return cachedBundle.value;
|
|
5346
6508
|
}
|
|
5347
|
-
const buffer =
|
|
6509
|
+
const buffer = readFileSync4(bundlePath);
|
|
5348
6510
|
const skills = parseBuiltInStudioSkills(buffer);
|
|
5349
6511
|
if (skills.length === 0) {
|
|
5350
6512
|
throw new Error(`No built-in skill documents found in ${bundlePath}`);
|
|
@@ -7061,13 +8223,20 @@ var init_tools = __esm({
|
|
|
7061
8223
|
openCloudClient;
|
|
7062
8224
|
cookieClient;
|
|
7063
8225
|
instanceManager;
|
|
8226
|
+
managedConnectionAssociations = Promise.resolve();
|
|
7064
8227
|
constructor(bridge) {
|
|
7065
8228
|
this.client = new StudioHttpClient(bridge);
|
|
7066
8229
|
this.bridge = bridge;
|
|
7067
8230
|
this.openCloudClient = new OpenCloudClient();
|
|
7068
8231
|
this.cookieClient = new RobloxCookieClient();
|
|
7069
8232
|
this.instanceManager = new StudioInstanceManager();
|
|
7070
|
-
this.bridge.onInstanceRegistered((instance) =>
|
|
8233
|
+
this.bridge.onInstanceRegistered((instance) => {
|
|
8234
|
+
const instanceManager = this.instanceManager;
|
|
8235
|
+
const association = this.managedConnectionAssociations.then(() => this._associateManagedEditConnection(instance, instanceManager));
|
|
8236
|
+
this.managedConnectionAssociations = association.catch((error) => {
|
|
8237
|
+
console.warn(`[robloxstudio-mcp] managed Studio connection association failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
8238
|
+
});
|
|
8239
|
+
});
|
|
7071
8240
|
}
|
|
7072
8241
|
_textResult(body) {
|
|
7073
8242
|
return { content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
@@ -7384,7 +8553,8 @@ var init_tools = __esm({
|
|
|
7384
8553
|
if (typeof state !== "object" || state === null || Array.isArray(state)) {
|
|
7385
8554
|
return state;
|
|
7386
8555
|
}
|
|
7387
|
-
const
|
|
8556
|
+
const rest = { ...state };
|
|
8557
|
+
delete rest.devices;
|
|
7388
8558
|
return rest;
|
|
7389
8559
|
}
|
|
7390
8560
|
_assertCanRestoreDeviceSimulatorState(state) {
|
|
@@ -8313,7 +9483,8 @@ ${code}`
|
|
|
8313
9483
|
};
|
|
8314
9484
|
entrySummaries.push(entrySummary);
|
|
8315
9485
|
try {
|
|
8316
|
-
const
|
|
9486
|
+
const settings = { ...entry };
|
|
9487
|
+
delete settings.label;
|
|
8317
9488
|
const applied = await this._executeDeviceSimulatorOperation(resolved.instanceId, resolved.role, "set", { settings });
|
|
8318
9489
|
entrySummary.applied = applied;
|
|
8319
9490
|
if (settleMs > 0)
|
|
@@ -8648,9 +9819,9 @@ ${code}`
|
|
|
8648
9819
|
_publicInstanceKey(instance) {
|
|
8649
9820
|
return `${instance.instanceId}:${instance.role}:${instance.connectedAt}`;
|
|
8650
9821
|
}
|
|
8651
|
-
_isLatestPublishedPlaceOpen(placeId) {
|
|
9822
|
+
async _isLatestPublishedPlaceOpen(placeId) {
|
|
8652
9823
|
const publishedInstanceId2 = `place:${placeId}`;
|
|
8653
|
-
return this.bridge.getPublicInstances().some((instance) => instance.placeId === placeId || instance.instanceId === publishedInstanceId2) || this.instanceManager.list().some((record) => record.closedAt === void 0 && record.source === "published_place" && record.placeId === placeId);
|
|
9824
|
+
return this.bridge.getPublicInstances().some((instance) => instance.placeId === placeId || instance.instanceId === publishedInstanceId2) || (await this.instanceManager.list()).some((record) => record.closedAt === void 0 && record.source === "published_place" && record.placeId === placeId);
|
|
8654
9825
|
}
|
|
8655
9826
|
_matchesManagedLaunch(record, instance) {
|
|
8656
9827
|
if (record.source === "published_place") {
|
|
@@ -8662,12 +9833,12 @@ ${code}`
|
|
|
8662
9833
|
}
|
|
8663
9834
|
return true;
|
|
8664
9835
|
}
|
|
8665
|
-
_associateManagedEditConnection(instance) {
|
|
9836
|
+
async _associateManagedEditConnection(instance, instanceManager) {
|
|
8666
9837
|
if (instance.role !== "edit")
|
|
8667
9838
|
return;
|
|
8668
|
-
const candidate =
|
|
9839
|
+
const candidate = (await instanceManager.pendingLaunches()).filter((record) => instance.connectedAt >= record.launchedAt - 1e3).filter((record) => this._matchesManagedLaunch(record, instance)).sort((a, b) => a.launchedAt - b.launchedAt)[0];
|
|
8669
9840
|
if (candidate)
|
|
8670
|
-
|
|
9841
|
+
await instanceManager.attachInstanceId(candidate, instance.instanceId);
|
|
8671
9842
|
}
|
|
8672
9843
|
async _deriveUniverseId(placeId) {
|
|
8673
9844
|
const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`);
|
|
@@ -8684,7 +9855,7 @@ ${code}`
|
|
|
8684
9855
|
async _waitForManagedEditConnection(record, beforeKeys, timeoutMs) {
|
|
8685
9856
|
const deadline = Date.now() + timeoutMs;
|
|
8686
9857
|
while (Date.now() < deadline) {
|
|
8687
|
-
this.instanceManager.refresh(record);
|
|
9858
|
+
await this.instanceManager.refresh(record);
|
|
8688
9859
|
if (record.state === "failed" || record.state === "exited" || record.closedAt !== void 0) {
|
|
8689
9860
|
return void 0;
|
|
8690
9861
|
}
|
|
@@ -8696,7 +9867,6 @@ ${code}`
|
|
|
8696
9867
|
return void 0;
|
|
8697
9868
|
}
|
|
8698
9869
|
_managedStatus(record) {
|
|
8699
|
-
this.instanceManager.refresh(record);
|
|
8700
9870
|
const connected = record.instanceId ? this.bridge.getPublicInstances().filter((instance) => instance.instanceId === record.instanceId) : [];
|
|
8701
9871
|
return {
|
|
8702
9872
|
launch_id: record.recordId,
|
|
@@ -8704,7 +9874,15 @@ ${code}`
|
|
|
8704
9874
|
managed: true,
|
|
8705
9875
|
state: record.state,
|
|
8706
9876
|
pid: record.nativeProcessId ?? record.spawnPid,
|
|
8707
|
-
|
|
9877
|
+
process_started_at_file_time: record.nativeProcessStartedAt,
|
|
9878
|
+
process_authorized: record.processAuthorizationState !== "pending",
|
|
9879
|
+
process_ownership_released: record.processAuthorizationState === "released",
|
|
9880
|
+
process_running: record.closedAt !== void 0 || record.exitedAt !== void 0 ? false : record.processObservationStatus === "running" ? true : record.processObservationStatus === "not_running" ? false : null,
|
|
9881
|
+
process_observation_status: record.processObservationStatus ?? "unknown",
|
|
9882
|
+
last_process_observation_at: record.lastProcessObservationAt ? new Date(record.lastProcessObservationAt).toISOString() : void 0,
|
|
9883
|
+
last_successful_process_observation_at: record.lastSuccessfulProcessObservationAt ? new Date(record.lastSuccessfulProcessObservationAt).toISOString() : void 0,
|
|
9884
|
+
last_process_observation_error: record.lastProcessObservationError,
|
|
9885
|
+
consecutive_confirmed_misses: record.consecutiveConfirmedMisses ?? 0,
|
|
8708
9886
|
source: record.source,
|
|
8709
9887
|
local_place_file: record.localPlaceFile,
|
|
8710
9888
|
place_id: record.placeId,
|
|
@@ -8730,8 +9908,8 @@ ${code}`
|
|
|
8730
9908
|
if (instance_id && launch_id) {
|
|
8731
9909
|
throw new Error("manage_instance accepts only one of instance_id or launch_id.");
|
|
8732
9910
|
}
|
|
8733
|
-
if (action !== "launch" && action !== "close" && action !== "status" && action !== "list_place_versions") {
|
|
8734
|
-
throw new Error("manage_instance requires action=launch|close|status|list_place_versions");
|
|
9911
|
+
if (action !== "launch" && action !== "authorize" && action !== "complete" && action !== "close" && action !== "status" && action !== "list_place_versions") {
|
|
9912
|
+
throw new Error("manage_instance requires action=launch|authorize|complete|close|status|list_place_versions");
|
|
8735
9913
|
}
|
|
8736
9914
|
if (action === "list_place_versions") {
|
|
8737
9915
|
if (!this.openCloudClient.hasApiKey()) {
|
|
@@ -8756,15 +9934,30 @@ ${code}`
|
|
|
8756
9934
|
body.next_page_token = response.nextPageToken;
|
|
8757
9935
|
return this._textResult(body);
|
|
8758
9936
|
}
|
|
9937
|
+
if (action === "close" || action === "status") {
|
|
9938
|
+
await this.managedConnectionAssociations;
|
|
9939
|
+
}
|
|
9940
|
+
if (action === "authorize") {
|
|
9941
|
+
if (!launch_id)
|
|
9942
|
+
throw new Error("manage_instance action=authorize requires launch_id.");
|
|
9943
|
+
const record2 = await this.instanceManager.authorizeByLaunchId(launch_id);
|
|
9944
|
+
return this._textResult(this._managedStatus(record2));
|
|
9945
|
+
}
|
|
9946
|
+
if (action === "complete") {
|
|
9947
|
+
if (!launch_id)
|
|
9948
|
+
throw new Error("manage_instance action=complete requires launch_id.");
|
|
9949
|
+
const record2 = await this.instanceManager.completeByLaunchId(launch_id);
|
|
9950
|
+
return this._textResult(this._managedStatus(record2));
|
|
9951
|
+
}
|
|
8759
9952
|
if (action === "status") {
|
|
8760
9953
|
if (launch_id) {
|
|
8761
|
-
const record2 = this.instanceManager.getByLaunchId(launch_id);
|
|
9954
|
+
const record2 = await this.instanceManager.getByLaunchId(launch_id);
|
|
8762
9955
|
if (!record2)
|
|
8763
9956
|
return this._textResult({ error: "Launch is not managed.", launch_id });
|
|
8764
9957
|
return this._textResult(this._managedStatus(record2));
|
|
8765
9958
|
}
|
|
8766
9959
|
if (instance_id) {
|
|
8767
|
-
const record2 = this.instanceManager.get(instance_id);
|
|
9960
|
+
const record2 = await this.instanceManager.get(instance_id);
|
|
8768
9961
|
const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
|
|
8769
9962
|
if (!record2 && connected2.length === 0) {
|
|
8770
9963
|
return this._textResult({ error: "Instance is not connected or managed.", instance_id });
|
|
@@ -8781,7 +9974,7 @@ ${code}`
|
|
|
8781
9974
|
});
|
|
8782
9975
|
}
|
|
8783
9976
|
return this._textResult({
|
|
8784
|
-
managed: this.instanceManager.list().filter((record2) => record2.closedAt === void 0).map((record2) => this._managedStatus(record2)),
|
|
9977
|
+
managed: (await this.instanceManager.list()).filter((record2) => record2.closedAt === void 0).map((record2) => this._managedStatus(record2)),
|
|
8785
9978
|
connected: this.bridge.getPublicInstances().map((instance) => ({
|
|
8786
9979
|
instance_id: instance.instanceId,
|
|
8787
9980
|
role: instance.role,
|
|
@@ -8793,11 +9986,11 @@ ${code}`
|
|
|
8793
9986
|
if (action === "close") {
|
|
8794
9987
|
let record2;
|
|
8795
9988
|
if (launch_id) {
|
|
8796
|
-
record2 = this.instanceManager.getByLaunchId(launch_id);
|
|
9989
|
+
record2 = await this.instanceManager.getByLaunchId(launch_id);
|
|
8797
9990
|
if (!record2)
|
|
8798
9991
|
return this._textResult({ error: "Launch is not managed.", launch_id });
|
|
8799
9992
|
const connectedInstanceId = record2.instanceId;
|
|
8800
|
-
const closeResult2 = record2.closedAt === void 0 ? this.instanceManager.close(record2) : { status: "already_closed" };
|
|
9993
|
+
const closeResult2 = record2.closedAt === void 0 ? await this.instanceManager.close(record2) : { status: "already_closed" };
|
|
8801
9994
|
if (connectedInstanceId) {
|
|
8802
9995
|
await this.bridge.unregisterInstanceIdEverywhere(connectedInstanceId);
|
|
8803
9996
|
await sleep(500);
|
|
@@ -8810,13 +10003,13 @@ ${code}`
|
|
|
8810
10003
|
});
|
|
8811
10004
|
}
|
|
8812
10005
|
if (instance_id) {
|
|
8813
|
-
const recordBeforeClose = this.instanceManager.get(instance_id);
|
|
8814
|
-
const managedClose = this.instanceManager.closeByInstanceId(instance_id);
|
|
10006
|
+
const recordBeforeClose = await this.instanceManager.get(instance_id);
|
|
10007
|
+
const managedClose = await this.instanceManager.closeByInstanceId(instance_id);
|
|
8815
10008
|
if (managedClose.status !== "not_found") {
|
|
8816
10009
|
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
8817
10010
|
await sleep(500);
|
|
8818
10011
|
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
8819
|
-
const closedRecord =
|
|
10012
|
+
const closedRecord = managedClose.launchId ? await this.instanceManager.getByLaunchId(managedClose.launchId) : recordBeforeClose;
|
|
8820
10013
|
return this._textResult({
|
|
8821
10014
|
...closedRecord ? this._managedStatus(closedRecord) : { instance_id },
|
|
8822
10015
|
close_status: managedClose.status,
|
|
@@ -8832,7 +10025,7 @@ ${code}`
|
|
|
8832
10025
|
});
|
|
8833
10026
|
}
|
|
8834
10027
|
try {
|
|
8835
|
-
this.instanceManager.closeConnectedInstance(edit);
|
|
10028
|
+
await this.instanceManager.closeConnectedInstance(edit);
|
|
8836
10029
|
await sleep(500);
|
|
8837
10030
|
} catch (error) {
|
|
8838
10031
|
return this._textResult({
|
|
@@ -8847,7 +10040,7 @@ ${code}`
|
|
|
8847
10040
|
message: "Studio instance closed."
|
|
8848
10041
|
});
|
|
8849
10042
|
} else {
|
|
8850
|
-
const active = this.instanceManager.list().filter((entry) => entry.closedAt === void 0);
|
|
10043
|
+
const active = (await this.instanceManager.list()).filter((entry) => entry.closedAt === void 0);
|
|
8851
10044
|
if (active.length === 0) {
|
|
8852
10045
|
return this._textResult({ message: "No managed Studio instances are active." });
|
|
8853
10046
|
}
|
|
@@ -8861,7 +10054,7 @@ ${code}`
|
|
|
8861
10054
|
}
|
|
8862
10055
|
if (record2.instanceId)
|
|
8863
10056
|
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
8864
|
-
const closeResult = this.instanceManager.close(record2);
|
|
10057
|
+
const closeResult = await this.instanceManager.close(record2);
|
|
8865
10058
|
if (record2.instanceId) {
|
|
8866
10059
|
await sleep(500);
|
|
8867
10060
|
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
@@ -8888,14 +10081,18 @@ ${code}`
|
|
|
8888
10081
|
studioExecutable = request.studio_executable;
|
|
8889
10082
|
}
|
|
8890
10083
|
const processEnvironment = parseStudioProcessEnvironmentPatch(request.process_environment);
|
|
8891
|
-
if (launchSource === "published_place" && placeId !== void 0 && this._isLatestPublishedPlaceOpen(placeId)) {
|
|
10084
|
+
if (launchSource === "published_place" && placeId !== void 0 && await this._isLatestPublishedPlaceOpen(placeId)) {
|
|
8892
10085
|
return this._textResult({
|
|
8893
10086
|
error: "Place is already open.",
|
|
8894
10087
|
message: `place_id ${placeId} is already connected. Use the existing instance or launch a specific place_revision.`
|
|
8895
10088
|
});
|
|
8896
10089
|
}
|
|
8897
10090
|
const universeId = launchSource === "published_place" || launchSource === "place_revision" ? await this._deriveUniverseId(placeId) : void 0;
|
|
8898
|
-
|
|
10091
|
+
if (request.require_process_identity !== void 0 && typeof request.require_process_identity !== "boolean") {
|
|
10092
|
+
throw new Error("require_process_identity must be a boolean when provided.");
|
|
10093
|
+
}
|
|
10094
|
+
const requireProcessIdentity = request.require_process_identity === true;
|
|
10095
|
+
const waitForConnection = !requireProcessIdentity && request.wait_for_connection !== false;
|
|
8899
10096
|
const timeoutMs = this._optionalPositiveInteger(request.timeout_ms, "timeout_ms") ?? 12e4;
|
|
8900
10097
|
const beforeKeys = new Set(this.bridge.getPublicInstances().map((instance) => this._publicInstanceKey(instance)));
|
|
8901
10098
|
const record = await this.instanceManager.launch({
|
|
@@ -8906,7 +10103,8 @@ ${code}`
|
|
|
8906
10103
|
placeVersion,
|
|
8907
10104
|
connectionTimeoutMs: timeoutMs,
|
|
8908
10105
|
studioExecutable,
|
|
8909
|
-
processEnvironment
|
|
10106
|
+
processEnvironment,
|
|
10107
|
+
...requireProcessIdentity ? { requireProcessIdentity: true } : {}
|
|
8910
10108
|
});
|
|
8911
10109
|
if (!waitForConnection) {
|
|
8912
10110
|
return this._textResult({
|
|
@@ -8917,11 +10115,11 @@ ${code}`
|
|
|
8917
10115
|
const connected = await this._waitForManagedEditConnection(record, beforeKeys, timeoutMs);
|
|
8918
10116
|
if (!connected) {
|
|
8919
10117
|
if (record.state === "launching") {
|
|
8920
|
-
this.instanceManager.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
|
|
10118
|
+
await this.instanceManager.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
|
|
8921
10119
|
}
|
|
8922
10120
|
if (record.closedAt === void 0) {
|
|
8923
10121
|
try {
|
|
8924
|
-
this.instanceManager.close(record);
|
|
10122
|
+
await this.instanceManager.close(record);
|
|
8925
10123
|
} catch {
|
|
8926
10124
|
}
|
|
8927
10125
|
}
|
|
@@ -8930,7 +10128,7 @@ ${code}`
|
|
|
8930
10128
|
error: record.failureReason ?? "Studio launched, but the MCP plugin did not connect before timeout."
|
|
8931
10129
|
});
|
|
8932
10130
|
}
|
|
8933
|
-
this.instanceManager.attachInstanceId(record, connected.instanceId);
|
|
10131
|
+
await this.instanceManager.attachInstanceId(record, connected.instanceId);
|
|
8934
10132
|
return this._textResult({
|
|
8935
10133
|
...this._managedStatus(record),
|
|
8936
10134
|
message: launchSource === "place_revision" ? `Studio opened place revision ${placeVersion}.` : "Studio opened."
|
|
@@ -10203,11 +11401,15 @@ ${code}`
|
|
|
10203
11401
|
}
|
|
10204
11402
|
async uploadGenerateModelReferenceImage(imageContent, instance_id) {
|
|
10205
11403
|
if (this.cookieClient.hasCookie()) {
|
|
10206
|
-
const result2 = await this.cookieClient.
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
11404
|
+
const result2 = await this.cookieClient.uploadImage({
|
|
11405
|
+
fileContent: imageContent,
|
|
11406
|
+
fileName: "generate-model-reference.png",
|
|
11407
|
+
displayName: STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL,
|
|
11408
|
+
description: STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL,
|
|
11409
|
+
userId: process.env.ROBLOX_CREATOR_USER_ID,
|
|
11410
|
+
groupId: process.env.ROBLOX_CREATOR_GROUP_ID
|
|
11411
|
+
});
|
|
11412
|
+
return result2.assetId;
|
|
10211
11413
|
}
|
|
10212
11414
|
if (!this.openCloudClient.hasApiKey()) {
|
|
10213
11415
|
throw new Error("image_path and image_base64 require Roblox asset upload credentials because GenerateModelAsync only accepts rbxassetid:// or rbxasset:// image inputs. Set ROBLOX_OPEN_CLOUD_API_KEY plus ROBLOX_CREATOR_USER_ID or ROBLOX_CREATOR_GROUP_ID, or pass image_asset_id.");
|
|
@@ -10241,8 +11443,17 @@ ${code}`
|
|
|
10241
11443
|
}
|
|
10242
11444
|
const fileContent = fs3.readFileSync(filePath);
|
|
10243
11445
|
const fileName = path5.basename(filePath);
|
|
11446
|
+
const resolvedGroupId = groupId || process.env.ROBLOX_CREATOR_GROUP_ID;
|
|
11447
|
+
const resolvedUserId = userId || process.env.ROBLOX_CREATOR_USER_ID;
|
|
10244
11448
|
if (assetType === "Decal" && this.cookieClient.hasCookie()) {
|
|
10245
|
-
const result2 = await this.cookieClient.
|
|
11449
|
+
const result2 = await this.cookieClient.uploadImage({
|
|
11450
|
+
fileContent,
|
|
11451
|
+
fileName,
|
|
11452
|
+
displayName,
|
|
11453
|
+
description: description || "",
|
|
11454
|
+
userId: resolvedUserId,
|
|
11455
|
+
groupId: resolvedGroupId
|
|
11456
|
+
});
|
|
10246
11457
|
return {
|
|
10247
11458
|
content: [{
|
|
10248
11459
|
type: "text",
|
|
@@ -10251,9 +11462,9 @@ ${code}`
|
|
|
10251
11462
|
response: {
|
|
10252
11463
|
assetId: String(result2.assetId),
|
|
10253
11464
|
displayName,
|
|
10254
|
-
assetType,
|
|
10255
|
-
decalId:
|
|
10256
|
-
imageId: String(result2.
|
|
11465
|
+
assetType: "Image",
|
|
11466
|
+
decalId: null,
|
|
11467
|
+
imageId: String(result2.assetId)
|
|
10257
11468
|
}
|
|
10258
11469
|
})
|
|
10259
11470
|
}]
|
|
@@ -10263,8 +11474,6 @@ ${code}`
|
|
|
10263
11474
|
const cookieHint = assetType === "Decal" ? " Alternatively, set ROBLOSECURITY to use cookie auth." : "";
|
|
10264
11475
|
throw new Error(`No auth configured for ${assetType} upload. Set ROBLOX_OPEN_CLOUD_API_KEY (needs asset:write scope).${cookieHint}`);
|
|
10265
11476
|
}
|
|
10266
|
-
const resolvedGroupId = groupId || process.env.ROBLOX_CREATOR_GROUP_ID;
|
|
10267
|
-
const resolvedUserId = userId || process.env.ROBLOX_CREATOR_USER_ID;
|
|
10268
11477
|
if (!resolvedUserId && !resolvedGroupId) {
|
|
10269
11478
|
throw new Error("Creator identity required for Open Cloud upload. Set ROBLOX_CREATOR_USER_ID or ROBLOX_CREATOR_GROUP_ID, or pass userId/groupId as parameters.");
|
|
10270
11479
|
}
|
|
@@ -11929,14 +13138,14 @@ var init_definitions = __esm({
|
|
|
11929
13138
|
{
|
|
11930
13139
|
name: "manage_instance",
|
|
11931
13140
|
category: "write",
|
|
11932
|
-
description: 'Launch, close, inspect, and find revisions for Studio instances. Every launch returns launch_id, native pid, source, and lifecycle state; status and close accept launch_id before the plugin connects and instance_id after association. Use action="launch" with source="baseplate" for a blank place, or source="local_file" with local_place_file for a local place; neither uses place_id. Use action="list_place_versions" with place_id to retrieve version numbers through Open Cloud asset versions, then action="launch" with source="place_revision", place_id, and place_version to open an older revision. action="launch" source="published_place" opens the latest published place and is blocked if that place_id is already connected; source="place_revision" is allowed because Studio opens explicit past revisions as anonymous local copies. Requires ROBLOX_OPEN_CLOUD_API_KEY with asset:read for list_place_versions.',
|
|
13141
|
+
description: 'Launch, authorize, complete, close, inspect, and find revisions for Studio instances. Every launch returns launch_id, native pid, source, and lifecycle state; status and close accept launch_id before the plugin connects and instance_id after association. Use action="launch" with source="baseplate" for a blank place, or source="local_file" with local_place_file for a local place; neither uses place_id. A process-identity launch requires action="authorize" after injection is prepared, followed by action="complete" only after the injected runtime is independently attested. Use action="list_place_versions" with place_id to retrieve version numbers through Open Cloud asset versions, then action="launch" with source="place_revision", place_id, and place_version to open an older revision. action="launch" source="published_place" opens the latest published place and is blocked if that place_id is already connected; source="place_revision" is allowed because Studio opens explicit past revisions as anonymous local copies. Requires ROBLOX_OPEN_CLOUD_API_KEY with asset:read for list_place_versions.',
|
|
11933
13142
|
inputSchema: {
|
|
11934
13143
|
type: "object",
|
|
11935
13144
|
properties: {
|
|
11936
13145
|
action: {
|
|
11937
13146
|
type: "string",
|
|
11938
|
-
enum: ["launch", "close", "status", "list_place_versions"],
|
|
11939
|
-
description: "Instance management action."
|
|
13147
|
+
enum: ["launch", "authorize", "complete", "close", "status", "list_place_versions"],
|
|
13148
|
+
description: "Instance management action. authorize resumes a protocol-v3 launch after the caller has prepared process-scoped injection. complete releases broker process ownership after the caller independently attests that injection finished."
|
|
11940
13149
|
},
|
|
11941
13150
|
source: {
|
|
11942
13151
|
type: "string",
|
|
@@ -11955,13 +13164,17 @@ var init_definitions = __esm({
|
|
|
11955
13164
|
type: "number",
|
|
11956
13165
|
description: 'Required for source="place_revision". Use action="list_place_versions" to discover available version numbers.'
|
|
11957
13166
|
},
|
|
13167
|
+
require_process_identity: {
|
|
13168
|
+
type: "boolean",
|
|
13169
|
+
description: 'For action="launch": require an exact native PID and process creation time, return launch_id immediately, and retain broker ownership of the native process until action="complete" succeeds. The process remains suspended until action="authorize" begins injection. If identity capture, authorization, or ownership completion fails, the broker stops the launched process.'
|
|
13170
|
+
},
|
|
11958
13171
|
wait_for_connection: {
|
|
11959
13172
|
type: "boolean",
|
|
11960
|
-
description: 'For action="launch": wait until the MCP plugin connects and return instance_id (default true). false returns launch_id immediately and continues association/failure tracking asynchronously.'
|
|
13173
|
+
description: 'For action="launch": wait until the MCP plugin connects and return instance_id (default true). false returns launch_id immediately and continues association/failure tracking asynchronously. Ignored when require_process_identity=true, which always returns the suspended launch immediately.'
|
|
11961
13174
|
},
|
|
11962
13175
|
timeout_ms: {
|
|
11963
13176
|
type: "number",
|
|
11964
|
-
description: 'For action="launch": max milliseconds for plugin connection (default 120000). The deadline also applies asynchronously when wait_for_connection=false.'
|
|
13177
|
+
description: 'For action="launch": max milliseconds for plugin connection (default 120000). The deadline also applies asynchronously when wait_for_connection=false. It does not apply when require_process_identity=true; that protocol uses the broker ownership-completion lease through action="complete".'
|
|
11965
13178
|
},
|
|
11966
13179
|
studio_executable: {
|
|
11967
13180
|
type: "string",
|
|
@@ -13182,7 +14395,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
13182
14395
|
{
|
|
13183
14396
|
name: "upload_asset",
|
|
13184
14397
|
category: "write",
|
|
13185
|
-
description: "Upload any supported asset type to Roblox: Audio (mp3/ogg/wav/flac), Decal (png/jpg/bmp/tga), Model (fbx/gltf/glb/rbxm/rbxmx), Animation (rbxm/rbxmx), or Video (mp4/mov). Decal supports ROBLOSECURITY cookie auth or ROBLOX_OPEN_CLOUD_API_KEY. All other types require Open Cloud API key with asset:write scope + creator ID. Audio: max 7 min, 100 uploads/month (ID-verified). Video: max 5 min, requires 13+ ID-verified.",
|
|
14398
|
+
description: "Upload any supported asset type to Roblox: Audio (mp3/ogg/wav/flac), Decal (png/jpg/bmp/tga), Model (fbx/gltf/glb/rbxm/rbxmx), Animation (rbxm/rbxmx), or Video (mp4/mov). Decal supports ROBLOSECURITY cookie auth through the Asset Manager user-auth API and returns the direct Image asset ID, or ROBLOX_OPEN_CLOUD_API_KEY. All other types require Open Cloud API key with asset:write scope + creator ID. Audio: max 7 min, 100 uploads/month (ID-verified). Video: max 5 min, requires 13+ ID-verified.",
|
|
13186
14399
|
inputSchema: {
|
|
13187
14400
|
type: "object",
|
|
13188
14401
|
properties: {
|
|
@@ -13788,7 +15001,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
13788
15001
|
});
|
|
13789
15002
|
|
|
13790
15003
|
// ../core/dist/install-plugin-helpers.js
|
|
13791
|
-
import { existsSync as existsSync6, readFileSync as
|
|
15004
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, unlinkSync } from "fs";
|
|
13792
15005
|
import { execSync } from "child_process";
|
|
13793
15006
|
import { join as join6 } from "path";
|
|
13794
15007
|
import { homedir as homedir5 } from "os";
|
|
@@ -13796,7 +15009,7 @@ function isWSL() {
|
|
|
13796
15009
|
if (process.platform !== "linux")
|
|
13797
15010
|
return false;
|
|
13798
15011
|
try {
|
|
13799
|
-
const v =
|
|
15012
|
+
const v = readFileSync6("/proc/version", "utf8");
|
|
13800
15013
|
return /microsoft|wsl/i.test(v);
|
|
13801
15014
|
} catch {
|
|
13802
15015
|
return false;
|
|
@@ -13881,7 +15094,7 @@ __export(install_plugin_exports, {
|
|
|
13881
15094
|
installBundledPlugin: () => installBundledPlugin,
|
|
13882
15095
|
installPlugin: () => installPlugin
|
|
13883
15096
|
});
|
|
13884
|
-
import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync7, mkdirSync as
|
|
15097
|
+
import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
13885
15098
|
import { dirname as dirname5, join as join7 } from "path";
|
|
13886
15099
|
import { fileURLToPath } from "url";
|
|
13887
15100
|
import { get } from "https";
|
|
@@ -13943,7 +15156,7 @@ function prepareInstall({
|
|
|
13943
15156
|
}) {
|
|
13944
15157
|
const pluginsFolder = getPluginsFolder();
|
|
13945
15158
|
if (!existsSync7(pluginsFolder)) {
|
|
13946
|
-
|
|
15159
|
+
mkdirSync4(pluginsFolder, { recursive: true });
|
|
13947
15160
|
}
|
|
13948
15161
|
handleVariantConflict({
|
|
13949
15162
|
pluginsFolder,
|
|
@@ -13964,14 +15177,14 @@ function bundledAssetPath() {
|
|
|
13964
15177
|
}
|
|
13965
15178
|
function packageVersion() {
|
|
13966
15179
|
const currentDir = dirname5(fileURLToPath(import.meta.url));
|
|
13967
|
-
const pkg = JSON.parse(
|
|
15180
|
+
const pkg = JSON.parse(readFileSync7(join7(currentDir, "..", "package.json"), "utf8"));
|
|
13968
15181
|
if (!pkg.version) {
|
|
13969
15182
|
throw new Error("Package version not found");
|
|
13970
15183
|
}
|
|
13971
15184
|
return pkg.version;
|
|
13972
15185
|
}
|
|
13973
15186
|
function bundledPluginVersion(source) {
|
|
13974
|
-
const match =
|
|
15187
|
+
const match = readFileSync7(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
|
|
13975
15188
|
return match ? match[1] : null;
|
|
13976
15189
|
}
|
|
13977
15190
|
function assertBundledPluginVersion(source) {
|
|
@@ -13985,8 +15198,8 @@ function assertBundledPluginVersion(source) {
|
|
|
13985
15198
|
}
|
|
13986
15199
|
function filesMatch(a, b) {
|
|
13987
15200
|
if (!existsSync7(b)) return false;
|
|
13988
|
-
const aBytes =
|
|
13989
|
-
const bBytes =
|
|
15201
|
+
const aBytes = readFileSync7(a);
|
|
15202
|
+
const bBytes = readFileSync7(b);
|
|
13990
15203
|
return aBytes.length === bBytes.length && aBytes.equals(bBytes);
|
|
13991
15204
|
}
|
|
13992
15205
|
async function installBundledPlugin(options = {}) {
|