@chrrxs/robloxstudio-mcp-inspector 2.22.3 → 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 +649 -325
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +3 -3
- package/studio-plugin/MCPPlugin.rbxmx +3 -3
package/dist/index.js
CHANGED
|
@@ -3073,7 +3073,8 @@ var init_roblox_cookie_client = __esm({
|
|
|
3073
3073
|
});
|
|
3074
3074
|
|
|
3075
3075
|
// ../core/dist/managed-instance-registry.js
|
|
3076
|
-
import * as fs from "fs";
|
|
3076
|
+
import * as fs from "fs/promises";
|
|
3077
|
+
import { randomUUID } from "crypto";
|
|
3077
3078
|
import * as os from "os";
|
|
3078
3079
|
import * as path from "path";
|
|
3079
3080
|
function defaultManagedInstanceRegistryDir() {
|
|
@@ -3088,8 +3089,8 @@ function defaultManagedInstanceRegistryDir() {
|
|
|
3088
3089
|
}
|
|
3089
3090
|
return path.join(os.homedir(), ".local", "state", "robloxstudio-mcp", "managed-instances", "v1");
|
|
3090
3091
|
}
|
|
3091
|
-
function
|
|
3092
|
-
|
|
3092
|
+
function delay(ms) {
|
|
3093
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
3093
3094
|
}
|
|
3094
3095
|
function ymd(timestamp) {
|
|
3095
3096
|
return new Date(timestamp).toISOString().slice(0, 10);
|
|
@@ -3104,92 +3105,131 @@ function isRecord(value) {
|
|
|
3104
3105
|
const record = value;
|
|
3105
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";
|
|
3106
3107
|
}
|
|
3107
|
-
|
|
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;
|
|
3108
3125
|
var init_managed_instance_registry = __esm({
|
|
3109
3126
|
"../core/dist/managed-instance-registry.js"() {
|
|
3110
3127
|
"use strict";
|
|
3111
3128
|
REGISTRY_VERSION = 1;
|
|
3112
3129
|
LOCK_STALE_MS = 1e4;
|
|
3113
3130
|
LOCK_RETRY_MS = 25;
|
|
3114
|
-
LOCK_TIMEOUT_MS =
|
|
3131
|
+
LOCK_TIMEOUT_MS = 15e3;
|
|
3115
3132
|
EVENT_RETENTION_DAYS = 2;
|
|
3116
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();
|
|
3117
3137
|
ManagedInstanceRegistry = class {
|
|
3118
3138
|
dir;
|
|
3119
3139
|
constructor(dir = defaultManagedInstanceRegistryDir()) {
|
|
3120
3140
|
this.dir = dir;
|
|
3121
3141
|
}
|
|
3122
|
-
upsert(record) {
|
|
3123
|
-
this.withLock(() => this.writeRecordUnlocked(record));
|
|
3142
|
+
async upsert(record) {
|
|
3143
|
+
await this.withLock(() => this.writeRecordUnlocked(record));
|
|
3124
3144
|
}
|
|
3125
|
-
attachInstanceId(recordId, instanceId) {
|
|
3126
|
-
this.withLock(() => {
|
|
3127
|
-
const record = this.readRecordUnlocked(recordId);
|
|
3145
|
+
async attachInstanceId(recordId, instanceId) {
|
|
3146
|
+
await this.withLock(async () => {
|
|
3147
|
+
const record = await this.readRecordUnlocked(recordId);
|
|
3128
3148
|
if (!record)
|
|
3129
3149
|
return;
|
|
3130
3150
|
record.instanceId = instanceId;
|
|
3131
3151
|
record.attachedAt = Date.now();
|
|
3132
|
-
this.writeRecordUnlocked(record);
|
|
3152
|
+
await this.writeRecordUnlocked(record);
|
|
3133
3153
|
});
|
|
3134
3154
|
}
|
|
3135
|
-
findOpenByInstanceId(instanceId, options) {
|
|
3136
|
-
return this.withLock(() => {
|
|
3137
|
-
this.sweepUnlocked(options);
|
|
3138
|
-
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);
|
|
3139
3159
|
});
|
|
3140
3160
|
}
|
|
3141
|
-
findAnyByInstanceId(instanceId) {
|
|
3142
|
-
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));
|
|
3143
3163
|
}
|
|
3144
|
-
findAnyByRecordId(recordId, options) {
|
|
3145
|
-
return this.withLock(() => {
|
|
3146
|
-
|
|
3147
|
-
|
|
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);
|
|
3148
3169
|
});
|
|
3149
3170
|
}
|
|
3150
|
-
listOpen(options) {
|
|
3151
|
-
return this.withLock(() => {
|
|
3152
|
-
this.sweepUnlocked(options);
|
|
3171
|
+
async listOpen(options) {
|
|
3172
|
+
return this.withLock(async () => {
|
|
3173
|
+
await this.sweepUnlocked(options);
|
|
3153
3174
|
return this.readOpenRecordsUnlocked();
|
|
3154
3175
|
});
|
|
3155
3176
|
}
|
|
3156
|
-
listOpenUnchecked() {
|
|
3177
|
+
async listOpenUnchecked() {
|
|
3157
3178
|
return this.withLock(() => this.readOpenRecordsUnlocked());
|
|
3158
3179
|
}
|
|
3159
|
-
markClosed(recordId, closedAt = Date.now()) {
|
|
3160
|
-
this.withLock(() => {
|
|
3161
|
-
const record = this.readRecordUnlocked(recordId);
|
|
3180
|
+
async markClosed(recordId, closedAt = Date.now()) {
|
|
3181
|
+
await this.withLock(async () => {
|
|
3182
|
+
const record = await this.readRecordUnlocked(recordId);
|
|
3162
3183
|
if (!record)
|
|
3163
3184
|
return;
|
|
3164
3185
|
record.closedAt = closedAt;
|
|
3165
|
-
this.writeRecordUnlocked(record);
|
|
3186
|
+
await this.writeRecordUnlocked(record);
|
|
3166
3187
|
});
|
|
3167
3188
|
}
|
|
3168
|
-
delete(recordId) {
|
|
3169
|
-
this.withLock(() => this.deleteRecordUnlocked(recordId));
|
|
3189
|
+
async delete(recordId) {
|
|
3190
|
+
await this.withLock(() => this.deleteRecordUnlocked(recordId));
|
|
3170
3191
|
}
|
|
3171
|
-
sweep(options) {
|
|
3172
|
-
this.withLock(() => this.sweepUnlocked(options));
|
|
3192
|
+
async sweep(options) {
|
|
3193
|
+
await this.withLock(() => this.sweepUnlocked(options));
|
|
3173
3194
|
}
|
|
3174
|
-
logEvent(event, now = Date.now()) {
|
|
3175
|
-
this.withLock(() => this.appendEventUnlocked(event, now));
|
|
3195
|
+
async logEvent(event, now = Date.now()) {
|
|
3196
|
+
await this.withLock(() => this.appendEventUnlocked(event, now));
|
|
3176
3197
|
}
|
|
3177
|
-
withLock(fn) {
|
|
3178
|
-
this.ensureDir();
|
|
3198
|
+
async withLock(fn) {
|
|
3199
|
+
await this.ensureDir();
|
|
3179
3200
|
const lockDir = path.join(this.dir, ".lock");
|
|
3180
3201
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
3181
|
-
|
|
3202
|
+
const owner = {
|
|
3203
|
+
pid: process.pid,
|
|
3204
|
+
token: randomUUID(),
|
|
3205
|
+
createdAt: Date.now()
|
|
3206
|
+
};
|
|
3207
|
+
for (; ; ) {
|
|
3182
3208
|
try {
|
|
3183
|
-
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
|
+
}
|
|
3184
3222
|
break;
|
|
3185
3223
|
} catch (error) {
|
|
3186
3224
|
const code = error.code;
|
|
3187
3225
|
if (code !== "EEXIST")
|
|
3188
3226
|
throw error;
|
|
3189
3227
|
try {
|
|
3190
|
-
const
|
|
3191
|
-
|
|
3192
|
-
|
|
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 });
|
|
3193
3233
|
continue;
|
|
3194
3234
|
}
|
|
3195
3235
|
} catch {
|
|
@@ -3198,40 +3238,58 @@ var init_managed_instance_registry = __esm({
|
|
|
3198
3238
|
if (Date.now() > deadline) {
|
|
3199
3239
|
throw new Error(`Timed out waiting for managed instance registry lock: ${lockDir}`);
|
|
3200
3240
|
}
|
|
3201
|
-
|
|
3241
|
+
await delay(LOCK_RETRY_MS);
|
|
3202
3242
|
}
|
|
3203
3243
|
}
|
|
3204
3244
|
try {
|
|
3205
|
-
return fn();
|
|
3245
|
+
return await fn();
|
|
3206
3246
|
} finally {
|
|
3207
|
-
|
|
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;
|
|
3208
3266
|
}
|
|
3209
3267
|
}
|
|
3210
|
-
ensureDir() {
|
|
3211
|
-
fs.
|
|
3268
|
+
async ensureDir() {
|
|
3269
|
+
await fs.mkdir(this.dir, { recursive: true });
|
|
3212
3270
|
}
|
|
3213
3271
|
recordPath(recordId) {
|
|
3214
3272
|
return path.join(this.dir, `${recordId}.json`);
|
|
3215
3273
|
}
|
|
3216
|
-
recordFilesUnlocked() {
|
|
3217
|
-
return fs.
|
|
3274
|
+
async recordFilesUnlocked() {
|
|
3275
|
+
return (await fs.readdir(this.dir)).filter((name) => name.endsWith(".json")).map((name) => path.join(this.dir, name));
|
|
3218
3276
|
}
|
|
3219
|
-
readRecordUnlocked(recordId) {
|
|
3277
|
+
async readRecordUnlocked(recordId) {
|
|
3220
3278
|
try {
|
|
3221
|
-
const parsed = JSON.parse(fs.
|
|
3279
|
+
const parsed = JSON.parse(await fs.readFile(this.recordPath(recordId), "utf8"));
|
|
3222
3280
|
return isRecord(parsed) ? parsed : void 0;
|
|
3223
3281
|
} catch {
|
|
3224
3282
|
return void 0;
|
|
3225
3283
|
}
|
|
3226
3284
|
}
|
|
3227
|
-
readOpenRecordsUnlocked() {
|
|
3228
|
-
return this.readRecordsUnlocked().filter((record) => record.closedAt === void 0);
|
|
3285
|
+
async readOpenRecordsUnlocked() {
|
|
3286
|
+
return (await this.readRecordsUnlocked()).filter((record) => record.closedAt === void 0);
|
|
3229
3287
|
}
|
|
3230
|
-
readRecordsUnlocked() {
|
|
3288
|
+
async readRecordsUnlocked() {
|
|
3231
3289
|
const records = [];
|
|
3232
|
-
for (const file of this.recordFilesUnlocked()) {
|
|
3290
|
+
for (const file of await this.recordFilesUnlocked()) {
|
|
3233
3291
|
try {
|
|
3234
|
-
const parsed = JSON.parse(fs.
|
|
3292
|
+
const parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
3235
3293
|
if (!isRecord(parsed))
|
|
3236
3294
|
continue;
|
|
3237
3295
|
records.push(parsed);
|
|
@@ -3240,48 +3298,48 @@ var init_managed_instance_registry = __esm({
|
|
|
3240
3298
|
}
|
|
3241
3299
|
return records;
|
|
3242
3300
|
}
|
|
3243
|
-
writeRecordUnlocked(record) {
|
|
3244
|
-
this.ensureDir();
|
|
3301
|
+
async writeRecordUnlocked(record) {
|
|
3302
|
+
await this.ensureDir();
|
|
3245
3303
|
const finalPath = this.recordPath(record.recordId);
|
|
3246
3304
|
const tmpPath = path.join(this.dir, `${record.recordId}.${process.pid}.${Date.now()}.tmp`);
|
|
3247
|
-
const fd = fs.
|
|
3305
|
+
const fd = await fs.open(tmpPath, "w");
|
|
3248
3306
|
try {
|
|
3249
|
-
|
|
3307
|
+
await fd.writeFile(`${JSON.stringify(record, null, 2)}
|
|
3250
3308
|
`, "utf8");
|
|
3251
|
-
|
|
3309
|
+
await fd.sync();
|
|
3252
3310
|
} finally {
|
|
3253
|
-
|
|
3311
|
+
await fd.close();
|
|
3254
3312
|
}
|
|
3255
|
-
fs.
|
|
3313
|
+
await fs.rename(tmpPath, finalPath);
|
|
3256
3314
|
}
|
|
3257
|
-
deleteRecordUnlocked(recordId) {
|
|
3258
|
-
fs.
|
|
3315
|
+
async deleteRecordUnlocked(recordId) {
|
|
3316
|
+
await fs.rm(this.recordPath(recordId), { force: true });
|
|
3259
3317
|
}
|
|
3260
|
-
appendEventUnlocked(event, now) {
|
|
3318
|
+
async appendEventUnlocked(event, now) {
|
|
3261
3319
|
const file = path.join(this.dir, `events-${ymd(now)}.jsonl`);
|
|
3262
|
-
fs.
|
|
3320
|
+
await fs.appendFile(file, `${JSON.stringify({
|
|
3263
3321
|
ts: new Date(now).toISOString(),
|
|
3264
3322
|
...event
|
|
3265
3323
|
})}
|
|
3266
3324
|
`, "utf8");
|
|
3267
3325
|
}
|
|
3268
|
-
cleanupOldEventLogsUnlocked(now) {
|
|
3326
|
+
async cleanupOldEventLogsUnlocked(now) {
|
|
3269
3327
|
const cutoff = ymd(now - EVENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3);
|
|
3270
|
-
for (const name of fs.
|
|
3328
|
+
for (const name of await fs.readdir(this.dir)) {
|
|
3271
3329
|
const date = eventLogDate(name);
|
|
3272
3330
|
if (!date || date >= cutoff)
|
|
3273
3331
|
continue;
|
|
3274
3332
|
try {
|
|
3275
|
-
fs.
|
|
3333
|
+
await fs.rm(path.join(this.dir, name), { force: true });
|
|
3276
3334
|
} catch {
|
|
3277
3335
|
}
|
|
3278
3336
|
}
|
|
3279
3337
|
}
|
|
3280
|
-
cleanupRecord(options, record) {
|
|
3338
|
+
async cleanupRecord(options, record) {
|
|
3281
3339
|
try {
|
|
3282
|
-
options.cleanupRecord?.(record);
|
|
3340
|
+
await options.cleanupRecord?.(record);
|
|
3283
3341
|
} catch {
|
|
3284
|
-
this.appendEventUnlocked({
|
|
3342
|
+
await this.appendEventUnlocked({
|
|
3285
3343
|
event: "registry_cleanup_failed",
|
|
3286
3344
|
recordId: record.recordId,
|
|
3287
3345
|
instanceId: record.instanceId,
|
|
@@ -3290,16 +3348,16 @@ var init_managed_instance_registry = __esm({
|
|
|
3290
3348
|
}, options.now ?? Date.now());
|
|
3291
3349
|
}
|
|
3292
3350
|
}
|
|
3293
|
-
sweepUnlocked(options) {
|
|
3351
|
+
async sweepUnlocked(options) {
|
|
3294
3352
|
const now = options.now ?? Date.now();
|
|
3295
|
-
this.cleanupOldEventLogsUnlocked(now);
|
|
3296
|
-
for (const file of this.recordFilesUnlocked()) {
|
|
3353
|
+
await this.cleanupOldEventLogsUnlocked(now);
|
|
3354
|
+
for (const file of await this.recordFilesUnlocked()) {
|
|
3297
3355
|
let parsed;
|
|
3298
3356
|
try {
|
|
3299
|
-
parsed = JSON.parse(fs.
|
|
3357
|
+
parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
3300
3358
|
} catch {
|
|
3301
|
-
fs.
|
|
3302
|
-
this.appendEventUnlocked({
|
|
3359
|
+
await fs.rm(file, { force: true });
|
|
3360
|
+
await this.appendEventUnlocked({
|
|
3303
3361
|
event: "registry_pruned_malformed_record",
|
|
3304
3362
|
reason: "parse_error",
|
|
3305
3363
|
action: "deleted_record"
|
|
@@ -3307,8 +3365,8 @@ var init_managed_instance_registry = __esm({
|
|
|
3307
3365
|
continue;
|
|
3308
3366
|
}
|
|
3309
3367
|
if (!parsed || typeof parsed !== "object") {
|
|
3310
|
-
fs.
|
|
3311
|
-
this.appendEventUnlocked({
|
|
3368
|
+
await fs.rm(file, { force: true });
|
|
3369
|
+
await this.appendEventUnlocked({
|
|
3312
3370
|
event: "registry_pruned_malformed_record",
|
|
3313
3371
|
reason: "invalid_shape",
|
|
3314
3372
|
action: "deleted_record"
|
|
@@ -3319,8 +3377,8 @@ var init_managed_instance_registry = __esm({
|
|
|
3319
3377
|
if (typeof version === "number" && version > REGISTRY_VERSION)
|
|
3320
3378
|
continue;
|
|
3321
3379
|
if (!isRecord(parsed)) {
|
|
3322
|
-
fs.
|
|
3323
|
-
this.appendEventUnlocked({
|
|
3380
|
+
await fs.rm(file, { force: true });
|
|
3381
|
+
await this.appendEventUnlocked({
|
|
3324
3382
|
event: "registry_pruned_malformed_record",
|
|
3325
3383
|
reason: "invalid_shape",
|
|
3326
3384
|
action: "deleted_record"
|
|
@@ -3330,8 +3388,8 @@ var init_managed_instance_registry = __esm({
|
|
|
3330
3388
|
const terminalAt = parsed.closedAt ?? parsed.exitedAt;
|
|
3331
3389
|
if (terminalAt !== void 0) {
|
|
3332
3390
|
if (now - terminalAt > TERMINAL_RECORD_RETENTION_MS) {
|
|
3333
|
-
fs.
|
|
3334
|
-
this.appendEventUnlocked({
|
|
3391
|
+
await fs.rm(file, { force: true });
|
|
3392
|
+
await this.appendEventUnlocked({
|
|
3335
3393
|
event: "registry_pruned_terminal_record",
|
|
3336
3394
|
recordId: parsed.recordId,
|
|
3337
3395
|
instanceId: parsed.instanceId,
|
|
@@ -3343,13 +3401,13 @@ var init_managed_instance_registry = __esm({
|
|
|
3343
3401
|
continue;
|
|
3344
3402
|
}
|
|
3345
3403
|
if (parsed.bootId !== options.currentBootId) {
|
|
3346
|
-
this.cleanupRecord(options, parsed);
|
|
3404
|
+
await this.cleanupRecord(options, parsed);
|
|
3347
3405
|
parsed.state = parsed.state === "failed" ? "failed" : "exited";
|
|
3348
3406
|
parsed.exitedAt = now;
|
|
3349
3407
|
parsed.closedAt = now;
|
|
3350
3408
|
parsed.failureReason ??= "Studio process belongs to a previous host boot.";
|
|
3351
|
-
this.writeRecordUnlocked(parsed);
|
|
3352
|
-
this.appendEventUnlocked({
|
|
3409
|
+
await this.writeRecordUnlocked(parsed);
|
|
3410
|
+
await this.appendEventUnlocked({
|
|
3353
3411
|
event: "registry_marked_previous_boot_exited",
|
|
3354
3412
|
recordId: parsed.recordId,
|
|
3355
3413
|
instanceId: parsed.instanceId,
|
|
@@ -3359,22 +3417,56 @@ var init_managed_instance_registry = __esm({
|
|
|
3359
3417
|
}, now);
|
|
3360
3418
|
continue;
|
|
3361
3419
|
}
|
|
3362
|
-
if (options.
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
parsed.
|
|
3367
|
-
parsed.
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
source: parsed.source,
|
|
3374
|
-
reason: "pid_not_running",
|
|
3375
|
-
action: "marked_exited_and_cleaned_baseplate"
|
|
3376
|
-
}, now);
|
|
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;
|
|
3377
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;
|
|
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);
|
|
3378
3470
|
}
|
|
3379
3471
|
}
|
|
3380
3472
|
};
|
|
@@ -3382,11 +3474,12 @@ var init_managed_instance_registry = __esm({
|
|
|
3382
3474
|
});
|
|
3383
3475
|
|
|
3384
3476
|
// ../core/dist/studio-instance-manager.js
|
|
3385
|
-
import { execFileSync, spawn } from "child_process";
|
|
3386
|
-
import { copyFileSync, existsSync as existsSync2, mkdirSync as
|
|
3387
|
-
import { randomUUID } from "crypto";
|
|
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";
|
|
3388
3480
|
import * as os2 from "os";
|
|
3389
3481
|
import * as path2 from "path";
|
|
3482
|
+
import { promisify } from "util";
|
|
3390
3483
|
function run(command, args, options = {}) {
|
|
3391
3484
|
return execFileSync(command, args, {
|
|
3392
3485
|
encoding: "utf8",
|
|
@@ -3394,17 +3487,29 @@ function run(command, args, options = {}) {
|
|
|
3394
3487
|
...options
|
|
3395
3488
|
}).trim();
|
|
3396
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
|
+
}
|
|
3397
3500
|
function isWsl() {
|
|
3398
3501
|
if (process.platform !== "linux")
|
|
3399
3502
|
return false;
|
|
3503
|
+
if (!process.env.WSL_INTEROP && !process.env.WSL_DISTRO_NAME)
|
|
3504
|
+
return false;
|
|
3400
3505
|
try {
|
|
3401
|
-
return /microsoft|wsl/i.test(
|
|
3506
|
+
return /microsoft|wsl/i.test(readFileSync2("/proc/version", "utf8"));
|
|
3402
3507
|
} catch {
|
|
3403
3508
|
return false;
|
|
3404
3509
|
}
|
|
3405
3510
|
}
|
|
3406
|
-
function
|
|
3407
|
-
return
|
|
3511
|
+
async function powershellAsync(script) {
|
|
3512
|
+
return runAsync("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
3408
3513
|
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
3409
3514
|
});
|
|
3410
3515
|
}
|
|
@@ -3421,15 +3526,33 @@ function windowsLocalAppData() {
|
|
|
3421
3526
|
return void 0;
|
|
3422
3527
|
}
|
|
3423
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
|
+
}
|
|
3424
3542
|
function toWslPath(windowsPath) {
|
|
3425
3543
|
if (!isWsl())
|
|
3426
3544
|
return windowsPath;
|
|
3427
3545
|
return run("wslpath", ["-u", windowsPath]);
|
|
3428
3546
|
}
|
|
3429
|
-
function
|
|
3547
|
+
async function toWslPathAsync(windowsPath) {
|
|
3548
|
+
if (!isWsl())
|
|
3549
|
+
return windowsPath;
|
|
3550
|
+
return runAsync("wslpath", ["-u", windowsPath]);
|
|
3551
|
+
}
|
|
3552
|
+
async function toStudioLaunchArgAsync(arg) {
|
|
3430
3553
|
if (!isWsl() || !path2.isAbsolute(arg) || !existsSync2(arg))
|
|
3431
3554
|
return arg;
|
|
3432
|
-
return
|
|
3555
|
+
return runAsync("wslpath", ["-w", arg]);
|
|
3433
3556
|
}
|
|
3434
3557
|
function powershellStringLiteral(value) {
|
|
3435
3558
|
return `'${value.replace(/'/g, "''")}'`;
|
|
@@ -3513,9 +3636,8 @@ function quoteWindowsCommandLineArg(value) {
|
|
|
3513
3636
|
}
|
|
3514
3637
|
return `${quoted}${"\\".repeat(backslashes * 2)}"`;
|
|
3515
3638
|
}
|
|
3516
|
-
function
|
|
3639
|
+
function buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment) {
|
|
3517
3640
|
const environmentPatch = parseStudioProcessEnvironmentPatch(processEnvironment);
|
|
3518
|
-
const windowsExe = toStudioLaunchArg(exe);
|
|
3519
3641
|
const commandLine = args.map(quoteWindowsCommandLineArg).join(" ");
|
|
3520
3642
|
return [
|
|
3521
3643
|
...environmentPatch ? powershellEnvironmentPatchStatements(environmentPatch) : [],
|
|
@@ -3524,7 +3646,7 @@ function buildWindowsStudioStartScript(exe, args, processEnvironment) {
|
|
|
3524
3646
|
`$psi.Arguments = ${powershellStringLiteral(commandLine)}`,
|
|
3525
3647
|
// With UseShellExecute=false, Studio inherits the synchronous
|
|
3526
3648
|
// powershell.exe invocation's stdout/stderr pipe handles under WSL. Those
|
|
3527
|
-
// handles keep
|
|
3649
|
+
// handles keep the PowerShell invocation waiting until Studio exits even though
|
|
3528
3650
|
// PowerShell already printed the PID. Shell execution prevents that
|
|
3529
3651
|
// inheritance while Process.Start still returns the native Studio PID.
|
|
3530
3652
|
"$psi.UseShellExecute = $true",
|
|
@@ -3532,9 +3654,10 @@ function buildWindowsStudioStartScript(exe, args, processEnvironment) {
|
|
|
3532
3654
|
'if ($null -eq $studio) { throw "Roblox Studio process did not start." }'
|
|
3533
3655
|
].join("; ");
|
|
3534
3656
|
}
|
|
3535
|
-
function spawnWindowsStudioFromWsl(exe, args, processEnvironment) {
|
|
3536
|
-
const
|
|
3537
|
-
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`);
|
|
3538
3661
|
const parsed = JSON.parse(output);
|
|
3539
3662
|
const nativePid = Number(parsed.pid);
|
|
3540
3663
|
const nativeStartedAt = typeof parsed.started === "string" && /^\d+$/u.test(parsed.started) ? parsed.started : void 0;
|
|
@@ -3576,7 +3699,7 @@ function resolveBaseplateTemplatePath() {
|
|
|
3576
3699
|
}
|
|
3577
3700
|
throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
|
|
3578
3701
|
}
|
|
3579
|
-
function
|
|
3702
|
+
function isProcessAlive2(pid) {
|
|
3580
3703
|
try {
|
|
3581
3704
|
process.kill(pid, 0);
|
|
3582
3705
|
return true;
|
|
@@ -3587,7 +3710,7 @@ function isProcessAlive(pid) {
|
|
|
3587
3710
|
function sweepStaleBaseplateFiles() {
|
|
3588
3711
|
let entries;
|
|
3589
3712
|
try {
|
|
3590
|
-
entries =
|
|
3713
|
+
entries = readdirSync(BASEPLATE_TEMP_DIR);
|
|
3591
3714
|
} catch {
|
|
3592
3715
|
return;
|
|
3593
3716
|
}
|
|
@@ -3596,18 +3719,18 @@ function sweepStaleBaseplateFiles() {
|
|
|
3596
3719
|
const match = BASEPLATE_TEMP_SWEEP_NAME.exec(entry);
|
|
3597
3720
|
if (!match)
|
|
3598
3721
|
continue;
|
|
3599
|
-
if (Number(match[1]) !== process.pid &&
|
|
3722
|
+
if (Number(match[1]) !== process.pid && isProcessAlive2(Number(match[1])))
|
|
3600
3723
|
continue;
|
|
3601
3724
|
const file = path2.join(BASEPLATE_TEMP_DIR, entry);
|
|
3602
3725
|
try {
|
|
3603
|
-
if (
|
|
3604
|
-
|
|
3726
|
+
if (statSync(file).mtimeMs < cutoff)
|
|
3727
|
+
rmSync(file, { force: true });
|
|
3605
3728
|
} catch {
|
|
3606
3729
|
}
|
|
3607
3730
|
}
|
|
3608
3731
|
}
|
|
3609
3732
|
function createBaseplatePlaceFile() {
|
|
3610
|
-
|
|
3733
|
+
mkdirSync2(BASEPLATE_TEMP_DIR, { recursive: true });
|
|
3611
3734
|
sweepStaleBaseplateFiles();
|
|
3612
3735
|
const file = path2.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
|
|
3613
3736
|
copyFileSync(resolveBaseplateTemplatePath(), file);
|
|
@@ -3624,7 +3747,7 @@ function cleanupManagedBaseplateFiles(record) {
|
|
|
3624
3747
|
return;
|
|
3625
3748
|
for (const file of [record.localPlaceFile, `${record.localPlaceFile}.lock`]) {
|
|
3626
3749
|
try {
|
|
3627
|
-
|
|
3750
|
+
rmSync(file, { force: true });
|
|
3628
3751
|
} catch {
|
|
3629
3752
|
}
|
|
3630
3753
|
}
|
|
@@ -3651,54 +3774,83 @@ function resolveStudioExe() {
|
|
|
3651
3774
|
if (!existsSync2(root)) {
|
|
3652
3775
|
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
3653
3776
|
}
|
|
3654
|
-
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);
|
|
3655
3778
|
if (candidates.length === 0) {
|
|
3656
3779
|
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
3657
3780
|
}
|
|
3658
3781
|
return candidates[0];
|
|
3659
3782
|
}
|
|
3660
|
-
function
|
|
3783
|
+
async function resolveStudioExeAsync() {
|
|
3784
|
+
if (process.env.ROBLOX_STUDIO_EXE)
|
|
3785
|
+
return process.env.ROBLOX_STUDIO_EXE;
|
|
3661
3786
|
if (process.platform === "darwin") {
|
|
3662
|
-
|
|
3663
|
-
try {
|
|
3664
|
-
out2 = run("pgrep", ["-fl", "RobloxStudio"]);
|
|
3665
|
-
} catch {
|
|
3666
|
-
return [];
|
|
3667
|
-
}
|
|
3668
|
-
return out2.split("\n").filter(Boolean).map((line) => {
|
|
3669
|
-
const [pid, ...rest] = line.trim().split(/\s+/);
|
|
3670
|
-
return { Id: Number(pid), Name: "RobloxStudio", Path: rest.join(" "), MainWindowTitle: "" };
|
|
3671
|
-
}).filter((proc) => Number.isFinite(proc.Id));
|
|
3787
|
+
return "/Applications/RobloxStudio.app/Contents/MacOS/RobloxStudio";
|
|
3672
3788
|
}
|
|
3673
|
-
if (process.platform !== "win32" && !isWsl())
|
|
3674
|
-
|
|
3675
|
-
|
|
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();
|
|
3676
3805
|
try {
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
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
|
+
};
|
|
3680
3836
|
}
|
|
3681
|
-
if (!out)
|
|
3682
|
-
return [];
|
|
3683
|
-
const parsed = JSON.parse(out);
|
|
3684
|
-
return Array.isArray(parsed) ? parsed : [parsed];
|
|
3685
3837
|
}
|
|
3686
|
-
function
|
|
3838
|
+
async function currentBootIdAsync() {
|
|
3687
3839
|
if (process.platform === "linux") {
|
|
3688
3840
|
try {
|
|
3689
|
-
return
|
|
3841
|
+
return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
3690
3842
|
} catch {
|
|
3691
3843
|
}
|
|
3692
3844
|
}
|
|
3693
3845
|
if (process.platform === "win32" || isWsl()) {
|
|
3694
3846
|
try {
|
|
3695
|
-
return
|
|
3847
|
+
return await powershellAsync('(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString("o")');
|
|
3696
3848
|
} catch {
|
|
3697
3849
|
}
|
|
3698
3850
|
}
|
|
3699
3851
|
if (process.platform === "darwin") {
|
|
3700
3852
|
try {
|
|
3701
|
-
return
|
|
3853
|
+
return await runAsync("sysctl", ["-n", "kern.boottime"]);
|
|
3702
3854
|
} catch {
|
|
3703
3855
|
}
|
|
3704
3856
|
}
|
|
@@ -3737,13 +3889,13 @@ function buildStudioLaunchArgs(options) {
|
|
|
3737
3889
|
];
|
|
3738
3890
|
}
|
|
3739
3891
|
}
|
|
3740
|
-
function
|
|
3892
|
+
function delay2(ms) {
|
|
3741
3893
|
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
3742
3894
|
}
|
|
3743
3895
|
function basenameAny(filePath) {
|
|
3744
3896
|
return path2.basename(filePath.replace(/\\/g, "/"));
|
|
3745
3897
|
}
|
|
3746
|
-
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, ENVIRONMENT_VARIABLE_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;
|
|
3747
3899
|
var init_studio_instance_manager = __esm({
|
|
3748
3900
|
"../core/dist/studio-instance-manager.js"() {
|
|
3749
3901
|
"use strict";
|
|
@@ -3751,27 +3903,39 @@ var init_studio_instance_manager = __esm({
|
|
|
3751
3903
|
BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
|
|
3752
3904
|
BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
|
|
3753
3905
|
BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
|
|
3906
|
+
execFileAsync = promisify(execFile);
|
|
3754
3907
|
ENVIRONMENT_VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
3755
3908
|
STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
3756
3909
|
BASEPLATE_TEMP_SWEEP_NAME = /^Baseplate-(\d+)-\d+\.rbxl(\.lock)?$/;
|
|
3757
3910
|
StudioInstanceManager = class {
|
|
3758
3911
|
managedByInstanceId = /* @__PURE__ */ new Map();
|
|
3759
3912
|
pending = /* @__PURE__ */ new Set();
|
|
3760
|
-
monitors = /* @__PURE__ */ new Map();
|
|
3761
3913
|
connectionTimers = /* @__PURE__ */ new Map();
|
|
3762
3914
|
registry;
|
|
3763
3915
|
processAdapter;
|
|
3916
|
+
confirmedExitMisses;
|
|
3917
|
+
confirmedExitGraceMs;
|
|
3918
|
+
snapshotCacheMs;
|
|
3919
|
+
coordinatorTimer;
|
|
3920
|
+
coordinatorRefresh;
|
|
3921
|
+
cachedSnapshot;
|
|
3922
|
+
snapshotInFlight;
|
|
3923
|
+
launchQueue = Promise.resolve();
|
|
3764
3924
|
constructor(options = {}) {
|
|
3765
3925
|
this.registry = options.registry ?? new ManagedInstanceRegistry(options.registryDir);
|
|
3766
3926
|
this.processAdapter = options.processAdapter ?? {};
|
|
3927
|
+
this.confirmedExitMisses = options.confirmedExitMisses ?? 2;
|
|
3928
|
+
this.confirmedExitGraceMs = options.confirmedExitGraceMs ?? 5e3;
|
|
3929
|
+
this.snapshotCacheMs = options.snapshotCacheMs ?? 0;
|
|
3767
3930
|
}
|
|
3768
|
-
list() {
|
|
3769
|
-
this.
|
|
3931
|
+
async list() {
|
|
3932
|
+
const snapshot = await this.getProcessSnapshot();
|
|
3933
|
+
await this.sweepRegistry(snapshot);
|
|
3770
3934
|
for (const record of [...this.managedByInstanceId.values(), ...this.pending]) {
|
|
3771
|
-
this.refresh(record);
|
|
3935
|
+
await this.refresh(record, snapshot);
|
|
3772
3936
|
}
|
|
3773
3937
|
const records = [...this.managedByInstanceId.values(), ...this.pending];
|
|
3774
|
-
for (const registryRecord of this.registry.
|
|
3938
|
+
for (const registryRecord of await this.registry.listOpenUnchecked()) {
|
|
3775
3939
|
const record = this.fromRegistryRecord(registryRecord);
|
|
3776
3940
|
if (records.some((existing) => record.recordId && existing.recordId === record.recordId || record.instanceId && existing.instanceId === record.instanceId)) {
|
|
3777
3941
|
continue;
|
|
@@ -3780,27 +3944,30 @@ var init_studio_instance_manager = __esm({
|
|
|
3780
3944
|
}
|
|
3781
3945
|
return records.filter((record) => record.closedAt === void 0).filter((instance, index, all) => all.indexOf(instance) === index);
|
|
3782
3946
|
}
|
|
3783
|
-
get(instanceId) {
|
|
3784
|
-
this.
|
|
3947
|
+
async get(instanceId) {
|
|
3948
|
+
const snapshot = await this.getProcessSnapshot();
|
|
3949
|
+
await this.sweepRegistry(snapshot);
|
|
3785
3950
|
const memoryRecord = this.managedByInstanceId.get(instanceId);
|
|
3786
3951
|
if (memoryRecord)
|
|
3787
|
-
return this.refresh(memoryRecord);
|
|
3788
|
-
const registryRecord = this.registry.findAnyByInstanceId(instanceId);
|
|
3789
|
-
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;
|
|
3790
3955
|
}
|
|
3791
|
-
getByLaunchId(launchId) {
|
|
3792
|
-
this.
|
|
3956
|
+
async getByLaunchId(launchId) {
|
|
3957
|
+
const snapshot = await this.getProcessSnapshot();
|
|
3958
|
+
await this.sweepRegistry(snapshot);
|
|
3793
3959
|
const memoryRecord = [...this.managedByInstanceId.values(), ...this.pending].find((record) => record.recordId === launchId);
|
|
3794
3960
|
if (memoryRecord)
|
|
3795
|
-
return this.refresh(memoryRecord);
|
|
3796
|
-
const registryRecord = this.registry.findAnyByRecordId(launchId
|
|
3797
|
-
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;
|
|
3798
3964
|
}
|
|
3799
|
-
pendingLaunches() {
|
|
3965
|
+
async pendingLaunches() {
|
|
3800
3966
|
const now = Date.now();
|
|
3967
|
+
const bootId = await this.getCurrentBootId();
|
|
3801
3968
|
const records = [...this.pending];
|
|
3802
|
-
for (const registryRecord of this.registry.listOpenUnchecked()) {
|
|
3803
|
-
if (registryRecord.bootId !==
|
|
3969
|
+
for (const registryRecord of await this.registry.listOpenUnchecked()) {
|
|
3970
|
+
if (registryRecord.bootId !== bootId)
|
|
3804
3971
|
continue;
|
|
3805
3972
|
if (records.some((record) => record.recordId === registryRecord.recordId))
|
|
3806
3973
|
continue;
|
|
@@ -3808,8 +3975,9 @@ var init_studio_instance_manager = __esm({
|
|
|
3808
3975
|
}
|
|
3809
3976
|
return records.filter((record) => record.instanceId === void 0).filter((record) => record.state === "launching").filter((record) => record.connectionDeadlineAt === void 0 || record.connectionDeadlineAt > now);
|
|
3810
3977
|
}
|
|
3811
|
-
attachInstanceId(record, instanceId) {
|
|
3812
|
-
this.
|
|
3978
|
+
async attachInstanceId(record, instanceId) {
|
|
3979
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
3980
|
+
await this.reconcileFromPositiveEvidence(record, snapshot);
|
|
3813
3981
|
if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited")
|
|
3814
3982
|
return;
|
|
3815
3983
|
if (record.instanceId && record.instanceId !== instanceId)
|
|
@@ -3820,47 +3988,59 @@ var init_studio_instance_manager = __esm({
|
|
|
3820
3988
|
this.clearConnectionTimer(record);
|
|
3821
3989
|
this.pending.delete(record);
|
|
3822
3990
|
this.managedByInstanceId.set(instanceId, record);
|
|
3823
|
-
this.persist(record);
|
|
3991
|
+
await this.persist(record);
|
|
3824
3992
|
}
|
|
3825
|
-
markFailed(record, reason) {
|
|
3826
|
-
this.refresh(record);
|
|
3993
|
+
async markFailed(record, reason) {
|
|
3827
3994
|
if (record.closedAt !== void 0 || record.state !== "launching")
|
|
3828
3995
|
return record;
|
|
3829
3996
|
record.state = "failed";
|
|
3830
3997
|
record.failedAt = Date.now();
|
|
3831
3998
|
record.failureReason = reason;
|
|
3832
3999
|
this.clearConnectionTimer(record);
|
|
3833
|
-
this.persist(record);
|
|
4000
|
+
await this.persist(record);
|
|
3834
4001
|
return record;
|
|
3835
4002
|
}
|
|
3836
|
-
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));
|
|
3837
4008
|
if (record.closedAt !== void 0)
|
|
3838
4009
|
return record;
|
|
3839
|
-
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
3840
|
-
const studioProcess = processId ? this.findProcessById(processId) : void 0;
|
|
3841
|
-
if (processId && (!studioProcess || !this.verifyProcessForRecord(record, studioProcess))) {
|
|
3842
|
-
return this.markProcessExited(record, void 0, studioProcess ? "Studio process identity changed; the retained PID was not reused." : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
|
|
3843
|
-
}
|
|
3844
4010
|
if (record.state === "launching" && record.connectionDeadlineAt !== void 0 && Date.now() >= record.connectionDeadlineAt) {
|
|
3845
4011
|
record.state = "failed";
|
|
3846
4012
|
record.failedAt = Date.now();
|
|
3847
4013
|
record.failureReason = "Studio launched, but the MCP plugin did not connect before timeout.";
|
|
3848
4014
|
this.clearConnectionTimer(record);
|
|
3849
|
-
this.persist(record);
|
|
4015
|
+
await this.persist(record);
|
|
3850
4016
|
}
|
|
3851
4017
|
return record;
|
|
3852
4018
|
}
|
|
3853
4019
|
async launch(options) {
|
|
3854
|
-
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);
|
|
3855
4035
|
const processEnvironment = parseStudioProcessEnvironmentPatch(options.processEnvironment);
|
|
3856
4036
|
if (options.studioExecutable !== void 0 && (typeof options.studioExecutable !== "string" || options.studioExecutable.length === 0 || options.studioExecutable.includes("\0"))) {
|
|
3857
4037
|
throw new Error("studio_executable must be a non-empty string without null characters.");
|
|
3858
4038
|
}
|
|
3859
4039
|
const preparedOptions = prepareStudioLaunchOptions(options);
|
|
3860
|
-
const bootId = this.getCurrentBootId();
|
|
3861
|
-
const before = new Set(
|
|
3862
|
-
const exe = preparedOptions.studioExecutable ?? this.processAdapter.resolveStudioExe
|
|
3863
|
-
const args = buildStudioLaunchArgs(preparedOptions).map(
|
|
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));
|
|
3864
4044
|
const spawnOptions = {
|
|
3865
4045
|
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
3866
4046
|
detached: true,
|
|
@@ -3870,9 +4050,9 @@ var init_studio_instance_manager = __esm({
|
|
|
3870
4050
|
let proc;
|
|
3871
4051
|
try {
|
|
3872
4052
|
if (this.processAdapter.spawnStudio) {
|
|
3873
|
-
proc = this.processAdapter.spawnStudio(exe, args, spawnOptions);
|
|
4053
|
+
proc = await this.processAdapter.spawnStudio(exe, args, spawnOptions);
|
|
3874
4054
|
} else if (isWsl()) {
|
|
3875
|
-
proc = spawnWindowsStudioFromWsl(exe, args, processEnvironment);
|
|
4055
|
+
proc = await spawnWindowsStudioFromWsl(exe, args, processEnvironment);
|
|
3876
4056
|
} else {
|
|
3877
4057
|
const child = spawn(exe, args, spawnOptions);
|
|
3878
4058
|
proc = {
|
|
@@ -3891,8 +4071,9 @@ var init_studio_instance_manager = __esm({
|
|
|
3891
4071
|
cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
|
|
3892
4072
|
throw error;
|
|
3893
4073
|
}
|
|
4074
|
+
const launchedAt = Date.now();
|
|
3894
4075
|
const record = {
|
|
3895
|
-
recordId:
|
|
4076
|
+
recordId: randomUUID2(),
|
|
3896
4077
|
source: options.source,
|
|
3897
4078
|
nativeProcessId: proc.nativePid,
|
|
3898
4079
|
nativeProcessStartedAt: proc.nativeStartedAt,
|
|
@@ -3903,23 +4084,27 @@ var init_studio_instance_manager = __esm({
|
|
|
3903
4084
|
universeId: preparedOptions.universeId,
|
|
3904
4085
|
placeVersion: preparedOptions.placeVersion,
|
|
3905
4086
|
localPlaceFile: preparedOptions.localPlaceFile,
|
|
3906
|
-
launchedAt
|
|
3907
|
-
connectionDeadlineAt:
|
|
4087
|
+
launchedAt,
|
|
4088
|
+
connectionDeadlineAt: launchedAt + (options.connectionTimeoutMs ?? 12e4),
|
|
3908
4089
|
state: "launching",
|
|
3909
4090
|
ownerPid: process.pid,
|
|
3910
4091
|
bootId,
|
|
3911
|
-
deleteLocalPlaceFileOnClose: options.source === "baseplate"
|
|
4092
|
+
deleteLocalPlaceFileOnClose: options.source === "baseplate",
|
|
4093
|
+
processObservationStatus: "running",
|
|
4094
|
+
lastProcessObservationAt: launchedAt,
|
|
4095
|
+
lastSuccessfulProcessObservationAt: launchedAt,
|
|
4096
|
+
consecutiveConfirmedMisses: 0
|
|
3912
4097
|
};
|
|
3913
4098
|
this.pending.add(record);
|
|
3914
4099
|
try {
|
|
3915
|
-
this.persist(record);
|
|
4100
|
+
await this.persist(record);
|
|
3916
4101
|
} catch (error) {
|
|
3917
4102
|
this.pending.delete(record);
|
|
3918
4103
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
3919
4104
|
let stopError;
|
|
3920
4105
|
if (processId) {
|
|
3921
4106
|
try {
|
|
3922
|
-
this.closeProcess(processId);
|
|
4107
|
+
await this.closeProcess(processId);
|
|
3923
4108
|
} catch (caught) {
|
|
3924
4109
|
stopError = caught;
|
|
3925
4110
|
}
|
|
@@ -3931,38 +4116,40 @@ var init_studio_instance_manager = __esm({
|
|
|
3931
4116
|
}
|
|
3932
4117
|
proc.unref();
|
|
3933
4118
|
proc.onExit?.((code, signal) => {
|
|
3934
|
-
this.markProcessExited(record, code ?? void 0, signal ? `Studio process exited from signal ${signal}.` : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
|
|
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."));
|
|
3935
4120
|
});
|
|
3936
4121
|
proc.onError?.((error) => {
|
|
3937
|
-
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}`));
|
|
3938
4123
|
});
|
|
3939
4124
|
const deadline = Date.now() + 5e3;
|
|
3940
4125
|
while (Date.now() < deadline && record.nativeProcessId === void 0) {
|
|
3941
|
-
const
|
|
4126
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4127
|
+
const created = snapshot.status === "ok" ? snapshot.processes.find((candidate) => !before.has(candidate.Id)) : void 0;
|
|
3942
4128
|
if (created) {
|
|
3943
4129
|
record.nativeProcessId = created.Id;
|
|
3944
4130
|
record.nativeProcessStartedAt = created.StartTimeUtcFileTime;
|
|
3945
|
-
this.persist(record);
|
|
4131
|
+
await this.persist(record);
|
|
3946
4132
|
break;
|
|
3947
4133
|
}
|
|
3948
|
-
await
|
|
4134
|
+
await delay2(250);
|
|
3949
4135
|
}
|
|
3950
4136
|
if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
|
|
3951
4137
|
record.nativeProcessId = proc.pid;
|
|
3952
|
-
this.persist(record);
|
|
4138
|
+
await this.persist(record);
|
|
3953
4139
|
}
|
|
3954
4140
|
if (record.nativeProcessId !== void 0 && record.nativeProcessStartedAt === void 0) {
|
|
3955
|
-
const
|
|
4141
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4142
|
+
const nativeProcess = snapshot.status === "ok" ? snapshot.processes.find((candidate) => candidate.Id === record.nativeProcessId) : void 0;
|
|
3956
4143
|
if (nativeProcess?.StartTimeUtcFileTime !== void 0) {
|
|
3957
4144
|
record.nativeProcessStartedAt = nativeProcess.StartTimeUtcFileTime;
|
|
3958
|
-
this.persist(record);
|
|
4145
|
+
await this.persist(record);
|
|
3959
4146
|
}
|
|
3960
4147
|
}
|
|
3961
|
-
this.
|
|
4148
|
+
this.startCoordinator(record);
|
|
3962
4149
|
return record;
|
|
3963
4150
|
}
|
|
3964
|
-
closeByLaunchId(launchId) {
|
|
3965
|
-
const record = this.getByLaunchId(launchId);
|
|
4151
|
+
async closeByLaunchId(launchId) {
|
|
4152
|
+
const record = await this.getByLaunchId(launchId);
|
|
3966
4153
|
if (!record)
|
|
3967
4154
|
return { status: "not_found", launchId };
|
|
3968
4155
|
if (record.closedAt !== void 0) {
|
|
@@ -3970,19 +4157,19 @@ var init_studio_instance_manager = __esm({
|
|
|
3970
4157
|
}
|
|
3971
4158
|
return this.close(record);
|
|
3972
4159
|
}
|
|
3973
|
-
closeByInstanceId(instanceId) {
|
|
3974
|
-
this.
|
|
4160
|
+
async closeByInstanceId(instanceId) {
|
|
4161
|
+
const snapshot = await this.getProcessSnapshot(true);
|
|
4162
|
+
await this.sweepRegistry(snapshot);
|
|
3975
4163
|
const memoryRecord = this.managedByInstanceId.get(instanceId);
|
|
3976
4164
|
if (memoryRecord)
|
|
3977
4165
|
return this.close(memoryRecord);
|
|
3978
|
-
const registryRecord = this.registry.findAnyByInstanceId(instanceId);
|
|
4166
|
+
const registryRecord = await this.registry.findAnyByInstanceId(instanceId);
|
|
3979
4167
|
if (!registryRecord) {
|
|
3980
|
-
this.sweepRegistry();
|
|
3981
4168
|
return { status: "not_found", instanceId };
|
|
3982
4169
|
}
|
|
3983
4170
|
if (registryRecord.closedAt !== void 0) {
|
|
3984
4171
|
this.cleanupManagedRecord(registryRecord);
|
|
3985
|
-
this.registry.logEvent({
|
|
4172
|
+
await this.registry.logEvent({
|
|
3986
4173
|
event: "registry_close_already_stopped",
|
|
3987
4174
|
recordId: registryRecord.recordId,
|
|
3988
4175
|
instanceId: registryRecord.instanceId,
|
|
@@ -3994,9 +4181,7 @@ var init_studio_instance_manager = __esm({
|
|
|
3994
4181
|
}
|
|
3995
4182
|
return this.close(this.fromRegistryRecord(registryRecord));
|
|
3996
4183
|
}
|
|
3997
|
-
close(record) {
|
|
3998
|
-
this.stopMonitor(record);
|
|
3999
|
-
this.refresh(record);
|
|
4184
|
+
async close(record) {
|
|
4000
4185
|
if (record.closedAt !== void 0) {
|
|
4001
4186
|
return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
4002
4187
|
}
|
|
@@ -4004,36 +4189,33 @@ var init_studio_instance_manager = __esm({
|
|
|
4004
4189
|
if (!processId) {
|
|
4005
4190
|
throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
|
|
4006
4191
|
}
|
|
4007
|
-
const
|
|
4008
|
-
|
|
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") {
|
|
4009
4199
|
this.cleanupManagedRecord(record);
|
|
4010
|
-
this.markProcessExited(record, void 0, record.failureReason);
|
|
4011
|
-
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({
|
|
4012
4202
|
event: "registry_close_already_stopped",
|
|
4013
4203
|
recordId: record.recordId,
|
|
4014
4204
|
instanceId: record.instanceId,
|
|
4015
4205
|
source: record.source,
|
|
4016
|
-
reason: "pid_not_running",
|
|
4206
|
+
reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
|
|
4017
4207
|
action: "marked_closed_and_cleaned_baseplate"
|
|
4018
4208
|
});
|
|
4019
4209
|
return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
4020
4210
|
}
|
|
4021
|
-
if (!this.verifyProcessForRecord(record, studioProcess)) {
|
|
4022
|
-
this.registry.logEvent({
|
|
4023
|
-
event: "registry_process_verification_failed",
|
|
4024
|
-
recordId: record.recordId,
|
|
4025
|
-
instanceId: record.instanceId,
|
|
4026
|
-
source: record.source,
|
|
4027
|
-
reason: "identity_mismatch"
|
|
4028
|
-
});
|
|
4029
|
-
throw new Error("Managed Studio process identity could not be verified.");
|
|
4030
|
-
}
|
|
4031
4211
|
try {
|
|
4032
|
-
this.closeProcess(processId);
|
|
4212
|
+
await this.closeProcess(processId);
|
|
4033
4213
|
} catch (error) {
|
|
4034
|
-
|
|
4214
|
+
const retry = await this.getProcessSnapshot(true);
|
|
4215
|
+
const retryObservation = this.observeRecord(record, retry);
|
|
4216
|
+
if (retryObservation.status === "running" || retryObservation.status === "unknown")
|
|
4035
4217
|
throw error;
|
|
4036
|
-
this.registry.logEvent({
|
|
4218
|
+
await this.registry.logEvent({
|
|
4037
4219
|
event: "registry_close_already_stopped",
|
|
4038
4220
|
recordId: record.recordId,
|
|
4039
4221
|
instanceId: record.instanceId,
|
|
@@ -4042,7 +4224,7 @@ var init_studio_instance_manager = __esm({
|
|
|
4042
4224
|
action: "marked_closed_and_cleaned_baseplate"
|
|
4043
4225
|
});
|
|
4044
4226
|
this.cleanupManagedRecord(record);
|
|
4045
|
-
this.markProcessExited(record, void 0, record.failureReason);
|
|
4227
|
+
await this.markProcessExited(record, void 0, record.failureReason);
|
|
4046
4228
|
return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
4047
4229
|
}
|
|
4048
4230
|
const closedAt = Date.now();
|
|
@@ -4050,25 +4232,33 @@ var init_studio_instance_manager = __esm({
|
|
|
4050
4232
|
record.exitedAt = record.exitedAt ?? closedAt;
|
|
4051
4233
|
if (record.state !== "failed")
|
|
4052
4234
|
record.state = "exited";
|
|
4235
|
+
record.processObservationStatus = "not_running";
|
|
4236
|
+
record.lastProcessObservationAt = closedAt;
|
|
4237
|
+
record.lastSuccessfulProcessObservationAt = closedAt;
|
|
4238
|
+
record.lastProcessObservationError = void 0;
|
|
4053
4239
|
this.cleanupManagedRecord(record);
|
|
4054
4240
|
this.markClosedInMemory(record);
|
|
4055
|
-
this.persist(record);
|
|
4241
|
+
await this.persist(record);
|
|
4056
4242
|
return { status: "closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
4057
4243
|
}
|
|
4058
|
-
closeConnectedInstance(instance) {
|
|
4059
|
-
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);
|
|
4060
4250
|
if (!process2) {
|
|
4061
4251
|
throw new Error(`Could not find a Studio process for connected instance "${instance.instanceId}".`);
|
|
4062
4252
|
}
|
|
4063
|
-
this.closeProcess(process2.Id);
|
|
4253
|
+
await this.closeProcess(process2.Id);
|
|
4064
4254
|
}
|
|
4065
|
-
closeProcess(processId) {
|
|
4255
|
+
async closeProcess(processId) {
|
|
4066
4256
|
if (this.processAdapter.stopProcess) {
|
|
4067
|
-
this.processAdapter.stopProcess(processId);
|
|
4257
|
+
await this.processAdapter.stopProcess(processId);
|
|
4068
4258
|
return;
|
|
4069
4259
|
}
|
|
4070
4260
|
if (process.platform === "win32" || isWsl()) {
|
|
4071
|
-
|
|
4261
|
+
await powershellAsync(`Stop-Process -Id ${Math.trunc(processId)} -Force -ErrorAction Stop`);
|
|
4072
4262
|
} else {
|
|
4073
4263
|
try {
|
|
4074
4264
|
process.kill(processId, "SIGTERM");
|
|
@@ -4078,8 +4268,7 @@ var init_studio_instance_manager = __esm({
|
|
|
4078
4268
|
}
|
|
4079
4269
|
}
|
|
4080
4270
|
}
|
|
4081
|
-
findProcessForConnectedInstance(instance) {
|
|
4082
|
-
const processes = this.listStudioProcesses();
|
|
4271
|
+
findProcessForConnectedInstance(instance, processes) {
|
|
4083
4272
|
if (processes.length === 0)
|
|
4084
4273
|
return void 0;
|
|
4085
4274
|
if (processes.length === 1)
|
|
@@ -4098,31 +4287,67 @@ var init_studio_instance_manager = __esm({
|
|
|
4098
4287
|
}
|
|
4099
4288
|
return void 0;
|
|
4100
4289
|
}
|
|
4101
|
-
|
|
4102
|
-
|
|
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;
|
|
4103
4324
|
}
|
|
4104
|
-
getCurrentBootId() {
|
|
4105
|
-
return this.processAdapter.currentBootId
|
|
4325
|
+
async getCurrentBootId() {
|
|
4326
|
+
return this.processAdapter.currentBootId ? await this.processAdapter.currentBootId() : currentBootIdAsync();
|
|
4106
4327
|
}
|
|
4107
|
-
registrySweepOptions() {
|
|
4328
|
+
async registrySweepOptions(snapshot) {
|
|
4108
4329
|
return {
|
|
4109
|
-
currentBootId: this.getCurrentBootId(),
|
|
4110
|
-
|
|
4111
|
-
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
|
|
4112
4335
|
};
|
|
4113
4336
|
}
|
|
4114
|
-
sweepRegistry() {
|
|
4115
|
-
this.registry.sweep(this.registrySweepOptions());
|
|
4337
|
+
async sweepRegistry(snapshot) {
|
|
4338
|
+
await this.registry.sweep(await this.registrySweepOptions(snapshot));
|
|
4116
4339
|
}
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4340
|
+
observeRecord(record, snapshot) {
|
|
4341
|
+
if (snapshot.status === "error") {
|
|
4342
|
+
return { status: "unknown", observedAt: snapshot.observedAt, error: snapshot.error };
|
|
4343
|
+
}
|
|
4121
4344
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
4122
4345
|
if (!processId)
|
|
4123
|
-
return
|
|
4124
|
-
const studioProcess =
|
|
4125
|
-
|
|
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" };
|
|
4126
4351
|
}
|
|
4127
4352
|
verifyProcessForRecord(record, studioProcess) {
|
|
4128
4353
|
const processName = `${studioProcess.Name ?? ""} ${studioProcess.Path ?? ""}`.toLowerCase();
|
|
@@ -4156,9 +4381,9 @@ var init_studio_instance_manager = __esm({
|
|
|
4156
4381
|
if (record.instanceId)
|
|
4157
4382
|
this.managedByInstanceId.delete(record.instanceId);
|
|
4158
4383
|
this.pending.delete(record);
|
|
4159
|
-
this.
|
|
4384
|
+
this.clearConnectionTimer(record);
|
|
4160
4385
|
}
|
|
4161
|
-
markProcessExited(record, exitCode, reason) {
|
|
4386
|
+
async markProcessExited(record, exitCode, reason) {
|
|
4162
4387
|
if (record.closedAt !== void 0)
|
|
4163
4388
|
return record;
|
|
4164
4389
|
const exitedAt = Date.now();
|
|
@@ -4166,43 +4391,44 @@ var init_studio_instance_manager = __esm({
|
|
|
4166
4391
|
record.closedAt = exitedAt;
|
|
4167
4392
|
if (record.state !== "failed")
|
|
4168
4393
|
record.state = "exited";
|
|
4394
|
+
record.processObservationStatus = "not_running";
|
|
4395
|
+
record.lastProcessObservationAt = exitedAt;
|
|
4396
|
+
record.lastSuccessfulProcessObservationAt = exitedAt;
|
|
4397
|
+
record.lastProcessObservationError = void 0;
|
|
4169
4398
|
if (exitCode !== void 0)
|
|
4170
4399
|
record.exitCode = exitCode;
|
|
4171
4400
|
if (reason)
|
|
4172
4401
|
record.failureReason = reason;
|
|
4173
4402
|
this.cleanupManagedRecord(record);
|
|
4174
4403
|
this.markClosedInMemory(record);
|
|
4175
|
-
this.persist(record);
|
|
4404
|
+
await this.persist(record);
|
|
4176
4405
|
return record;
|
|
4177
4406
|
}
|
|
4178
|
-
|
|
4179
|
-
if (!record.recordId || record.closedAt !== void 0
|
|
4407
|
+
startCoordinator(record) {
|
|
4408
|
+
if (!record.recordId || record.closedAt !== void 0)
|
|
4180
4409
|
return;
|
|
4181
4410
|
if (record.state === "launching" && record.connectionDeadlineAt !== void 0) {
|
|
4182
4411
|
const timeout = setTimeout(() => {
|
|
4183
|
-
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."));
|
|
4184
4413
|
}, Math.max(0, record.connectionDeadlineAt - Date.now()));
|
|
4185
4414
|
if (typeof timeout === "object" && "unref" in timeout)
|
|
4186
4415
|
timeout.unref();
|
|
4187
4416
|
this.connectionTimers.set(record.recordId, timeout);
|
|
4188
4417
|
}
|
|
4189
|
-
|
|
4190
|
-
this.refresh(record);
|
|
4191
|
-
if (record.closedAt !== void 0)
|
|
4192
|
-
this.stopMonitor(record);
|
|
4193
|
-
}, 5e3);
|
|
4194
|
-
if (typeof timer === "object" && "unref" in timer)
|
|
4195
|
-
timer.unref();
|
|
4196
|
-
this.monitors.set(record.recordId, timer);
|
|
4197
|
-
}
|
|
4198
|
-
stopMonitor(record) {
|
|
4199
|
-
if (!record.recordId)
|
|
4418
|
+
if (this.coordinatorTimer)
|
|
4200
4419
|
return;
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
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
|
+
}
|
|
4206
4432
|
}
|
|
4207
4433
|
clearConnectionTimer(record) {
|
|
4208
4434
|
if (!record.recordId)
|
|
@@ -4212,8 +4438,80 @@ var init_studio_instance_manager = __esm({
|
|
|
4212
4438
|
clearTimeout(timer);
|
|
4213
4439
|
this.connectionTimers.delete(record.recordId);
|
|
4214
4440
|
}
|
|
4215
|
-
|
|
4216
|
-
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));
|
|
4217
4515
|
}
|
|
4218
4516
|
toRegistryRecord(record) {
|
|
4219
4517
|
if (!record.recordId)
|
|
@@ -4245,7 +4543,13 @@ var init_studio_instance_manager = __esm({
|
|
|
4245
4543
|
failureReason: record.failureReason,
|
|
4246
4544
|
closedAt: record.closedAt,
|
|
4247
4545
|
ownerPid: record.ownerPid,
|
|
4248
|
-
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
|
|
4249
4553
|
};
|
|
4250
4554
|
}
|
|
4251
4555
|
fromRegistryRecord(record) {
|
|
@@ -4274,7 +4578,13 @@ var init_studio_instance_manager = __esm({
|
|
|
4274
4578
|
closedAt: record.closedAt,
|
|
4275
4579
|
ownerPid: record.ownerPid,
|
|
4276
4580
|
bootId: record.bootId,
|
|
4277
|
-
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
|
|
4278
4588
|
};
|
|
4279
4589
|
}
|
|
4280
4590
|
};
|
|
@@ -5085,7 +5395,7 @@ var init_esm = __esm({
|
|
|
5085
5395
|
|
|
5086
5396
|
// ../core/dist/studio-skills.js
|
|
5087
5397
|
import { createHash as createHash2 } from "crypto";
|
|
5088
|
-
import { existsSync as existsSync4, readFileSync as
|
|
5398
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
5089
5399
|
import * as path4 from "path";
|
|
5090
5400
|
function decompressLz4Block(input, outputLength) {
|
|
5091
5401
|
const output = Buffer.allocUnsafe(outputLength);
|
|
@@ -5297,7 +5607,7 @@ function discoverNamedFile(root, fileName, depth = 0) {
|
|
|
5297
5607
|
const matches = [];
|
|
5298
5608
|
let entries;
|
|
5299
5609
|
try {
|
|
5300
|
-
entries =
|
|
5610
|
+
entries = readdirSync2(root, { withFileTypes: true });
|
|
5301
5611
|
} catch {
|
|
5302
5612
|
return matches;
|
|
5303
5613
|
}
|
|
@@ -5311,7 +5621,7 @@ function discoverNamedFile(root, fileName, depth = 0) {
|
|
|
5311
5621
|
}
|
|
5312
5622
|
return matches;
|
|
5313
5623
|
}
|
|
5314
|
-
function resolveAssistantBundlePath(studioExe
|
|
5624
|
+
function resolveAssistantBundlePath(studioExe) {
|
|
5315
5625
|
const override = process.env.ROBLOX_STUDIO_ASSISTANT_BUNDLE;
|
|
5316
5626
|
if (override) {
|
|
5317
5627
|
if (!existsSync4(override)) {
|
|
@@ -5319,7 +5629,7 @@ function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
|
|
|
5319
5629
|
}
|
|
5320
5630
|
return override;
|
|
5321
5631
|
}
|
|
5322
|
-
const exeDirectory = path4.dirname(studioExe);
|
|
5632
|
+
const exeDirectory = path4.dirname(studioExe ?? resolveStudioExe());
|
|
5323
5633
|
const roots = [
|
|
5324
5634
|
path4.join(exeDirectory, "BuiltInStandalonePlugins"),
|
|
5325
5635
|
path4.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
|
|
@@ -5333,18 +5643,18 @@ function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
|
|
|
5333
5643
|
for (const candidate of discoverNamedFile(root, "Assistant.rbxm"))
|
|
5334
5644
|
candidates.add(candidate);
|
|
5335
5645
|
}
|
|
5336
|
-
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;
|
|
5337
5647
|
if (!newest) {
|
|
5338
5648
|
throw new Error(`Studio Assistant bundle not found for ${studioExe}. Set ROBLOX_STUDIO_ASSISTANT_BUNDLE to the installed Assistant.rbxm path.`);
|
|
5339
5649
|
}
|
|
5340
5650
|
return newest;
|
|
5341
5651
|
}
|
|
5342
5652
|
function loadBuiltInStudioSkills(bundlePath = resolveAssistantBundlePath()) {
|
|
5343
|
-
const stats =
|
|
5653
|
+
const stats = statSync2(bundlePath);
|
|
5344
5654
|
if (cachedBundle?.path === bundlePath && cachedBundle.modifiedAt === stats.mtimeMs && cachedBundle.size === stats.size) {
|
|
5345
5655
|
return cachedBundle.value;
|
|
5346
5656
|
}
|
|
5347
|
-
const buffer =
|
|
5657
|
+
const buffer = readFileSync4(bundlePath);
|
|
5348
5658
|
const skills = parseBuiltInStudioSkills(buffer);
|
|
5349
5659
|
if (skills.length === 0) {
|
|
5350
5660
|
throw new Error(`No built-in skill documents found in ${bundlePath}`);
|
|
@@ -7061,13 +7371,20 @@ var init_tools = __esm({
|
|
|
7061
7371
|
openCloudClient;
|
|
7062
7372
|
cookieClient;
|
|
7063
7373
|
instanceManager;
|
|
7374
|
+
managedConnectionAssociations = Promise.resolve();
|
|
7064
7375
|
constructor(bridge) {
|
|
7065
7376
|
this.client = new StudioHttpClient(bridge);
|
|
7066
7377
|
this.bridge = bridge;
|
|
7067
7378
|
this.openCloudClient = new OpenCloudClient();
|
|
7068
7379
|
this.cookieClient = new RobloxCookieClient();
|
|
7069
7380
|
this.instanceManager = new StudioInstanceManager();
|
|
7070
|
-
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
|
+
});
|
|
7071
7388
|
}
|
|
7072
7389
|
_textResult(body) {
|
|
7073
7390
|
return { content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
@@ -8648,9 +8965,9 @@ ${code}`
|
|
|
8648
8965
|
_publicInstanceKey(instance) {
|
|
8649
8966
|
return `${instance.instanceId}:${instance.role}:${instance.connectedAt}`;
|
|
8650
8967
|
}
|
|
8651
|
-
_isLatestPublishedPlaceOpen(placeId) {
|
|
8968
|
+
async _isLatestPublishedPlaceOpen(placeId) {
|
|
8652
8969
|
const publishedInstanceId2 = `place:${placeId}`;
|
|
8653
|
-
return this.bridge.getPublicInstances().some((instance) => instance.placeId === placeId || instance.instanceId === publishedInstanceId2) || this.instanceManager.list().some((record) => record.closedAt === void 0 && record.source === "published_place" && record.placeId === placeId);
|
|
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);
|
|
8654
8971
|
}
|
|
8655
8972
|
_matchesManagedLaunch(record, instance) {
|
|
8656
8973
|
if (record.source === "published_place") {
|
|
@@ -8662,12 +8979,12 @@ ${code}`
|
|
|
8662
8979
|
}
|
|
8663
8980
|
return true;
|
|
8664
8981
|
}
|
|
8665
|
-
_associateManagedEditConnection(instance) {
|
|
8982
|
+
async _associateManagedEditConnection(instance, instanceManager) {
|
|
8666
8983
|
if (instance.role !== "edit")
|
|
8667
8984
|
return;
|
|
8668
|
-
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];
|
|
8669
8986
|
if (candidate)
|
|
8670
|
-
|
|
8987
|
+
await instanceManager.attachInstanceId(candidate, instance.instanceId);
|
|
8671
8988
|
}
|
|
8672
8989
|
async _deriveUniverseId(placeId) {
|
|
8673
8990
|
const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`);
|
|
@@ -8684,7 +9001,7 @@ ${code}`
|
|
|
8684
9001
|
async _waitForManagedEditConnection(record, beforeKeys, timeoutMs) {
|
|
8685
9002
|
const deadline = Date.now() + timeoutMs;
|
|
8686
9003
|
while (Date.now() < deadline) {
|
|
8687
|
-
this.instanceManager.refresh(record);
|
|
9004
|
+
await this.instanceManager.refresh(record);
|
|
8688
9005
|
if (record.state === "failed" || record.state === "exited" || record.closedAt !== void 0) {
|
|
8689
9006
|
return void 0;
|
|
8690
9007
|
}
|
|
@@ -8696,7 +9013,6 @@ ${code}`
|
|
|
8696
9013
|
return void 0;
|
|
8697
9014
|
}
|
|
8698
9015
|
_managedStatus(record) {
|
|
8699
|
-
this.instanceManager.refresh(record);
|
|
8700
9016
|
const connected = record.instanceId ? this.bridge.getPublicInstances().filter((instance) => instance.instanceId === record.instanceId) : [];
|
|
8701
9017
|
return {
|
|
8702
9018
|
launch_id: record.recordId,
|
|
@@ -8704,7 +9020,12 @@ ${code}`
|
|
|
8704
9020
|
managed: true,
|
|
8705
9021
|
state: record.state,
|
|
8706
9022
|
pid: record.nativeProcessId ?? record.spawnPid,
|
|
8707
|
-
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,
|
|
8708
9029
|
source: record.source,
|
|
8709
9030
|
local_place_file: record.localPlaceFile,
|
|
8710
9031
|
place_id: record.placeId,
|
|
@@ -8756,15 +9077,18 @@ ${code}`
|
|
|
8756
9077
|
body.next_page_token = response.nextPageToken;
|
|
8757
9078
|
return this._textResult(body);
|
|
8758
9079
|
}
|
|
9080
|
+
if (action === "status" || action === "close") {
|
|
9081
|
+
await this.managedConnectionAssociations;
|
|
9082
|
+
}
|
|
8759
9083
|
if (action === "status") {
|
|
8760
9084
|
if (launch_id) {
|
|
8761
|
-
const record2 = this.instanceManager.getByLaunchId(launch_id);
|
|
9085
|
+
const record2 = await this.instanceManager.getByLaunchId(launch_id);
|
|
8762
9086
|
if (!record2)
|
|
8763
9087
|
return this._textResult({ error: "Launch is not managed.", launch_id });
|
|
8764
9088
|
return this._textResult(this._managedStatus(record2));
|
|
8765
9089
|
}
|
|
8766
9090
|
if (instance_id) {
|
|
8767
|
-
const record2 = this.instanceManager.get(instance_id);
|
|
9091
|
+
const record2 = await this.instanceManager.get(instance_id);
|
|
8768
9092
|
const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
|
|
8769
9093
|
if (!record2 && connected2.length === 0) {
|
|
8770
9094
|
return this._textResult({ error: "Instance is not connected or managed.", instance_id });
|
|
@@ -8781,7 +9105,7 @@ ${code}`
|
|
|
8781
9105
|
});
|
|
8782
9106
|
}
|
|
8783
9107
|
return this._textResult({
|
|
8784
|
-
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)),
|
|
8785
9109
|
connected: this.bridge.getPublicInstances().map((instance) => ({
|
|
8786
9110
|
instance_id: instance.instanceId,
|
|
8787
9111
|
role: instance.role,
|
|
@@ -8793,11 +9117,11 @@ ${code}`
|
|
|
8793
9117
|
if (action === "close") {
|
|
8794
9118
|
let record2;
|
|
8795
9119
|
if (launch_id) {
|
|
8796
|
-
record2 = this.instanceManager.getByLaunchId(launch_id);
|
|
9120
|
+
record2 = await this.instanceManager.getByLaunchId(launch_id);
|
|
8797
9121
|
if (!record2)
|
|
8798
9122
|
return this._textResult({ error: "Launch is not managed.", launch_id });
|
|
8799
9123
|
const connectedInstanceId = record2.instanceId;
|
|
8800
|
-
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" };
|
|
8801
9125
|
if (connectedInstanceId) {
|
|
8802
9126
|
await this.bridge.unregisterInstanceIdEverywhere(connectedInstanceId);
|
|
8803
9127
|
await sleep(500);
|
|
@@ -8810,13 +9134,13 @@ ${code}`
|
|
|
8810
9134
|
});
|
|
8811
9135
|
}
|
|
8812
9136
|
if (instance_id) {
|
|
8813
|
-
const recordBeforeClose = this.instanceManager.get(instance_id);
|
|
8814
|
-
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);
|
|
8815
9139
|
if (managedClose.status !== "not_found") {
|
|
8816
9140
|
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
8817
9141
|
await sleep(500);
|
|
8818
9142
|
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
8819
|
-
const closedRecord =
|
|
9143
|
+
const closedRecord = managedClose.launchId ? await this.instanceManager.getByLaunchId(managedClose.launchId) : recordBeforeClose;
|
|
8820
9144
|
return this._textResult({
|
|
8821
9145
|
...closedRecord ? this._managedStatus(closedRecord) : { instance_id },
|
|
8822
9146
|
close_status: managedClose.status,
|
|
@@ -8832,7 +9156,7 @@ ${code}`
|
|
|
8832
9156
|
});
|
|
8833
9157
|
}
|
|
8834
9158
|
try {
|
|
8835
|
-
this.instanceManager.closeConnectedInstance(edit);
|
|
9159
|
+
await this.instanceManager.closeConnectedInstance(edit);
|
|
8836
9160
|
await sleep(500);
|
|
8837
9161
|
} catch (error) {
|
|
8838
9162
|
return this._textResult({
|
|
@@ -8847,7 +9171,7 @@ ${code}`
|
|
|
8847
9171
|
message: "Studio instance closed."
|
|
8848
9172
|
});
|
|
8849
9173
|
} else {
|
|
8850
|
-
const active = this.instanceManager.list().filter((entry) => entry.closedAt === void 0);
|
|
9174
|
+
const active = (await this.instanceManager.list()).filter((entry) => entry.closedAt === void 0);
|
|
8851
9175
|
if (active.length === 0) {
|
|
8852
9176
|
return this._textResult({ message: "No managed Studio instances are active." });
|
|
8853
9177
|
}
|
|
@@ -8861,7 +9185,7 @@ ${code}`
|
|
|
8861
9185
|
}
|
|
8862
9186
|
if (record2.instanceId)
|
|
8863
9187
|
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
8864
|
-
const closeResult = this.instanceManager.close(record2);
|
|
9188
|
+
const closeResult = await this.instanceManager.close(record2);
|
|
8865
9189
|
if (record2.instanceId) {
|
|
8866
9190
|
await sleep(500);
|
|
8867
9191
|
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
@@ -8888,7 +9212,7 @@ ${code}`
|
|
|
8888
9212
|
studioExecutable = request.studio_executable;
|
|
8889
9213
|
}
|
|
8890
9214
|
const processEnvironment = parseStudioProcessEnvironmentPatch(request.process_environment);
|
|
8891
|
-
if (launchSource === "published_place" && placeId !== void 0 && this._isLatestPublishedPlaceOpen(placeId)) {
|
|
9215
|
+
if (launchSource === "published_place" && placeId !== void 0 && await this._isLatestPublishedPlaceOpen(placeId)) {
|
|
8892
9216
|
return this._textResult({
|
|
8893
9217
|
error: "Place is already open.",
|
|
8894
9218
|
message: `place_id ${placeId} is already connected. Use the existing instance or launch a specific place_revision.`
|
|
@@ -8917,11 +9241,11 @@ ${code}`
|
|
|
8917
9241
|
const connected = await this._waitForManagedEditConnection(record, beforeKeys, timeoutMs);
|
|
8918
9242
|
if (!connected) {
|
|
8919
9243
|
if (record.state === "launching") {
|
|
8920
|
-
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.");
|
|
8921
9245
|
}
|
|
8922
9246
|
if (record.closedAt === void 0) {
|
|
8923
9247
|
try {
|
|
8924
|
-
this.instanceManager.close(record);
|
|
9248
|
+
await this.instanceManager.close(record);
|
|
8925
9249
|
} catch {
|
|
8926
9250
|
}
|
|
8927
9251
|
}
|
|
@@ -8930,7 +9254,7 @@ ${code}`
|
|
|
8930
9254
|
error: record.failureReason ?? "Studio launched, but the MCP plugin did not connect before timeout."
|
|
8931
9255
|
});
|
|
8932
9256
|
}
|
|
8933
|
-
this.instanceManager.attachInstanceId(record, connected.instanceId);
|
|
9257
|
+
await this.instanceManager.attachInstanceId(record, connected.instanceId);
|
|
8934
9258
|
return this._textResult({
|
|
8935
9259
|
...this._managedStatus(record),
|
|
8936
9260
|
message: launchSource === "place_revision" ? `Studio opened place revision ${placeVersion}.` : "Studio opened."
|
|
@@ -13788,7 +14112,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
13788
14112
|
});
|
|
13789
14113
|
|
|
13790
14114
|
// ../core/dist/install-plugin-helpers.js
|
|
13791
|
-
import { existsSync as existsSync6, readFileSync as
|
|
14115
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, unlinkSync } from "fs";
|
|
13792
14116
|
import { execSync } from "child_process";
|
|
13793
14117
|
import { join as join6 } from "path";
|
|
13794
14118
|
import { homedir as homedir5 } from "os";
|
|
@@ -13796,7 +14120,7 @@ function isWSL() {
|
|
|
13796
14120
|
if (process.platform !== "linux")
|
|
13797
14121
|
return false;
|
|
13798
14122
|
try {
|
|
13799
|
-
const v =
|
|
14123
|
+
const v = readFileSync6("/proc/version", "utf8");
|
|
13800
14124
|
return /microsoft|wsl/i.test(v);
|
|
13801
14125
|
} catch {
|
|
13802
14126
|
return false;
|
|
@@ -13881,7 +14205,7 @@ __export(install_plugin_exports, {
|
|
|
13881
14205
|
installBundledPlugin: () => installBundledPlugin,
|
|
13882
14206
|
installPlugin: () => installPlugin
|
|
13883
14207
|
});
|
|
13884
|
-
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";
|
|
13885
14209
|
import { dirname as dirname5, join as join7 } from "path";
|
|
13886
14210
|
import { fileURLToPath } from "url";
|
|
13887
14211
|
import { get } from "https";
|
|
@@ -13943,7 +14267,7 @@ function prepareInstall({
|
|
|
13943
14267
|
}) {
|
|
13944
14268
|
const pluginsFolder = getPluginsFolder();
|
|
13945
14269
|
if (!existsSync7(pluginsFolder)) {
|
|
13946
|
-
|
|
14270
|
+
mkdirSync4(pluginsFolder, { recursive: true });
|
|
13947
14271
|
}
|
|
13948
14272
|
handleVariantConflict({
|
|
13949
14273
|
pluginsFolder,
|
|
@@ -13964,14 +14288,14 @@ function bundledAssetPath() {
|
|
|
13964
14288
|
}
|
|
13965
14289
|
function packageVersion() {
|
|
13966
14290
|
const currentDir = dirname5(fileURLToPath(import.meta.url));
|
|
13967
|
-
const pkg = JSON.parse(
|
|
14291
|
+
const pkg = JSON.parse(readFileSync7(join7(currentDir, "..", "package.json"), "utf8"));
|
|
13968
14292
|
if (!pkg.version) {
|
|
13969
14293
|
throw new Error("Package version not found");
|
|
13970
14294
|
}
|
|
13971
14295
|
return pkg.version;
|
|
13972
14296
|
}
|
|
13973
14297
|
function bundledPluginVersion(source) {
|
|
13974
|
-
const match =
|
|
14298
|
+
const match = readFileSync7(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
|
|
13975
14299
|
return match ? match[1] : null;
|
|
13976
14300
|
}
|
|
13977
14301
|
function assertBundledPluginVersion(source) {
|
|
@@ -13985,8 +14309,8 @@ function assertBundledPluginVersion(source) {
|
|
|
13985
14309
|
}
|
|
13986
14310
|
function filesMatch(a, b) {
|
|
13987
14311
|
if (!existsSync7(b)) return false;
|
|
13988
|
-
const aBytes =
|
|
13989
|
-
const bBytes =
|
|
14312
|
+
const aBytes = readFileSync7(a);
|
|
14313
|
+
const bBytes = readFileSync7(b);
|
|
13990
14314
|
return aBytes.length === bBytes.length && aBytes.equals(bBytes);
|
|
13991
14315
|
}
|
|
13992
14316
|
async function installBundledPlugin(options = {}) {
|