@yydb/yyds 0.0.13 → 0.0.15
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/bin/yyds.mjs +52 -15
- package/package.json +12 -9
- package/readme.md +5 -4
- package/src/bindings.ts +11 -1
- package/src/node/runtime.ts +31 -6
- package/src/node/webui-cmd.ts +137 -0
- package/src/node/webui-static.test.ts +12 -0
- package/src/node/webui-static.ts +137 -0
package/bin/yyds.mjs
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { loadYydsNative } from "../src/node/load.ts";
|
|
3
3
|
import { parseStartOptions, runNodeStart } from "../src/node/runtime.ts";
|
|
4
|
+
import { parseWebuiArgs, runWebui } from "../src/node/webui-cmd.ts";
|
|
4
5
|
|
|
5
6
|
function usage() {
|
|
6
|
-
return "usage: yyds <init|start|status|stop>
|
|
7
|
+
return "usage: yyds <init|start|status|stop|webui> ...";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function startUsage() {
|
|
11
|
+
return "usage: yyds start --data-dir DIR --cluster-id ID --node-id ID [--redis-port PORT] [--pgsql-port PORT] [--mysql-port PORT] [--mongo-port PORT] [--wire-port PORT]";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function webuiUsage() {
|
|
15
|
+
return "usage: yyds webui [--endpoint URL | --data-dir DIR --cluster-id ID --node-id ID [--wire-port PORT]] [--port NUMBER] [--no-open]";
|
|
7
16
|
}
|
|
8
17
|
|
|
9
18
|
function parseCommonArgs(command, args) {
|
|
10
|
-
const options = {
|
|
19
|
+
const options = {
|
|
20
|
+
dataDir: "",
|
|
21
|
+
clusterId: "",
|
|
22
|
+
nodeId: "",
|
|
23
|
+
redisPort: 6379,
|
|
24
|
+
pgsqlPort: undefined,
|
|
25
|
+
mysqlPort: undefined,
|
|
26
|
+
mongoPort: undefined,
|
|
27
|
+
wirePort: undefined,
|
|
28
|
+
};
|
|
11
29
|
for (let index = 0; index < args.length; index += 1) {
|
|
12
30
|
const flag = args[index];
|
|
13
31
|
const value = args[index + 1];
|
|
@@ -26,22 +44,32 @@ function parseCommonArgs(command, args) {
|
|
|
26
44
|
index += 1;
|
|
27
45
|
break;
|
|
28
46
|
case "--redis-port":
|
|
29
|
-
if (command !== "start")
|
|
47
|
+
if (command !== "start")
|
|
48
|
+
throw new Error(`option ${flag} is not valid for this command`);
|
|
30
49
|
options.redisPort = Number(value);
|
|
31
50
|
index += 1;
|
|
32
51
|
break;
|
|
33
52
|
case "--pgsql-port":
|
|
34
|
-
if (command !== "start")
|
|
53
|
+
if (command !== "start")
|
|
54
|
+
throw new Error(`option ${flag} is not valid for this command`);
|
|
35
55
|
options.pgsqlPort = Number(value);
|
|
36
56
|
index += 1;
|
|
37
57
|
break;
|
|
38
58
|
case "--mysql-port":
|
|
39
|
-
if (command !== "start")
|
|
59
|
+
if (command !== "start")
|
|
60
|
+
throw new Error(`option ${flag} is not valid for this command`);
|
|
40
61
|
options.mysqlPort = Number(value);
|
|
41
62
|
index += 1;
|
|
42
63
|
break;
|
|
64
|
+
case "--mongo-port":
|
|
65
|
+
if (command !== "start")
|
|
66
|
+
throw new Error(`option ${flag} is not valid for this command`);
|
|
67
|
+
options.mongoPort = Number(value);
|
|
68
|
+
index += 1;
|
|
69
|
+
break;
|
|
43
70
|
case "--wire-port":
|
|
44
|
-
if (command !== "start")
|
|
71
|
+
if (command !== "start")
|
|
72
|
+
throw new Error(`option ${flag} is not valid for this command`);
|
|
45
73
|
options.wirePort = Number(value);
|
|
46
74
|
index += 1;
|
|
47
75
|
break;
|
|
@@ -51,7 +79,7 @@ function parseCommonArgs(command, args) {
|
|
|
51
79
|
}
|
|
52
80
|
if (!options.dataDir) throw new Error(usage());
|
|
53
81
|
if ((command === "init" || command === "start") && (!options.clusterId || !options.nodeId)) {
|
|
54
|
-
throw new Error(
|
|
82
|
+
throw new Error(startUsage());
|
|
55
83
|
}
|
|
56
84
|
return options;
|
|
57
85
|
}
|
|
@@ -59,6 +87,12 @@ function parseCommonArgs(command, args) {
|
|
|
59
87
|
async function main(args) {
|
|
60
88
|
const command = args[0];
|
|
61
89
|
const rest = args.slice(1);
|
|
90
|
+
|
|
91
|
+
if (command === "webui") {
|
|
92
|
+
await runWebui(parseWebuiArgs(rest));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
62
96
|
const native = loadYydsNative();
|
|
63
97
|
const options = parseCommonArgs(command, rest);
|
|
64
98
|
|
|
@@ -77,18 +111,21 @@ async function main(args) {
|
|
|
77
111
|
redisPort: options.redisPort,
|
|
78
112
|
pgsqlPort: options.pgsqlPort,
|
|
79
113
|
mysqlPort: options.mysqlPort,
|
|
114
|
+
mongoPort: options.mongoPort,
|
|
80
115
|
wirePort: options.wirePort,
|
|
81
116
|
});
|
|
82
117
|
return;
|
|
83
118
|
case "status": {
|
|
84
119
|
const snapshot = native.nodeInspect(options.dataDir);
|
|
85
|
-
console.log(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
120
|
+
console.log(
|
|
121
|
+
JSON.stringify({
|
|
122
|
+
directory: snapshot.directory,
|
|
123
|
+
identity: JSON.parse(snapshot.identity),
|
|
124
|
+
ownership: snapshot.ownership,
|
|
125
|
+
runtime: snapshot.runtime ? JSON.parse(snapshot.runtime) : null,
|
|
126
|
+
health: snapshot.health,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
92
129
|
return;
|
|
93
130
|
}
|
|
94
131
|
case "stop": {
|
|
@@ -102,7 +139,7 @@ async function main(args) {
|
|
|
102
139
|
return;
|
|
103
140
|
}
|
|
104
141
|
default:
|
|
105
|
-
throw new Error(command ? `unknown command '${command}'` : usage());
|
|
142
|
+
throw new Error(command ? `unknown command '${command}'` : `${usage()}\n${startUsage()}\n${webuiUsage()}`);
|
|
106
143
|
}
|
|
107
144
|
}
|
|
108
145
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yydb/yyds",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "Node.js facade, CLI, and native bindings for local YYDS nodes.",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -32,18 +32,20 @@
|
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"bin",
|
|
35
|
-
"src"
|
|
35
|
+
"src",
|
|
36
|
+
"webui"
|
|
36
37
|
],
|
|
37
38
|
"scripts": {
|
|
39
|
+
"build": "node scripts/bundle-webui.mjs",
|
|
38
40
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
39
|
-
"test": "node --experimental-strip-types --test \"./tests/**/*.test.ts\""
|
|
41
|
+
"test": "node --experimental-strip-types --test \"./tests/**/*.test.ts\" \"./src/node/**/*.test.ts\""
|
|
40
42
|
},
|
|
41
43
|
"repository": {
|
|
42
44
|
"type": "git",
|
|
43
45
|
"url": "git+https://github.com/yy-database/yyds.git",
|
|
44
|
-
"directory": "projects/packages/yyds"
|
|
46
|
+
"directory": "projects/yyds.ts/packages/yyds"
|
|
45
47
|
},
|
|
46
|
-
"homepage": "https://github.com/yy-database/yyds/tree/dev/projects/packages/yyds#readme",
|
|
48
|
+
"homepage": "https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/yyds#readme",
|
|
47
49
|
"bugs": {
|
|
48
50
|
"url": "https://github.com/yy-database/yyds/issues"
|
|
49
51
|
},
|
|
@@ -57,13 +59,14 @@
|
|
|
57
59
|
"access": "public"
|
|
58
60
|
},
|
|
59
61
|
"optionalDependencies": {
|
|
60
|
-
"@yydb/yyds-win32-x64": "0.0.
|
|
61
|
-
"@yydb/yyds-linux-x64": "0.0.
|
|
62
|
-
"@yydb/yyds-darwin-x64": "0.0.
|
|
63
|
-
"@yydb/yyds-darwin-arm64": "0.0.
|
|
62
|
+
"@yydb/yyds-win32-x64": "0.0.15",
|
|
63
|
+
"@yydb/yyds-linux-x64": "0.0.15",
|
|
64
|
+
"@yydb/yyds-darwin-x64": "0.0.15",
|
|
65
|
+
"@yydb/yyds-darwin-arm64": "0.0.15"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
68
|
"@types/node": "^22.15.0",
|
|
69
|
+
"@yydb/yyds-webui": "0.0.15",
|
|
67
70
|
"typescript": "^5.9.2"
|
|
68
71
|
},
|
|
69
72
|
"engines": {
|
package/readme.md
CHANGED
|
@@ -10,7 +10,7 @@ Node.js facade for running a local YYDS node: native lifecycle blocks, a small C
|
|
|
10
10
|
| `@yydb/yyds/node/cluster` | `launchYydsServer` for `redis-server`, `mysqld`, `postgres` |
|
|
11
11
|
| `@yydb/yyds/node/runtime` | `runNodeStart`, `parseStartOptions` |
|
|
12
12
|
|
|
13
|
-
The `yyds` binary provides `init`, `start`, `status`, and `
|
|
13
|
+
The `yyds` binary provides `init`, `start`, `status`, `stop`, and `webui`. Native addons for your platform are pulled in automatically as optional dependencies.
|
|
14
14
|
|
|
15
15
|
## Example
|
|
16
16
|
|
|
@@ -30,10 +30,11 @@ yyds init --data-dir ./node-data --cluster-id local --node-id node-1
|
|
|
30
30
|
yyds start --data-dir ./node-data --cluster-id local --node-id node-1
|
|
31
31
|
yyds status --data-dir ./node-data
|
|
32
32
|
yyds stop --data-dir ./node-data
|
|
33
|
+
yyds webui --data-dir ./node-data --cluster-id local --node-id node-1
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
## Related packages
|
|
36
37
|
|
|
37
|
-
- [`@yydb/redis`](https://github.com/yy-database/yyds/tree/dev/projects/packages/redis)
|
|
38
|
-
- [`@yydb/mysql`](https://github.com/yy-database/yyds/tree/dev/projects/packages/mysql)
|
|
39
|
-
- [`@yydb/postgresql`](https://github.com/yy-database/yyds/tree/dev/projects/packages/postgresql)
|
|
38
|
+
- [`@yydb/redis`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/redis) �?Redis protocol disguise
|
|
39
|
+
- [`@yydb/mysql`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/mysql) �?MySQL protocol disguise
|
|
40
|
+
- [`@yydb/postgresql`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/postgresql) �?PostgreSQL protocol disguise
|
package/src/bindings.ts
CHANGED
|
@@ -7,9 +7,18 @@ export interface YydsBindings {
|
|
|
7
7
|
acquire(dataDir: string, identity: unknown): unknown;
|
|
8
8
|
};
|
|
9
9
|
LocalExecutorHandle: new (dataDir: string) => unknown;
|
|
10
|
-
RedisServiceHandle: new (
|
|
10
|
+
RedisServiceHandle: new (
|
|
11
|
+
lease: unknown,
|
|
12
|
+
port: number,
|
|
13
|
+
executor: unknown,
|
|
14
|
+
) => RedisServiceBinding;
|
|
11
15
|
PgsqlServiceHandle: new (lease: unknown, port: number) => PgsqlServiceBinding;
|
|
12
16
|
MysqlServiceHandle: new (lease: unknown, port: number) => MysqlServiceBinding;
|
|
17
|
+
MongoServiceHandle: new (
|
|
18
|
+
lease: unknown,
|
|
19
|
+
port: number,
|
|
20
|
+
executor: unknown,
|
|
21
|
+
) => MongoServiceBinding;
|
|
13
22
|
WireServerHandle: new (port: number, executor: unknown) => WireServiceBinding;
|
|
14
23
|
nodeInit(dataDir: string, identity: unknown): void;
|
|
15
24
|
nodeInspect(dataDir: string): NodeStatusBinding;
|
|
@@ -30,6 +39,7 @@ export interface ServiceBinding {
|
|
|
30
39
|
export type RedisServiceBinding = ServiceBinding;
|
|
31
40
|
export type PgsqlServiceBinding = ServiceBinding;
|
|
32
41
|
export type MysqlServiceBinding = ServiceBinding;
|
|
42
|
+
export type MongoServiceBinding = ServiceBinding;
|
|
33
43
|
export type WireServiceBinding = ServiceBinding;
|
|
34
44
|
|
|
35
45
|
export interface NodeStatusBinding {
|
package/src/node/runtime.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface StartNodeOptions {
|
|
|
12
12
|
redisPort?: number;
|
|
13
13
|
pgsqlPort?: number;
|
|
14
14
|
mysqlPort?: number;
|
|
15
|
+
mongoPort?: number;
|
|
15
16
|
wirePort?: number;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -33,7 +34,8 @@ export function parseStartOptions(protocol: YydsProtocol, args: string[]): Start
|
|
|
33
34
|
const argument = args[index];
|
|
34
35
|
if (["--port", "-p", "-P"].includes(argument)) {
|
|
35
36
|
const value = Number(args[++index]);
|
|
36
|
-
if (!Number.isInteger(value) || value < 0)
|
|
37
|
+
if (!Number.isInteger(value) || value < 0)
|
|
38
|
+
throw new Error(`missing port after ${argument}`);
|
|
37
39
|
if (protocol === "redis") options.redisPort = value;
|
|
38
40
|
if (protocol === "mysql") options.mysqlPort = value;
|
|
39
41
|
if (protocol === "postgresql") options.pgsqlPort = value;
|
|
@@ -63,6 +65,10 @@ export function parseStartOptions(protocol: YydsProtocol, args: string[]): Start
|
|
|
63
65
|
options.mysqlPort = Number(args[++index]);
|
|
64
66
|
continue;
|
|
65
67
|
}
|
|
68
|
+
if (argument === "--mongo-port") {
|
|
69
|
+
options.mongoPort = Number(args[++index]);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
66
72
|
if (argument === "--wire-port") {
|
|
67
73
|
options.wirePort = Number(args[++index]);
|
|
68
74
|
continue;
|
|
@@ -89,10 +95,27 @@ export async function runNodeStart(options: StartNodeOptions): Promise<void> {
|
|
|
89
95
|
const identity = new native.NodeIdentityHandle(options.clusterId, options.nodeId);
|
|
90
96
|
const lease = native.NodeLeaseHandle.acquire(options.dataDir, identity);
|
|
91
97
|
const executor = new native.LocalExecutorHandle(options.dataDir);
|
|
92
|
-
const redis = new native.RedisServiceHandle(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
const redis = new native.RedisServiceHandle(
|
|
99
|
+
lease,
|
|
100
|
+
options.redisPort ?? DEFAULT_REDIS_PORT,
|
|
101
|
+
executor,
|
|
102
|
+
);
|
|
103
|
+
const pgsql =
|
|
104
|
+
options.pgsqlPort === undefined
|
|
105
|
+
? undefined
|
|
106
|
+
: new native.PgsqlServiceHandle(lease, options.pgsqlPort);
|
|
107
|
+
const mysql =
|
|
108
|
+
options.mysqlPort === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: new native.MysqlServiceHandle(lease, options.mysqlPort);
|
|
111
|
+
const mongo =
|
|
112
|
+
options.mongoPort === undefined
|
|
113
|
+
? undefined
|
|
114
|
+
: new native.MongoServiceHandle(lease, options.mongoPort, executor);
|
|
115
|
+
const wire =
|
|
116
|
+
options.wirePort === undefined
|
|
117
|
+
? undefined
|
|
118
|
+
: new native.WireServerHandle(options.wirePort, executor);
|
|
96
119
|
|
|
97
120
|
const runtime = {
|
|
98
121
|
version: 1,
|
|
@@ -107,7 +130,7 @@ export async function runNodeStart(options: StartNodeOptions): Promise<void> {
|
|
|
107
130
|
native.nodeWriteRuntimeStatus(options.dataDir, JSON.stringify(runtime));
|
|
108
131
|
|
|
109
132
|
process.stdout.write(
|
|
110
|
-
`yyds running pid=${runtime.pid} redis=${runtime.redisAddress} pgsql=${runtime.pgsqlAddress ?? "null"} mysql=${runtime.mysqlAddress ?? "null"} wire=${runtime.wireAddress ?? "null"}\n`,
|
|
133
|
+
`yyds running pid=${runtime.pid} redis=${runtime.redisAddress} pgsql=${runtime.pgsqlAddress ?? "null"} mysql=${runtime.mysqlAddress ?? "null"} mongo=${mongo?.address() ?? "null"} wire=${runtime.wireAddress ?? "null"}\n`,
|
|
111
134
|
);
|
|
112
135
|
|
|
113
136
|
while (true) {
|
|
@@ -115,6 +138,7 @@ export async function runNodeStart(options: StartNodeOptions): Promise<void> {
|
|
|
115
138
|
if (redis.isFinished()) break;
|
|
116
139
|
if (pgsql?.isFinished()) break;
|
|
117
140
|
if (mysql?.isFinished()) break;
|
|
141
|
+
if (mongo?.isFinished()) break;
|
|
118
142
|
if (wire?.isFinished()) break;
|
|
119
143
|
await sleep(50);
|
|
120
144
|
}
|
|
@@ -122,6 +146,7 @@ export async function runNodeStart(options: StartNodeOptions): Promise<void> {
|
|
|
122
146
|
redis.stop();
|
|
123
147
|
pgsql?.stop();
|
|
124
148
|
mysql?.stop();
|
|
149
|
+
mongo?.stop();
|
|
125
150
|
wire?.stop();
|
|
126
151
|
native.nodeClearRuntimeArtifacts(options.dataDir, runtime.generation);
|
|
127
152
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ChildProcess } from "node:child_process";
|
|
4
|
+
import type { Server } from "node:http";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
openBrowser,
|
|
8
|
+
resolveWebuiDist,
|
|
9
|
+
spawnStart,
|
|
10
|
+
startStaticServer,
|
|
11
|
+
waitForWire,
|
|
12
|
+
wsEndpointFromWirePort,
|
|
13
|
+
} from "./webui-static.ts";
|
|
14
|
+
|
|
15
|
+
export interface WebuiOptions {
|
|
16
|
+
dataDir: string;
|
|
17
|
+
clusterId: string;
|
|
18
|
+
nodeId: string;
|
|
19
|
+
wirePort: number;
|
|
20
|
+
webuiPort: number;
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
open: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parsePort(raw: string | undefined, fallback: number, label: string): number {
|
|
26
|
+
const value = Number(raw ?? fallback);
|
|
27
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
28
|
+
throw new Error(`invalid ${label}: ${raw ?? fallback}`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseWebuiArgs(args: string[]): WebuiOptions {
|
|
34
|
+
const options: WebuiOptions = {
|
|
35
|
+
dataDir: "",
|
|
36
|
+
clusterId: "",
|
|
37
|
+
nodeId: "",
|
|
38
|
+
wirePort: 7700,
|
|
39
|
+
webuiPort: 8780,
|
|
40
|
+
open: true,
|
|
41
|
+
};
|
|
42
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
43
|
+
const flag = args[index];
|
|
44
|
+
const value = args[index + 1];
|
|
45
|
+
switch (flag) {
|
|
46
|
+
case "--data-dir":
|
|
47
|
+
options.dataDir = value ?? "";
|
|
48
|
+
index += 1;
|
|
49
|
+
break;
|
|
50
|
+
case "--cluster-id":
|
|
51
|
+
options.clusterId = value ?? "";
|
|
52
|
+
index += 1;
|
|
53
|
+
break;
|
|
54
|
+
case "--node-id":
|
|
55
|
+
options.nodeId = value ?? "";
|
|
56
|
+
index += 1;
|
|
57
|
+
break;
|
|
58
|
+
case "--wire-port":
|
|
59
|
+
options.wirePort = parsePort(value, options.wirePort, "wire-port");
|
|
60
|
+
index += 1;
|
|
61
|
+
break;
|
|
62
|
+
case "--port":
|
|
63
|
+
options.webuiPort = parsePort(value, options.webuiPort, "port");
|
|
64
|
+
index += 1;
|
|
65
|
+
break;
|
|
66
|
+
case "--endpoint":
|
|
67
|
+
options.endpoint = value ?? "";
|
|
68
|
+
index += 1;
|
|
69
|
+
break;
|
|
70
|
+
case "--no-open":
|
|
71
|
+
options.open = false;
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`unknown option '${flag}'`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!options.endpoint && (!options.dataDir || !options.clusterId || !options.nodeId)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"webui requires --endpoint or --data-dir with --cluster-id and --node-id",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return options;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function runWebui(options: WebuiOptions): Promise<void> {
|
|
86
|
+
const endpoint = options.endpoint ?? wsEndpointFromWirePort(options.wirePort);
|
|
87
|
+
const distRoot = resolveWebuiDist();
|
|
88
|
+
if (!fs.existsSync(join(distRoot, "index.html"))) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"WebUI assets not found. Run `pnpm --filter @yydb/yyds build` to bundle webui/.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let startChild: ChildProcess | null = null;
|
|
95
|
+
if (!options.endpoint) {
|
|
96
|
+
startChild = spawnStart({
|
|
97
|
+
dataDir: options.dataDir,
|
|
98
|
+
clusterId: options.clusterId,
|
|
99
|
+
nodeId: options.nodeId,
|
|
100
|
+
wirePort: options.wirePort,
|
|
101
|
+
});
|
|
102
|
+
startChild.stdout?.on("data", (chunk) => process.stdout.write(chunk));
|
|
103
|
+
startChild.stderr?.on("data", (chunk) => process.stderr.write(chunk));
|
|
104
|
+
startChild.on("exit", (code) => {
|
|
105
|
+
if (code && code !== 0) {
|
|
106
|
+
console.error(`yyds start exited with code ${code}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
await waitForWire(endpoint, 8000);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const server = await startStaticServer(distRoot, options.webuiPort);
|
|
113
|
+
const pageUrl = `http://127.0.0.1:${options.webuiPort}/?endpoint=${encodeURIComponent(endpoint)}`;
|
|
114
|
+
|
|
115
|
+
console.log(`WebUI ${pageUrl}`);
|
|
116
|
+
console.log(`Wire ${endpoint}`);
|
|
117
|
+
|
|
118
|
+
if (options.open) {
|
|
119
|
+
openBrowser(pageUrl);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await waitForShutdown(server, startChild);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function waitForShutdown(server: Server, startChild: ChildProcess | null): Promise<void> {
|
|
126
|
+
return new Promise((resolveWait) => {
|
|
127
|
+
const shutdown = () => {
|
|
128
|
+
server.close();
|
|
129
|
+
if (startChild && !startChild.killed) {
|
|
130
|
+
startChild.kill();
|
|
131
|
+
}
|
|
132
|
+
resolveWait();
|
|
133
|
+
};
|
|
134
|
+
process.once("SIGINT", shutdown);
|
|
135
|
+
process.once("SIGTERM", shutdown);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { wsEndpointFromWirePort } from "./webui-static.ts";
|
|
5
|
+
|
|
6
|
+
test("wsEndpointFromWirePort maps loopback wire port to WebSocket URL", () => {
|
|
7
|
+
assert.equal(wsEndpointFromWirePort(7700), "ws://127.0.0.1:7700/wire");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("wsEndpointFromWirePort accepts custom host", () => {
|
|
11
|
+
assert.equal(wsEndpointFromWirePort(9001, "10.0.0.5"), "ws://10.0.0.5:9001/wire");
|
|
12
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { createServer, type Server } from "node:http";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const MIME: Record<string, string> = {
|
|
9
|
+
".html": "text/html; charset=utf-8",
|
|
10
|
+
".js": "text/javascript; charset=utf-8",
|
|
11
|
+
".css": "text/css; charset=utf-8",
|
|
12
|
+
".json": "application/json; charset=utf-8",
|
|
13
|
+
".svg": "image/svg+xml",
|
|
14
|
+
".woff2": "font/woff2",
|
|
15
|
+
".png": "image/png",
|
|
16
|
+
".ico": "image/x-icon",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function contentType(path: string): string {
|
|
20
|
+
const extension = path.slice(path.lastIndexOf("."));
|
|
21
|
+
return MIME[extension] ?? "application/octet-stream";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveWebuiDist(): string {
|
|
25
|
+
const bundled = join(dirname(fileURLToPath(import.meta.url)), "../../webui");
|
|
26
|
+
if (existsSync(join(bundled, "index.html"))) {
|
|
27
|
+
return bundled;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const pkgJson = require.resolve("@yydb/yyds-webui/package.json");
|
|
32
|
+
const external = join(dirname(pkgJson), "dist", "cdn");
|
|
33
|
+
if (existsSync(join(external, "index.html"))) {
|
|
34
|
+
return external;
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// @yydb/yyds-webui not installed
|
|
38
|
+
}
|
|
39
|
+
return bundled;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function wsEndpointFromWirePort(wirePort: number, host = "127.0.0.1"): string {
|
|
43
|
+
return `ws://${host}:${wirePort}/wire`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function startStaticServer(root: string, port: number): Promise<Server> {
|
|
47
|
+
const absRoot = resolve(root);
|
|
48
|
+
return new Promise((resolvePromise, reject) => {
|
|
49
|
+
const server = createServer((request, response) => {
|
|
50
|
+
const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
|
|
51
|
+
const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//, "");
|
|
52
|
+
const target = resolve(absRoot, relative);
|
|
53
|
+
if (!target.startsWith(absRoot)) {
|
|
54
|
+
response.writeHead(403);
|
|
55
|
+
response.end("forbidden");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const bytes = readFileSync(target);
|
|
60
|
+
response.writeHead(200, { "Content-Type": contentType(target) });
|
|
61
|
+
response.end(bytes);
|
|
62
|
+
} catch {
|
|
63
|
+
response.writeHead(404);
|
|
64
|
+
response.end("not found");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
server.listen(port, "127.0.0.1", () => resolvePromise(server));
|
|
68
|
+
server.on("error", reject);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface StartSpawnOptions {
|
|
73
|
+
dataDir: string;
|
|
74
|
+
clusterId: string;
|
|
75
|
+
nodeId: string;
|
|
76
|
+
wirePort: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function spawnStart(options: StartSpawnOptions): ChildProcess {
|
|
80
|
+
const cli = fileURLToPath(new URL("../../bin/yyds.mjs", import.meta.url));
|
|
81
|
+
const args = [
|
|
82
|
+
cli,
|
|
83
|
+
"start",
|
|
84
|
+
"--data-dir",
|
|
85
|
+
options.dataDir,
|
|
86
|
+
"--cluster-id",
|
|
87
|
+
options.clusterId,
|
|
88
|
+
"--node-id",
|
|
89
|
+
options.nodeId,
|
|
90
|
+
"--wire-port",
|
|
91
|
+
String(options.wirePort),
|
|
92
|
+
];
|
|
93
|
+
return spawn(process.execPath, args, {
|
|
94
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
95
|
+
env: process.env,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function openBrowser(url: string): void {
|
|
100
|
+
const platform = process.platform;
|
|
101
|
+
if (platform === "win32") {
|
|
102
|
+
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const opener = platform === "darwin" ? "open" : "xdg-open";
|
|
106
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function sleep(ms: number): Promise<void> {
|
|
110
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function waitForWire(endpoint: string, timeoutMs = 8000): Promise<void> {
|
|
114
|
+
const deadline = Date.now() + timeoutMs;
|
|
115
|
+
while (Date.now() < deadline) {
|
|
116
|
+
try {
|
|
117
|
+
const socket = new WebSocket(endpoint);
|
|
118
|
+
const opened = await new Promise<boolean>((resolveOpen) => {
|
|
119
|
+
const timer = setTimeout(() => resolveOpen(false), 500);
|
|
120
|
+
socket.addEventListener("open", () => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
resolveOpen(true);
|
|
123
|
+
});
|
|
124
|
+
socket.addEventListener("error", () => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
resolveOpen(false);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
socket.close();
|
|
130
|
+
if (opened) return;
|
|
131
|
+
} catch {
|
|
132
|
+
await sleep(400);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await sleep(200);
|
|
136
|
+
}
|
|
137
|
+
}
|