@chrrxs/robloxstudio-mcp-inspector 2.22.2 → 2.22.4
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
CHANGED
|
@@ -889,6 +889,7 @@ function requiredClosedLineRange(body, toolName) {
|
|
|
889
889
|
}
|
|
890
890
|
function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
891
891
|
const app = express();
|
|
892
|
+
const studioLifecycleCallable = !allowedTools || allowedTools.has("manage_instance");
|
|
892
893
|
let mcpServerActive = false;
|
|
893
894
|
let lastMCPActivity = 0;
|
|
894
895
|
let mcpServerStartTime = 0;
|
|
@@ -968,8 +969,15 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
968
969
|
res.json({
|
|
969
970
|
status: "ok",
|
|
970
971
|
service: "robloxstudio-mcp",
|
|
972
|
+
serverName: serverConfig?.name ?? "robloxstudio-mcp",
|
|
971
973
|
version: serverConfig?.version,
|
|
972
974
|
serverVersion: serverConfig?.version,
|
|
975
|
+
capabilities: studioLifecycleCallable ? {
|
|
976
|
+
studioLifecycle: {
|
|
977
|
+
protocolVersion: 1,
|
|
978
|
+
endpoint: "/mcp/manage_instance"
|
|
979
|
+
}
|
|
980
|
+
} : {},
|
|
973
981
|
pluginConnected: instances.length > 0,
|
|
974
982
|
instanceCount: instances.length,
|
|
975
983
|
instances: publicInstances,
|
|
@@ -3065,7 +3073,8 @@ var init_roblox_cookie_client = __esm({
|
|
|
3065
3073
|
});
|
|
3066
3074
|
|
|
3067
3075
|
// ../core/dist/managed-instance-registry.js
|
|
3068
|
-
import * as fs from "fs";
|
|
3076
|
+
import * as fs from "fs/promises";
|
|
3077
|
+
import { randomUUID } from "crypto";
|
|
3069
3078
|
import * as os from "os";
|
|
3070
3079
|
import * as path from "path";
|
|
3071
3080
|
function defaultManagedInstanceRegistryDir() {
|
|
@@ -3080,8 +3089,8 @@ function defaultManagedInstanceRegistryDir() {
|
|
|
3080
3089
|
}
|
|
3081
3090
|
return path.join(os.homedir(), ".local", "state", "robloxstudio-mcp", "managed-instances", "v1");
|
|
3082
3091
|
}
|
|
3083
|
-
function
|
|
3084
|
-
|
|
3092
|
+
function delay(ms) {
|
|
3093
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
3085
3094
|
}
|
|
3086
3095
|
function ymd(timestamp) {
|
|
3087
3096
|
return new Date(timestamp).toISOString().slice(0, 10);
|
|
@@ -3096,92 +3105,131 @@ function isRecord(value) {
|
|
|
3096
3105
|
const record = value;
|
|
3097
3106
|
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";
|
|
3098
3107
|
}
|
|
3099
|
-
|
|
3108
|
+
function isLockOwner(value) {
|
|
3109
|
+
if (!value || typeof value !== "object")
|
|
3110
|
+
return false;
|
|
3111
|
+
const owner = value;
|
|
3112
|
+
return Number.isInteger(owner.pid) && owner.pid > 0 && typeof owner.token === "string" && owner.token.length > 0 && typeof owner.createdAt === "number";
|
|
3113
|
+
}
|
|
3114
|
+
function isProcessAlive(pid) {
|
|
3115
|
+
if (pid === process.pid)
|
|
3116
|
+
return true;
|
|
3117
|
+
try {
|
|
3118
|
+
process.kill(pid, 0);
|
|
3119
|
+
return true;
|
|
3120
|
+
} catch (error) {
|
|
3121
|
+
return error.code === "EPERM";
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
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, ManagedInstanceRegistry;
|
|
3100
3125
|
var init_managed_instance_registry = __esm({
|
|
3101
3126
|
"../core/dist/managed-instance-registry.js"() {
|
|
3102
3127
|
"use strict";
|
|
3103
3128
|
REGISTRY_VERSION = 1;
|
|
3104
3129
|
LOCK_STALE_MS = 1e4;
|
|
3105
3130
|
LOCK_RETRY_MS = 25;
|
|
3106
|
-
LOCK_TIMEOUT_MS =
|
|
3131
|
+
LOCK_TIMEOUT_MS = 15e3;
|
|
3107
3132
|
EVENT_RETENTION_DAYS = 2;
|
|
3108
3133
|
TERMINAL_RECORD_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
3134
|
+
DEFAULT_CONFIRMED_EXIT_MISSES = 2;
|
|
3135
|
+
DEFAULT_CONFIRMED_EXIT_GRACE_MS = 5e3;
|
|
3136
|
+
activeLockTokens = /* @__PURE__ */ new Set();
|
|
3109
3137
|
ManagedInstanceRegistry = class {
|
|
3110
3138
|
dir;
|
|
3111
3139
|
constructor(dir = defaultManagedInstanceRegistryDir()) {
|
|
3112
3140
|
this.dir = dir;
|
|
3113
3141
|
}
|
|
3114
|
-
upsert(record) {
|
|
3115
|
-
this.withLock(() => this.writeRecordUnlocked(record));
|
|
3142
|
+
async upsert(record) {
|
|
3143
|
+
await this.withLock(() => this.writeRecordUnlocked(record));
|
|
3116
3144
|
}
|
|
3117
|
-
attachInstanceId(recordId, instanceId) {
|
|
3118
|
-
this.withLock(() => {
|
|
3119
|
-
const record = this.readRecordUnlocked(recordId);
|
|
3145
|
+
async attachInstanceId(recordId, instanceId) {
|
|
3146
|
+
await this.withLock(async () => {
|
|
3147
|
+
const record = await this.readRecordUnlocked(recordId);
|
|
3120
3148
|
if (!record)
|
|
3121
3149
|
return;
|
|
3122
3150
|
record.instanceId = instanceId;
|
|
3123
3151
|
record.attachedAt = Date.now();
|
|
3124
|
-
this.writeRecordUnlocked(record);
|
|
3152
|
+
await this.writeRecordUnlocked(record);
|
|
3125
3153
|
});
|
|
3126
3154
|
}
|
|
3127
|
-
findOpenByInstanceId(instanceId, options) {
|
|
3128
|
-
return this.withLock(() => {
|
|
3129
|
-
this.sweepUnlocked(options);
|
|
3130
|
-
return this.readOpenRecordsUnlocked().find((record) => record.instanceId === instanceId);
|
|
3155
|
+
async findOpenByInstanceId(instanceId, options) {
|
|
3156
|
+
return this.withLock(async () => {
|
|
3157
|
+
await this.sweepUnlocked(options);
|
|
3158
|
+
return (await this.readOpenRecordsUnlocked()).find((record) => record.instanceId === instanceId);
|
|
3131
3159
|
});
|
|
3132
3160
|
}
|
|
3133
|
-
findAnyByInstanceId(instanceId) {
|
|
3134
|
-
return this.withLock(() => this.readRecordsUnlocked().find((record) => record.instanceId === instanceId));
|
|
3161
|
+
async findAnyByInstanceId(instanceId) {
|
|
3162
|
+
return this.withLock(async () => (await this.readRecordsUnlocked()).find((record) => record.instanceId === instanceId));
|
|
3135
3163
|
}
|
|
3136
|
-
findAnyByRecordId(recordId, options) {
|
|
3137
|
-
return this.withLock(() => {
|
|
3138
|
-
|
|
3139
|
-
|
|
3164
|
+
async findAnyByRecordId(recordId, options) {
|
|
3165
|
+
return this.withLock(async () => {
|
|
3166
|
+
if (options)
|
|
3167
|
+
await this.sweepUnlocked(options);
|
|
3168
|
+
return (await this.readRecordsUnlocked()).find((record) => record.recordId === recordId);
|
|
3140
3169
|
});
|
|
3141
3170
|
}
|
|
3142
|
-
listOpen(options) {
|
|
3143
|
-
return this.withLock(() => {
|
|
3144
|
-
this.sweepUnlocked(options);
|
|
3171
|
+
async listOpen(options) {
|
|
3172
|
+
return this.withLock(async () => {
|
|
3173
|
+
await this.sweepUnlocked(options);
|
|
3145
3174
|
return this.readOpenRecordsUnlocked();
|
|
3146
3175
|
});
|
|
3147
3176
|
}
|
|
3148
|
-
listOpenUnchecked() {
|
|
3177
|
+
async listOpenUnchecked() {
|
|
3149
3178
|
return this.withLock(() => this.readOpenRecordsUnlocked());
|
|
3150
3179
|
}
|
|
3151
|
-
markClosed(recordId, closedAt = Date.now()) {
|
|
3152
|
-
this.withLock(() => {
|
|
3153
|
-
const record = this.readRecordUnlocked(recordId);
|
|
3180
|
+
async markClosed(recordId, closedAt = Date.now()) {
|
|
3181
|
+
await this.withLock(async () => {
|
|
3182
|
+
const record = await this.readRecordUnlocked(recordId);
|
|
3154
3183
|
if (!record)
|
|
3155
3184
|
return;
|
|
3156
3185
|
record.closedAt = closedAt;
|
|
3157
|
-
this.writeRecordUnlocked(record);
|
|
3186
|
+
await this.writeRecordUnlocked(record);
|
|
3158
3187
|
});
|
|
3159
3188
|
}
|
|
3160
|
-
delete(recordId) {
|
|
3161
|
-
this.withLock(() => this.deleteRecordUnlocked(recordId));
|
|
3189
|
+
async delete(recordId) {
|
|
3190
|
+
await this.withLock(() => this.deleteRecordUnlocked(recordId));
|
|
3162
3191
|
}
|
|
3163
|
-
sweep(options) {
|
|
3164
|
-
this.withLock(() => this.sweepUnlocked(options));
|
|
3192
|
+
async sweep(options) {
|
|
3193
|
+
await this.withLock(() => this.sweepUnlocked(options));
|
|
3165
3194
|
}
|
|
3166
|
-
logEvent(event, now = Date.now()) {
|
|
3167
|
-
this.withLock(() => this.appendEventUnlocked(event, now));
|
|
3195
|
+
async logEvent(event, now = Date.now()) {
|
|
3196
|
+
await this.withLock(() => this.appendEventUnlocked(event, now));
|
|
3168
3197
|
}
|
|
3169
|
-
withLock(fn) {
|
|
3170
|
-
this.ensureDir();
|
|
3198
|
+
async withLock(fn) {
|
|
3199
|
+
await this.ensureDir();
|
|
3171
3200
|
const lockDir = path.join(this.dir, ".lock");
|
|
3172
3201
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
3173
|
-
|
|
3202
|
+
const owner = {
|
|
3203
|
+
pid: process.pid,
|
|
3204
|
+
token: randomUUID(),
|
|
3205
|
+
createdAt: Date.now()
|
|
3206
|
+
};
|
|
3207
|
+
for (; ; ) {
|
|
3174
3208
|
try {
|
|
3175
|
-
fs.
|
|
3209
|
+
await fs.mkdir(lockDir);
|
|
3210
|
+
activeLockTokens.add(owner.token);
|
|
3211
|
+
try {
|
|
3212
|
+
await fs.writeFile(path.join(lockDir, "owner.json"), `${JSON.stringify(owner)}
|
|
3213
|
+
`, {
|
|
3214
|
+
encoding: "utf8",
|
|
3215
|
+
flag: "wx"
|
|
3216
|
+
});
|
|
3217
|
+
} catch (error) {
|
|
3218
|
+
activeLockTokens.delete(owner.token);
|
|
3219
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
3220
|
+
throw error;
|
|
3221
|
+
}
|
|
3176
3222
|
break;
|
|
3177
3223
|
} catch (error) {
|
|
3178
3224
|
const code = error.code;
|
|
3179
3225
|
if (code !== "EEXIST")
|
|
3180
3226
|
throw error;
|
|
3181
3227
|
try {
|
|
3182
|
-
const
|
|
3183
|
-
|
|
3184
|
-
|
|
3228
|
+
const stat2 = await fs.stat(lockDir);
|
|
3229
|
+
const currentOwner = await this.readLockOwner(lockDir, stat2.isDirectory());
|
|
3230
|
+
const ownerIsActive = currentOwner?.pid === process.pid ? activeLockTokens.has(currentOwner.token) : currentOwner ? isProcessAlive(currentOwner.pid) : void 0;
|
|
3231
|
+
if (ownerIsActive === false || currentOwner === void 0 && Date.now() - stat2.mtimeMs > LOCK_STALE_MS) {
|
|
3232
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
3185
3233
|
continue;
|
|
3186
3234
|
}
|
|
3187
3235
|
} catch {
|
|
@@ -3190,40 +3238,58 @@ var init_managed_instance_registry = __esm({
|
|
|
3190
3238
|
if (Date.now() > deadline) {
|
|
3191
3239
|
throw new Error(`Timed out waiting for managed instance registry lock: ${lockDir}`);
|
|
3192
3240
|
}
|
|
3193
|
-
|
|
3241
|
+
await delay(LOCK_RETRY_MS);
|
|
3194
3242
|
}
|
|
3195
3243
|
}
|
|
3196
3244
|
try {
|
|
3197
|
-
return fn();
|
|
3245
|
+
return await fn();
|
|
3198
3246
|
} finally {
|
|
3199
|
-
|
|
3247
|
+
try {
|
|
3248
|
+
const stat2 = await fs.stat(lockDir);
|
|
3249
|
+
const currentOwner = await this.readLockOwner(lockDir, stat2.isDirectory());
|
|
3250
|
+
if (currentOwner?.pid === owner.pid && currentOwner.token === owner.token) {
|
|
3251
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
3252
|
+
}
|
|
3253
|
+
} catch {
|
|
3254
|
+
} finally {
|
|
3255
|
+
activeLockTokens.delete(owner.token);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
async readLockOwner(lockPath, isDirectory) {
|
|
3260
|
+
try {
|
|
3261
|
+
const contents = await fs.readFile(isDirectory ? path.join(lockPath, "owner.json") : lockPath, "utf8");
|
|
3262
|
+
const parsed = JSON.parse(contents);
|
|
3263
|
+
return isLockOwner(parsed) ? parsed : void 0;
|
|
3264
|
+
} catch {
|
|
3265
|
+
return void 0;
|
|
3200
3266
|
}
|
|
3201
3267
|
}
|
|
3202
|
-
ensureDir() {
|
|
3203
|
-
fs.
|
|
3268
|
+
async ensureDir() {
|
|
3269
|
+
await fs.mkdir(this.dir, { recursive: true });
|
|
3204
3270
|
}
|
|
3205
3271
|
recordPath(recordId) {
|
|
3206
3272
|
return path.join(this.dir, `${recordId}.json`);
|
|
3207
3273
|
}
|
|
3208
|
-
recordFilesUnlocked() {
|
|
3209
|
-
return fs.
|
|
3274
|
+
async recordFilesUnlocked() {
|
|
3275
|
+
return (await fs.readdir(this.dir)).filter((name) => name.endsWith(".json")).map((name) => path.join(this.dir, name));
|
|
3210
3276
|
}
|
|
3211
|
-
readRecordUnlocked(recordId) {
|
|
3277
|
+
async readRecordUnlocked(recordId) {
|
|
3212
3278
|
try {
|
|
3213
|
-
const parsed = JSON.parse(fs.
|
|
3279
|
+
const parsed = JSON.parse(await fs.readFile(this.recordPath(recordId), "utf8"));
|
|
3214
3280
|
return isRecord(parsed) ? parsed : void 0;
|
|
3215
3281
|
} catch {
|
|
3216
3282
|
return void 0;
|
|
3217
3283
|
}
|
|
3218
3284
|
}
|
|
3219
|
-
readOpenRecordsUnlocked() {
|
|
3220
|
-
return this.readRecordsUnlocked().filter((record) => record.closedAt === void 0);
|
|
3285
|
+
async readOpenRecordsUnlocked() {
|
|
3286
|
+
return (await this.readRecordsUnlocked()).filter((record) => record.closedAt === void 0);
|
|
3221
3287
|
}
|
|
3222
|
-
readRecordsUnlocked() {
|
|
3288
|
+
async readRecordsUnlocked() {
|
|
3223
3289
|
const records = [];
|
|
3224
|
-
for (const file of this.recordFilesUnlocked()) {
|
|
3290
|
+
for (const file of await this.recordFilesUnlocked()) {
|
|
3225
3291
|
try {
|
|
3226
|
-
const parsed = JSON.parse(fs.
|
|
3292
|
+
const parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
3227
3293
|
if (!isRecord(parsed))
|
|
3228
3294
|
continue;
|
|
3229
3295
|
records.push(parsed);
|
|
@@ -3232,48 +3298,48 @@ var init_managed_instance_registry = __esm({
|
|
|
3232
3298
|
}
|
|
3233
3299
|
return records;
|
|
3234
3300
|
}
|
|
3235
|
-
writeRecordUnlocked(record) {
|
|
3236
|
-
this.ensureDir();
|
|
3301
|
+
async writeRecordUnlocked(record) {
|
|
3302
|
+
await this.ensureDir();
|
|
3237
3303
|
const finalPath = this.recordPath(record.recordId);
|
|
3238
3304
|
const tmpPath = path.join(this.dir, `${record.recordId}.${process.pid}.${Date.now()}.tmp`);
|
|
3239
|
-
const fd = fs.
|
|
3305
|
+
const fd = await fs.open(tmpPath, "w");
|
|
3240
3306
|
try {
|
|
3241
|
-
|
|
3307
|
+
await fd.writeFile(`${JSON.stringify(record, null, 2)}
|
|
3242
3308
|
`, "utf8");
|
|
3243
|
-
|
|
3309
|
+
await fd.sync();
|
|
3244
3310
|
} finally {
|
|
3245
|
-
|
|
3311
|
+
await fd.close();
|
|
3246
3312
|
}
|
|
3247
|
-
fs.
|
|
3313
|
+
await fs.rename(tmpPath, finalPath);
|
|
3248
3314
|
}
|
|
3249
|
-
deleteRecordUnlocked(recordId) {
|
|
3250
|
-
fs.
|
|
3315
|
+
async deleteRecordUnlocked(recordId) {
|
|
3316
|
+
await fs.rm(this.recordPath(recordId), { force: true });
|
|
3251
3317
|
}
|
|
3252
|
-
appendEventUnlocked(event, now) {
|
|
3318
|
+
async appendEventUnlocked(event, now) {
|
|
3253
3319
|
const file = path.join(this.dir, `events-${ymd(now)}.jsonl`);
|
|
3254
|
-
fs.
|
|
3320
|
+
await fs.appendFile(file, `${JSON.stringify({
|
|
3255
3321
|
ts: new Date(now).toISOString(),
|
|
3256
3322
|
...event
|
|
3257
3323
|
})}
|
|
3258
3324
|
`, "utf8");
|
|
3259
3325
|
}
|
|
3260
|
-
cleanupOldEventLogsUnlocked(now) {
|
|
3326
|
+
async cleanupOldEventLogsUnlocked(now) {
|
|
3261
3327
|
const cutoff = ymd(now - EVENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3);
|
|
3262
|
-
for (const name of fs.
|
|
3328
|
+
for (const name of await fs.readdir(this.dir)) {
|
|
3263
3329
|
const date = eventLogDate(name);
|
|
3264
3330
|
if (!date || date >= cutoff)
|
|
3265
3331
|
continue;
|
|
3266
3332
|
try {
|
|
3267
|
-
fs.
|
|
3333
|
+
await fs.rm(path.join(this.dir, name), { force: true });
|
|
3268
3334
|
} catch {
|
|
3269
3335
|
}
|
|
3270
3336
|
}
|
|
3271
3337
|
}
|
|
3272
|
-
cleanupRecord(options, record) {
|
|
3338
|
+
async cleanupRecord(options, record) {
|
|
3273
3339
|
try {
|
|
3274
|
-
options.cleanupRecord?.(record);
|
|
3340
|
+
await options.cleanupRecord?.(record);
|
|
3275
3341
|
} catch {
|
|
3276
|
-
this.appendEventUnlocked({
|
|
3342
|
+
await this.appendEventUnlocked({
|
|
3277
3343
|
event: "registry_cleanup_failed",
|
|
3278
3344
|
recordId: record.recordId,
|
|
3279
3345
|
instanceId: record.instanceId,
|
|
@@ -3282,16 +3348,16 @@ var init_managed_instance_registry = __esm({
|
|
|
3282
3348
|
}, options.now ?? Date.now());
|
|
3283
3349
|
}
|
|
3284
3350
|
}
|
|
3285
|
-
sweepUnlocked(options) {
|
|
3351
|
+
async sweepUnlocked(options) {
|
|
3286
3352
|
const now = options.now ?? Date.now();
|
|
3287
|
-
this.cleanupOldEventLogsUnlocked(now);
|
|
3288
|
-
for (const file of this.recordFilesUnlocked()) {
|
|
3353
|
+
await this.cleanupOldEventLogsUnlocked(now);
|
|
3354
|
+
for (const file of await this.recordFilesUnlocked()) {
|
|
3289
3355
|
let parsed;
|
|
3290
3356
|
try {
|
|
3291
|
-
parsed = JSON.parse(fs.
|
|
3357
|
+
parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
3292
3358
|
} catch {
|
|
3293
|
-
fs.
|
|
3294
|
-
this.appendEventUnlocked({
|
|
3359
|
+
await fs.rm(file, { force: true });
|
|
3360
|
+
await this.appendEventUnlocked({
|
|
3295
3361
|
event: "registry_pruned_malformed_record",
|
|
3296
3362
|
reason: "parse_error",
|
|
3297
3363
|
action: "deleted_record"
|
|
@@ -3299,8 +3365,8 @@ var init_managed_instance_registry = __esm({
|
|
|
3299
3365
|
continue;
|
|
3300
3366
|
}
|
|
3301
3367
|
if (!parsed || typeof parsed !== "object") {
|
|
3302
|
-
fs.
|
|
3303
|
-
this.appendEventUnlocked({
|
|
3368
|
+
await fs.rm(file, { force: true });
|
|
3369
|
+
await this.appendEventUnlocked({
|
|
3304
3370
|
event: "registry_pruned_malformed_record",
|
|
3305
3371
|
reason: "invalid_shape",
|
|
3306
3372
|
action: "deleted_record"
|
|
@@ -3311,8 +3377,8 @@ var init_managed_instance_registry = __esm({
|
|
|
3311
3377
|
if (typeof version === "number" && version > REGISTRY_VERSION)
|
|
3312
3378
|
continue;
|
|
3313
3379
|
if (!isRecord(parsed)) {
|
|
3314
|
-
fs.
|
|
3315
|
-
this.appendEventUnlocked({
|
|
3380
|
+
await fs.rm(file, { force: true });
|
|
3381
|
+
await this.appendEventUnlocked({
|
|
3316
3382
|
event: "registry_pruned_malformed_record",
|
|
3317
3383
|
reason: "invalid_shape",
|
|
3318
3384
|
action: "deleted_record"
|
|
@@ -3322,8 +3388,8 @@ var init_managed_instance_registry = __esm({
|
|
|
3322
3388
|
const terminalAt = parsed.closedAt ?? parsed.exitedAt;
|
|
3323
3389
|
if (terminalAt !== void 0) {
|
|
3324
3390
|
if (now - terminalAt > TERMINAL_RECORD_RETENTION_MS) {
|
|
3325
|
-
fs.
|
|
3326
|
-
this.appendEventUnlocked({
|
|
3391
|
+
await fs.rm(file, { force: true });
|
|
3392
|
+
await this.appendEventUnlocked({
|
|
3327
3393
|
event: "registry_pruned_terminal_record",
|
|
3328
3394
|
recordId: parsed.recordId,
|
|
3329
3395
|
instanceId: parsed.instanceId,
|
|
@@ -3335,13 +3401,13 @@ var init_managed_instance_registry = __esm({
|
|
|
3335
3401
|
continue;
|
|
3336
3402
|
}
|
|
3337
3403
|
if (parsed.bootId !== options.currentBootId) {
|
|
3338
|
-
this.cleanupRecord(options, parsed);
|
|
3404
|
+
await this.cleanupRecord(options, parsed);
|
|
3339
3405
|
parsed.state = parsed.state === "failed" ? "failed" : "exited";
|
|
3340
3406
|
parsed.exitedAt = now;
|
|
3341
3407
|
parsed.closedAt = now;
|
|
3342
3408
|
parsed.failureReason ??= "Studio process belongs to a previous host boot.";
|
|
3343
|
-
this.writeRecordUnlocked(parsed);
|
|
3344
|
-
this.appendEventUnlocked({
|
|
3409
|
+
await this.writeRecordUnlocked(parsed);
|
|
3410
|
+
await this.appendEventUnlocked({
|
|
3345
3411
|
event: "registry_marked_previous_boot_exited",
|
|
3346
3412
|
recordId: parsed.recordId,
|
|
3347
3413
|
instanceId: parsed.instanceId,
|
|
@@ -3351,22 +3417,56 @@ var init_managed_instance_registry = __esm({
|
|
|
3351
3417
|
}, now);
|
|
3352
3418
|
continue;
|
|
3353
3419
|
}
|
|
3354
|
-
if (options.
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
parsed.
|
|
3359
|
-
parsed.
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3420
|
+
if (!options.observeProcess || !(parsed.nativeProcessId || parsed.spawnPid))
|
|
3421
|
+
continue;
|
|
3422
|
+
const observation = await options.observeProcess(parsed);
|
|
3423
|
+
if (observation.status === "unknown") {
|
|
3424
|
+
parsed.processObservationStatus = "unknown";
|
|
3425
|
+
parsed.lastProcessObservationAt = observation.observedAt;
|
|
3426
|
+
parsed.lastProcessObservationError = observation.error;
|
|
3427
|
+
parsed.consecutiveConfirmedMisses = 0;
|
|
3428
|
+
parsed.firstConfirmedMissAt = void 0;
|
|
3429
|
+
await this.writeRecordUnlocked(parsed);
|
|
3430
|
+
continue;
|
|
3431
|
+
}
|
|
3432
|
+
const previousObservationAt = parsed.lastProcessObservationAt;
|
|
3433
|
+
parsed.lastSuccessfulProcessObservationAt = observation.observedAt;
|
|
3434
|
+
parsed.lastProcessObservationAt = observation.observedAt;
|
|
3435
|
+
parsed.lastProcessObservationError = void 0;
|
|
3436
|
+
if (observation.status === "running") {
|
|
3437
|
+
parsed.processObservationStatus = "running";
|
|
3438
|
+
parsed.consecutiveConfirmedMisses = 0;
|
|
3439
|
+
parsed.firstConfirmedMissAt = void 0;
|
|
3440
|
+
await this.writeRecordUnlocked(parsed);
|
|
3441
|
+
continue;
|
|
3369
3442
|
}
|
|
3443
|
+
const isNewObservation = previousObservationAt !== observation.observedAt;
|
|
3444
|
+
parsed.processObservationStatus = "not_running";
|
|
3445
|
+
if (previousObservationAt !== observation.observedAt) {
|
|
3446
|
+
parsed.consecutiveConfirmedMisses = (parsed.consecutiveConfirmedMisses ?? 0) + 1;
|
|
3447
|
+
parsed.firstConfirmedMissAt ??= observation.observedAt;
|
|
3448
|
+
}
|
|
3449
|
+
const requiredMisses = options.confirmedExitMisses ?? DEFAULT_CONFIRMED_EXIT_MISSES;
|
|
3450
|
+
const graceMs = options.confirmedExitGraceMs ?? DEFAULT_CONFIRMED_EXIT_GRACE_MS;
|
|
3451
|
+
const confirmedAbsent = observation.reason === "identity_mismatch" || isNewObservation && (parsed.consecutiveConfirmedMisses ?? 0) >= requiredMisses && observation.observedAt - (parsed.firstConfirmedMissAt ?? observation.observedAt) >= graceMs;
|
|
3452
|
+
if (!confirmedAbsent) {
|
|
3453
|
+
await this.writeRecordUnlocked(parsed);
|
|
3454
|
+
continue;
|
|
3455
|
+
}
|
|
3456
|
+
await this.cleanupRecord(options, parsed);
|
|
3457
|
+
parsed.state = parsed.state === "failed" ? "failed" : "exited";
|
|
3458
|
+
parsed.exitedAt = observation.observedAt;
|
|
3459
|
+
parsed.closedAt = observation.observedAt;
|
|
3460
|
+
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.";
|
|
3461
|
+
await this.writeRecordUnlocked(parsed);
|
|
3462
|
+
await this.appendEventUnlocked({
|
|
3463
|
+
event: "registry_marked_process_exited",
|
|
3464
|
+
recordId: parsed.recordId,
|
|
3465
|
+
instanceId: parsed.instanceId,
|
|
3466
|
+
source: parsed.source,
|
|
3467
|
+
reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
|
|
3468
|
+
action: "marked_exited_and_cleaned_baseplate"
|
|
3469
|
+
}, now);
|
|
3370
3470
|
}
|
|
3371
3471
|
}
|
|
3372
3472
|
};
|
|
@@ -3374,11 +3474,12 @@ var init_managed_instance_registry = __esm({
|
|
|
3374
3474
|
});
|
|
3375
3475
|
|
|
3376
3476
|
// ../core/dist/studio-instance-manager.js
|
|
3377
|
-
import { execFileSync, spawn } from "child_process";
|
|
3378
|
-
import { copyFileSync, existsSync as existsSync2, mkdirSync as
|
|
3379
|
-
import { randomUUID } from "crypto";
|
|
3477
|
+
import { execFile, execFileSync, spawn } from "child_process";
|
|
3478
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, realpathSync, rmSync, statSync } from "fs";
|
|
3479
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3380
3480
|
import * as os2 from "os";
|
|
3381
3481
|
import * as path2 from "path";
|
|
3482
|
+
import { promisify } from "util";
|
|
3382
3483
|
function run(command, args, options = {}) {
|
|
3383
3484
|
return execFileSync(command, args, {
|
|
3384
3485
|
encoding: "utf8",
|
|
@@ -3386,17 +3487,29 @@ function run(command, args, options = {}) {
|
|
|
3386
3487
|
...options
|
|
3387
3488
|
}).trim();
|
|
3388
3489
|
}
|
|
3490
|
+
async function runAsync(command, args, options = {}) {
|
|
3491
|
+
const result = await execFileAsync(command, args, {
|
|
3492
|
+
encoding: "utf8",
|
|
3493
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
3494
|
+
timeout: 15e3,
|
|
3495
|
+
killSignal: "SIGKILL",
|
|
3496
|
+
...options
|
|
3497
|
+
});
|
|
3498
|
+
return `${result.stdout}`.trim();
|
|
3499
|
+
}
|
|
3389
3500
|
function isWsl() {
|
|
3390
3501
|
if (process.platform !== "linux")
|
|
3391
3502
|
return false;
|
|
3503
|
+
if (!process.env.WSL_INTEROP && !process.env.WSL_DISTRO_NAME)
|
|
3504
|
+
return false;
|
|
3392
3505
|
try {
|
|
3393
|
-
return /microsoft|wsl/i.test(
|
|
3506
|
+
return /microsoft|wsl/i.test(readFileSync2("/proc/version", "utf8"));
|
|
3394
3507
|
} catch {
|
|
3395
3508
|
return false;
|
|
3396
3509
|
}
|
|
3397
3510
|
}
|
|
3398
|
-
function
|
|
3399
|
-
return
|
|
3511
|
+
async function powershellAsync(script) {
|
|
3512
|
+
return runAsync("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
3400
3513
|
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
3401
3514
|
});
|
|
3402
3515
|
}
|
|
@@ -3413,19 +3526,96 @@ function windowsLocalAppData() {
|
|
|
3413
3526
|
return void 0;
|
|
3414
3527
|
}
|
|
3415
3528
|
}
|
|
3529
|
+
async function windowsLocalAppDataAsync() {
|
|
3530
|
+
if (process.platform === "win32")
|
|
3531
|
+
return process.env.LOCALAPPDATA;
|
|
3532
|
+
if (!isWsl())
|
|
3533
|
+
return void 0;
|
|
3534
|
+
try {
|
|
3535
|
+
return await runAsync("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
|
|
3536
|
+
cwd: existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
3537
|
+
});
|
|
3538
|
+
} catch {
|
|
3539
|
+
return void 0;
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3416
3542
|
function toWslPath(windowsPath) {
|
|
3417
3543
|
if (!isWsl())
|
|
3418
3544
|
return windowsPath;
|
|
3419
3545
|
return run("wslpath", ["-u", windowsPath]);
|
|
3420
3546
|
}
|
|
3421
|
-
function
|
|
3547
|
+
async function toWslPathAsync(windowsPath) {
|
|
3548
|
+
if (!isWsl())
|
|
3549
|
+
return windowsPath;
|
|
3550
|
+
return runAsync("wslpath", ["-u", windowsPath]);
|
|
3551
|
+
}
|
|
3552
|
+
async function toStudioLaunchArgAsync(arg) {
|
|
3422
3553
|
if (!isWsl() || !path2.isAbsolute(arg) || !existsSync2(arg))
|
|
3423
3554
|
return arg;
|
|
3424
|
-
return
|
|
3555
|
+
return runAsync("wslpath", ["-w", arg]);
|
|
3425
3556
|
}
|
|
3426
3557
|
function powershellStringLiteral(value) {
|
|
3427
3558
|
return `'${value.replace(/'/g, "''")}'`;
|
|
3428
3559
|
}
|
|
3560
|
+
function parseStudioProcessEnvironmentPatch(value) {
|
|
3561
|
+
if (value === void 0)
|
|
3562
|
+
return void 0;
|
|
3563
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
3564
|
+
throw new Error("process_environment must be an object when provided.");
|
|
3565
|
+
}
|
|
3566
|
+
const raw = value;
|
|
3567
|
+
const unsupported = Object.keys(raw).filter((key) => key !== "set" && key !== "remove");
|
|
3568
|
+
if (unsupported.length > 0) {
|
|
3569
|
+
throw new Error(`process_environment contains unsupported field(s): ${unsupported.join(", ")}.`);
|
|
3570
|
+
}
|
|
3571
|
+
let set;
|
|
3572
|
+
if (raw.set !== void 0) {
|
|
3573
|
+
if (raw.set === null || typeof raw.set !== "object" || Array.isArray(raw.set)) {
|
|
3574
|
+
throw new Error("process_environment.set must be an object mapping names to string values.");
|
|
3575
|
+
}
|
|
3576
|
+
set = {};
|
|
3577
|
+
for (const [name, setting] of Object.entries(raw.set)) {
|
|
3578
|
+
if (!ENVIRONMENT_VARIABLE_NAME.test(name)) {
|
|
3579
|
+
throw new Error(`Invalid process environment variable name "${name}".`);
|
|
3580
|
+
}
|
|
3581
|
+
if (typeof setting !== "string") {
|
|
3582
|
+
throw new Error(`process_environment.set.${name} must be a string.`);
|
|
3583
|
+
}
|
|
3584
|
+
if (setting.includes("\0")) {
|
|
3585
|
+
throw new Error(`process_environment.set.${name} must not contain a null character.`);
|
|
3586
|
+
}
|
|
3587
|
+
set[name] = setting;
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
let remove;
|
|
3591
|
+
if (raw.remove !== void 0) {
|
|
3592
|
+
if (!Array.isArray(raw.remove) || raw.remove.some((name) => typeof name !== "string")) {
|
|
3593
|
+
throw new Error("process_environment.remove must be an array of environment variable names.");
|
|
3594
|
+
}
|
|
3595
|
+
remove = [...new Set(raw.remove)];
|
|
3596
|
+
for (const name of remove) {
|
|
3597
|
+
if (!ENVIRONMENT_VARIABLE_NAME.test(name)) {
|
|
3598
|
+
throw new Error(`Invalid process environment variable name "${name}".`);
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
return { set, remove };
|
|
3603
|
+
}
|
|
3604
|
+
function patchedProcessEnvironment(patch) {
|
|
3605
|
+
const environment = { ...process.env };
|
|
3606
|
+
for (const name of patch.remove ?? [])
|
|
3607
|
+
delete environment[name];
|
|
3608
|
+
for (const [name, value] of Object.entries(patch.set ?? {}))
|
|
3609
|
+
environment[name] = value;
|
|
3610
|
+
return environment;
|
|
3611
|
+
}
|
|
3612
|
+
function powershellEnvironmentPatchStatements(patch) {
|
|
3613
|
+
const target = "[EnvironmentVariableTarget]::Process";
|
|
3614
|
+
return [
|
|
3615
|
+
...(patch.remove ?? []).map((name) => `[Environment]::SetEnvironmentVariable(${powershellStringLiteral(name)}, $null, ${target})`),
|
|
3616
|
+
...Object.entries(patch.set ?? {}).map(([name, value]) => `[Environment]::SetEnvironmentVariable(${powershellStringLiteral(name)}, ${powershellStringLiteral(value)}, ${target})`)
|
|
3617
|
+
];
|
|
3618
|
+
}
|
|
3429
3619
|
function quoteWindowsCommandLineArg(value) {
|
|
3430
3620
|
if (value.length > 0 && !/[\s"]/u.test(value))
|
|
3431
3621
|
return value;
|
|
@@ -3446,16 +3636,17 @@ function quoteWindowsCommandLineArg(value) {
|
|
|
3446
3636
|
}
|
|
3447
3637
|
return `${quoted}${"\\".repeat(backslashes * 2)}"`;
|
|
3448
3638
|
}
|
|
3449
|
-
function
|
|
3450
|
-
const
|
|
3639
|
+
function buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment) {
|
|
3640
|
+
const environmentPatch = parseStudioProcessEnvironmentPatch(processEnvironment);
|
|
3451
3641
|
const commandLine = args.map(quoteWindowsCommandLineArg).join(" ");
|
|
3452
3642
|
return [
|
|
3643
|
+
...environmentPatch ? powershellEnvironmentPatchStatements(environmentPatch) : [],
|
|
3453
3644
|
"$psi = New-Object System.Diagnostics.ProcessStartInfo",
|
|
3454
3645
|
`$psi.FileName = ${powershellStringLiteral(windowsExe)}`,
|
|
3455
3646
|
`$psi.Arguments = ${powershellStringLiteral(commandLine)}`,
|
|
3456
3647
|
// With UseShellExecute=false, Studio inherits the synchronous
|
|
3457
3648
|
// powershell.exe invocation's stdout/stderr pipe handles under WSL. Those
|
|
3458
|
-
// handles keep
|
|
3649
|
+
// handles keep the PowerShell invocation waiting until Studio exits even though
|
|
3459
3650
|
// PowerShell already printed the PID. Shell execution prevents that
|
|
3460
3651
|
// inheritance while Process.Start still returns the native Studio PID.
|
|
3461
3652
|
"$psi.UseShellExecute = $true",
|
|
@@ -3463,9 +3654,10 @@ function buildWindowsStudioStartScript(exe, args) {
|
|
|
3463
3654
|
'if ($null -eq $studio) { throw "Roblox Studio process did not start." }'
|
|
3464
3655
|
].join("; ");
|
|
3465
3656
|
}
|
|
3466
|
-
function spawnWindowsStudioFromWsl(exe, args) {
|
|
3467
|
-
const
|
|
3468
|
-
const
|
|
3657
|
+
async function spawnWindowsStudioFromWsl(exe, args, processEnvironment) {
|
|
3658
|
+
const windowsExe = await toStudioLaunchArgAsync(exe);
|
|
3659
|
+
const script = buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment);
|
|
3660
|
+
const output = await powershellAsync(`${script}; [PSCustomObject]@{ pid = $studio.Id; started = $studio.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } | ConvertTo-Json -Compress`);
|
|
3469
3661
|
const parsed = JSON.parse(output);
|
|
3470
3662
|
const nativePid = Number(parsed.pid);
|
|
3471
3663
|
const nativeStartedAt = typeof parsed.started === "string" && /^\d+$/u.test(parsed.started) ? parsed.started : void 0;
|
|
@@ -3507,7 +3699,7 @@ function resolveBaseplateTemplatePath() {
|
|
|
3507
3699
|
}
|
|
3508
3700
|
throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
|
|
3509
3701
|
}
|
|
3510
|
-
function
|
|
3702
|
+
function isProcessAlive2(pid) {
|
|
3511
3703
|
try {
|
|
3512
3704
|
process.kill(pid, 0);
|
|
3513
3705
|
return true;
|
|
@@ -3518,7 +3710,7 @@ function isProcessAlive(pid) {
|
|
|
3518
3710
|
function sweepStaleBaseplateFiles() {
|
|
3519
3711
|
let entries;
|
|
3520
3712
|
try {
|
|
3521
|
-
entries =
|
|
3713
|
+
entries = readdirSync(BASEPLATE_TEMP_DIR);
|
|
3522
3714
|
} catch {
|
|
3523
3715
|
return;
|
|
3524
3716
|
}
|
|
@@ -3527,18 +3719,18 @@ function sweepStaleBaseplateFiles() {
|
|
|
3527
3719
|
const match = BASEPLATE_TEMP_SWEEP_NAME.exec(entry);
|
|
3528
3720
|
if (!match)
|
|
3529
3721
|
continue;
|
|
3530
|
-
if (Number(match[1]) !== process.pid &&
|
|
3722
|
+
if (Number(match[1]) !== process.pid && isProcessAlive2(Number(match[1])))
|
|
3531
3723
|
continue;
|
|
3532
3724
|
const file = path2.join(BASEPLATE_TEMP_DIR, entry);
|
|
3533
3725
|
try {
|
|
3534
|
-
if (
|
|
3535
|
-
|
|
3726
|
+
if (statSync(file).mtimeMs < cutoff)
|
|
3727
|
+
rmSync(file, { force: true });
|
|
3536
3728
|
} catch {
|
|
3537
3729
|
}
|
|
3538
3730
|
}
|
|
3539
3731
|
}
|
|
3540
3732
|
function createBaseplatePlaceFile() {
|
|
3541
|
-
|
|
3733
|
+
mkdirSync2(BASEPLATE_TEMP_DIR, { recursive: true });
|
|
3542
3734
|
sweepStaleBaseplateFiles();
|
|
3543
3735
|
const file = path2.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
|
|
3544
3736
|
copyFileSync(resolveBaseplateTemplatePath(), file);
|
|
@@ -3555,7 +3747,7 @@ function cleanupManagedBaseplateFiles(record) {
|
|
|
3555
3747
|
return;
|
|
3556
3748
|
for (const file of [record.localPlaceFile, `${record.localPlaceFile}.lock`]) {
|
|
3557
3749
|
try {
|
|
3558
|
-
|
|
3750
|
+
rmSync(file, { force: true });
|
|
3559
3751
|
} catch {
|
|
3560
3752
|
}
|
|
3561
3753
|
}
|
|
@@ -3582,54 +3774,83 @@ function resolveStudioExe() {
|
|
|
3582
3774
|
if (!existsSync2(root)) {
|
|
3583
3775
|
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
3584
3776
|
}
|
|
3585
|
-
const candidates =
|
|
3777
|
+
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);
|
|
3586
3778
|
if (candidates.length === 0) {
|
|
3587
3779
|
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
3588
3780
|
}
|
|
3589
3781
|
return candidates[0];
|
|
3590
3782
|
}
|
|
3591
|
-
function
|
|
3783
|
+
async function resolveStudioExeAsync() {
|
|
3784
|
+
if (process.env.ROBLOX_STUDIO_EXE)
|
|
3785
|
+
return process.env.ROBLOX_STUDIO_EXE;
|
|
3592
3786
|
if (process.platform === "darwin") {
|
|
3593
|
-
|
|
3594
|
-
try {
|
|
3595
|
-
out2 = run("pgrep", ["-fl", "RobloxStudio"]);
|
|
3596
|
-
} catch {
|
|
3597
|
-
return [];
|
|
3598
|
-
}
|
|
3599
|
-
return out2.split("\n").filter(Boolean).map((line) => {
|
|
3600
|
-
const [pid, ...rest] = line.trim().split(/\s+/);
|
|
3601
|
-
return { Id: Number(pid), Name: "RobloxStudio", Path: rest.join(" "), MainWindowTitle: "" };
|
|
3602
|
-
}).filter((proc) => Number.isFinite(proc.Id));
|
|
3787
|
+
return "/Applications/RobloxStudio.app/Contents/MacOS/RobloxStudio";
|
|
3603
3788
|
}
|
|
3604
|
-
if (process.platform !== "win32" && !isWsl())
|
|
3605
|
-
|
|
3606
|
-
|
|
3789
|
+
if (process.platform !== "win32" && !isWsl()) {
|
|
3790
|
+
throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
|
|
3791
|
+
}
|
|
3792
|
+
const localAppData = await windowsLocalAppDataAsync();
|
|
3793
|
+
const root = localAppData ? path2.join(await toWslPathAsync(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
3794
|
+
if (!existsSync2(root)) {
|
|
3795
|
+
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
3796
|
+
}
|
|
3797
|
+
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);
|
|
3798
|
+
if (candidates.length === 0) {
|
|
3799
|
+
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
3800
|
+
}
|
|
3801
|
+
return candidates[0];
|
|
3802
|
+
}
|
|
3803
|
+
async function observeStudioProcesses() {
|
|
3804
|
+
const observedAt = Date.now();
|
|
3607
3805
|
try {
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3806
|
+
if (process.platform === "darwin") {
|
|
3807
|
+
let out2 = "";
|
|
3808
|
+
try {
|
|
3809
|
+
out2 = await runAsync("pgrep", ["-fl", "RobloxStudio"]);
|
|
3810
|
+
} catch (error) {
|
|
3811
|
+
const code = error.code;
|
|
3812
|
+
if (Number(code) === 1)
|
|
3813
|
+
return { status: "ok", observedAt, processes: [] };
|
|
3814
|
+
throw error;
|
|
3815
|
+
}
|
|
3816
|
+
const processes = out2.split("\n").filter(Boolean).map((line) => {
|
|
3817
|
+
const [pid, ...rest] = line.trim().split(/\s+/);
|
|
3818
|
+
return { Id: Number(pid), Name: "RobloxStudio", Path: rest.join(" "), MainWindowTitle: "" };
|
|
3819
|
+
}).filter((proc) => Number.isFinite(proc.Id));
|
|
3820
|
+
return { status: "ok", observedAt, processes };
|
|
3821
|
+
}
|
|
3822
|
+
if (process.platform !== "win32" && !isWsl()) {
|
|
3823
|
+
return { status: "ok", observedAt, processes: [] };
|
|
3824
|
+
}
|
|
3825
|
+
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");
|
|
3826
|
+
if (!out)
|
|
3827
|
+
return { status: "ok", observedAt, processes: [] };
|
|
3828
|
+
const parsed = JSON.parse(out);
|
|
3829
|
+
return { status: "ok", observedAt, processes: Array.isArray(parsed) ? parsed : [parsed] };
|
|
3830
|
+
} catch (error) {
|
|
3831
|
+
return {
|
|
3832
|
+
status: "error",
|
|
3833
|
+
observedAt,
|
|
3834
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3835
|
+
};
|
|
3611
3836
|
}
|
|
3612
|
-
if (!out)
|
|
3613
|
-
return [];
|
|
3614
|
-
const parsed = JSON.parse(out);
|
|
3615
|
-
return Array.isArray(parsed) ? parsed : [parsed];
|
|
3616
3837
|
}
|
|
3617
|
-
function
|
|
3838
|
+
async function currentBootIdAsync() {
|
|
3618
3839
|
if (process.platform === "linux") {
|
|
3619
3840
|
try {
|
|
3620
|
-
return
|
|
3841
|
+
return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
3621
3842
|
} catch {
|
|
3622
3843
|
}
|
|
3623
3844
|
}
|
|
3624
3845
|
if (process.platform === "win32" || isWsl()) {
|
|
3625
3846
|
try {
|
|
3626
|
-
return
|
|
3847
|
+
return await powershellAsync('(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString("o")');
|
|
3627
3848
|
} catch {
|
|
3628
3849
|
}
|
|
3629
3850
|
}
|
|
3630
3851
|
if (process.platform === "darwin") {
|
|
3631
3852
|
try {
|
|
3632
|
-
return
|
|
3853
|
+
return await runAsync("sysctl", ["-n", "kern.boottime"]);
|
|
3633
3854
|
} catch {
|
|
3634
3855
|
}
|
|
3635
3856
|
}
|
|
@@ -3668,13 +3889,13 @@ function buildStudioLaunchArgs(options) {
|
|
|
3668
3889
|
];
|
|
3669
3890
|
}
|
|
3670
3891
|
}
|
|
3671
|
-
function
|
|
3892
|
+
function delay2(ms) {
|
|
3672
3893
|
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
3673
3894
|
}
|
|
3674
3895
|
function basenameAny(filePath) {
|
|
3675
3896
|
return path2.basename(filePath.replace(/\\/g, "/"));
|
|
3676
3897
|
}
|
|
3677
|
-
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
|
|
3898
|
+
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, execFileAsync, ENVIRONMENT_VARIABLE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
|
|
3678
3899
|
var init_studio_instance_manager = __esm({
|
|
3679
3900
|
"../core/dist/studio-instance-manager.js"() {
|
|
3680
3901
|
"use strict";
|
|
@@ -3682,26 +3903,39 @@ var init_studio_instance_manager = __esm({
|
|
|
3682
3903
|
BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
|
|
3683
3904
|
BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
|
|
3684
3905
|
BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
|
|
3906
|
+
execFileAsync = promisify(execFile);
|
|
3907
|
+
ENVIRONMENT_VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
3685
3908
|
STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
3686
3909
|
BASEPLATE_TEMP_SWEEP_NAME = /^Baseplate-(\d+)-\d+\.rbxl(\.lock)?$/;
|
|
3687
3910
|
StudioInstanceManager = class {
|
|
3688
3911
|
managedByInstanceId = /* @__PURE__ */ new Map();
|
|
3689
3912
|
pending = /* @__PURE__ */ new Set();
|
|
3690
|
-
monitors = /* @__PURE__ */ new Map();
|
|
3691
3913
|
connectionTimers = /* @__PURE__ */ new Map();
|
|
3692
3914
|
registry;
|
|
3693
3915
|
processAdapter;
|
|
3916
|
+
confirmedExitMisses;
|
|
3917
|
+
confirmedExitGraceMs;
|
|
3918
|
+
snapshotCacheMs;
|
|
3919
|
+
coordinatorTimer;
|
|
3920
|
+
coordinatorRefresh;
|
|
3921
|
+
cachedSnapshot;
|
|
3922
|
+
snapshotInFlight;
|
|
3923
|
+
launchQueue = Promise.resolve();
|
|
3694
3924
|
constructor(options = {}) {
|
|
3695
3925
|
this.registry = options.registry ?? new ManagedInstanceRegistry(options.registryDir);
|
|
3696
3926
|
this.processAdapter = options.processAdapter ?? {};
|
|
3927
|
+
this.confirmedExitMisses = options.confirmedExitMisses ?? 2;
|
|
3928
|
+
this.confirmedExitGraceMs = options.confirmedExitGraceMs ?? 5e3;
|
|
3929
|
+
this.snapshotCacheMs = options.snapshotCacheMs ?? 0;
|
|
3697
3930
|
}
|
|
3698
|
-
list() {
|
|
3699
|
-
this.
|
|
3931
|
+
async list() {
|
|
3932
|
+
const snapshot = await this.getProcessSnapshot();
|
|
3933
|
+
await this.sweepRegistry(snapshot);
|
|
3700
3934
|
for (const record of [...this.managedByInstanceId.values(), ...this.pending]) {
|
|
3701
|
-
this.refresh(record);
|
|
3935
|
+
await this.refresh(record, snapshot);
|
|
3702
3936
|
}
|
|
3703
3937
|
const records = [...this.managedByInstanceId.values(), ...this.pending];
|
|
3704
|
-
for (const registryRecord of this.registry.
|
|
3938
|
+
for (const registryRecord of await this.registry.listOpenUnchecked()) {
|
|
3705
3939
|
const record = this.fromRegistryRecord(registryRecord);
|
|
3706
3940
|
if (records.some((existing) => record.recordId && existing.recordId === record.recordId || record.instanceId && existing.instanceId === record.instanceId)) {
|
|
3707
3941
|
continue;
|
|
@@ -3710,27 +3944,30 @@ var init_studio_instance_manager = __esm({
|
|
|
3710
3944
|
}
|
|
3711
3945
|
return records.filter((record) => record.closedAt === void 0).filter((instance, index, all) => all.indexOf(instance) === index);
|
|
3712
3946
|
}
|
|
3713
|
-
get(instanceId) {
|
|
3714
|
-
this.
|
|
3947
|
+
async get(instanceId) {
|
|
3948
|
+
const snapshot = await this.getProcessSnapshot();
|
|
3949
|
+
await this.sweepRegistry(snapshot);
|
|
3715
3950
|
const memoryRecord = this.managedByInstanceId.get(instanceId);
|
|
3716
3951
|
if (memoryRecord)
|
|
3717
|
-
return this.refresh(memoryRecord);
|
|
3718
|
-
const registryRecord = this.registry.findAnyByInstanceId(instanceId);
|
|
3719
|
-
return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
|
|
3952
|
+
return this.refresh(memoryRecord, snapshot);
|
|
3953
|
+
const registryRecord = await this.registry.findAnyByInstanceId(instanceId);
|
|
3954
|
+
return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord), snapshot) : void 0;
|
|
3720
3955
|
}
|
|
3721
|
-
getByLaunchId(launchId) {
|
|
3722
|
-
this.
|
|
3956
|
+
async getByLaunchId(launchId) {
|
|
3957
|
+
const snapshot = await this.getProcessSnapshot();
|
|
3958
|
+
await this.sweepRegistry(snapshot);
|
|
3723
3959
|
const memoryRecord = [...this.managedByInstanceId.values(), ...this.pending].find((record) => record.recordId === launchId);
|
|
3724
3960
|
if (memoryRecord)
|
|
3725
|
-
return this.refresh(memoryRecord);
|
|
3726
|
-
const registryRecord = this.registry.findAnyByRecordId(launchId
|
|
3727
|
-
return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
|
|
3961
|
+
return this.refresh(memoryRecord, snapshot);
|
|
3962
|
+
const registryRecord = await this.registry.findAnyByRecordId(launchId);
|
|
3963
|
+
return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord), snapshot) : void 0;
|
|
3728
3964
|
}
|
|
3729
|
-
pendingLaunches() {
|
|
3965
|
+
async pendingLaunches() {
|
|
3730
3966
|
const now = Date.now();
|
|
3967
|
+
const bootId = await this.getCurrentBootId();
|
|
3731
3968
|
const records = [...this.pending];
|
|
3732
|
-
for (const registryRecord of this.registry.listOpenUnchecked()) {
|
|
3733
|
-
if (registryRecord.bootId !==
|
|
3969
|
+
for (const registryRecord of await this.registry.listOpenUnchecked()) {
|
|
3970
|
+
if (registryRecord.bootId !== bootId)
|
|
3734
3971
|
continue;
|
|
3735
3972
|
if (records.some((record) => record.recordId === registryRecord.recordId))
|
|
3736
3973
|
continue;
|
|
@@ -3738,8 +3975,9 @@ var init_studio_instance_manager = __esm({
|
|
|
3738
3975
|
}
|
|
3739
3976
|
return records.filter((record) => record.instanceId === void 0).filter((record) => record.state === "launching").filter((record) => record.connectionDeadlineAt === void 0 || record.connectionDeadlineAt > now);
|
|
3740
3977
|
}
|
|
3741
|
-
attachInstanceId(record, instanceId) {
|
|
3742
|
-
this.
|
|
3978
|
+
async attachInstanceId(record, instanceId) {
|
|
3979
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
3980
|
+
await this.reconcileFromPositiveEvidence(record, snapshot);
|
|
3743
3981
|
if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited")
|
|
3744
3982
|
return;
|
|
3745
3983
|
if (record.instanceId && record.instanceId !== instanceId)
|
|
@@ -3750,54 +3988,71 @@ var init_studio_instance_manager = __esm({
|
|
|
3750
3988
|
this.clearConnectionTimer(record);
|
|
3751
3989
|
this.pending.delete(record);
|
|
3752
3990
|
this.managedByInstanceId.set(instanceId, record);
|
|
3753
|
-
this.persist(record);
|
|
3991
|
+
await this.persist(record);
|
|
3754
3992
|
}
|
|
3755
|
-
markFailed(record, reason) {
|
|
3756
|
-
this.refresh(record);
|
|
3993
|
+
async markFailed(record, reason) {
|
|
3757
3994
|
if (record.closedAt !== void 0 || record.state !== "launching")
|
|
3758
3995
|
return record;
|
|
3759
3996
|
record.state = "failed";
|
|
3760
3997
|
record.failedAt = Date.now();
|
|
3761
3998
|
record.failureReason = reason;
|
|
3762
3999
|
this.clearConnectionTimer(record);
|
|
3763
|
-
this.persist(record);
|
|
4000
|
+
await this.persist(record);
|
|
3764
4001
|
return record;
|
|
3765
4002
|
}
|
|
3766
|
-
refresh(record) {
|
|
4003
|
+
async refresh(record, providedSnapshot) {
|
|
4004
|
+
if (record.closedAt !== void 0)
|
|
4005
|
+
return record;
|
|
4006
|
+
const snapshot = providedSnapshot ?? await this.getProcessSnapshot();
|
|
4007
|
+
await this.applyProcessObservation(record, this.observeRecord(record, snapshot));
|
|
3767
4008
|
if (record.closedAt !== void 0)
|
|
3768
4009
|
return record;
|
|
3769
|
-
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
3770
|
-
const studioProcess = processId ? this.findProcessById(processId) : void 0;
|
|
3771
|
-
if (processId && (!studioProcess || !this.verifyProcessForRecord(record, studioProcess))) {
|
|
3772
|
-
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.");
|
|
3773
|
-
}
|
|
3774
4010
|
if (record.state === "launching" && record.connectionDeadlineAt !== void 0 && Date.now() >= record.connectionDeadlineAt) {
|
|
3775
4011
|
record.state = "failed";
|
|
3776
4012
|
record.failedAt = Date.now();
|
|
3777
4013
|
record.failureReason = "Studio launched, but the MCP plugin did not connect before timeout.";
|
|
3778
4014
|
this.clearConnectionTimer(record);
|
|
3779
|
-
this.persist(record);
|
|
4015
|
+
await this.persist(record);
|
|
3780
4016
|
}
|
|
3781
4017
|
return record;
|
|
3782
4018
|
}
|
|
3783
4019
|
async launch(options) {
|
|
3784
|
-
this.
|
|
4020
|
+
const previous = this.launchQueue;
|
|
4021
|
+
let release;
|
|
4022
|
+
this.launchQueue = new Promise((resolve5) => {
|
|
4023
|
+
release = resolve5;
|
|
4024
|
+
});
|
|
4025
|
+
await previous;
|
|
4026
|
+
try {
|
|
4027
|
+
return await this.launchSerialized(options);
|
|
4028
|
+
} finally {
|
|
4029
|
+
release();
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
async launchSerialized(options) {
|
|
4033
|
+
const initialSnapshot = await this.getProcessSnapshot(true);
|
|
4034
|
+
await this.sweepRegistry(initialSnapshot);
|
|
4035
|
+
const processEnvironment = parseStudioProcessEnvironmentPatch(options.processEnvironment);
|
|
4036
|
+
if (options.studioExecutable !== void 0 && (typeof options.studioExecutable !== "string" || options.studioExecutable.length === 0 || options.studioExecutable.includes("\0"))) {
|
|
4037
|
+
throw new Error("studio_executable must be a non-empty string without null characters.");
|
|
4038
|
+
}
|
|
3785
4039
|
const preparedOptions = prepareStudioLaunchOptions(options);
|
|
3786
|
-
const bootId = this.getCurrentBootId();
|
|
3787
|
-
const before = new Set(
|
|
3788
|
-
const exe = this.processAdapter.resolveStudioExe
|
|
3789
|
-
const args = buildStudioLaunchArgs(preparedOptions).map(
|
|
4040
|
+
const bootId = await this.getCurrentBootId();
|
|
4041
|
+
const before = new Set(initialSnapshot.status === "ok" ? initialSnapshot.processes.map((proc2) => proc2.Id) : []);
|
|
4042
|
+
const exe = preparedOptions.studioExecutable ?? (this.processAdapter.resolveStudioExe ? await this.processAdapter.resolveStudioExe() : await resolveStudioExeAsync());
|
|
4043
|
+
const args = await Promise.all(buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArgAsync));
|
|
3790
4044
|
const spawnOptions = {
|
|
3791
4045
|
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
3792
4046
|
detached: true,
|
|
3793
|
-
stdio: "ignore"
|
|
4047
|
+
stdio: "ignore",
|
|
4048
|
+
...processEnvironment ? { env: patchedProcessEnvironment(processEnvironment) } : {}
|
|
3794
4049
|
};
|
|
3795
4050
|
let proc;
|
|
3796
4051
|
try {
|
|
3797
4052
|
if (this.processAdapter.spawnStudio) {
|
|
3798
|
-
proc = this.processAdapter.spawnStudio(exe, args, spawnOptions);
|
|
4053
|
+
proc = await this.processAdapter.spawnStudio(exe, args, spawnOptions);
|
|
3799
4054
|
} else if (isWsl()) {
|
|
3800
|
-
proc = spawnWindowsStudioFromWsl(exe, args);
|
|
4055
|
+
proc = await spawnWindowsStudioFromWsl(exe, args, processEnvironment);
|
|
3801
4056
|
} else {
|
|
3802
4057
|
const child = spawn(exe, args, spawnOptions);
|
|
3803
4058
|
proc = {
|
|
@@ -3816,8 +4071,9 @@ var init_studio_instance_manager = __esm({
|
|
|
3816
4071
|
cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
|
|
3817
4072
|
throw error;
|
|
3818
4073
|
}
|
|
4074
|
+
const launchedAt = Date.now();
|
|
3819
4075
|
const record = {
|
|
3820
|
-
recordId:
|
|
4076
|
+
recordId: randomUUID2(),
|
|
3821
4077
|
source: options.source,
|
|
3822
4078
|
nativeProcessId: proc.nativePid,
|
|
3823
4079
|
nativeProcessStartedAt: proc.nativeStartedAt,
|
|
@@ -3828,23 +4084,27 @@ var init_studio_instance_manager = __esm({
|
|
|
3828
4084
|
universeId: preparedOptions.universeId,
|
|
3829
4085
|
placeVersion: preparedOptions.placeVersion,
|
|
3830
4086
|
localPlaceFile: preparedOptions.localPlaceFile,
|
|
3831
|
-
launchedAt
|
|
3832
|
-
connectionDeadlineAt:
|
|
4087
|
+
launchedAt,
|
|
4088
|
+
connectionDeadlineAt: launchedAt + (options.connectionTimeoutMs ?? 12e4),
|
|
3833
4089
|
state: "launching",
|
|
3834
4090
|
ownerPid: process.pid,
|
|
3835
4091
|
bootId,
|
|
3836
|
-
deleteLocalPlaceFileOnClose: options.source === "baseplate"
|
|
4092
|
+
deleteLocalPlaceFileOnClose: options.source === "baseplate",
|
|
4093
|
+
processObservationStatus: "running",
|
|
4094
|
+
lastProcessObservationAt: launchedAt,
|
|
4095
|
+
lastSuccessfulProcessObservationAt: launchedAt,
|
|
4096
|
+
consecutiveConfirmedMisses: 0
|
|
3837
4097
|
};
|
|
3838
4098
|
this.pending.add(record);
|
|
3839
4099
|
try {
|
|
3840
|
-
this.persist(record);
|
|
4100
|
+
await this.persist(record);
|
|
3841
4101
|
} catch (error) {
|
|
3842
4102
|
this.pending.delete(record);
|
|
3843
4103
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
3844
4104
|
let stopError;
|
|
3845
4105
|
if (processId) {
|
|
3846
4106
|
try {
|
|
3847
|
-
this.closeProcess(processId);
|
|
4107
|
+
await this.closeProcess(processId);
|
|
3848
4108
|
} catch (caught) {
|
|
3849
4109
|
stopError = caught;
|
|
3850
4110
|
}
|
|
@@ -3856,38 +4116,40 @@ var init_studio_instance_manager = __esm({
|
|
|
3856
4116
|
}
|
|
3857
4117
|
proc.unref();
|
|
3858
4118
|
proc.onExit?.((code, signal) => {
|
|
3859
|
-
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.");
|
|
4119
|
+
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."));
|
|
3860
4120
|
});
|
|
3861
4121
|
proc.onError?.((error) => {
|
|
3862
|
-
this.markFailed(record, `Studio process failed to start: ${error.message}`);
|
|
4122
|
+
this.runInBackground("persisting a Studio process launch failure", this.markFailed(record, `Studio process failed to start: ${error.message}`));
|
|
3863
4123
|
});
|
|
3864
4124
|
const deadline = Date.now() + 5e3;
|
|
3865
4125
|
while (Date.now() < deadline && record.nativeProcessId === void 0) {
|
|
3866
|
-
const
|
|
4126
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4127
|
+
const created = snapshot.status === "ok" ? snapshot.processes.find((candidate) => !before.has(candidate.Id)) : void 0;
|
|
3867
4128
|
if (created) {
|
|
3868
4129
|
record.nativeProcessId = created.Id;
|
|
3869
4130
|
record.nativeProcessStartedAt = created.StartTimeUtcFileTime;
|
|
3870
|
-
this.persist(record);
|
|
4131
|
+
await this.persist(record);
|
|
3871
4132
|
break;
|
|
3872
4133
|
}
|
|
3873
|
-
await
|
|
4134
|
+
await delay2(250);
|
|
3874
4135
|
}
|
|
3875
4136
|
if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
|
|
3876
4137
|
record.nativeProcessId = proc.pid;
|
|
3877
|
-
this.persist(record);
|
|
4138
|
+
await this.persist(record);
|
|
3878
4139
|
}
|
|
3879
4140
|
if (record.nativeProcessId !== void 0 && record.nativeProcessStartedAt === void 0) {
|
|
3880
|
-
const
|
|
4141
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4142
|
+
const nativeProcess = snapshot.status === "ok" ? snapshot.processes.find((candidate) => candidate.Id === record.nativeProcessId) : void 0;
|
|
3881
4143
|
if (nativeProcess?.StartTimeUtcFileTime !== void 0) {
|
|
3882
4144
|
record.nativeProcessStartedAt = nativeProcess.StartTimeUtcFileTime;
|
|
3883
|
-
this.persist(record);
|
|
4145
|
+
await this.persist(record);
|
|
3884
4146
|
}
|
|
3885
4147
|
}
|
|
3886
|
-
this.
|
|
4148
|
+
this.startCoordinator(record);
|
|
3887
4149
|
return record;
|
|
3888
4150
|
}
|
|
3889
|
-
closeByLaunchId(launchId) {
|
|
3890
|
-
const record = this.getByLaunchId(launchId);
|
|
4151
|
+
async closeByLaunchId(launchId) {
|
|
4152
|
+
const record = await this.getByLaunchId(launchId);
|
|
3891
4153
|
if (!record)
|
|
3892
4154
|
return { status: "not_found", launchId };
|
|
3893
4155
|
if (record.closedAt !== void 0) {
|
|
@@ -3895,19 +4157,19 @@ var init_studio_instance_manager = __esm({
|
|
|
3895
4157
|
}
|
|
3896
4158
|
return this.close(record);
|
|
3897
4159
|
}
|
|
3898
|
-
closeByInstanceId(instanceId) {
|
|
3899
|
-
this.
|
|
4160
|
+
async closeByInstanceId(instanceId) {
|
|
4161
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4162
|
+
await this.sweepRegistry(snapshot);
|
|
3900
4163
|
const memoryRecord = this.managedByInstanceId.get(instanceId);
|
|
3901
4164
|
if (memoryRecord)
|
|
3902
4165
|
return this.close(memoryRecord);
|
|
3903
|
-
const registryRecord = this.registry.findAnyByInstanceId(instanceId);
|
|
4166
|
+
const registryRecord = await this.registry.findAnyByInstanceId(instanceId);
|
|
3904
4167
|
if (!registryRecord) {
|
|
3905
|
-
this.sweepRegistry();
|
|
3906
4168
|
return { status: "not_found", instanceId };
|
|
3907
4169
|
}
|
|
3908
4170
|
if (registryRecord.closedAt !== void 0) {
|
|
3909
4171
|
this.cleanupManagedRecord(registryRecord);
|
|
3910
|
-
this.registry.logEvent({
|
|
4172
|
+
await this.registry.logEvent({
|
|
3911
4173
|
event: "registry_close_already_stopped",
|
|
3912
4174
|
recordId: registryRecord.recordId,
|
|
3913
4175
|
instanceId: registryRecord.instanceId,
|
|
@@ -3919,9 +4181,7 @@ var init_studio_instance_manager = __esm({
|
|
|
3919
4181
|
}
|
|
3920
4182
|
return this.close(this.fromRegistryRecord(registryRecord));
|
|
3921
4183
|
}
|
|
3922
|
-
close(record) {
|
|
3923
|
-
this.stopMonitor(record);
|
|
3924
|
-
this.refresh(record);
|
|
4184
|
+
async close(record) {
|
|
3925
4185
|
if (record.closedAt !== void 0) {
|
|
3926
4186
|
return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
3927
4187
|
}
|
|
@@ -3929,36 +4189,33 @@ var init_studio_instance_manager = __esm({
|
|
|
3929
4189
|
if (!processId) {
|
|
3930
4190
|
throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
|
|
3931
4191
|
}
|
|
3932
|
-
const
|
|
3933
|
-
|
|
4192
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4193
|
+
const observation = this.observeRecord(record, snapshot);
|
|
4194
|
+
if (observation.status === "unknown") {
|
|
4195
|
+
await this.applyProcessObservation(record, observation);
|
|
4196
|
+
throw new Error(`Cannot verify the managed Studio process because process observation failed: ${observation.error}`);
|
|
4197
|
+
}
|
|
4198
|
+
if (observation.status === "not_running") {
|
|
3934
4199
|
this.cleanupManagedRecord(record);
|
|
3935
|
-
this.markProcessExited(record, void 0, record.failureReason);
|
|
3936
|
-
this.registry.logEvent({
|
|
4200
|
+
await this.markProcessExited(record, void 0, observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : record.failureReason);
|
|
4201
|
+
await this.registry.logEvent({
|
|
3937
4202
|
event: "registry_close_already_stopped",
|
|
3938
4203
|
recordId: record.recordId,
|
|
3939
4204
|
instanceId: record.instanceId,
|
|
3940
4205
|
source: record.source,
|
|
3941
|
-
reason: "pid_not_running",
|
|
4206
|
+
reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
|
|
3942
4207
|
action: "marked_closed_and_cleaned_baseplate"
|
|
3943
4208
|
});
|
|
3944
4209
|
return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
3945
4210
|
}
|
|
3946
|
-
if (!this.verifyProcessForRecord(record, studioProcess)) {
|
|
3947
|
-
this.registry.logEvent({
|
|
3948
|
-
event: "registry_process_verification_failed",
|
|
3949
|
-
recordId: record.recordId,
|
|
3950
|
-
instanceId: record.instanceId,
|
|
3951
|
-
source: record.source,
|
|
3952
|
-
reason: "identity_mismatch"
|
|
3953
|
-
});
|
|
3954
|
-
throw new Error("Managed Studio process identity could not be verified.");
|
|
3955
|
-
}
|
|
3956
4211
|
try {
|
|
3957
|
-
this.closeProcess(processId);
|
|
4212
|
+
await this.closeProcess(processId);
|
|
3958
4213
|
} catch (error) {
|
|
3959
|
-
|
|
4214
|
+
const retry = await this.getProcessSnapshot(true);
|
|
4215
|
+
const retryObservation = this.observeRecord(record, retry);
|
|
4216
|
+
if (retryObservation.status === "running" || retryObservation.status === "unknown")
|
|
3960
4217
|
throw error;
|
|
3961
|
-
this.registry.logEvent({
|
|
4218
|
+
await this.registry.logEvent({
|
|
3962
4219
|
event: "registry_close_already_stopped",
|
|
3963
4220
|
recordId: record.recordId,
|
|
3964
4221
|
instanceId: record.instanceId,
|
|
@@ -3967,7 +4224,7 @@ var init_studio_instance_manager = __esm({
|
|
|
3967
4224
|
action: "marked_closed_and_cleaned_baseplate"
|
|
3968
4225
|
});
|
|
3969
4226
|
this.cleanupManagedRecord(record);
|
|
3970
|
-
this.markProcessExited(record, void 0, record.failureReason);
|
|
4227
|
+
await this.markProcessExited(record, void 0, record.failureReason);
|
|
3971
4228
|
return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
3972
4229
|
}
|
|
3973
4230
|
const closedAt = Date.now();
|
|
@@ -3975,25 +4232,33 @@ var init_studio_instance_manager = __esm({
|
|
|
3975
4232
|
record.exitedAt = record.exitedAt ?? closedAt;
|
|
3976
4233
|
if (record.state !== "failed")
|
|
3977
4234
|
record.state = "exited";
|
|
4235
|
+
record.processObservationStatus = "not_running";
|
|
4236
|
+
record.lastProcessObservationAt = closedAt;
|
|
4237
|
+
record.lastSuccessfulProcessObservationAt = closedAt;
|
|
4238
|
+
record.lastProcessObservationError = void 0;
|
|
3978
4239
|
this.cleanupManagedRecord(record);
|
|
3979
4240
|
this.markClosedInMemory(record);
|
|
3980
|
-
this.persist(record);
|
|
4241
|
+
await this.persist(record);
|
|
3981
4242
|
return { status: "closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
3982
4243
|
}
|
|
3983
|
-
closeConnectedInstance(instance) {
|
|
3984
|
-
const
|
|
4244
|
+
async closeConnectedInstance(instance) {
|
|
4245
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4246
|
+
if (snapshot.status === "error") {
|
|
4247
|
+
throw new Error(`Could not enumerate Studio processes: ${snapshot.error}`);
|
|
4248
|
+
}
|
|
4249
|
+
const process2 = this.findProcessForConnectedInstance(instance, snapshot.processes);
|
|
3985
4250
|
if (!process2) {
|
|
3986
4251
|
throw new Error(`Could not find a Studio process for connected instance "${instance.instanceId}".`);
|
|
3987
4252
|
}
|
|
3988
|
-
this.closeProcess(process2.Id);
|
|
4253
|
+
await this.closeProcess(process2.Id);
|
|
3989
4254
|
}
|
|
3990
|
-
closeProcess(processId) {
|
|
4255
|
+
async closeProcess(processId) {
|
|
3991
4256
|
if (this.processAdapter.stopProcess) {
|
|
3992
|
-
this.processAdapter.stopProcess(processId);
|
|
4257
|
+
await this.processAdapter.stopProcess(processId);
|
|
3993
4258
|
return;
|
|
3994
4259
|
}
|
|
3995
4260
|
if (process.platform === "win32" || isWsl()) {
|
|
3996
|
-
|
|
4261
|
+
await powershellAsync(`Stop-Process -Id ${Math.trunc(processId)} -Force -ErrorAction Stop`);
|
|
3997
4262
|
} else {
|
|
3998
4263
|
try {
|
|
3999
4264
|
process.kill(processId, "SIGTERM");
|
|
@@ -4003,8 +4268,7 @@ var init_studio_instance_manager = __esm({
|
|
|
4003
4268
|
}
|
|
4004
4269
|
}
|
|
4005
4270
|
}
|
|
4006
|
-
findProcessForConnectedInstance(instance) {
|
|
4007
|
-
const processes = this.listStudioProcesses();
|
|
4271
|
+
findProcessForConnectedInstance(instance, processes) {
|
|
4008
4272
|
if (processes.length === 0)
|
|
4009
4273
|
return void 0;
|
|
4010
4274
|
if (processes.length === 1)
|
|
@@ -4023,31 +4287,67 @@ var init_studio_instance_manager = __esm({
|
|
|
4023
4287
|
}
|
|
4024
4288
|
return void 0;
|
|
4025
4289
|
}
|
|
4026
|
-
|
|
4027
|
-
|
|
4290
|
+
async getProcessSnapshot(force = false) {
|
|
4291
|
+
const now = Date.now();
|
|
4292
|
+
if (!force && this.snapshotCacheMs > 0 && this.cachedSnapshot && now - this.cachedSnapshot.observedAt <= this.snapshotCacheMs) {
|
|
4293
|
+
return this.cachedSnapshot;
|
|
4294
|
+
}
|
|
4295
|
+
if (this.snapshotInFlight)
|
|
4296
|
+
return this.snapshotInFlight;
|
|
4297
|
+
this.snapshotInFlight = (async () => {
|
|
4298
|
+
try {
|
|
4299
|
+
let snapshot;
|
|
4300
|
+
if (this.processAdapter.observeStudioProcesses) {
|
|
4301
|
+
snapshot = await this.processAdapter.observeStudioProcesses();
|
|
4302
|
+
} else if (this.processAdapter.listStudioProcesses) {
|
|
4303
|
+
const observedAt = Date.now();
|
|
4304
|
+
const processes = await this.processAdapter.listStudioProcesses();
|
|
4305
|
+
snapshot = { status: "ok", observedAt, processes };
|
|
4306
|
+
} else {
|
|
4307
|
+
snapshot = await observeStudioProcesses();
|
|
4308
|
+
}
|
|
4309
|
+
this.cachedSnapshot = snapshot;
|
|
4310
|
+
return snapshot;
|
|
4311
|
+
} catch (error) {
|
|
4312
|
+
const snapshot = {
|
|
4313
|
+
status: "error",
|
|
4314
|
+
observedAt: Date.now(),
|
|
4315
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4316
|
+
};
|
|
4317
|
+
this.cachedSnapshot = snapshot;
|
|
4318
|
+
return snapshot;
|
|
4319
|
+
} finally {
|
|
4320
|
+
this.snapshotInFlight = void 0;
|
|
4321
|
+
}
|
|
4322
|
+
})();
|
|
4323
|
+
return this.snapshotInFlight;
|
|
4028
4324
|
}
|
|
4029
|
-
getCurrentBootId() {
|
|
4030
|
-
return this.processAdapter.currentBootId
|
|
4325
|
+
async getCurrentBootId() {
|
|
4326
|
+
return this.processAdapter.currentBootId ? await this.processAdapter.currentBootId() : currentBootIdAsync();
|
|
4031
4327
|
}
|
|
4032
|
-
registrySweepOptions() {
|
|
4328
|
+
async registrySweepOptions(snapshot) {
|
|
4033
4329
|
return {
|
|
4034
|
-
currentBootId: this.getCurrentBootId(),
|
|
4035
|
-
|
|
4036
|
-
cleanupRecord: (record) => this.cleanupManagedRecord(record)
|
|
4330
|
+
currentBootId: await this.getCurrentBootId(),
|
|
4331
|
+
observeProcess: (record) => this.observeRecord(this.fromRegistryRecord(record), snapshot),
|
|
4332
|
+
cleanupRecord: (record) => this.cleanupManagedRecord(record),
|
|
4333
|
+
confirmedExitMisses: this.confirmedExitMisses,
|
|
4334
|
+
confirmedExitGraceMs: this.confirmedExitGraceMs
|
|
4037
4335
|
};
|
|
4038
4336
|
}
|
|
4039
|
-
sweepRegistry() {
|
|
4040
|
-
this.registry.sweep(this.registrySweepOptions());
|
|
4041
|
-
}
|
|
4042
|
-
findProcessById(processId) {
|
|
4043
|
-
return this.listStudioProcesses().find((proc) => proc.Id === processId);
|
|
4337
|
+
async sweepRegistry(snapshot) {
|
|
4338
|
+
await this.registry.sweep(await this.registrySweepOptions(snapshot));
|
|
4044
4339
|
}
|
|
4045
|
-
|
|
4340
|
+
observeRecord(record, snapshot) {
|
|
4341
|
+
if (snapshot.status === "error") {
|
|
4342
|
+
return { status: "unknown", observedAt: snapshot.observedAt, error: snapshot.error };
|
|
4343
|
+
}
|
|
4046
4344
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
4047
4345
|
if (!processId)
|
|
4048
|
-
return
|
|
4049
|
-
const studioProcess =
|
|
4050
|
-
|
|
4346
|
+
return { status: "running", observedAt: snapshot.observedAt };
|
|
4347
|
+
const studioProcess = snapshot.processes.find((candidate) => candidate.Id === processId);
|
|
4348
|
+
if (!studioProcess)
|
|
4349
|
+
return { status: "not_running", observedAt: snapshot.observedAt, reason: "missing" };
|
|
4350
|
+
return this.verifyProcessForRecord(record, studioProcess) ? { status: "running", observedAt: snapshot.observedAt } : { status: "not_running", observedAt: snapshot.observedAt, reason: "identity_mismatch" };
|
|
4051
4351
|
}
|
|
4052
4352
|
verifyProcessForRecord(record, studioProcess) {
|
|
4053
4353
|
const processName = `${studioProcess.Name ?? ""} ${studioProcess.Path ?? ""}`.toLowerCase();
|
|
@@ -4081,9 +4381,9 @@ var init_studio_instance_manager = __esm({
|
|
|
4081
4381
|
if (record.instanceId)
|
|
4082
4382
|
this.managedByInstanceId.delete(record.instanceId);
|
|
4083
4383
|
this.pending.delete(record);
|
|
4084
|
-
this.
|
|
4384
|
+
this.clearConnectionTimer(record);
|
|
4085
4385
|
}
|
|
4086
|
-
markProcessExited(record, exitCode, reason) {
|
|
4386
|
+
async markProcessExited(record, exitCode, reason) {
|
|
4087
4387
|
if (record.closedAt !== void 0)
|
|
4088
4388
|
return record;
|
|
4089
4389
|
const exitedAt = Date.now();
|
|
@@ -4091,43 +4391,44 @@ var init_studio_instance_manager = __esm({
|
|
|
4091
4391
|
record.closedAt = exitedAt;
|
|
4092
4392
|
if (record.state !== "failed")
|
|
4093
4393
|
record.state = "exited";
|
|
4394
|
+
record.processObservationStatus = "not_running";
|
|
4395
|
+
record.lastProcessObservationAt = exitedAt;
|
|
4396
|
+
record.lastSuccessfulProcessObservationAt = exitedAt;
|
|
4397
|
+
record.lastProcessObservationError = void 0;
|
|
4094
4398
|
if (exitCode !== void 0)
|
|
4095
4399
|
record.exitCode = exitCode;
|
|
4096
4400
|
if (reason)
|
|
4097
4401
|
record.failureReason = reason;
|
|
4098
4402
|
this.cleanupManagedRecord(record);
|
|
4099
4403
|
this.markClosedInMemory(record);
|
|
4100
|
-
this.persist(record);
|
|
4404
|
+
await this.persist(record);
|
|
4101
4405
|
return record;
|
|
4102
4406
|
}
|
|
4103
|
-
|
|
4104
|
-
if (!record.recordId || record.closedAt !== void 0
|
|
4407
|
+
startCoordinator(record) {
|
|
4408
|
+
if (!record.recordId || record.closedAt !== void 0)
|
|
4105
4409
|
return;
|
|
4106
4410
|
if (record.state === "launching" && record.connectionDeadlineAt !== void 0) {
|
|
4107
4411
|
const timeout = setTimeout(() => {
|
|
4108
|
-
this.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
|
|
4412
|
+
this.runInBackground("persisting a Studio plugin connection timeout", this.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout."));
|
|
4109
4413
|
}, Math.max(0, record.connectionDeadlineAt - Date.now()));
|
|
4110
4414
|
if (typeof timeout === "object" && "unref" in timeout)
|
|
4111
4415
|
timeout.unref();
|
|
4112
4416
|
this.connectionTimers.set(record.recordId, timeout);
|
|
4113
4417
|
}
|
|
4114
|
-
|
|
4115
|
-
this.refresh(record);
|
|
4116
|
-
if (record.closedAt !== void 0)
|
|
4117
|
-
this.stopMonitor(record);
|
|
4118
|
-
}, 5e3);
|
|
4119
|
-
if (typeof timer === "object" && "unref" in timer)
|
|
4120
|
-
timer.unref();
|
|
4121
|
-
this.monitors.set(record.recordId, timer);
|
|
4122
|
-
}
|
|
4123
|
-
stopMonitor(record) {
|
|
4124
|
-
if (!record.recordId)
|
|
4418
|
+
if (this.coordinatorTimer)
|
|
4125
4419
|
return;
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4420
|
+
this.coordinatorTimer = setInterval(() => {
|
|
4421
|
+
if (this.coordinatorRefresh)
|
|
4422
|
+
return;
|
|
4423
|
+
this.coordinatorRefresh = this.refreshOwnedRecords().catch((error) => {
|
|
4424
|
+
this.reportBackgroundFailure("refreshing managed Studio records", error);
|
|
4425
|
+
}).finally(() => {
|
|
4426
|
+
this.coordinatorRefresh = void 0;
|
|
4427
|
+
});
|
|
4428
|
+
}, 5e3);
|
|
4429
|
+
if (typeof this.coordinatorTimer === "object" && "unref" in this.coordinatorTimer) {
|
|
4430
|
+
this.coordinatorTimer.unref();
|
|
4431
|
+
}
|
|
4131
4432
|
}
|
|
4132
4433
|
clearConnectionTimer(record) {
|
|
4133
4434
|
if (!record.recordId)
|
|
@@ -4137,8 +4438,80 @@ var init_studio_instance_manager = __esm({
|
|
|
4137
4438
|
clearTimeout(timer);
|
|
4138
4439
|
this.connectionTimers.delete(record.recordId);
|
|
4139
4440
|
}
|
|
4140
|
-
|
|
4141
|
-
this.
|
|
4441
|
+
async refreshOwnedRecords() {
|
|
4442
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4443
|
+
await this.sweepRegistry(snapshot);
|
|
4444
|
+
for (const record of [...this.managedByInstanceId.values(), ...this.pending]) {
|
|
4445
|
+
await this.refresh(record, snapshot);
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
runInBackground(context, operation) {
|
|
4449
|
+
void operation.catch((error) => this.reportBackgroundFailure(context, error));
|
|
4450
|
+
}
|
|
4451
|
+
reportBackgroundFailure(context, error) {
|
|
4452
|
+
console.warn(`[robloxstudio-mcp] failed while ${context}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4453
|
+
}
|
|
4454
|
+
async applyProcessObservation(record, observation) {
|
|
4455
|
+
if (record.closedAt !== void 0)
|
|
4456
|
+
return;
|
|
4457
|
+
const previousObservationAt = record.lastProcessObservationAt;
|
|
4458
|
+
record.lastProcessObservationAt = observation.observedAt;
|
|
4459
|
+
if (observation.status === "unknown") {
|
|
4460
|
+
record.processObservationStatus = "unknown";
|
|
4461
|
+
record.lastProcessObservationError = observation.error;
|
|
4462
|
+
record.consecutiveConfirmedMisses = 0;
|
|
4463
|
+
record.firstConfirmedMissAt = void 0;
|
|
4464
|
+
await this.persist(record);
|
|
4465
|
+
return;
|
|
4466
|
+
}
|
|
4467
|
+
record.lastSuccessfulProcessObservationAt = observation.observedAt;
|
|
4468
|
+
record.lastProcessObservationError = void 0;
|
|
4469
|
+
if (observation.status === "running") {
|
|
4470
|
+
record.processObservationStatus = "running";
|
|
4471
|
+
record.consecutiveConfirmedMisses = 0;
|
|
4472
|
+
record.firstConfirmedMissAt = void 0;
|
|
4473
|
+
await this.persist(record);
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
record.processObservationStatus = "not_running";
|
|
4477
|
+
if (previousObservationAt !== observation.observedAt) {
|
|
4478
|
+
record.consecutiveConfirmedMisses = (record.consecutiveConfirmedMisses ?? 0) + 1;
|
|
4479
|
+
record.firstConfirmedMissAt ??= observation.observedAt;
|
|
4480
|
+
}
|
|
4481
|
+
const confirmedAbsent = observation.reason === "identity_mismatch" || (record.consecutiveConfirmedMisses ?? 0) >= this.confirmedExitMisses && observation.observedAt - (record.firstConfirmedMissAt ?? observation.observedAt) >= this.confirmedExitGraceMs;
|
|
4482
|
+
if (!confirmedAbsent) {
|
|
4483
|
+
await this.persist(record);
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
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.");
|
|
4487
|
+
}
|
|
4488
|
+
async reconcileFromPositiveEvidence(record, snapshot) {
|
|
4489
|
+
if (record.closedAt === void 0)
|
|
4490
|
+
return;
|
|
4491
|
+
if (record.failureReason !== "Studio process exited." && record.failureReason !== "Studio process exited before the MCP plugin connected.")
|
|
4492
|
+
return;
|
|
4493
|
+
if (snapshot.status !== "ok")
|
|
4494
|
+
return;
|
|
4495
|
+
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
4496
|
+
const studioProcess = processId ? snapshot.processes.find((candidate) => candidate.Id === processId) : void 0;
|
|
4497
|
+
if (!studioProcess || !this.verifyProcessForRecord(record, studioProcess))
|
|
4498
|
+
return;
|
|
4499
|
+
if (record.exitCode !== void 0)
|
|
4500
|
+
return;
|
|
4501
|
+
record.closedAt = void 0;
|
|
4502
|
+
record.exitedAt = void 0;
|
|
4503
|
+
record.failureReason = void 0;
|
|
4504
|
+
record.state = record.instanceId ? "connected" : "launching";
|
|
4505
|
+
record.processObservationStatus = "running";
|
|
4506
|
+
record.lastProcessObservationAt = snapshot.observedAt;
|
|
4507
|
+
record.lastSuccessfulProcessObservationAt = snapshot.observedAt;
|
|
4508
|
+
record.lastProcessObservationError = void 0;
|
|
4509
|
+
record.consecutiveConfirmedMisses = 0;
|
|
4510
|
+
record.firstConfirmedMissAt = void 0;
|
|
4511
|
+
await this.persist(record);
|
|
4512
|
+
}
|
|
4513
|
+
async persist(record) {
|
|
4514
|
+
await this.registry.upsert(this.toRegistryRecord(record));
|
|
4142
4515
|
}
|
|
4143
4516
|
toRegistryRecord(record) {
|
|
4144
4517
|
if (!record.recordId)
|
|
@@ -4170,7 +4543,13 @@ var init_studio_instance_manager = __esm({
|
|
|
4170
4543
|
failureReason: record.failureReason,
|
|
4171
4544
|
closedAt: record.closedAt,
|
|
4172
4545
|
ownerPid: record.ownerPid,
|
|
4173
|
-
bootId: record.bootId
|
|
4546
|
+
bootId: record.bootId,
|
|
4547
|
+
processObservationStatus: record.processObservationStatus,
|
|
4548
|
+
lastProcessObservationAt: record.lastProcessObservationAt,
|
|
4549
|
+
lastSuccessfulProcessObservationAt: record.lastSuccessfulProcessObservationAt,
|
|
4550
|
+
lastProcessObservationError: record.lastProcessObservationError,
|
|
4551
|
+
consecutiveConfirmedMisses: record.consecutiveConfirmedMisses,
|
|
4552
|
+
firstConfirmedMissAt: record.firstConfirmedMissAt
|
|
4174
4553
|
};
|
|
4175
4554
|
}
|
|
4176
4555
|
fromRegistryRecord(record) {
|
|
@@ -4199,7 +4578,13 @@ var init_studio_instance_manager = __esm({
|
|
|
4199
4578
|
closedAt: record.closedAt,
|
|
4200
4579
|
ownerPid: record.ownerPid,
|
|
4201
4580
|
bootId: record.bootId,
|
|
4202
|
-
deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose
|
|
4581
|
+
deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose,
|
|
4582
|
+
processObservationStatus: record.processObservationStatus,
|
|
4583
|
+
lastProcessObservationAt: record.lastProcessObservationAt,
|
|
4584
|
+
lastSuccessfulProcessObservationAt: record.lastSuccessfulProcessObservationAt,
|
|
4585
|
+
lastProcessObservationError: record.lastProcessObservationError,
|
|
4586
|
+
consecutiveConfirmedMisses: record.consecutiveConfirmedMisses,
|
|
4587
|
+
firstConfirmedMissAt: record.firstConfirmedMissAt
|
|
4203
4588
|
};
|
|
4204
4589
|
}
|
|
4205
4590
|
};
|
|
@@ -5010,7 +5395,7 @@ var init_esm = __esm({
|
|
|
5010
5395
|
|
|
5011
5396
|
// ../core/dist/studio-skills.js
|
|
5012
5397
|
import { createHash as createHash2 } from "crypto";
|
|
5013
|
-
import { existsSync as existsSync4, readFileSync as
|
|
5398
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
5014
5399
|
import * as path4 from "path";
|
|
5015
5400
|
function decompressLz4Block(input, outputLength) {
|
|
5016
5401
|
const output = Buffer.allocUnsafe(outputLength);
|
|
@@ -5222,7 +5607,7 @@ function discoverNamedFile(root, fileName, depth = 0) {
|
|
|
5222
5607
|
const matches = [];
|
|
5223
5608
|
let entries;
|
|
5224
5609
|
try {
|
|
5225
|
-
entries =
|
|
5610
|
+
entries = readdirSync2(root, { withFileTypes: true });
|
|
5226
5611
|
} catch {
|
|
5227
5612
|
return matches;
|
|
5228
5613
|
}
|
|
@@ -5236,7 +5621,7 @@ function discoverNamedFile(root, fileName, depth = 0) {
|
|
|
5236
5621
|
}
|
|
5237
5622
|
return matches;
|
|
5238
5623
|
}
|
|
5239
|
-
function resolveAssistantBundlePath(studioExe
|
|
5624
|
+
function resolveAssistantBundlePath(studioExe) {
|
|
5240
5625
|
const override = process.env.ROBLOX_STUDIO_ASSISTANT_BUNDLE;
|
|
5241
5626
|
if (override) {
|
|
5242
5627
|
if (!existsSync4(override)) {
|
|
@@ -5244,7 +5629,7 @@ function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
|
|
|
5244
5629
|
}
|
|
5245
5630
|
return override;
|
|
5246
5631
|
}
|
|
5247
|
-
const exeDirectory = path4.dirname(studioExe);
|
|
5632
|
+
const exeDirectory = path4.dirname(studioExe ?? resolveStudioExe());
|
|
5248
5633
|
const roots = [
|
|
5249
5634
|
path4.join(exeDirectory, "BuiltInStandalonePlugins"),
|
|
5250
5635
|
path4.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
|
|
@@ -5258,18 +5643,18 @@ function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
|
|
|
5258
5643
|
for (const candidate of discoverNamedFile(root, "Assistant.rbxm"))
|
|
5259
5644
|
candidates.add(candidate);
|
|
5260
5645
|
}
|
|
5261
|
-
const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt:
|
|
5646
|
+
const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt: statSync2(candidate).mtimeMs })).sort((left, right) => right.modifiedAt - left.modifiedAt)[0]?.candidate;
|
|
5262
5647
|
if (!newest) {
|
|
5263
5648
|
throw new Error(`Studio Assistant bundle not found for ${studioExe}. Set ROBLOX_STUDIO_ASSISTANT_BUNDLE to the installed Assistant.rbxm path.`);
|
|
5264
5649
|
}
|
|
5265
5650
|
return newest;
|
|
5266
5651
|
}
|
|
5267
5652
|
function loadBuiltInStudioSkills(bundlePath = resolveAssistantBundlePath()) {
|
|
5268
|
-
const stats =
|
|
5653
|
+
const stats = statSync2(bundlePath);
|
|
5269
5654
|
if (cachedBundle?.path === bundlePath && cachedBundle.modifiedAt === stats.mtimeMs && cachedBundle.size === stats.size) {
|
|
5270
5655
|
return cachedBundle.value;
|
|
5271
5656
|
}
|
|
5272
|
-
const buffer =
|
|
5657
|
+
const buffer = readFileSync4(bundlePath);
|
|
5273
5658
|
const skills = parseBuiltInStudioSkills(buffer);
|
|
5274
5659
|
if (skills.length === 0) {
|
|
5275
5660
|
throw new Error(`No built-in skill documents found in ${bundlePath}`);
|
|
@@ -6986,13 +7371,20 @@ var init_tools = __esm({
|
|
|
6986
7371
|
openCloudClient;
|
|
6987
7372
|
cookieClient;
|
|
6988
7373
|
instanceManager;
|
|
7374
|
+
managedConnectionAssociations = Promise.resolve();
|
|
6989
7375
|
constructor(bridge) {
|
|
6990
7376
|
this.client = new StudioHttpClient(bridge);
|
|
6991
7377
|
this.bridge = bridge;
|
|
6992
7378
|
this.openCloudClient = new OpenCloudClient();
|
|
6993
7379
|
this.cookieClient = new RobloxCookieClient();
|
|
6994
7380
|
this.instanceManager = new StudioInstanceManager();
|
|
6995
|
-
this.bridge.onInstanceRegistered((instance) =>
|
|
7381
|
+
this.bridge.onInstanceRegistered((instance) => {
|
|
7382
|
+
const instanceManager = this.instanceManager;
|
|
7383
|
+
const association = this.managedConnectionAssociations.then(() => this._associateManagedEditConnection(instance, instanceManager));
|
|
7384
|
+
this.managedConnectionAssociations = association.catch((error) => {
|
|
7385
|
+
console.warn(`[robloxstudio-mcp] managed Studio connection association failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7386
|
+
});
|
|
7387
|
+
});
|
|
6996
7388
|
}
|
|
6997
7389
|
_textResult(body) {
|
|
6998
7390
|
return { content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
@@ -8573,9 +8965,9 @@ ${code}`
|
|
|
8573
8965
|
_publicInstanceKey(instance) {
|
|
8574
8966
|
return `${instance.instanceId}:${instance.role}:${instance.connectedAt}`;
|
|
8575
8967
|
}
|
|
8576
|
-
_isLatestPublishedPlaceOpen(placeId) {
|
|
8968
|
+
async _isLatestPublishedPlaceOpen(placeId) {
|
|
8577
8969
|
const publishedInstanceId2 = `place:${placeId}`;
|
|
8578
|
-
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);
|
|
8970
|
+
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);
|
|
8579
8971
|
}
|
|
8580
8972
|
_matchesManagedLaunch(record, instance) {
|
|
8581
8973
|
if (record.source === "published_place") {
|
|
@@ -8587,12 +8979,12 @@ ${code}`
|
|
|
8587
8979
|
}
|
|
8588
8980
|
return true;
|
|
8589
8981
|
}
|
|
8590
|
-
_associateManagedEditConnection(instance) {
|
|
8982
|
+
async _associateManagedEditConnection(instance, instanceManager) {
|
|
8591
8983
|
if (instance.role !== "edit")
|
|
8592
8984
|
return;
|
|
8593
|
-
const candidate =
|
|
8985
|
+
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];
|
|
8594
8986
|
if (candidate)
|
|
8595
|
-
|
|
8987
|
+
await instanceManager.attachInstanceId(candidate, instance.instanceId);
|
|
8596
8988
|
}
|
|
8597
8989
|
async _deriveUniverseId(placeId) {
|
|
8598
8990
|
const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`);
|
|
@@ -8609,7 +9001,7 @@ ${code}`
|
|
|
8609
9001
|
async _waitForManagedEditConnection(record, beforeKeys, timeoutMs) {
|
|
8610
9002
|
const deadline = Date.now() + timeoutMs;
|
|
8611
9003
|
while (Date.now() < deadline) {
|
|
8612
|
-
this.instanceManager.refresh(record);
|
|
9004
|
+
await this.instanceManager.refresh(record);
|
|
8613
9005
|
if (record.state === "failed" || record.state === "exited" || record.closedAt !== void 0) {
|
|
8614
9006
|
return void 0;
|
|
8615
9007
|
}
|
|
@@ -8621,7 +9013,6 @@ ${code}`
|
|
|
8621
9013
|
return void 0;
|
|
8622
9014
|
}
|
|
8623
9015
|
_managedStatus(record) {
|
|
8624
|
-
this.instanceManager.refresh(record);
|
|
8625
9016
|
const connected = record.instanceId ? this.bridge.getPublicInstances().filter((instance) => instance.instanceId === record.instanceId) : [];
|
|
8626
9017
|
return {
|
|
8627
9018
|
launch_id: record.recordId,
|
|
@@ -8629,7 +9020,12 @@ ${code}`
|
|
|
8629
9020
|
managed: true,
|
|
8630
9021
|
state: record.state,
|
|
8631
9022
|
pid: record.nativeProcessId ?? record.spawnPid,
|
|
8632
|
-
process_running: record.closedAt
|
|
9023
|
+
process_running: record.closedAt !== void 0 || record.exitedAt !== void 0 ? false : record.processObservationStatus === "running" ? true : record.processObservationStatus === "not_running" ? false : null,
|
|
9024
|
+
process_observation_status: record.processObservationStatus ?? "unknown",
|
|
9025
|
+
last_process_observation_at: record.lastProcessObservationAt ? new Date(record.lastProcessObservationAt).toISOString() : void 0,
|
|
9026
|
+
last_successful_process_observation_at: record.lastSuccessfulProcessObservationAt ? new Date(record.lastSuccessfulProcessObservationAt).toISOString() : void 0,
|
|
9027
|
+
last_process_observation_error: record.lastProcessObservationError,
|
|
9028
|
+
consecutive_confirmed_misses: record.consecutiveConfirmedMisses ?? 0,
|
|
8633
9029
|
source: record.source,
|
|
8634
9030
|
local_place_file: record.localPlaceFile,
|
|
8635
9031
|
place_id: record.placeId,
|
|
@@ -8681,15 +9077,18 @@ ${code}`
|
|
|
8681
9077
|
body.next_page_token = response.nextPageToken;
|
|
8682
9078
|
return this._textResult(body);
|
|
8683
9079
|
}
|
|
9080
|
+
if (action === "status" || action === "close") {
|
|
9081
|
+
await this.managedConnectionAssociations;
|
|
9082
|
+
}
|
|
8684
9083
|
if (action === "status") {
|
|
8685
9084
|
if (launch_id) {
|
|
8686
|
-
const record2 = this.instanceManager.getByLaunchId(launch_id);
|
|
9085
|
+
const record2 = await this.instanceManager.getByLaunchId(launch_id);
|
|
8687
9086
|
if (!record2)
|
|
8688
9087
|
return this._textResult({ error: "Launch is not managed.", launch_id });
|
|
8689
9088
|
return this._textResult(this._managedStatus(record2));
|
|
8690
9089
|
}
|
|
8691
9090
|
if (instance_id) {
|
|
8692
|
-
const record2 = this.instanceManager.get(instance_id);
|
|
9091
|
+
const record2 = await this.instanceManager.get(instance_id);
|
|
8693
9092
|
const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
|
|
8694
9093
|
if (!record2 && connected2.length === 0) {
|
|
8695
9094
|
return this._textResult({ error: "Instance is not connected or managed.", instance_id });
|
|
@@ -8706,7 +9105,7 @@ ${code}`
|
|
|
8706
9105
|
});
|
|
8707
9106
|
}
|
|
8708
9107
|
return this._textResult({
|
|
8709
|
-
managed: this.instanceManager.list().filter((record2) => record2.closedAt === void 0).map((record2) => this._managedStatus(record2)),
|
|
9108
|
+
managed: (await this.instanceManager.list()).filter((record2) => record2.closedAt === void 0).map((record2) => this._managedStatus(record2)),
|
|
8710
9109
|
connected: this.bridge.getPublicInstances().map((instance) => ({
|
|
8711
9110
|
instance_id: instance.instanceId,
|
|
8712
9111
|
role: instance.role,
|
|
@@ -8718,11 +9117,11 @@ ${code}`
|
|
|
8718
9117
|
if (action === "close") {
|
|
8719
9118
|
let record2;
|
|
8720
9119
|
if (launch_id) {
|
|
8721
|
-
record2 = this.instanceManager.getByLaunchId(launch_id);
|
|
9120
|
+
record2 = await this.instanceManager.getByLaunchId(launch_id);
|
|
8722
9121
|
if (!record2)
|
|
8723
9122
|
return this._textResult({ error: "Launch is not managed.", launch_id });
|
|
8724
9123
|
const connectedInstanceId = record2.instanceId;
|
|
8725
|
-
const closeResult2 = record2.closedAt === void 0 ? this.instanceManager.close(record2) : { status: "already_closed" };
|
|
9124
|
+
const closeResult2 = record2.closedAt === void 0 ? await this.instanceManager.close(record2) : { status: "already_closed" };
|
|
8726
9125
|
if (connectedInstanceId) {
|
|
8727
9126
|
await this.bridge.unregisterInstanceIdEverywhere(connectedInstanceId);
|
|
8728
9127
|
await sleep(500);
|
|
@@ -8730,19 +9129,21 @@ ${code}`
|
|
|
8730
9129
|
}
|
|
8731
9130
|
return this._textResult({
|
|
8732
9131
|
...this._managedStatus(record2),
|
|
9132
|
+
close_status: closeResult2.status,
|
|
8733
9133
|
message: closeResult2.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
|
|
8734
9134
|
});
|
|
8735
9135
|
}
|
|
8736
9136
|
if (instance_id) {
|
|
8737
|
-
const recordBeforeClose = this.instanceManager.get(instance_id);
|
|
8738
|
-
const managedClose = this.instanceManager.closeByInstanceId(instance_id);
|
|
9137
|
+
const recordBeforeClose = await this.instanceManager.get(instance_id);
|
|
9138
|
+
const managedClose = await this.instanceManager.closeByInstanceId(instance_id);
|
|
8739
9139
|
if (managedClose.status !== "not_found") {
|
|
8740
9140
|
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
8741
9141
|
await sleep(500);
|
|
8742
9142
|
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
8743
|
-
const closedRecord =
|
|
9143
|
+
const closedRecord = managedClose.launchId ? await this.instanceManager.getByLaunchId(managedClose.launchId) : recordBeforeClose;
|
|
8744
9144
|
return this._textResult({
|
|
8745
9145
|
...closedRecord ? this._managedStatus(closedRecord) : { instance_id },
|
|
9146
|
+
close_status: managedClose.status,
|
|
8746
9147
|
message: managedClose.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
|
|
8747
9148
|
});
|
|
8748
9149
|
}
|
|
@@ -8755,7 +9156,7 @@ ${code}`
|
|
|
8755
9156
|
});
|
|
8756
9157
|
}
|
|
8757
9158
|
try {
|
|
8758
|
-
this.instanceManager.closeConnectedInstance(edit);
|
|
9159
|
+
await this.instanceManager.closeConnectedInstance(edit);
|
|
8759
9160
|
await sleep(500);
|
|
8760
9161
|
} catch (error) {
|
|
8761
9162
|
return this._textResult({
|
|
@@ -8766,10 +9167,11 @@ ${code}`
|
|
|
8766
9167
|
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
8767
9168
|
return this._textResult({
|
|
8768
9169
|
instance_id,
|
|
9170
|
+
close_status: "closed",
|
|
8769
9171
|
message: "Studio instance closed."
|
|
8770
9172
|
});
|
|
8771
9173
|
} else {
|
|
8772
|
-
const active = this.instanceManager.list().filter((entry) => entry.closedAt === void 0);
|
|
9174
|
+
const active = (await this.instanceManager.list()).filter((entry) => entry.closedAt === void 0);
|
|
8773
9175
|
if (active.length === 0) {
|
|
8774
9176
|
return this._textResult({ message: "No managed Studio instances are active." });
|
|
8775
9177
|
}
|
|
@@ -8783,13 +9185,14 @@ ${code}`
|
|
|
8783
9185
|
}
|
|
8784
9186
|
if (record2.instanceId)
|
|
8785
9187
|
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
8786
|
-
const closeResult = this.instanceManager.close(record2);
|
|
9188
|
+
const closeResult = await this.instanceManager.close(record2);
|
|
8787
9189
|
if (record2.instanceId) {
|
|
8788
9190
|
await sleep(500);
|
|
8789
9191
|
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
8790
9192
|
}
|
|
8791
9193
|
return this._textResult({
|
|
8792
9194
|
...this._managedStatus(record2),
|
|
9195
|
+
close_status: closeResult.status,
|
|
8793
9196
|
message: closeResult.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
|
|
8794
9197
|
});
|
|
8795
9198
|
}
|
|
@@ -8801,7 +9204,15 @@ ${code}`
|
|
|
8801
9204
|
const placeId = launchSource === "published_place" || launchSource === "place_revision" ? this._positiveInteger(request.place_id, "place_id") : void 0;
|
|
8802
9205
|
const placeVersion = launchSource === "place_revision" ? this._positiveInteger(request.place_version, "place_version") : void 0;
|
|
8803
9206
|
const localPlaceFile = typeof request.local_place_file === "string" ? request.local_place_file : void 0;
|
|
8804
|
-
|
|
9207
|
+
let studioExecutable;
|
|
9208
|
+
if (request.studio_executable !== void 0) {
|
|
9209
|
+
if (typeof request.studio_executable !== "string" || request.studio_executable.length === 0) {
|
|
9210
|
+
throw new Error("studio_executable must be a non-empty string when provided.");
|
|
9211
|
+
}
|
|
9212
|
+
studioExecutable = request.studio_executable;
|
|
9213
|
+
}
|
|
9214
|
+
const processEnvironment = parseStudioProcessEnvironmentPatch(request.process_environment);
|
|
9215
|
+
if (launchSource === "published_place" && placeId !== void 0 && await this._isLatestPublishedPlaceOpen(placeId)) {
|
|
8805
9216
|
return this._textResult({
|
|
8806
9217
|
error: "Place is already open.",
|
|
8807
9218
|
message: `place_id ${placeId} is already connected. Use the existing instance or launch a specific place_revision.`
|
|
@@ -8817,7 +9228,9 @@ ${code}`
|
|
|
8817
9228
|
placeId,
|
|
8818
9229
|
universeId,
|
|
8819
9230
|
placeVersion,
|
|
8820
|
-
connectionTimeoutMs: timeoutMs
|
|
9231
|
+
connectionTimeoutMs: timeoutMs,
|
|
9232
|
+
studioExecutable,
|
|
9233
|
+
processEnvironment
|
|
8821
9234
|
});
|
|
8822
9235
|
if (!waitForConnection) {
|
|
8823
9236
|
return this._textResult({
|
|
@@ -8828,11 +9241,11 @@ ${code}`
|
|
|
8828
9241
|
const connected = await this._waitForManagedEditConnection(record, beforeKeys, timeoutMs);
|
|
8829
9242
|
if (!connected) {
|
|
8830
9243
|
if (record.state === "launching") {
|
|
8831
|
-
this.instanceManager.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
|
|
9244
|
+
await this.instanceManager.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
|
|
8832
9245
|
}
|
|
8833
9246
|
if (record.closedAt === void 0) {
|
|
8834
9247
|
try {
|
|
8835
|
-
this.instanceManager.close(record);
|
|
9248
|
+
await this.instanceManager.close(record);
|
|
8836
9249
|
} catch {
|
|
8837
9250
|
}
|
|
8838
9251
|
}
|
|
@@ -8841,7 +9254,7 @@ ${code}`
|
|
|
8841
9254
|
error: record.failureReason ?? "Studio launched, but the MCP plugin did not connect before timeout."
|
|
8842
9255
|
});
|
|
8843
9256
|
}
|
|
8844
|
-
this.instanceManager.attachInstanceId(record, connected.instanceId);
|
|
9257
|
+
await this.instanceManager.attachInstanceId(record, connected.instanceId);
|
|
8845
9258
|
return this._textResult({
|
|
8846
9259
|
...this._managedStatus(record),
|
|
8847
9260
|
message: launchSource === "place_revision" ? `Studio opened place revision ${placeVersion}.` : "Studio opened."
|
|
@@ -11874,6 +12287,35 @@ var init_definitions = __esm({
|
|
|
11874
12287
|
type: "number",
|
|
11875
12288
|
description: 'For action="launch": max milliseconds for plugin connection (default 120000). The deadline also applies asynchronously when wait_for_connection=false.'
|
|
11876
12289
|
},
|
|
12290
|
+
studio_executable: {
|
|
12291
|
+
type: "string",
|
|
12292
|
+
description: 'For action="launch": exact Roblox Studio executable to launch instead of auto-discovering a version.'
|
|
12293
|
+
},
|
|
12294
|
+
process_environment: {
|
|
12295
|
+
type: "object",
|
|
12296
|
+
description: 'For action="launch": process-scoped environment patch applied only while creating Studio. Values are never retained in the managed-instance registry.',
|
|
12297
|
+
properties: {
|
|
12298
|
+
set: {
|
|
12299
|
+
type: "object",
|
|
12300
|
+
description: "Environment variables to set for the Studio process.",
|
|
12301
|
+
propertyNames: {
|
|
12302
|
+
pattern: "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
12303
|
+
},
|
|
12304
|
+
additionalProperties: {
|
|
12305
|
+
type: "string"
|
|
12306
|
+
}
|
|
12307
|
+
},
|
|
12308
|
+
remove: {
|
|
12309
|
+
type: "array",
|
|
12310
|
+
description: "Environment variables to remove from the Studio process environment.",
|
|
12311
|
+
items: {
|
|
12312
|
+
type: "string",
|
|
12313
|
+
pattern: "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
12314
|
+
}
|
|
12315
|
+
}
|
|
12316
|
+
},
|
|
12317
|
+
additionalProperties: false
|
|
12318
|
+
},
|
|
11877
12319
|
max_page_size: {
|
|
11878
12320
|
type: "number",
|
|
11879
12321
|
description: 'For action="list_place_versions": number of versions to return, clamped to 1-50 (default 10).'
|
|
@@ -12243,7 +12685,7 @@ var init_definitions = __esm({
|
|
|
12243
12685
|
{
|
|
12244
12686
|
name: "get_runtime_logs",
|
|
12245
12687
|
category: "read",
|
|
12246
|
-
description: "Read the in-memory log buffers captured by Studio plugin peers. Each buffer captures ~64 KB of recent LogService output; runtime peers seed from LogService:GetLogHistory() at plugin load so early startup logs emitted before the plugin finishes loading can still be returned, then continue capturing LogService.MessageOut entries. Oldest entries drop when over budget. Entries include capturedBy for the plugin buffer that observed the log. In ordinary Studio play/run sessions, LogService reflects logs across edit/server/client, so script-origin peer is not reliable and entries omit peer. In StudioTestService multiplayer sessions only, peer attribution is reliable and entries also include peer. target=all (default) merges buffers and dedups same-message-and-level entries captured within 2s across different buffers.",
|
|
12688
|
+
description: "Read the in-memory log buffers captured by Studio plugin peers. Each buffer captures ~64 KB of recent LogService output; runtime peers seed from LogService:GetLogHistory() at plugin load so early startup logs emitted before the plugin finishes loading can still be returned, then continue capturing LogService.MessageOut entries. Live structured LogService entries include their context dictionary as optional data; Roblox GetLogHistory does not expose context for entries seeded at plugin load. Oldest entries drop when over budget. Entries include capturedBy for the plugin buffer that observed the log. In ordinary Studio play/run sessions, LogService reflects logs across edit/server/client, so script-origin peer is not reliable and entries omit peer. In StudioTestService multiplayer sessions only, peer attribution is reliable and entries also include peer. target=all (default) merges buffers and dedups same-message-and-level entries captured within 2s across different buffers.",
|
|
12247
12689
|
inputSchema: {
|
|
12248
12690
|
type: "object",
|
|
12249
12691
|
properties: {
|
|
@@ -13670,7 +14112,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
13670
14112
|
});
|
|
13671
14113
|
|
|
13672
14114
|
// ../core/dist/install-plugin-helpers.js
|
|
13673
|
-
import { existsSync as existsSync6, readFileSync as
|
|
14115
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, unlinkSync } from "fs";
|
|
13674
14116
|
import { execSync } from "child_process";
|
|
13675
14117
|
import { join as join6 } from "path";
|
|
13676
14118
|
import { homedir as homedir5 } from "os";
|
|
@@ -13678,7 +14120,7 @@ function isWSL() {
|
|
|
13678
14120
|
if (process.platform !== "linux")
|
|
13679
14121
|
return false;
|
|
13680
14122
|
try {
|
|
13681
|
-
const v =
|
|
14123
|
+
const v = readFileSync6("/proc/version", "utf8");
|
|
13682
14124
|
return /microsoft|wsl/i.test(v);
|
|
13683
14125
|
} catch {
|
|
13684
14126
|
return false;
|
|
@@ -13763,7 +14205,7 @@ __export(install_plugin_exports, {
|
|
|
13763
14205
|
installBundledPlugin: () => installBundledPlugin,
|
|
13764
14206
|
installPlugin: () => installPlugin
|
|
13765
14207
|
});
|
|
13766
|
-
import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync7, mkdirSync as
|
|
14208
|
+
import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
13767
14209
|
import { dirname as dirname5, join as join7 } from "path";
|
|
13768
14210
|
import { fileURLToPath } from "url";
|
|
13769
14211
|
import { get } from "https";
|
|
@@ -13825,7 +14267,7 @@ function prepareInstall({
|
|
|
13825
14267
|
}) {
|
|
13826
14268
|
const pluginsFolder = getPluginsFolder();
|
|
13827
14269
|
if (!existsSync7(pluginsFolder)) {
|
|
13828
|
-
|
|
14270
|
+
mkdirSync4(pluginsFolder, { recursive: true });
|
|
13829
14271
|
}
|
|
13830
14272
|
handleVariantConflict({
|
|
13831
14273
|
pluginsFolder,
|
|
@@ -13846,14 +14288,14 @@ function bundledAssetPath() {
|
|
|
13846
14288
|
}
|
|
13847
14289
|
function packageVersion() {
|
|
13848
14290
|
const currentDir = dirname5(fileURLToPath(import.meta.url));
|
|
13849
|
-
const pkg = JSON.parse(
|
|
14291
|
+
const pkg = JSON.parse(readFileSync7(join7(currentDir, "..", "package.json"), "utf8"));
|
|
13850
14292
|
if (!pkg.version) {
|
|
13851
14293
|
throw new Error("Package version not found");
|
|
13852
14294
|
}
|
|
13853
14295
|
return pkg.version;
|
|
13854
14296
|
}
|
|
13855
14297
|
function bundledPluginVersion(source) {
|
|
13856
|
-
const match =
|
|
14298
|
+
const match = readFileSync7(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
|
|
13857
14299
|
return match ? match[1] : null;
|
|
13858
14300
|
}
|
|
13859
14301
|
function assertBundledPluginVersion(source) {
|
|
@@ -13867,8 +14309,8 @@ function assertBundledPluginVersion(source) {
|
|
|
13867
14309
|
}
|
|
13868
14310
|
function filesMatch(a, b) {
|
|
13869
14311
|
if (!existsSync7(b)) return false;
|
|
13870
|
-
const aBytes =
|
|
13871
|
-
const bBytes =
|
|
14312
|
+
const aBytes = readFileSync7(a);
|
|
14313
|
+
const bBytes = readFileSync7(b);
|
|
13872
14314
|
return aBytes.length === bBytes.length && aBytes.equals(bBytes);
|
|
13873
14315
|
}
|
|
13874
14316
|
async function installBundledPlugin(options = {}) {
|