@tomflow/proflow-dev-tunnel 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/deployment/adapter.d.ts +1 -0
- package/dist/deployment/adapter.js +2 -0
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/src/resource-adapter.d.ts +1 -0
- package/dist/src/resource-adapter.js +144 -13
- package/package.json +1 -1
- package/proflow.module.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @tomflow/proflow-dev-tunnel
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Make managed dev-tunnel stop idempotent when no owned tunnel process remains, while preserving UNKNOWN for genuinely ambiguous stop failures.
|
|
8
|
+
|
|
9
|
+
## 0.1.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Detach the managed dev-tunnel host from the short-lived Platform CLI process and persist an ownership-checked process record so later status/stop/restart invocations can manage the same tunnel without turning `platform start` into a daemon.
|
|
14
|
+
|
|
3
15
|
## 0.1.3
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -134,6 +134,7 @@ export declare const behaviorAdapter: {
|
|
|
134
134
|
export declare function createProductionBinding(input: {
|
|
135
135
|
moduleRef: string;
|
|
136
136
|
config: Record<string, string>;
|
|
137
|
+
workspaceRoot: string;
|
|
137
138
|
}): Promise<{
|
|
138
139
|
behaviorAdapter: Record<string, unknown>;
|
|
139
140
|
} | undefined>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { verifyPublicIngress } from "../src/resource-adapter.js";
|
|
3
4
|
import { descriptor } from "./descriptor.js";
|
|
4
5
|
const success = (data) => ({
|
|
@@ -290,6 +291,7 @@ export async function createProductionBinding(input) {
|
|
|
290
291
|
runtime: createDevTunnelRuntime({
|
|
291
292
|
publicBaseUrl,
|
|
292
293
|
...(input.config.tunnelId ? { tunnelId: input.config.tunnelId } : {}),
|
|
294
|
+
processStateFile: join(input.workspaceRoot, ".proflow", "runtime", "external-resources", "dev-tunnel", "process.json"),
|
|
293
295
|
}),
|
|
294
296
|
verifyFileRelay: async () => (await readEvidence())?.fileRelay ?? {
|
|
295
297
|
verified: false,
|
|
@@ -3,7 +3,7 @@ export declare const descriptor: {
|
|
|
3
3
|
readonly contractVersion: "1.0.0";
|
|
4
4
|
readonly moduleRef: "dev-tunnel";
|
|
5
5
|
readonly packageName: "@tomflow/proflow-dev-tunnel";
|
|
6
|
-
readonly moduleVersion: "0.1.
|
|
6
|
+
readonly moduleVersion: "0.1.5";
|
|
7
7
|
readonly kind: "external-resource";
|
|
8
8
|
readonly templateVersion: "1.0.0";
|
|
9
9
|
readonly platformCompatibility: ">=1.0.0 <2.0.0";
|
|
@@ -3,7 +3,7 @@ export const descriptor = {
|
|
|
3
3
|
contractVersion: "1.0.0",
|
|
4
4
|
moduleRef: "dev-tunnel",
|
|
5
5
|
packageName: "@tomflow/proflow-dev-tunnel",
|
|
6
|
-
moduleVersion: "0.1.
|
|
6
|
+
moduleVersion: "0.1.5",
|
|
7
7
|
kind: "external-resource",
|
|
8
8
|
templateVersion: "1.0.0",
|
|
9
9
|
platformCompatibility: ">=1.0.0 <2.0.0",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
2
4
|
import { connect } from "node:tls";
|
|
3
5
|
const LOGIN_ARGS = ["user", "show"];
|
|
4
6
|
const LOGIN_TIMEOUT_MS = 10_000;
|
|
@@ -31,10 +33,83 @@ function defaultCommandRunner(command, args, options) {
|
|
|
31
33
|
});
|
|
32
34
|
});
|
|
33
35
|
}
|
|
36
|
+
async function readProcessRecord(file) {
|
|
37
|
+
if (!file)
|
|
38
|
+
return undefined;
|
|
39
|
+
try {
|
|
40
|
+
const raw = JSON.parse(await readFile(file, "utf8"));
|
|
41
|
+
if (raw.contract !== "proflow.dev-tunnel-process.v1" ||
|
|
42
|
+
typeof raw.pid !== "number" ||
|
|
43
|
+
!Number.isInteger(raw.pid) ||
|
|
44
|
+
raw.pid <= 0 ||
|
|
45
|
+
typeof raw.command !== "string" ||
|
|
46
|
+
raw.command.length === 0 ||
|
|
47
|
+
typeof raw.tunnelId !== "string" ||
|
|
48
|
+
raw.tunnelId.length === 0 ||
|
|
49
|
+
typeof raw.startedAt !== "string" ||
|
|
50
|
+
Number.isNaN(Date.parse(raw.startedAt)))
|
|
51
|
+
return undefined;
|
|
52
|
+
return raw;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function writeProcessRecord(file, record) {
|
|
59
|
+
await mkdir(dirname(file), { recursive: true });
|
|
60
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
61
|
+
await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, {
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
mode: 0o600,
|
|
64
|
+
});
|
|
65
|
+
await rename(temporary, file);
|
|
66
|
+
}
|
|
67
|
+
function processAlive(pid) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function processCommandLine(pid) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const command = process.platform === "win32" ? "powershell.exe" : "ps";
|
|
79
|
+
const args = process.platform === "win32"
|
|
80
|
+
? [
|
|
81
|
+
"-NoProfile",
|
|
82
|
+
"-Command",
|
|
83
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
|
|
84
|
+
]
|
|
85
|
+
: ["-p", String(pid), "-o", "command="];
|
|
86
|
+
execFile(command, args, { timeout: 2_000 }, (error, stdout) => {
|
|
87
|
+
resolve(error ? undefined : String(stdout));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async function processRecordIsOwned(record, command, tunnelId) {
|
|
92
|
+
if (record.command !== command ||
|
|
93
|
+
record.tunnelId !== tunnelId ||
|
|
94
|
+
!processAlive(record.pid))
|
|
95
|
+
return false;
|
|
96
|
+
const commandLine = await processCommandLine(record.pid);
|
|
97
|
+
return (commandLine?.includes(command) === true && commandLine.includes(tunnelId));
|
|
98
|
+
}
|
|
99
|
+
async function waitForProcessExit(pid, timeoutMs = 2_000) {
|
|
100
|
+
const deadline = Date.now() + timeoutMs;
|
|
101
|
+
while (Date.now() < deadline) {
|
|
102
|
+
if (!processAlive(pid))
|
|
103
|
+
return true;
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
105
|
+
}
|
|
106
|
+
return !processAlive(pid);
|
|
107
|
+
}
|
|
34
108
|
export function createDevTunnelRuntime(input) {
|
|
35
109
|
const command = input.command ?? "devtunnel";
|
|
36
110
|
const tunnelId = input.tunnelId;
|
|
37
111
|
const publicBaseUrl = input.publicBaseUrl;
|
|
112
|
+
const processStateFile = input.processStateFile;
|
|
38
113
|
const run = input.runCommand ?? defaultCommandRunner;
|
|
39
114
|
let child;
|
|
40
115
|
const observeLogin = async () => {
|
|
@@ -58,12 +133,26 @@ export function createDevTunnelRuntime(input) {
|
|
|
58
133
|
...(publicBaseUrl === undefined ? {} : { publicBaseUrl }),
|
|
59
134
|
};
|
|
60
135
|
};
|
|
136
|
+
const ownedPersistedPid = async () => {
|
|
137
|
+
if (!processStateFile || !tunnelId)
|
|
138
|
+
return undefined;
|
|
139
|
+
const record = await readProcessRecord(processStateFile);
|
|
140
|
+
if (!record)
|
|
141
|
+
return undefined;
|
|
142
|
+
if (await processRecordIsOwned(record, command, tunnelId))
|
|
143
|
+
return record.pid;
|
|
144
|
+
await rm(processStateFile, { force: true });
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
61
147
|
return {
|
|
62
148
|
command,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
149
|
+
async status() {
|
|
150
|
+
if (await ownedPersistedPid())
|
|
151
|
+
return observe("RUNNING");
|
|
152
|
+
if (child?.pid && processAlive(child.pid))
|
|
153
|
+
return observe("RUNNING");
|
|
154
|
+
return observe("UNKNOWN");
|
|
155
|
+
},
|
|
67
156
|
loginStatus: () => observeLogin(),
|
|
68
157
|
publicBaseUrl: () => publicBaseUrl,
|
|
69
158
|
async start() {
|
|
@@ -73,20 +162,24 @@ export function createDevTunnelRuntime(input) {
|
|
|
73
162
|
if (tunnelId === undefined) {
|
|
74
163
|
throw new TypeError("tunnelId is required to host the configured persistent tunnel");
|
|
75
164
|
}
|
|
76
|
-
if (
|
|
165
|
+
if (await ownedPersistedPid())
|
|
77
166
|
return observe("RUNNING");
|
|
78
|
-
child
|
|
167
|
+
if (child?.pid && processAlive(child.pid))
|
|
168
|
+
return observe("RUNNING");
|
|
169
|
+
const spawned = spawn(command, ["host", tunnelId], {
|
|
170
|
+
stdio: "ignore",
|
|
171
|
+
detached: true,
|
|
172
|
+
});
|
|
79
173
|
const confirmed = await new Promise((resolve) => {
|
|
80
174
|
let settled = false;
|
|
81
175
|
const fail = () => {
|
|
82
176
|
if (settled)
|
|
83
177
|
return;
|
|
84
178
|
settled = true;
|
|
85
|
-
child = undefined;
|
|
86
179
|
resolve("FAILED");
|
|
87
180
|
};
|
|
88
|
-
|
|
89
|
-
|
|
181
|
+
spawned.once("error", fail);
|
|
182
|
+
spawned.once("exit", fail);
|
|
90
183
|
setTimeout(() => {
|
|
91
184
|
if (settled)
|
|
92
185
|
return;
|
|
@@ -94,18 +187,56 @@ export function createDevTunnelRuntime(input) {
|
|
|
94
187
|
resolve("RUNNING");
|
|
95
188
|
}, START_CONFIRM_MS);
|
|
96
189
|
});
|
|
97
|
-
if (confirmed === "FAILED") {
|
|
190
|
+
if (confirmed === "FAILED" || spawned.pid === undefined) {
|
|
98
191
|
throw new Error("devtunnel host process failed to start");
|
|
99
192
|
}
|
|
193
|
+
child = spawned;
|
|
194
|
+
spawned.once("exit", () => {
|
|
195
|
+
if (child === spawned)
|
|
196
|
+
child = undefined;
|
|
197
|
+
if (processStateFile)
|
|
198
|
+
void rm(processStateFile, { force: true });
|
|
199
|
+
});
|
|
200
|
+
if (processStateFile) {
|
|
201
|
+
try {
|
|
202
|
+
await writeProcessRecord(processStateFile, {
|
|
203
|
+
contract: "proflow.dev-tunnel-process.v1",
|
|
204
|
+
pid: spawned.pid,
|
|
205
|
+
command,
|
|
206
|
+
tunnelId,
|
|
207
|
+
startedAt: new Date().toISOString(),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
spawned.kill();
|
|
212
|
+
child = undefined;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
spawned.unref();
|
|
100
217
|
return observe("RUNNING");
|
|
101
218
|
},
|
|
102
219
|
async stop() {
|
|
103
|
-
|
|
104
|
-
|
|
220
|
+
const persistedPid = await ownedPersistedPid();
|
|
221
|
+
const pid = persistedPid ?? child?.pid;
|
|
222
|
+
if (!pid || !processAlive(pid)) {
|
|
223
|
+
if (processStateFile)
|
|
224
|
+
await rm(processStateFile, { force: true });
|
|
105
225
|
child = undefined;
|
|
106
226
|
return observe("STOPPED");
|
|
107
227
|
}
|
|
108
|
-
|
|
228
|
+
try {
|
|
229
|
+
process.kill(pid, "SIGTERM");
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return observe("UNKNOWN");
|
|
233
|
+
}
|
|
234
|
+
if (!(await waitForProcessExit(pid)))
|
|
235
|
+
return observe("UNKNOWN");
|
|
236
|
+
if (processStateFile)
|
|
237
|
+
await rm(processStateFile, { force: true });
|
|
238
|
+
child = undefined;
|
|
239
|
+
return observe("STOPPED");
|
|
109
240
|
},
|
|
110
241
|
async restart() {
|
|
111
242
|
const stopped = await this.stop();
|
package/package.json
CHANGED
package/proflow.module.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"contractVersion": "1.0.0",
|
|
4
4
|
"moduleRef": "dev-tunnel",
|
|
5
5
|
"packageName": "@tomflow/proflow-dev-tunnel",
|
|
6
|
-
"moduleVersion": "0.1.
|
|
6
|
+
"moduleVersion": "0.1.5",
|
|
7
7
|
"kind": "external-resource",
|
|
8
8
|
"templateVersion": "1.0.0",
|
|
9
9
|
"platformCompatibility": ">=1.0.0 <2.0.0",
|