@ystemsrx/cfshare 0.1.5 → 0.1.6
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/src/cli.js +131 -1
- package/dist/src/manager.d.ts +15 -1
- package/dist/src/manager.d.ts.map +1 -1
- package/dist/src/manager.js +479 -110
- package/dist/src/tools.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +148 -1
- package/src/manager.ts +569 -115
- package/src/tools.ts +1 -1
package/src/manager.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
CfsharePolicy,
|
|
24
24
|
ExposureRecord,
|
|
25
25
|
ExposureSession,
|
|
26
|
+
ExposureStats,
|
|
26
27
|
ExposureStatus,
|
|
27
28
|
ExposureType,
|
|
28
29
|
FilePresentationMode,
|
|
@@ -140,6 +141,29 @@ type ManifestResponseMeta = {
|
|
|
140
141
|
total_size_bytes: number;
|
|
141
142
|
};
|
|
142
143
|
|
|
144
|
+
type PersistedSessionRecord = {
|
|
145
|
+
id: string;
|
|
146
|
+
type: ExposureType;
|
|
147
|
+
status: ExposureStatus;
|
|
148
|
+
createdAt: string;
|
|
149
|
+
expiresAt: string;
|
|
150
|
+
localUrl: string;
|
|
151
|
+
publicUrl?: string;
|
|
152
|
+
sourcePort?: number;
|
|
153
|
+
originPort: number;
|
|
154
|
+
tunnelPort: number;
|
|
155
|
+
workspaceDir?: string;
|
|
156
|
+
processPid?: number;
|
|
157
|
+
ownerPid?: number;
|
|
158
|
+
fileMode?: "normal" | "zip";
|
|
159
|
+
filePresentation?: FilePresentationMode;
|
|
160
|
+
maxDownloads?: number;
|
|
161
|
+
manifest?: ManifestEntry[];
|
|
162
|
+
access: AccessState;
|
|
163
|
+
stats: ExposureStats;
|
|
164
|
+
lastError?: string;
|
|
165
|
+
};
|
|
166
|
+
|
|
143
167
|
function nowIso(): string {
|
|
144
168
|
return toLocalIso(new Date());
|
|
145
169
|
}
|
|
@@ -310,6 +334,19 @@ function ensureString(input: unknown): string | undefined {
|
|
|
310
334
|
return trimmed || undefined;
|
|
311
335
|
}
|
|
312
336
|
|
|
337
|
+
function asNumber(input: unknown): number | undefined {
|
|
338
|
+
if (typeof input === "number" && Number.isFinite(input)) {
|
|
339
|
+
return Math.trunc(input);
|
|
340
|
+
}
|
|
341
|
+
if (typeof input === "string" && input.trim()) {
|
|
342
|
+
const parsed = Number(input);
|
|
343
|
+
if (Number.isFinite(parsed)) {
|
|
344
|
+
return Math.trunc(parsed);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
313
350
|
function buildContentDisposition(params: {
|
|
314
351
|
mode: FilePresentationMode;
|
|
315
352
|
filePath: string;
|
|
@@ -727,6 +764,7 @@ export class CfshareManager {
|
|
|
727
764
|
private readonly workspaceRoot: string;
|
|
728
765
|
private readonly auditFile: string;
|
|
729
766
|
private readonly sessionsFile: string;
|
|
767
|
+
private readonly sessionsDir: string;
|
|
730
768
|
private readonly exportsDir: string;
|
|
731
769
|
|
|
732
770
|
private initialized = false;
|
|
@@ -753,6 +791,7 @@ export class CfshareManager {
|
|
|
753
791
|
this.workspaceRoot = path.join(this.stateDir, "workspaces");
|
|
754
792
|
this.auditFile = path.join(this.stateDir, "audit.jsonl");
|
|
755
793
|
this.sessionsFile = path.join(this.stateDir, "sessions.json");
|
|
794
|
+
this.sessionsDir = path.join(this.stateDir, "sessions");
|
|
756
795
|
this.exportsDir = path.join(this.stateDir, "exports");
|
|
757
796
|
this.cloudflaredPathInput = this.pluginConfig.cloudflaredPath ?? "cloudflared";
|
|
758
797
|
}
|
|
@@ -771,6 +810,7 @@ export class CfshareManager {
|
|
|
771
810
|
await mkdirp(this.stateDir);
|
|
772
811
|
await mkdirp(this.workspaceRoot);
|
|
773
812
|
await mkdirp(this.exportsDir);
|
|
813
|
+
await mkdirp(this.sessionsDir);
|
|
774
814
|
await this.reloadPolicy();
|
|
775
815
|
this.startGuard();
|
|
776
816
|
this.initialized = true;
|
|
@@ -803,16 +843,213 @@ export class CfshareManager {
|
|
|
803
843
|
}
|
|
804
844
|
}
|
|
805
845
|
|
|
806
|
-
private
|
|
807
|
-
|
|
846
|
+
private sessionRecordPath(id: string): string {
|
|
847
|
+
return path.join(this.sessionsDir, `${encodeURIComponent(id)}.json`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
private toPersistedSessionRecord(session: ExposureSession): PersistedSessionRecord {
|
|
851
|
+
return {
|
|
808
852
|
id: session.id,
|
|
809
853
|
type: session.type,
|
|
810
854
|
status: session.status,
|
|
855
|
+
createdAt: session.createdAt,
|
|
811
856
|
expiresAt: session.expiresAt,
|
|
857
|
+
localUrl: session.localUrl,
|
|
858
|
+
publicUrl: session.publicUrl,
|
|
859
|
+
sourcePort: session.sourcePort,
|
|
860
|
+
originPort: session.originPort,
|
|
861
|
+
tunnelPort: session.tunnelPort,
|
|
812
862
|
workspaceDir: session.workspaceDir,
|
|
813
863
|
processPid: session.process?.pid,
|
|
814
|
-
|
|
815
|
-
|
|
864
|
+
ownerPid: process.pid,
|
|
865
|
+
fileMode: session.fileMode,
|
|
866
|
+
filePresentation: session.filePresentation,
|
|
867
|
+
maxDownloads: session.maxDownloads,
|
|
868
|
+
manifest: session.manifest,
|
|
869
|
+
access: session.access,
|
|
870
|
+
stats: session.stats,
|
|
871
|
+
lastError: session.lastError,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private parsePersistedSessionRecord(raw: unknown): PersistedSessionRecord | undefined {
|
|
876
|
+
if (!raw || typeof raw !== "object") {
|
|
877
|
+
return undefined;
|
|
878
|
+
}
|
|
879
|
+
const row = raw as Record<string, unknown>;
|
|
880
|
+
const id = ensureString(row.id);
|
|
881
|
+
const type = ensureString(row.type);
|
|
882
|
+
const status = ensureString(row.status);
|
|
883
|
+
if (!id || (type !== "port" && type !== "files")) {
|
|
884
|
+
return undefined;
|
|
885
|
+
}
|
|
886
|
+
const normalizedStatus: ExposureStatus =
|
|
887
|
+
status === "starting" || status === "running" || status === "stopped" || status === "error" || status === "expired"
|
|
888
|
+
? status
|
|
889
|
+
: "error";
|
|
890
|
+
const accessRaw = (row.access ?? {}) as Record<string, unknown>;
|
|
891
|
+
const accessModeRaw = ensureString(accessRaw.mode);
|
|
892
|
+
const accessMode: AccessMode =
|
|
893
|
+
accessModeRaw === "token" || accessModeRaw === "basic" || accessModeRaw === "none" ? accessModeRaw : "none";
|
|
894
|
+
const statsRaw = (row.stats ?? {}) as Record<string, unknown>;
|
|
895
|
+
return {
|
|
896
|
+
id,
|
|
897
|
+
type,
|
|
898
|
+
status: normalizedStatus,
|
|
899
|
+
createdAt: ensureString(row.createdAt) || nowIso(),
|
|
900
|
+
expiresAt: ensureString(row.expiresAt) || nowIso(),
|
|
901
|
+
localUrl: ensureString(row.localUrl) || "",
|
|
902
|
+
publicUrl: ensureString(row.publicUrl) || undefined,
|
|
903
|
+
sourcePort: asNumber(row.sourcePort),
|
|
904
|
+
originPort: asNumber(row.originPort) ?? 0,
|
|
905
|
+
tunnelPort: asNumber(row.tunnelPort) ?? 0,
|
|
906
|
+
workspaceDir: ensureString(row.workspaceDir) || undefined,
|
|
907
|
+
processPid: asNumber(row.processPid),
|
|
908
|
+
ownerPid: asNumber(row.ownerPid),
|
|
909
|
+
fileMode: ensureString(row.fileMode) === "zip" ? "zip" : "normal",
|
|
910
|
+
filePresentation: normalizeFilePresentation(ensureString(row.filePresentation) || "download"),
|
|
911
|
+
maxDownloads: asNumber(row.maxDownloads),
|
|
912
|
+
manifest: Array.isArray(row.manifest) ? (row.manifest as ManifestEntry[]) : undefined,
|
|
913
|
+
access: {
|
|
914
|
+
mode: accessMode,
|
|
915
|
+
protectOrigin: Boolean(accessRaw.protectOrigin),
|
|
916
|
+
allowlistPaths: normalizeAllowlistPaths(accessRaw.allowlistPaths as string[] | undefined),
|
|
917
|
+
token: ensureString(accessRaw.token) || undefined,
|
|
918
|
+
username: ensureString(accessRaw.username) || undefined,
|
|
919
|
+
password: ensureString(accessRaw.password) || undefined,
|
|
920
|
+
},
|
|
921
|
+
stats: {
|
|
922
|
+
requests: asNumber(statsRaw.requests) ?? 0,
|
|
923
|
+
downloads: asNumber(statsRaw.downloads) ?? 0,
|
|
924
|
+
bytesSent: asNumber(statsRaw.bytesSent) ?? 0,
|
|
925
|
+
lastAccessAt: ensureString(statsRaw.lastAccessAt) || undefined,
|
|
926
|
+
},
|
|
927
|
+
lastError: ensureString(row.lastError) || undefined,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private async readPersistedSessions(): Promise<Map<string, PersistedSessionRecord>> {
|
|
932
|
+
const out = new Map<string, PersistedSessionRecord>();
|
|
933
|
+
let sessionsDirMissing = false;
|
|
934
|
+
const entries = await fs.readdir(this.sessionsDir, { withFileTypes: true }).catch((error) => {
|
|
935
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
936
|
+
sessionsDirMissing = true;
|
|
937
|
+
}
|
|
938
|
+
return [];
|
|
939
|
+
});
|
|
940
|
+
for (const entry of entries) {
|
|
941
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const abs = path.join(this.sessionsDir, entry.name);
|
|
945
|
+
try {
|
|
946
|
+
const raw = await fs.readFile(abs, "utf8");
|
|
947
|
+
const record = this.parsePersistedSessionRecord(JSON.parse(raw));
|
|
948
|
+
if (record) {
|
|
949
|
+
out.set(record.id, record);
|
|
950
|
+
}
|
|
951
|
+
} catch {
|
|
952
|
+
// ignore malformed session files
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (out.size > 0 || !sessionsDirMissing) {
|
|
957
|
+
return out;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Legacy fallback for snapshots generated before per-session persistence.
|
|
961
|
+
try {
|
|
962
|
+
const raw = await fs.readFile(this.sessionsFile, "utf8");
|
|
963
|
+
const rows = JSON.parse(raw);
|
|
964
|
+
if (!Array.isArray(rows)) {
|
|
965
|
+
return out;
|
|
966
|
+
}
|
|
967
|
+
for (const row of rows) {
|
|
968
|
+
const record = this.parsePersistedSessionRecord({
|
|
969
|
+
...(row as Record<string, unknown>),
|
|
970
|
+
createdAt: (row as Record<string, unknown>).createdAt ?? nowIso(),
|
|
971
|
+
localUrl: (row as Record<string, unknown>).localUrl ?? "",
|
|
972
|
+
originPort: (row as Record<string, unknown>).originPort ?? 0,
|
|
973
|
+
tunnelPort: (row as Record<string, unknown>).tunnelPort ?? 0,
|
|
974
|
+
access:
|
|
975
|
+
(row as Record<string, unknown>).access ??
|
|
976
|
+
({
|
|
977
|
+
mode: "none",
|
|
978
|
+
protectOrigin: false,
|
|
979
|
+
allowlistPaths: [],
|
|
980
|
+
} as AccessState),
|
|
981
|
+
stats:
|
|
982
|
+
(row as Record<string, unknown>).stats ??
|
|
983
|
+
({
|
|
984
|
+
requests: 0,
|
|
985
|
+
downloads: 0,
|
|
986
|
+
bytesSent: 0,
|
|
987
|
+
} as ExposureStats),
|
|
988
|
+
});
|
|
989
|
+
if (record) {
|
|
990
|
+
out.set(record.id, record);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
} catch {
|
|
994
|
+
// ignore missing legacy snapshot
|
|
995
|
+
}
|
|
996
|
+
return out;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private async writePersistedSessionRecord(record: PersistedSessionRecord): Promise<void> {
|
|
1000
|
+
const target = this.sessionRecordPath(record.id);
|
|
1001
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
1002
|
+
await fs.writeFile(tmp, JSON.stringify(record, null, 2), "utf8");
|
|
1003
|
+
await fs.rename(tmp, target);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
private async deletePersistedSessionRecord(id: string): Promise<void> {
|
|
1007
|
+
await fs.rm(this.sessionRecordPath(id), { force: true });
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
private isProcessAlive(pid: number | undefined): boolean {
|
|
1011
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
process.kill(pid, 0);
|
|
1016
|
+
return true;
|
|
1017
|
+
} catch {
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
private async persistSessionsSnapshot(): Promise<void> {
|
|
1023
|
+
const activeIds = new Set<string>();
|
|
1024
|
+
const records = Array.from(this.sessions.values()).map((session) => {
|
|
1025
|
+
const record = this.toPersistedSessionRecord(session);
|
|
1026
|
+
activeIds.add(record.id);
|
|
1027
|
+
return record;
|
|
1028
|
+
});
|
|
1029
|
+
await Promise.all(records.map((record) => this.writePersistedSessionRecord(record)));
|
|
1030
|
+
|
|
1031
|
+
const persisted = await this.readPersistedSessions();
|
|
1032
|
+
for (const record of persisted.values()) {
|
|
1033
|
+
if (record.ownerPid !== process.pid || activeIds.has(record.id)) {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
await this.deletePersistedSessionRecord(record.id);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const mergedRows = Array.from((await this.readPersistedSessions()).values())
|
|
1040
|
+
.map((record) => ({
|
|
1041
|
+
id: record.id,
|
|
1042
|
+
type: record.type,
|
|
1043
|
+
status: record.status,
|
|
1044
|
+
expiresAt: record.expiresAt,
|
|
1045
|
+
workspaceDir: record.workspaceDir,
|
|
1046
|
+
processPid: record.processPid,
|
|
1047
|
+
ownerPid: record.ownerPid,
|
|
1048
|
+
publicUrl: record.publicUrl,
|
|
1049
|
+
localUrl: record.localUrl,
|
|
1050
|
+
}))
|
|
1051
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1052
|
+
await fs.writeFile(this.sessionsFile, JSON.stringify(mergedRows, null, 2), "utf8");
|
|
816
1053
|
}
|
|
817
1054
|
|
|
818
1055
|
private makeAccessState(params: {
|
|
@@ -845,24 +1082,27 @@ export class CfshareManager {
|
|
|
845
1082
|
};
|
|
846
1083
|
}
|
|
847
1084
|
|
|
848
|
-
private
|
|
849
|
-
const base = session.publicUrl;
|
|
1085
|
+
private makeResponsePublicUrlFromValues(base: string | undefined, access: AccessState): string | undefined {
|
|
850
1086
|
if (!base) {
|
|
851
1087
|
return undefined;
|
|
852
1088
|
}
|
|
853
|
-
if (
|
|
1089
|
+
if (access.mode !== "token" || !access.token) {
|
|
854
1090
|
return base;
|
|
855
1091
|
}
|
|
856
1092
|
try {
|
|
857
1093
|
const out = new URL(base);
|
|
858
|
-
out.searchParams.set("token",
|
|
1094
|
+
out.searchParams.set("token", access.token);
|
|
859
1095
|
return out.toString();
|
|
860
1096
|
} catch {
|
|
861
1097
|
const sep = base.includes("?") ? "&" : "?";
|
|
862
|
-
return `${base}${sep}token=${encodeURIComponent(
|
|
1098
|
+
return `${base}${sep}token=${encodeURIComponent(access.token)}`;
|
|
863
1099
|
}
|
|
864
1100
|
}
|
|
865
1101
|
|
|
1102
|
+
private makeResponsePublicUrl(session: Pick<ExposureSession, "publicUrl" | "access">): string | undefined {
|
|
1103
|
+
return this.makeResponsePublicUrlFromValues(session.publicUrl, session.access);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
866
1106
|
private buildRateLimiter(policy: RateLimitPolicy): (ip: string) => boolean {
|
|
867
1107
|
if (!policy.enabled) {
|
|
868
1108
|
return () => true;
|
|
@@ -2086,8 +2326,27 @@ export class CfshareManager {
|
|
|
2086
2326
|
}
|
|
2087
2327
|
}
|
|
2088
2328
|
|
|
2089
|
-
exposureList(): ExposureRecord[] {
|
|
2090
|
-
|
|
2329
|
+
async exposureList(): Promise<ExposureRecord[]> {
|
|
2330
|
+
await this.ensureInitialized();
|
|
2331
|
+
const persisted = await this.readPersistedSessions();
|
|
2332
|
+
const out = new Map<string, ExposureRecord>();
|
|
2333
|
+
for (const session of this.sessions.values()) {
|
|
2334
|
+
out.set(session.id, this.makeExposureRecord(session));
|
|
2335
|
+
}
|
|
2336
|
+
for (const record of persisted.values()) {
|
|
2337
|
+
if (out.has(record.id)) {
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
out.set(record.id, {
|
|
2341
|
+
id: record.id,
|
|
2342
|
+
type: record.type,
|
|
2343
|
+
status: record.status,
|
|
2344
|
+
public_url: this.makeResponsePublicUrlFromValues(record.publicUrl, record.access),
|
|
2345
|
+
local_url: record.localUrl,
|
|
2346
|
+
expires_at: record.expiresAt,
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
return Array.from(out.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
2091
2350
|
}
|
|
2092
2351
|
|
|
2093
2352
|
private normalizeRequestedIds(rawIds: string[]): string[] {
|
|
@@ -2107,36 +2366,81 @@ export class CfshareManager {
|
|
|
2107
2366
|
return out;
|
|
2108
2367
|
}
|
|
2109
2368
|
|
|
2110
|
-
private
|
|
2369
|
+
private matchesExposureFilterByValues(
|
|
2370
|
+
type: ExposureType,
|
|
2371
|
+
status: ExposureStatus,
|
|
2372
|
+
filter?: ExposureFilter,
|
|
2373
|
+
): boolean {
|
|
2111
2374
|
if (!filter) {
|
|
2112
2375
|
return true;
|
|
2113
2376
|
}
|
|
2114
|
-
if (filter.status &&
|
|
2377
|
+
if (filter.status && status !== filter.status) {
|
|
2115
2378
|
return false;
|
|
2116
2379
|
}
|
|
2117
|
-
if (filter.type &&
|
|
2380
|
+
if (filter.type && type !== filter.type) {
|
|
2118
2381
|
return false;
|
|
2119
2382
|
}
|
|
2120
2383
|
return true;
|
|
2121
2384
|
}
|
|
2122
2385
|
|
|
2386
|
+
private matchesExposureFilter(session: ExposureSession, filter?: ExposureFilter): boolean {
|
|
2387
|
+
return this.matchesExposureFilterByValues(session.type, session.status, filter);
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
private async loadSessionLookupMap(): Promise<
|
|
2391
|
+
Map<string, { live?: ExposureSession; persisted?: PersistedSessionRecord }>
|
|
2392
|
+
> {
|
|
2393
|
+
const out = new Map<string, { live?: ExposureSession; persisted?: PersistedSessionRecord }>();
|
|
2394
|
+
for (const session of this.sessions.values()) {
|
|
2395
|
+
out.set(session.id, { live: session });
|
|
2396
|
+
}
|
|
2397
|
+
const persisted = await this.readPersistedSessions();
|
|
2398
|
+
for (const [id, record] of persisted.entries()) {
|
|
2399
|
+
const current = out.get(id);
|
|
2400
|
+
if (current) {
|
|
2401
|
+
current.persisted = record;
|
|
2402
|
+
} else {
|
|
2403
|
+
out.set(id, { persisted: record });
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
return out;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2123
2409
|
private resolveExposureSelection(query: {
|
|
2124
2410
|
id?: string;
|
|
2125
2411
|
ids?: string[];
|
|
2126
2412
|
filter?: ExposureFilter;
|
|
2127
|
-
}
|
|
2413
|
+
}, lookupMap: Map<string, { live?: ExposureSession; persisted?: PersistedSessionRecord }>): {
|
|
2414
|
+
selectorUsed: boolean;
|
|
2415
|
+
selectedIds: string[];
|
|
2416
|
+
missingIds: string[];
|
|
2417
|
+
} {
|
|
2128
2418
|
const explicitIds = this.normalizeRequestedIds([
|
|
2129
2419
|
...(typeof query.id === "string" ? [query.id] : []),
|
|
2130
2420
|
...((query.ids ?? []).filter((value): value is string => typeof value === "string")),
|
|
2131
2421
|
]);
|
|
2132
2422
|
|
|
2133
2423
|
const hasAll = explicitIds.includes("all");
|
|
2134
|
-
const
|
|
2424
|
+
const allIds = Array.from(lookupMap.keys());
|
|
2135
2425
|
|
|
2136
2426
|
if (hasAll) {
|
|
2137
|
-
const selectedIds =
|
|
2138
|
-
|
|
2139
|
-
|
|
2427
|
+
const selectedIds = allIds.filter((id) => {
|
|
2428
|
+
const lookup = lookupMap.get(id);
|
|
2429
|
+
if (!lookup) {
|
|
2430
|
+
return false;
|
|
2431
|
+
}
|
|
2432
|
+
if (lookup.live) {
|
|
2433
|
+
return this.matchesExposureFilter(lookup.live, query.filter);
|
|
2434
|
+
}
|
|
2435
|
+
if (lookup.persisted) {
|
|
2436
|
+
return this.matchesExposureFilterByValues(
|
|
2437
|
+
lookup.persisted.type,
|
|
2438
|
+
lookup.persisted.status,
|
|
2439
|
+
query.filter,
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
return false;
|
|
2443
|
+
});
|
|
2140
2444
|
return { selectorUsed: true, selectedIds, missingIds: [] };
|
|
2141
2445
|
}
|
|
2142
2446
|
|
|
@@ -2144,12 +2448,19 @@ export class CfshareManager {
|
|
|
2144
2448
|
const selectedIds: string[] = [];
|
|
2145
2449
|
const missingIds: string[] = [];
|
|
2146
2450
|
for (const id of explicitIds) {
|
|
2147
|
-
const
|
|
2148
|
-
if (!
|
|
2451
|
+
const lookup = lookupMap.get(id);
|
|
2452
|
+
if (!lookup) {
|
|
2149
2453
|
missingIds.push(id);
|
|
2150
2454
|
continue;
|
|
2151
2455
|
}
|
|
2152
|
-
if (this.matchesExposureFilter(
|
|
2456
|
+
if (lookup.live && this.matchesExposureFilter(lookup.live, query.filter)) {
|
|
2457
|
+
selectedIds.push(id);
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
if (
|
|
2461
|
+
lookup.persisted &&
|
|
2462
|
+
this.matchesExposureFilterByValues(lookup.persisted.type, lookup.persisted.status, query.filter)
|
|
2463
|
+
) {
|
|
2153
2464
|
selectedIds.push(id);
|
|
2154
2465
|
}
|
|
2155
2466
|
}
|
|
@@ -2157,9 +2468,23 @@ export class CfshareManager {
|
|
|
2157
2468
|
}
|
|
2158
2469
|
|
|
2159
2470
|
if (query.filter) {
|
|
2160
|
-
const selectedIds =
|
|
2161
|
-
|
|
2162
|
-
|
|
2471
|
+
const selectedIds = allIds.filter((id) => {
|
|
2472
|
+
const lookup = lookupMap.get(id);
|
|
2473
|
+
if (!lookup) {
|
|
2474
|
+
return false;
|
|
2475
|
+
}
|
|
2476
|
+
if (lookup.live) {
|
|
2477
|
+
return this.matchesExposureFilter(lookup.live, query.filter);
|
|
2478
|
+
}
|
|
2479
|
+
if (lookup.persisted) {
|
|
2480
|
+
return this.matchesExposureFilterByValues(
|
|
2481
|
+
lookup.persisted.type,
|
|
2482
|
+
lookup.persisted.status,
|
|
2483
|
+
query.filter,
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
return false;
|
|
2487
|
+
});
|
|
2163
2488
|
return { selectorUsed: true, selectedIds, missingIds: [] };
|
|
2164
2489
|
}
|
|
2165
2490
|
|
|
@@ -2180,34 +2505,9 @@ export class CfshareManager {
|
|
|
2180
2505
|
? await probeLocalPort(session.sourcePort ?? session.originPort)
|
|
2181
2506
|
: Boolean(session.originServer?.listening);
|
|
2182
2507
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
const probeUrl = new URL(session.publicUrl);
|
|
2187
|
-
if (session.access.mode === "token" && session.access.token) {
|
|
2188
|
-
probeUrl.searchParams.set("token", session.access.token);
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
const controller = new AbortController();
|
|
2192
|
-
const timer = setTimeout(() => controller.abort(), DEFAULT_PROBE_TIMEOUT_MS);
|
|
2193
|
-
const headers: Record<string, string> = {};
|
|
2194
|
-
if (session.access.mode === "basic" && session.access.username && session.access.password) {
|
|
2195
|
-
headers.authorization = `Basic ${Buffer.from(
|
|
2196
|
-
`${session.access.username}:${session.access.password}`,
|
|
2197
|
-
).toString("base64")}`;
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
const response = await fetch(probeUrl.toString(), {
|
|
2201
|
-
method: "HEAD",
|
|
2202
|
-
signal: controller.signal,
|
|
2203
|
-
headers,
|
|
2204
|
-
});
|
|
2205
|
-
clearTimeout(timer);
|
|
2206
|
-
publicProbe = { ok: response.ok, status: response.status };
|
|
2207
|
-
} catch (error) {
|
|
2208
|
-
publicProbe = { ok: false, error: String(error) };
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2508
|
+
const publicProbe = opts?.probe_public
|
|
2509
|
+
? await this.probePublicEndpoint(session.publicUrl, session.access)
|
|
2510
|
+
: undefined;
|
|
2211
2511
|
|
|
2212
2512
|
const fileSharing =
|
|
2213
2513
|
session.type === "files"
|
|
@@ -2251,6 +2551,98 @@ export class CfshareManager {
|
|
|
2251
2551
|
return detail;
|
|
2252
2552
|
}
|
|
2253
2553
|
|
|
2554
|
+
private async probePublicEndpoint(
|
|
2555
|
+
publicUrl: string | undefined,
|
|
2556
|
+
access: AccessState,
|
|
2557
|
+
): Promise<{ ok: boolean; status?: number; error?: string } | undefined> {
|
|
2558
|
+
if (!publicUrl) {
|
|
2559
|
+
return undefined;
|
|
2560
|
+
}
|
|
2561
|
+
try {
|
|
2562
|
+
const probeUrl = new URL(publicUrl);
|
|
2563
|
+
if (access.mode === "token" && access.token) {
|
|
2564
|
+
probeUrl.searchParams.set("token", access.token);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
const controller = new AbortController();
|
|
2568
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_PROBE_TIMEOUT_MS);
|
|
2569
|
+
const headers: Record<string, string> = {};
|
|
2570
|
+
if (access.mode === "basic" && access.username && access.password) {
|
|
2571
|
+
headers.authorization = `Basic ${Buffer.from(
|
|
2572
|
+
`${access.username}:${access.password}`,
|
|
2573
|
+
).toString("base64")}`;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
const response = await fetch(probeUrl.toString(), {
|
|
2577
|
+
method: "HEAD",
|
|
2578
|
+
signal: controller.signal,
|
|
2579
|
+
headers,
|
|
2580
|
+
});
|
|
2581
|
+
clearTimeout(timer);
|
|
2582
|
+
return { ok: response.ok, status: response.status };
|
|
2583
|
+
} catch (error) {
|
|
2584
|
+
return { ok: false, error: String(error) };
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
private async buildPersistedExposureDetail(
|
|
2589
|
+
record: PersistedSessionRecord,
|
|
2590
|
+
opts?: {
|
|
2591
|
+
probe_public?: boolean;
|
|
2592
|
+
include_manifest?: boolean;
|
|
2593
|
+
manifest_limit?: number;
|
|
2594
|
+
},
|
|
2595
|
+
): Promise<Record<string, unknown>> {
|
|
2596
|
+
const tunnelAlive = this.isProcessAlive(record.processPid);
|
|
2597
|
+
const probePort =
|
|
2598
|
+
record.type === "port" ? (record.sourcePort ?? record.originPort) : record.originPort;
|
|
2599
|
+
const originAlive = probePort > 0 ? await probeLocalPort(probePort) : false;
|
|
2600
|
+
const publicProbe = opts?.probe_public
|
|
2601
|
+
? await this.probePublicEndpoint(record.publicUrl, record.access)
|
|
2602
|
+
: undefined;
|
|
2603
|
+
|
|
2604
|
+
const fileSharing =
|
|
2605
|
+
record.type === "files"
|
|
2606
|
+
? {
|
|
2607
|
+
mode: record.fileMode ?? "normal",
|
|
2608
|
+
presentation: record.filePresentation ?? "download",
|
|
2609
|
+
}
|
|
2610
|
+
: undefined;
|
|
2611
|
+
const includeManifest = Boolean(opts?.include_manifest);
|
|
2612
|
+
const manifestBundle =
|
|
2613
|
+
record.type === "files" ? this.makeManifestResponse(record.manifest, opts?.manifest_limit) : undefined;
|
|
2614
|
+
|
|
2615
|
+
const detail: Record<string, unknown> = {
|
|
2616
|
+
id: record.id,
|
|
2617
|
+
type: record.type,
|
|
2618
|
+
created_at: record.createdAt,
|
|
2619
|
+
status: {
|
|
2620
|
+
state: record.status,
|
|
2621
|
+
tunnel_alive: tunnelAlive,
|
|
2622
|
+
origin_alive: originAlive,
|
|
2623
|
+
public_probe: publicProbe,
|
|
2624
|
+
},
|
|
2625
|
+
port: {
|
|
2626
|
+
source_port: record.sourcePort,
|
|
2627
|
+
origin_port: record.originPort,
|
|
2628
|
+
tunnel_port: record.tunnelPort,
|
|
2629
|
+
},
|
|
2630
|
+
public_url: this.makeResponsePublicUrlFromValues(record.publicUrl, record.access),
|
|
2631
|
+
expires_at: record.expiresAt,
|
|
2632
|
+
local_url: record.localUrl,
|
|
2633
|
+
stats: record.stats,
|
|
2634
|
+
file_sharing: fileSharing,
|
|
2635
|
+
last_error: record.lastError,
|
|
2636
|
+
};
|
|
2637
|
+
if (manifestBundle) {
|
|
2638
|
+
detail.manifest_meta = manifestBundle.manifest_meta;
|
|
2639
|
+
if (includeManifest) {
|
|
2640
|
+
detail.manifest = manifestBundle.manifest;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return detail;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2254
2646
|
private projectExposureDetail(
|
|
2255
2647
|
detail: Record<string, unknown>,
|
|
2256
2648
|
fields?: ExposureGetField[],
|
|
@@ -2283,8 +2675,47 @@ export class CfshareManager {
|
|
|
2283
2675
|
return out;
|
|
2284
2676
|
}
|
|
2285
2677
|
|
|
2678
|
+
private async stopPersistedSession(
|
|
2679
|
+
record: PersistedSessionRecord,
|
|
2680
|
+
opts: { reason?: string; expired?: boolean } | undefined,
|
|
2681
|
+
cleaned: string[],
|
|
2682
|
+
): Promise<void> {
|
|
2683
|
+
const uniquePids = Array.from(
|
|
2684
|
+
new Set([record.ownerPid, record.processPid].filter((pid): pid is number => Boolean(pid && pid > 0))),
|
|
2685
|
+
);
|
|
2686
|
+
for (const pid of uniquePids) {
|
|
2687
|
+
try {
|
|
2688
|
+
process.kill(pid, 0);
|
|
2689
|
+
process.kill(pid, "SIGTERM");
|
|
2690
|
+
} catch (error) {
|
|
2691
|
+
const errno = error as NodeJS.ErrnoException;
|
|
2692
|
+
if (errno?.code !== "ESRCH") {
|
|
2693
|
+
throw error;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
if (record.workspaceDir && (await fileExists(record.workspaceDir))) {
|
|
2699
|
+
await fs.rm(record.workspaceDir, { recursive: true, force: true });
|
|
2700
|
+
cleaned.push(record.workspaceDir);
|
|
2701
|
+
}
|
|
2702
|
+
await this.deletePersistedSessionRecord(record.id);
|
|
2703
|
+
|
|
2704
|
+
await this.writeAudit({
|
|
2705
|
+
ts: nowIso(),
|
|
2706
|
+
event: opts?.expired ? "exposure_expired" : "exposure_stopped",
|
|
2707
|
+
id: record.id,
|
|
2708
|
+
type: record.type,
|
|
2709
|
+
details: {
|
|
2710
|
+
reason: opts?.reason,
|
|
2711
|
+
public_url: record.publicUrl,
|
|
2712
|
+
},
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2286
2716
|
async exposureGet(params: ExposureGetParams): Promise<Record<string, unknown>> {
|
|
2287
2717
|
await this.ensureInitialized();
|
|
2718
|
+
const lookupMap = await this.loadSessionLookupMap();
|
|
2288
2719
|
|
|
2289
2720
|
const fields = Array.isArray(params.fields) ? this.normalizeRequestedIds(params.fields) : undefined;
|
|
2290
2721
|
const typedFields = fields as ExposureGetField[] | undefined;
|
|
@@ -2295,7 +2726,7 @@ export class CfshareManager {
|
|
|
2295
2726
|
id: params.id,
|
|
2296
2727
|
ids: params.ids,
|
|
2297
2728
|
filter: params.filter,
|
|
2298
|
-
});
|
|
2729
|
+
}, lookupMap);
|
|
2299
2730
|
|
|
2300
2731
|
if (!selection.selectorUsed) {
|
|
2301
2732
|
throw new Error("id, ids, or filter is required");
|
|
@@ -2303,11 +2734,18 @@ export class CfshareManager {
|
|
|
2303
2734
|
|
|
2304
2735
|
if (legacySingle) {
|
|
2305
2736
|
const legacyId = params.id as string;
|
|
2306
|
-
const
|
|
2307
|
-
if (!
|
|
2737
|
+
const lookup = lookupMap.get(legacyId);
|
|
2738
|
+
if (!lookup) {
|
|
2308
2739
|
return { id: legacyId, status: "not_found" };
|
|
2309
2740
|
}
|
|
2310
|
-
|
|
2741
|
+
if (lookup.live) {
|
|
2742
|
+
return await this.buildExposureDetail(lookup.live, {
|
|
2743
|
+
probe_public: params.opts?.probe_public,
|
|
2744
|
+
include_manifest: true,
|
|
2745
|
+
manifest_limit: MAX_RESPONSE_MANIFEST_ITEMS,
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
return await this.buildPersistedExposureDetail(lookup.persisted as PersistedSessionRecord, {
|
|
2311
2749
|
probe_public: params.opts?.probe_public,
|
|
2312
2750
|
include_manifest: true,
|
|
2313
2751
|
manifest_limit: MAX_RESPONSE_MANIFEST_ITEMS,
|
|
@@ -2323,16 +2761,22 @@ export class CfshareManager {
|
|
|
2323
2761
|
const selectedIdsTruncated = selection.selectedIds.length > responseSelectedIds.length;
|
|
2324
2762
|
const items: Record<string, unknown>[] = [];
|
|
2325
2763
|
for (const id of responseSelectedIds) {
|
|
2326
|
-
const
|
|
2327
|
-
if (!
|
|
2764
|
+
const lookup = lookupMap.get(id);
|
|
2765
|
+
if (!lookup) {
|
|
2328
2766
|
items.push(this.makeExposureGetNotFound(id, typedFields));
|
|
2329
2767
|
continue;
|
|
2330
2768
|
}
|
|
2331
|
-
const detail =
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2769
|
+
const detail = lookup.live
|
|
2770
|
+
? await this.buildExposureDetail(lookup.live, {
|
|
2771
|
+
probe_public: params.opts?.probe_public,
|
|
2772
|
+
include_manifest: manifestRequested,
|
|
2773
|
+
manifest_limit: manifestLimit,
|
|
2774
|
+
})
|
|
2775
|
+
: await this.buildPersistedExposureDetail(lookup.persisted as PersistedSessionRecord, {
|
|
2776
|
+
probe_public: params.opts?.probe_public,
|
|
2777
|
+
include_manifest: manifestRequested,
|
|
2778
|
+
manifest_limit: manifestLimit,
|
|
2779
|
+
});
|
|
2336
2780
|
items.push(this.projectExposureDetail(detail, typedFields));
|
|
2337
2781
|
}
|
|
2338
2782
|
for (const missingId of selection.missingIds) {
|
|
@@ -2356,6 +2800,7 @@ export class CfshareManager {
|
|
|
2356
2800
|
opts?: { reason?: string; expired?: boolean; keepAudit?: boolean },
|
|
2357
2801
|
): Promise<Record<string, unknown>> {
|
|
2358
2802
|
await this.ensureInitialized();
|
|
2803
|
+
const lookupMap = await this.loadSessionLookupMap();
|
|
2359
2804
|
|
|
2360
2805
|
const requested = this.normalizeRequestedIds(Array.isArray(idOrIds) ? idOrIds : [idOrIds]);
|
|
2361
2806
|
if (requested.length === 0) {
|
|
@@ -2367,13 +2812,13 @@ export class CfshareManager {
|
|
|
2367
2812
|
const failed: Array<{ id: string; error: string }> = [];
|
|
2368
2813
|
|
|
2369
2814
|
if (includeAll) {
|
|
2370
|
-
ids.push(...Array.from(
|
|
2815
|
+
ids.push(...Array.from(lookupMap.keys()));
|
|
2371
2816
|
if (ids.length === 0) {
|
|
2372
2817
|
return { stopped: [], failed: [{ id: "all", error: "not_found" }], cleaned: [] };
|
|
2373
2818
|
}
|
|
2374
2819
|
} else {
|
|
2375
2820
|
for (const id of requested) {
|
|
2376
|
-
if (
|
|
2821
|
+
if (lookupMap.has(id)) {
|
|
2377
2822
|
ids.push(id);
|
|
2378
2823
|
} else {
|
|
2379
2824
|
failed.push({ id, error: "not_found" });
|
|
@@ -2390,48 +2835,56 @@ export class CfshareManager {
|
|
|
2390
2835
|
const cleaned: string[] = [];
|
|
2391
2836
|
|
|
2392
2837
|
for (const id of stopIds) {
|
|
2393
|
-
const
|
|
2394
|
-
if (!
|
|
2838
|
+
const lookup = lookupMap.get(id);
|
|
2839
|
+
if (!lookup) {
|
|
2395
2840
|
failed.push({ id, error: "not_found" });
|
|
2396
2841
|
continue;
|
|
2397
2842
|
}
|
|
2843
|
+
const session = lookup.live;
|
|
2398
2844
|
try {
|
|
2399
|
-
if (session
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2845
|
+
if (session) {
|
|
2846
|
+
if (session.timeoutHandle) {
|
|
2847
|
+
clearTimeout(session.timeoutHandle);
|
|
2848
|
+
session.timeoutHandle = undefined;
|
|
2849
|
+
}
|
|
2403
2850
|
|
|
2404
|
-
|
|
2851
|
+
await this.terminateProcess(session.process);
|
|
2405
2852
|
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2853
|
+
if (session.proxyServer?.listening) {
|
|
2854
|
+
await new Promise<void>((resolve) => session.proxyServer?.close(() => resolve()));
|
|
2855
|
+
}
|
|
2856
|
+
if (session.originServer?.listening) {
|
|
2857
|
+
await new Promise<void>((resolve) => session.originServer?.close(() => resolve()));
|
|
2858
|
+
}
|
|
2412
2859
|
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2860
|
+
if (session.workspaceDir && (await fileExists(session.workspaceDir))) {
|
|
2861
|
+
await fs.rm(session.workspaceDir, { recursive: true, force: true });
|
|
2862
|
+
cleaned.push(session.workspaceDir);
|
|
2863
|
+
}
|
|
2417
2864
|
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2865
|
+
session.status = opts?.expired ? "expired" : "stopped";
|
|
2866
|
+
if (opts?.reason) {
|
|
2867
|
+
session.lastError = opts.reason;
|
|
2868
|
+
this.appendLog(session, "manager", `stop reason: ${opts.reason}`);
|
|
2869
|
+
}
|
|
2870
|
+
stopped.push(id);
|
|
2871
|
+
|
|
2872
|
+
await this.writeAudit({
|
|
2873
|
+
ts: nowIso(),
|
|
2874
|
+
event: opts?.expired ? "exposure_expired" : "exposure_stopped",
|
|
2875
|
+
id: session.id,
|
|
2876
|
+
type: session.type,
|
|
2877
|
+
details: {
|
|
2878
|
+
reason: opts?.reason,
|
|
2879
|
+
public_url: session.publicUrl,
|
|
2880
|
+
},
|
|
2881
|
+
});
|
|
2882
|
+
} else if (lookup.persisted) {
|
|
2883
|
+
await this.stopPersistedSession(lookup.persisted, opts, cleaned);
|
|
2884
|
+
stopped.push(id);
|
|
2885
|
+
} else {
|
|
2886
|
+
failed.push({ id, error: "not_found" });
|
|
2422
2887
|
}
|
|
2423
|
-
stopped.push(id);
|
|
2424
|
-
|
|
2425
|
-
await this.writeAudit({
|
|
2426
|
-
ts: nowIso(),
|
|
2427
|
-
event: opts?.expired ? "exposure_expired" : "exposure_stopped",
|
|
2428
|
-
id: session.id,
|
|
2429
|
-
type: session.type,
|
|
2430
|
-
details: {
|
|
2431
|
-
reason: opts?.reason,
|
|
2432
|
-
public_url: session.publicUrl,
|
|
2433
|
-
},
|
|
2434
|
-
});
|
|
2435
2888
|
} catch (error) {
|
|
2436
2889
|
failed.push({ id, error: String(error) });
|
|
2437
2890
|
} finally {
|
|
@@ -2611,11 +3064,13 @@ export class CfshareManager {
|
|
|
2611
3064
|
private async runGc(): Promise<Record<string, unknown>> {
|
|
2612
3065
|
const removedWorkspaces: string[] = [];
|
|
2613
3066
|
const killedPids: number[] = [];
|
|
3067
|
+
const persisted = await this.readPersistedSessions();
|
|
2614
3068
|
|
|
2615
3069
|
const activeWorkspaces = new Set(
|
|
2616
|
-
|
|
2617
|
-
.map((session) => session.workspaceDir)
|
|
2618
|
-
.
|
|
3070
|
+
[
|
|
3071
|
+
...Array.from(this.sessions.values()).map((session) => session.workspaceDir),
|
|
3072
|
+
...Array.from(persisted.values()).map((session) => session.workspaceDir),
|
|
3073
|
+
].filter((entry): entry is string => Boolean(entry)),
|
|
2619
3074
|
);
|
|
2620
3075
|
|
|
2621
3076
|
const workspaces = await fs.readdir(this.workspaceRoot, { withFileTypes: true }).catch(() => []);
|
|
@@ -2631,23 +3086,22 @@ export class CfshareManager {
|
|
|
2631
3086
|
removedWorkspaces.push(abs);
|
|
2632
3087
|
}
|
|
2633
3088
|
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
3089
|
+
for (const record of persisted.values()) {
|
|
3090
|
+
if (this.sessions.has(record.id)) {
|
|
3091
|
+
continue;
|
|
3092
|
+
}
|
|
3093
|
+
const pids = Array.from(
|
|
3094
|
+
new Set([record.ownerPid, record.processPid].filter((pid): pid is number => Boolean(pid && pid > 0))),
|
|
3095
|
+
);
|
|
3096
|
+
for (const pid of pids) {
|
|
2641
3097
|
try {
|
|
2642
|
-
process.kill(
|
|
2643
|
-
process.kill(
|
|
2644
|
-
killedPids.push(
|
|
3098
|
+
process.kill(pid, 0);
|
|
3099
|
+
process.kill(pid, "SIGTERM");
|
|
3100
|
+
killedPids.push(pid);
|
|
2645
3101
|
} catch {
|
|
2646
|
-
// ignore
|
|
3102
|
+
// ignore dead pid
|
|
2647
3103
|
}
|
|
2648
3104
|
}
|
|
2649
|
-
} catch {
|
|
2650
|
-
// ignore missing snapshot
|
|
2651
3105
|
}
|
|
2652
3106
|
|
|
2653
3107
|
await this.persistSessionsSnapshot();
|