@procedures/server 0.10.0 → 0.12.0

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/src/run.ts CHANGED
@@ -26,12 +26,13 @@ import {
26
26
  type ProcedureModule,
27
27
  type ProcedureRegistry,
28
28
  } from "@procedures/client";
29
- import { createPeer, type Peer, type PeerEndpoint } from "@procedures/peer";
29
+ import type { RunOptions } from "@mark1russell7/service";
30
+ import { createPeer, peerHostOptions, type Peer, type PeerEndpoint } from "@procedures/peer";
30
31
  import peerModule from "@procedures/peer/module";
31
32
  import { parseConfig } from "./config.js";
33
+ import { runProcedureHost } from "./host.js";
32
34
  import { serverModule } from "./module.js";
33
35
  import type { ServerConfig } from "./types.js";
34
- import { seconds } from "@procedures/client";
35
36
 
36
37
  /** A package name and the subpath after it: "@a/b/register" gives ["@a/b", "./register"]. */
37
38
  function splitSpec(spec: string): { name: string; subpath: string } {
@@ -152,27 +153,42 @@ export interface RunningServer {
152
153
  refusedUnderRoot?: string[] | undefined;
153
154
  }
154
155
 
155
- /** The token of a configuration: `--token`, else `SERVER_TOKEN`, else a random one. `--no-token`: none. */
156
+ /**
157
+ * The token of a configuration: `--token`, else `SERVER_TOKEN`, else a random one of 32 bytes, the
158
+ * size of the token of `mark --server` (CLI-8). `--no-token`: none.
159
+ */
156
160
  function tokenOf(config: ServerConfig): { token: string | undefined; generated: boolean } {
157
161
  const envToken = process.env["SERVER_TOKEN"] || undefined;
158
162
  if (config.token === null) return { token: undefined, generated: false };
159
163
  if (config.token !== undefined) return { token: config.token, generated: false };
160
164
  if (envToken !== undefined) return { token: envToken, generated: false };
161
- return { token: randomBytes(24).toString("hex"), generated: true };
165
+ return { token: randomBytes(32).toString("hex"), generated: true };
162
166
  }
163
167
 
164
168
  /** The options of `startServer()` and `runServer()`. */
165
169
  export interface ServerRunOptions {
166
170
  /** The folders that a `--procedures` package resolves from, first to last (default: the current folder). */
167
171
  resolveFrom?: readonly string[] | undefined;
172
+ /** The options of `run()` of `runServer()`: a process port and a clock, for a test (CLI-8). */
173
+ run?: RunOptions | undefined;
168
174
  }
169
175
 
170
- /** This function starts the server of a configuration. It returns when the transports listen. */
171
- export async function startServer(
172
- config: ServerConfig,
173
- log: (line: string) => void = console.log,
174
- options: ServerRunOptions = {}
175
- ): Promise<RunningServer> {
176
+ /** What the server of a configuration serves: its registry, its policy, its token and its root. */
177
+ interface PreparedServer {
178
+ registry: ProcedureRegistry;
179
+ policy: HostPolicy;
180
+ token: string | undefined;
181
+ generated: boolean;
182
+ confined: Confined | undefined;
183
+ }
184
+
185
+ /** A new peer id. */
186
+ function newPeerId(): string {
187
+ return `peer-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
188
+ }
189
+
190
+ /** This function builds the registry and the host policy of a configuration. */
191
+ async function prepareServer(config: ServerConfig, log: (line: string) => void, options: ServerRunOptions): Promise<PreparedServer> {
176
192
  // The server.* procedures: those of the peer package (server.create, server.call and the
177
193
  // others), and server.start, server.stop and server.status of this package
178
194
  const modules: ProcedureModule[] = [peerModule, serverModule];
@@ -205,12 +221,20 @@ export async function startServer(
205
221
  // bus, which is also the ctx.bus of the procedures.
206
222
  events: { topics: config.events ?? [] },
207
223
  });
208
- const peer = await createPeer({
209
- id: `peer-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
210
- transports: config.transports,
211
- registry,
212
- policy,
213
- });
224
+ return { registry, policy, token, generated, confined };
225
+ }
226
+
227
+ /**
228
+ * This function starts the server of a configuration outside a service, as a peer. It returns when
229
+ * the transports listen. `peer.stop()` stops it. The `server` CLI uses `runServer()` instead.
230
+ */
231
+ export async function startServer(
232
+ config: ServerConfig,
233
+ log: (line: string) => void = console.log,
234
+ options: ServerRunOptions = {}
235
+ ): Promise<RunningServer> {
236
+ const { registry, policy, token, generated, confined } = await prepareServer(config, log, options);
237
+ const peer = await createPeer({ id: newPeerId(), transports: config.transports, registry, policy });
214
238
  await peer.start();
215
239
  return {
216
240
  peer,
@@ -224,8 +248,10 @@ export async function startServer(
224
248
  }
225
249
 
226
250
  /**
227
- * The `server` CLI: parse the options, start the server, print where it listens, and keep the
228
- * process alive until a signal. `mark serve` calls it with its own arguments.
251
+ * The `server` CLI: parse the options, start the server under `run()` of `@mark1russell7/service`,
252
+ * and print where it listens. `mark serve` calls it with its own arguments. The runner owns the
253
+ * signals, the stop in stages and the exit: this function installs no signal handler and no
254
+ * hard-exit timer (CLI-8). Its promise ends when the lifecycle ends.
229
255
  */
230
256
  export async function runServer(argv: string[], options: ServerRunOptions = {}): Promise<void> {
231
257
  const config = parseConfig(argv);
@@ -235,19 +261,33 @@ export async function runServer(argv: string[], options: ServerRunOptions = {}):
235
261
  console.log(" Transports:", config.transports.map((t) => t.type).join(", "));
236
262
  }
237
263
 
238
- const running = await startServer(config, console.log, options);
264
+ const { registry, policy, token, generated, confined } = await prepareServer(config, console.log, options);
265
+ const { host, life } = await runProcedureHost({
266
+ name: "procedure-server",
267
+ mode: "foreground",
268
+ host: peerHostOptions({ id: newPeerId(), transports: config.transports, registry, policy }, "procedures"),
269
+ log: console.log,
270
+ ...(options.run ? { run: options.run } : {}),
271
+ });
272
+ if (life.state !== "running") {
273
+ // The startup failed: the lifecycle stops the parts that started, and the process exits with 1
274
+ console.error("Server failed to start:");
275
+ for (const error of life.errors) console.error(` ${error}`);
276
+ await life.done;
277
+ return;
278
+ }
239
279
 
240
280
  console.log("\nServer started!");
241
- console.log(` Server ID: ${running.peer.id}`);
242
- console.log(` Procedures: ${running.peer.getServer().procedureCount}`);
281
+ console.log(` Server ID: ${host.id}`);
282
+ console.log(` Procedures: ${host.server.procedureCount}`);
243
283
  console.log("\nEndpoints:");
244
- for (const endpoint of running.endpoints) {
284
+ for (const endpoint of host.info.endpoints()) {
245
285
  console.log(` [${endpoint.type}] ${endpoint.address}`);
246
286
  }
247
- if (running.token === undefined) {
287
+ if (token === undefined) {
248
288
  console.log("\nNo token: any program on this computer can call the server.");
249
289
  } else {
250
- console.log(`\nToken${running.generatedToken ? " (random, for this run)" : ""}: ${running.token}`);
290
+ console.log(`\nToken${generated ? " (random, for this run)" : ""}: ${token}`);
251
291
  console.log(" Send it as 'Authorization: Bearer <token>', or as ?token=<token> for WebSocket.");
252
292
  }
253
293
  if (config.expose.length > 0) {
@@ -255,34 +295,12 @@ export async function runServer(argv: string[], options: ServerRunOptions = {}):
255
295
  }
256
296
  const events = config.events ?? [];
257
297
  console.log(events.length > 0 ? `Event topics (events.subscribe): ${events.join(", ")}` : "Event topics: none (--events adds them)");
258
- if (running.fsRoot !== undefined) {
259
- console.log(`File root: ${running.fsRoot} (each procedure refuses a path outside it)`);
260
- console.log(` Refused under the root: ${running.refusedUnderRoot?.length ?? 0} procedures that the root cannot confine`);
298
+ if (confined !== undefined) {
299
+ console.log(`File root: ${confined.root} (each procedure refuses a path outside it)`);
300
+ console.log(` Refused under the root: ${confined.refused.length} procedures that the root cannot confine`);
261
301
  }
262
302
  console.log("\nPress Ctrl+C to stop the server.");
263
-
264
- // The peer stops before the process exits, with a limit of 10 seconds. A failed stop exits
265
- // with 1, and a second signal exits at once (roadmap R5).
266
- let stopping = false;
267
- const stop = (): void => {
268
- if (stopping) process.exit(1);
269
- stopping = true;
270
- console.log("\nShutting down...");
271
- setTimeout(() => process.exit(1), 10_000).unref();
272
- // The calls in flight have 5 seconds to end (roadmap R14)
273
- running.peer.stop({ grace: seconds(5) }).then(
274
- () => process.exit(0),
275
- (error: unknown) => {
276
- console.error(`The server did not stop cleanly: ${error instanceof Error ? error.message : String(error)}`);
277
- process.exit(1);
278
- },
279
- );
280
- };
281
- process.on("SIGINT", stop);
282
- process.on("SIGTERM", stop);
283
-
284
- // Keep the process alive
285
- await new Promise(() => {
286
- // Never resolves - keeps process running
287
- });
303
+ // A signal starts the stop in stages: the stop of the intake, the drain, the close of the
304
+ // listeners and of the storages. A second Ctrl-C exits at once (the profile localDev, D6).
305
+ await life.done;
288
306
  }