From bbe521e5fb75f3d9285e6099e81509b3d5ba09dd Mon Sep 17 00:00:00 2001 From: Hexagon Date: Tue, 23 Apr 2024 21:09:11 +0200 Subject: [PATCH] Internal refactor --- README.md | 2 +- application.meta.ts | 2 +- deno.json | 11 +- docs/src/_data.json | 2 +- docs/src/changelog.md | 12 + docs/src/examples/telemetry/README.md | 4 +- .../telemetry/task-with-telemetry-1.ts | 2 +- .../telemetry/task-with-telemetry-2.ts | 2 +- lib/cli/config.ts | 2 +- lib/cli/main.ts | 85 ++++--- lib/cli/status.ts | 7 +- lib/common/eventemitter.ts | 61 ----- lib/common/ipc.ts | 224 ------------------ lib/common/restclient.ts | 52 ---- lib/common/utils.ts | 44 ---- lib/core/api.ts | 132 +++-------- lib/core/cluster.ts | 7 +- lib/{common => core}/port.ts | 17 +- lib/core/process.ts | 61 ++--- lib/core/pup.ts | 27 ++- lib/core/rest.ts | 98 ++++++-- lib/core/status.ts | 6 +- telemetry.ts | 220 ----------------- test/cli/args.test.ts | 221 ----------------- test/common/eventemitter.test.ts | 83 ------- test/common/ipc.test.ts | 97 -------- test/core/pup.test.ts | 22 +- test/main.test.ts | 42 ---- test/telemetry.test.ts | 40 ---- versions.json | 14 ++ 30 files changed, 277 insertions(+), 1322 deletions(-) delete mode 100644 lib/common/eventemitter.ts delete mode 100644 lib/common/ipc.ts delete mode 100644 lib/common/restclient.ts delete mode 100644 lib/common/utils.ts rename lib/{common => core}/port.ts (81%) delete mode 100644 telemetry.ts delete mode 100644 test/common/eventemitter.test.ts delete mode 100644 test/common/ipc.test.ts delete mode 100644 test/main.test.ts delete mode 100644 test/telemetry.test.ts diff --git a/README.md b/README.md index 381db6f..8c1e8cc 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ _For detailed documentation, visit [pup.56k.guru](https://pup.56k.guru)._ To install Pup, open your terminal and execute the following command: ```bash -deno run -Ar jsr:@pup/pup@1.0.0-rc.31 setup --channel prerelease +deno run -Ar jsr:@pup/pup@1.0.0-rc.32 setup --channel prerelease ``` This command downloads the latest version of Pup and installs it on your system. The `--channel prerelease` option is included as there is no stable version of Pup yet. Read more abour release diff --git a/application.meta.ts b/application.meta.ts index 34afd90..c891960 100644 --- a/application.meta.ts +++ b/application.meta.ts @@ -21,7 +21,7 @@ const Application = { name: "pup", - version: "1.0.0-rc.31", + version: "1.0.0-rc.32", url: "jsr:@pup/pup@$VERSION", canary_url: "https://raw.githubusercontent.com/Hexagon/pup/main/pup.ts", deno: null, /* Minimum stable version of Deno required to run Pup (without --unstable-* flags) */ diff --git a/deno.json b/deno.json index fe4a7bd..e406fcb 100644 --- a/deno.json +++ b/deno.json @@ -1,12 +1,10 @@ { "name": "@pup/pup", - "version": "1.0.0-rc.31", + "version": "1.0.0-rc.32", "exports": { ".": "./pup.ts", - "./mod.ts": "./mod.ts", - "./telemetry": "./telemetry.ts", - "./telemetry.ts": "./telemetry.ts" + "./lib": "./mod.ts" }, "unstable": [ @@ -49,16 +47,17 @@ "@cross/service": "jsr:@cross/service@^1.0.3", "@cross/test": "jsr:@cross/test@^0.0.9", "@cross/utils": "jsr:@cross/utils@^0.11.0", - "@hexagon/bundlee": "jsr:@hexagon/bundlee@^0.9.6/mod.ts", "@hexagon/croner": "jsr:@hexagon/croner@^8.0.1", "@oak/oak": "jsr:@oak/oak@^15.0.0", + "@pup/api-client": "jsr:@pup/api-client@^1.0.0", + "@pup/api-definitions": "jsr:@pup/api-definitions@^1.0.0", + "@pup/common": "jsr:@pup/common@^1.0.0", "@std/assert": "jsr:@std/assert@^0.223.0", "@std/async": "jsr:@std/async@^0.223.0", "@std/encoding": "jsr:@std/encoding@^0.223.0", "@std/io": "jsr:@std/io@^0.223.0", "@std/path": "jsr:@std/path@^0.223.0", "@std/semver": "jsr:@std/semver@^0.223.0", - "@std/testing": "jsr:@std/testing@^0.223.0", "@std/uuid": "jsr:@std/uuid@^0.223.0", "dax-sh": "npm:dax-sh@^0.40.0", "filesize": "npm:filesize@^10.1.1", diff --git a/docs/src/_data.json b/docs/src/_data.json index c97aa90..b18fd67 100644 --- a/docs/src/_data.json +++ b/docs/src/_data.json @@ -6,7 +6,7 @@ "description": "Universal Process Manager" }, "substitute": { - "$PUP_VERSION": "1.0.0-rc.31" + "$PUP_VERSION": "1.0.0-rc.32" }, "top_links": [ { diff --git a/docs/src/changelog.md b/docs/src/changelog.md index 0c1f6b1..c558605 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -9,6 +9,18 @@ nav_order: 13 All notable changes to this project will be documented in this section. +## [1.0.0-rc.32] - 2024-04-23 + +**Breaking:** Any plugins or packages importing telemetry from `@pup/pup/telemetry.ts` needs to be updated to import from `@pup/telemetry` instead. + +## Changes + +- chore(core): Internal refactor, splitting several parts of pup into separate reusable libraries: + - `@pup/common` - : Common utilities across Pup, Telemetry and Plug-ins. + - `@pup/api-definitions` - : API definitions used by Pup and it's Rest API. + - `@pup/api-client` - : The Pup Rest API, used by the cli interface, telemetry and plugins. + - `@pup/telemetry` - : Runtime agnostic library for enabling Node, Deno and Bun process with Pup Telemetry and IPC capabilities. + ## [1.0.0-rc.31] - 2024-04-23 - fix(telementry): Tear down in correct order diff --git a/docs/src/examples/telemetry/README.md b/docs/src/examples/telemetry/README.md index 4c338a0..c5d5860 100644 --- a/docs/src/examples/telemetry/README.md +++ b/docs/src/examples/telemetry/README.md @@ -18,7 +18,7 @@ This example demonstrates the telemetry feature of pup, which ... The simplest use case, where you only want to monitor your client metrics is used like this: ```ts -import { PupTelemetry } from "jsr:@pup/pup@$PUP_VERSION/telemetry" +import { PupTelemetry } from "jsr:@pup/telemetry" new PupTelemetry() @@ -32,7 +32,7 @@ telemetry (including main process) using `status` on the cli. ```ts // PupTelemetry is a singleton, so it can be imported one or many times in your application -import { PupTelemetry } from "jsr:@pup/pup@$PUP_VERSION/telemetry" // Pin this to a specific version of pup +import { PupTelemetry } from "jsr:@pup/telemetry" const telemetry = new PupTelemetry() // One part of your application ... diff --git a/docs/src/examples/telemetry/task-with-telemetry-1.ts b/docs/src/examples/telemetry/task-with-telemetry-1.ts index 4160149..9b84e17 100644 --- a/docs/src/examples/telemetry/task-with-telemetry-1.ts +++ b/docs/src/examples/telemetry/task-with-telemetry-1.ts @@ -1,6 +1,6 @@ // See docs/examples/telemetry/README.md for full documentation on telemetry, including using the IPC // - Pin this to the latest version of pup, or include in import map -import { PupTelemetry } from "jsr:@pup/pup@1.0.0-rc.31/telemetry" +import { PupTelemetry } from "jsr:@pup/pup-telemetry" const telemetry = new PupTelemetry(1) // The task diff --git a/docs/src/examples/telemetry/task-with-telemetry-2.ts b/docs/src/examples/telemetry/task-with-telemetry-2.ts index 461067d..7bea559 100644 --- a/docs/src/examples/telemetry/task-with-telemetry-2.ts +++ b/docs/src/examples/telemetry/task-with-telemetry-2.ts @@ -1,6 +1,6 @@ // See docs/examples/telemetry/README.md for full documentation on telemetry, including using the IPC // - Pin this to the latest version of pup, or include in import map -import { PupTelemetry } from "jsr:@pup/pup@1.0.0-rc.31/telemetry" +import { PupTelemetry } from "jsr:@pup/telemetry" const telemetry = new PupTelemetry(1) // The task diff --git a/lib/cli/config.ts b/lib/cli/config.ts index 005d09a..131d17e 100644 --- a/lib/cli/config.ts +++ b/lib/cli/config.ts @@ -11,7 +11,7 @@ import JSON5 from "json5" import { join } from "@std/path" import { exists, readFile, writeFile } from "@cross/fs" import type { ArgsParser } from "@cross/utils" -import { toResolvedAbsolutePath } from "../common/utils.ts" +import { toResolvedAbsolutePath } from "@pup/common/path" import { exit } from "node:process" /** diff --git a/lib/cli/main.ts b/lib/cli/main.ts index fadb110..5611fc2 100644 --- a/lib/cli/main.ts +++ b/lib/cli/main.ts @@ -17,7 +17,7 @@ import { printStatus } from "./status.ts" import { upgrade } from "./upgrade.ts" // Import common utilities -import { toPersistentPath, toResolvedAbsolutePath, toTempPath } from "../common/utils.ts" +import { toPersistentPath, toResolvedAbsolutePath, toTempPath } from "@pup/common/path" import { exists, readFile } from "@cross/fs" // Import external dependencies @@ -31,8 +31,7 @@ import { installService, uninstallService } from "@cross/service" import { Colors, exit } from "@cross/utils" import { chdir, cwd } from "@cross/fs" import { GenerateToken } from "../common/token.ts" -import { RestClient } from "../common/restclient.ts" -import { ApiApplicationState } from "../core/api.ts" +import { PupRestClient } from "@pup/api-client" import { CurrentRuntime, Runtime } from "@cross/runtime" import { Prop } from "../common/prop.ts" import { encodeBase64 } from "@std/encoding/base64" @@ -223,7 +222,7 @@ async function main() { // Send api request const apiBaseUrl = `http://${configuration.api?.hostname || DEFAULT_REST_API_HOSTNAME}:${port}` - client = new RestClient(apiBaseUrl, token!) + client = new PupRestClient(apiBaseUrl, token!) } catch (_e) { /* Ignore */ } @@ -475,17 +474,19 @@ async function main() { console.error("Can not print status, could not create api client.") return exit(1) } - const responseState = await client.get("/state") - if (responseState.ok) { - const dataState: ApiApplicationState = await responseState.json() - console.log("") - printHeader() - await printStatus(configFile!, configuration!, cwd(), dataState) - exit(0) - console.log("Action completed successfully") - exit(0) - } else { - console.error("Action failed: Invalid response received.") + try { + const responseState = await client.getState() + if (responseState.data) { + console.log("") + printHeader() + printStatus(configFile!, configuration!, cwd(), responseState.data) + exit(0) + } else { + console.error("Action failed: Invalid response received.") + exit(1) + } + } catch (_e) { + console.error("Action failed: Could not contact the Pup instance.") exit(1) } } @@ -499,17 +500,45 @@ async function main() { exit(1) } if (baseArgument === op) { - let url = `/${op.toLowerCase().trim()}` - if (secondaryBaseArgument) { - url = `/processes/${secondaryBaseArgument.toLocaleLowerCase().trim()}${url}` - } - const result = await client!.post(url, undefined) - if (result.ok) { - console.log("Action completed successfully") - exit(0) - } else { - console.error("Action failed: Invalid response received.") - exit(1) + try { + let responseState // Declare responseState in the outer try/catch scope + switch (op) { // Use 'switch' instead of 'switch case' + case "restart": + if (secondaryBaseArgument) responseState = await client?.restartProcess(secondaryBaseArgument.toLocaleLowerCase().trim()) + break + case "start": + // Implement the call to client?.startProcess(...) + if (secondaryBaseArgument) responseState = await client?.startProcess(secondaryBaseArgument.toLocaleLowerCase().trim()) + break + case "stop": + // Implement the call to client?.stopProcess(...) + if (secondaryBaseArgument) responseState = await client?.stopProcess(secondaryBaseArgument.toLocaleLowerCase().trim()) + break + case "block": + // Implement the call to client?.blockProcess(...) + if (secondaryBaseArgument) responseState = await client?.blockProcess(secondaryBaseArgument.toLocaleLowerCase().trim()) + break + case "unblock": + // Implement the call to client?.unblockProcess(...) + if (secondaryBaseArgument) responseState = await client?.unblockProcess(secondaryBaseArgument.toLocaleLowerCase().trim()) + break + case "terminate": + responseState = await client?.terminate() + break + default: + console.error(`Invalid operation: ${op}`) + return exit(1) + } + if (!responseState?.error) { + console.error("Success") + return exit(0) + } else { + console.error(`Error: ${responseState.error}`) + return exit(1) + } + } catch (e) { + console.error("Action failed:", e) + return exit(1) } } } @@ -522,8 +551,8 @@ async function main() { * Error handling: Pup already running */ try { - const response = await client?.get("/state") - if (response?.ok) { + const response = await client?.getState() + if (response) { console.warn(`Pup already running. Exiting.`) exit(1) } diff --git a/lib/cli/status.ts b/lib/cli/status.ts index c77935d..6fe1154 100644 --- a/lib/cli/status.ts +++ b/lib/cli/status.ts @@ -6,7 +6,8 @@ * @license MIT */ -import { type ProcessInformation, ProcessState } from "../core/process.ts" +import { type ProcessInformation } from "../core/process.ts" +import { ApiProcessState } from "@pup/api-definitions" import { type Column, Columns, type Row } from "./columns.ts" import { Colors } from "@cross/utils" import { filesize } from "filesize" @@ -14,7 +15,7 @@ import { blockedFormatter, codeFormatter, naFormatter, statusFormatter } from ". import { timeagoFormatter } from "./formatters/times.ts" import { Configuration, DEFAULT_REST_API_HOSTNAME } from "../core/configuration.ts" import { resolve } from "@std/path" -import { ApiApplicationState } from "../core/api.ts" +import { ApiApplicationState } from "@pup/api-definitions" /** * Helper which print the status of all running processes, @@ -53,7 +54,7 @@ export function printStatus(configFile: string, configuration: Configuration, cw taskTable.push({ Id: " " + currentTask.id, Type: currentTask.type.slice(0, 4) || "N/A", - Status: ProcessState[currentTask.status] || "N/A", + Status: ApiProcessState[currentTask.status] || "N/A", Blocked: currentTask.blocked ? "Yes" : "No", Started: timeagoFormatter(currentTask.started ? currentTask.started : "N/A"), Exited: timeagoFormatter(currentTask.exited ? currentTask.exited : "N/A"), diff --git a/lib/common/eventemitter.ts b/lib/common/eventemitter.ts deleted file mode 100644 index d638ec7..0000000 --- a/lib/common/eventemitter.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Simple EventEmitter implementation for Pup - * - * @file lib/common/eventemitter.ts - * @license MIT - */ - -export type EventHandler = (eventData?: t) => void - -class EventEmitter { - // deno-lint-ignore no-explicit-any - listeners: Map>> = new Map>>() - - /** - * Registers an event listener for the specified event. - * @param {string} event - The name of the event to listen for. - * @param {EventHandler} fn - The callback function to execute when the event is triggered. - */ - on(event: string, fn: EventHandler) { - if (this.listeners.has(event)) { - this.listeners.get(event)?.push(fn) - } else { - this.listeners.set(event, [fn]) - } - } - - /** - * Removes an event listener for the specified event. - * @param {string} event - The name of the event to remove the listener from. - * @param {EventHandler} fn - The callback function to remove from the event listeners. - */ - off(event: string, fn: EventHandler) { - const existingFns = this.listeners.get(event) - if (existingFns) { - this.listeners.set( - event, - existingFns.filter((existingFn) => existingFn !== fn), - ) - } - } - - /** - * Emits an event, calling all registered event listeners for the specified event. - * @param {string} event - The name of the event to emit. - * @param {T} eventData - Optional event data to be passed to the event listeners. - */ - emit(event: string, eventData?: T) { - const fns = this.listeners.get(event) - if (fns) { - for (const fn of fns) { - fn(eventData) - } - } - } - - close() { - this.listeners.clear() - } -} - -export { EventEmitter } diff --git a/lib/common/ipc.ts b/lib/common/ipc.ts deleted file mode 100644 index f93ebaf..0000000 --- a/lib/common/ipc.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Compact file-based IPC mechanism for Deno. - * - * Manages file permissions and ensures that the messages were sent within a reasonable amount of time. - * - * The class is used due to Deno's current lack of support for secure cross-platform sockets. - * - * @file lib/common/ipc.ts - * @license MIT - */ - -import { exists } from "@cross/fs" -import { basename, dirname, join } from "@std/path" -import { debounce } from "@std/async" -import { toResolvedAbsolutePath } from "./utils.ts" - -export interface IpcValidatedMessage { - pid: number | null - sent: Date | null - data: string | null - errors: string[] -} - -export class FileIPC { - public MAX_DATA_LENGTH = 1024 - private filePath: string - private dirPath: string - private fileName: string - private staleMessageLimitMs: number - private debounceTimeMs: number - private messageQueue: IpcValidatedMessage[][] = [] - private aborted = false - private watcher?: Deno.FsWatcher - - constructor(filePath: string, staleMessageLimitMs?: number, debounceTimeMs?: number) { - this.filePath = toResolvedAbsolutePath(filePath) - this.dirPath = toResolvedAbsolutePath(dirname(filePath)) // Get directory of the file - this.fileName = basename(filePath) // Get name of the file - this.staleMessageLimitMs = staleMessageLimitMs ?? 30000 - this.debounceTimeMs = debounceTimeMs ?? 100 - } - - public getFilePath(): string { - return this.filePath - } - - /** - * startWatching method initiates a file watcher on the filePath. - * When a file modification event occurs, it will debounce the call to extractMessages to ensure it doesn't - * get called more than once in a short amount of time (as specified by debounceTimeMs). The received messages - * from the extractMessages call are then added to the messageQueue to be consumed by the receiveData generator. - */ - private async startWatching() { - // Stop if aborted - if (this.aborted) return - - // Create directory if it doesn't exist - await Deno.mkdir(this.dirPath, { recursive: true }) - - // Make an initial call to extractMessages to ensure that any existing messages are consumed - const messages = await this.extractMessages() - if (messages.length > 0) { - this.messageQueue.push(messages) - } - - // Watch the directory, not the file - this.watcher = Deno.watchFs(this.dirPath) - for await (const event of this.watcher) { - // Stop if aborted - if (this.aborted) break - - // Check that the event pertains to the correct file - if (event.kind === "modify" && event.paths.includes(join(this.dirPath, this.fileName))) { - debounce(async () => { - try { - const messages = await this.extractMessages() - if (messages.length > 0) { - this.messageQueue.push(messages) - } - } catch (_e) { /* Ignore errors */ } - }, this.debounceTimeMs)() - } - } - } - - /** - * extractMessages is a private helper function that reads from the IPC file, validates the messages - * and returns them as an array of IpcValidatedMessage. It also handles the removal of the file after - * reading and validates the data based on the staleMessageLimitMs. - * - * This function is called every time a 'modify' event is detected by the file watcher started in startWatching method. - * - * Note: This function should only be used internally by the FileIPC class and is not meant to be exposed to external consumers. - */ - private async extractMessages(): Promise { - if (await exists(this.filePath)) { - let fileContent - try { - fileContent = await Deno.readTextFile(this.filePath) - } catch (_e) { - throw new Error(`Could not read '${this.filePath}'`) - } - - try { - await Deno.remove(this.filePath) - } catch (_e) { - throw new Error(`Failed to remove '${this.filePath}', aborting ipc read.`) - } - - const receivedMessages: IpcValidatedMessage[] = [] - - try { - const messages = JSON.parse(fileContent || "[]") - for (const messageObj of messages) { - let validatedPid: number | null = null - let validatedSent: Date | null = null - let validatedData: string | null = null - const errors: string[] = [] - - // Validate pid - try { - validatedPid = parseInt(messageObj.pid) - } catch (_e) { - errors.push("Invalid data received: pid") - } - - // Validate sent - try { - validatedSent = new Date(Date.parse(messageObj.sent)) - } catch (_e) { - errors.push("Invalid data received: sent") - } - - // Validate data - if ( - validatedSent !== null && - validatedSent.getTime() >= Date.now() - this.staleMessageLimitMs - ) { - if (!messageObj.data) { - errors.push("Invalid data received: missing") - } else if (typeof messageObj.data !== "string") { - errors.push("Invalid data received: not string") - } else if (messageObj.data.length >= this.MAX_DATA_LENGTH) { - errors.push("Invalid data received: too long") - } else { - validatedData = messageObj.data - } - } else { - errors.push("Invalid data received: stale") - } - - receivedMessages.push({ - pid: validatedPid, - sent: validatedSent, - data: validatedData, - errors, - }) - } - return receivedMessages - } catch (_e) { - throw new Error(`Invalid content in ${this.filePath}.ipc`) - } - } else { - return [] - } - } - - /** - * Send data using the file-based IPC. - * - * Will append to file in `this.filePath` if it exists, otherwise create a new one - * - * @param data - Data to be sent. - */ - async sendData(data: string): Promise { - // Create directory if it doesn't exist - await Deno.mkdir(this.dirPath, { recursive: true }) - - try { - const fileContent = await Deno.readTextFile(this.filePath).catch(() => "") - const messages = JSON.parse(fileContent || "[]") - messages.push({ pid: Deno.pid, data, sent: new Date().toISOString() }) - await Deno.writeTextFile(this.filePath, JSON.stringify(messages), { create: true }) - } catch (_e) { - console.error("Error sending data, read or write failed.") - } - } - - async *receiveData(): AsyncGenerator { - if (!this.watcher) this.startWatching() - - while (!this.aborted) { - if (this.messageQueue.length > 0) { - const messages = this.messageQueue.shift() - if (messages) { - yield messages - } - } else { - await new Promise((resolve) => setTimeout(resolve, this.debounceTimeMs)) - } - } - } - - /** - * Close the file-based IPC and remove the IPC file. - */ - async close(leaveFile?: boolean): Promise { - // Flag as aborted - this.aborted = true - - // Stop watching - if (this.watcher) { - this.watcher.close() - } - // Try to remove file, ignore failure - if (!leaveFile) { - try { - await Deno.remove(this.filePath) - } catch (_e) { - // Ignore - } - } - } -} diff --git a/lib/common/restclient.ts b/lib/common/restclient.ts deleted file mode 100644 index 6e2a428..0000000 --- a/lib/common/restclient.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * A "standard" client for the Pup Rest API - * - * @file lib/common/restclient.ts - * @license MIT - */ - -export class RestClient { - private baseUrl: string // Declare the types - private token: string - - constructor(baseUrl: string, token: string) { - this.baseUrl = baseUrl - this.token = token - } - - get(path: string) { - return this.fetch(path, { method: "GET" }) - } - - // deno-lint-ignore no-explicit-any - post(path: string, data: any) { // 'any' for flexibility on the data type - return this.fetch(path, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: data ? JSON.stringify(data) : undefined, - }) - } - - /** - * @throws - */ - // deno-lint-ignore no-explicit-any - async fetch(path: string, options: RequestInit): Promise { - // Use RequestInit for options and allow 'any' for the response for now - const headers = { - "Authorization": "Bearer " + this.token, - ...options.headers, - } - const response = await fetch(this.baseUrl + path, { - ...options, - headers, - }) - - if (!response.ok) { - const errorText = `Request failed: ${response.statusText}` - throw new Error(errorText) - } - - return response - } -} diff --git a/lib/common/utils.ts b/lib/common/utils.ts deleted file mode 100644 index a7c8c7d..0000000 --- a/lib/common/utils.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Common utility functions for Pup - * - * @file lib/common/utils.ts - * @license MIT - */ - -import { isAbsolute, parse, resolve } from "@std/path" -import { cwd, mkdir } from "@cross/fs" - -export function toResolvedAbsolutePath(path: string, cwdInput?: string) { - const cwdToUse = cwdInput || cwd() - if (!isAbsolute(path)) { - return resolve(cwdToUse, path) - } else { - return resolve(path) - } -} - -/** - * Generate a temporary path for the instance of a given configuration file. - * @function - * @param {string} configFile - The path to the configuration file. - * @returns {string} The temporary path associated with the configuration file. - */ -export async function toTempPath(configFile: string) { - const resolvedPath = parse(toResolvedAbsolutePath(configFile)) - const tempPath = toResolvedAbsolutePath(`${resolvedPath.dir}/.pup/${resolvedPath.name}${resolvedPath.ext}-tmp`) - await mkdir(tempPath, { recursive: true }) - return tempPath -} - -/** - * Generate a persistent storage path for the instance started by a given configuration file. - * @function - * @param {string} configFile - The path to the configuration file. - * @returns {string} The persistent storage path associated with the configuration file. - */ -export async function toPersistentPath(configFile: string) { - const resolvedPath = parse(toResolvedAbsolutePath(configFile)) - const persistentStoragePath = resolve(`${resolvedPath.dir}/.pup/${resolvedPath.name}${resolvedPath.ext}-data`) - await mkdir(persistentStoragePath, { recursive: true }) - return persistentStoragePath -} diff --git a/lib/core/api.ts b/lib/core/api.ts index 025eb8f..bde676a 100644 --- a/lib/core/api.ts +++ b/lib/core/api.ts @@ -1,92 +1,15 @@ /** - * Classes and interfaces related to the programmatic api of Pup - * - * This is a DRAFT for api version 1 + * A common programmatic interface to the pup core exposing selected features while allowing internal changes, to be used by the rest client and similar features. * * @file lib/core/api.ts * @license MIT */ -import type { EventEmitter } from "../common/eventemitter.ts" +import type { EventEmitter } from "@pup/common/eventemitter" import type { LogEventData } from "./logger.ts" import type { Pup } from "./pup.ts" -import type { Configuration, ProcessLoggerConfiguration } from "./configuration.ts" -import type { ProcessState } from "./process.ts" -import { TelemetryData } from "../../telemetry.ts" - -export interface ApiPaths { - temporaryStorage?: string - persistentStorage?: string - configFilePath?: string -} - -export interface ApiProcessData { - status: ApiProcessInformation - config: ApiProcessConfiguration -} - -/** - * These interfaces are basically copies of the ones in pup core, - * but specific to the api, to make any incompabilities between the - * api and core apparent. - */ -interface ApiProcessInformation { - id: string - status: ProcessState - code?: number - signal?: string - pid?: number - started?: Date - exited?: Date - blocked?: boolean - restarts?: number - updated: Date - pendingRestartReason?: string - type: "cluster" | "process" | "worker" -} - -interface ApiClusterConfiguration { - instances?: number - commonPort?: number - startPort?: number - strategy?: string -} - -interface ApiProcessConfiguration { - id: string - cmd?: string - worker?: string[] - env?: Record - cwd?: string - cluster?: ApiClusterConfiguration - pidFile?: string - watch?: string[] - autostart?: boolean - cron?: string - timeout?: number - overrun?: boolean - logger?: ProcessLoggerConfiguration - restart?: string - restartDelayMs?: number - restartLimit?: number -} - -export interface ApiApplicationState { - pid: number - version: string - status: string - updated: string - started: string - memory: Deno.MemoryUsage - port: number - systemMemory: Deno.SystemMemoryInfo - loadAvg: number[] - osUptime: number - osRelease: string - denoVersion: { deno: string; v8: string; typescript: string } - type: string - processes: ApiProcessInformation[] -} +import type { Configuration } from "./configuration.ts" +import type { ApiApplicationState, ApiPaths, ApiProcessData, ApiTelemetryData } from "@pup/api-definitions" /** * Exposes selected features of pup to Plugins and APIs @@ -104,6 +27,8 @@ export class PupApi { configFilePath: pup.configFilePath, } } + + // State and configuration public getConfiguration(): Configuration { return this._pup.configuration } @@ -117,29 +42,48 @@ export class PupApi { return statuses } public applicationState(): ApiApplicationState { - return this._pup.status.applicationState(this._pup.allProcesses(), this._pup.port) + return this._pup.status.applicationState(this._pup.allProcesses(), this._pup.port) as ApiApplicationState } - public terminate(forceQuitMs: number) { + + // Global actions + public terminate(forceQuitMs: number): boolean { this._pup.terminate(forceQuitMs) + return true } - public start(id: string, reason: string) { - this._pup.start(id, reason) + + // Process actions + // - Amending process "all" + public start(id: string, reason: string): boolean { + const processesToStart = (id === "all") ? this.allProcessStates() : [this.allProcessStates().find((p) => p.status.id === id)] + const results = processesToStart.map((process) => this._pup.start(process!.status.id, reason)) + return results.filter((r) => r).length > 0 } - public restart(id: string, reason: string) { - this._pup.restart(id, reason) + public restart(id: string, reason: string): boolean { + const processesToStart = (id === "all") ? this.allProcessStates() : [this.allProcessStates().find((p) => p.status.id === id)] + const results = processesToStart.map((process) => this._pup.restart(process!.status.id, reason)) + return results.filter((r) => r).length > 0 } - public stop(id: string, reason: string) { - this._pup.stop(id, reason) + public async stop(id: string, reason: string): Promise { + const processesToStart = (id === "all") ? this.allProcessStates() : [this.allProcessStates().find((p) => p.status.id === id)] + const results = await Promise.all([processesToStart.map((process) => this._pup.stop(process!.status.id, reason))]) + return results.filter((r) => r).length > 0 } - public block(id: string, reason: string) { - this._pup.block(id, reason) + public block(id: string, reason: string): boolean { + const processesToStart = (id === "all") ? this.allProcessStates() : [this.allProcessStates().find((p) => p.status.id === id)] + const results = processesToStart.map((process) => this._pup.block(process!.status.id, reason)) + return results.filter((r) => r).length > 0 } - public unblock(id: string, reason: string) { - this._pup.unblock(id, reason) + public unblock(id: string, reason: string): boolean { + const processesToStart = (id === "all") ? this.allProcessStates() : [this.allProcessStates().find((p) => p.status.id === id)] + const results = processesToStart.map((process) => this._pup.unblock(process!.status.id, reason)) + return results.filter((r) => r).length > 0 } - public telemetry(data: TelemetryData): boolean { + + // Interface for Pup to receive telemetry data from processes + public telemetry(data: ApiTelemetryData): boolean { return this._pup.telemetry(data) } + public log(severity: "log" | "error" | "info" | "warn", consumer: string, message: string) { this._pup.logger[severity](`api-${consumer}`, message) } diff --git a/lib/core/cluster.ts b/lib/core/cluster.ts index 3bc4e50..8bdf86b 100644 --- a/lib/core/cluster.ts +++ b/lib/core/cluster.ts @@ -7,7 +7,8 @@ * @license MIT */ -import { Process, type ProcessInformation, ProcessState } from "./process.ts" +import { Process, type ProcessInformation } from "./process.ts" +import { ApiProcessState } from "@pup/api-definitions" import { LOAD_BALANCER_DEFAULT_VALIDATION_INTERVAL_S, type ProcessConfiguration } from "./configuration.ts" import type { Pup } from "./pup.ts" import { BalancingStrategy, type LoadBalancerStartOperation } from "./loadbalancer.ts" @@ -143,7 +144,7 @@ class Cluster extends Process { public getStatus(): ProcessInformation { const clusterStatus: ProcessInformation = { id: this.getConfig().id, - status: ProcessState.CREATED, + status: ApiProcessState.CREATED, blocked: false, updated: new Date(), type: "cluster", @@ -160,7 +161,7 @@ class Cluster extends Process { clusterStatus.status = Array.from(uniqueStatuses)[0] } else { // Instances have varying statuses - clusterStatus.status = ProcessState.MIXED + clusterStatus.status = ApiProcessState.MIXED } // Set blocked flag if all children are blocked diff --git a/lib/common/port.ts b/lib/core/port.ts similarity index 81% rename from lib/common/port.ts rename to lib/core/port.ts index 8cd9794..18c29f7 100644 --- a/lib/common/port.ts +++ b/lib/core/port.ts @@ -30,12 +30,17 @@ interface FindFreePortOptions { // deno-lint-ignore require-await async function isPortAvailable(port: number) { return new Promise((resolve) => { - const server = createServer() - server.on("error", () => resolve(false)) - server.listen(port, () => { - server.close() - resolve(true) - }) + try { + const server = createServer() + server.on("error", () => resolve(false)) + server.listen(port, () => { + server.close() + // Allow some time before returning that the port is free + setTimeout(() => resolve(true), 10) + }) + } catch (_e) { + resolve(false) + } }) } diff --git a/lib/core/process.ts b/lib/core/process.ts index fa824ad..89d9578 100644 --- a/lib/core/process.ts +++ b/lib/core/process.ts @@ -10,36 +10,21 @@ import { Runner } from "./runner.ts" import { WorkerRunner } from "./worker.ts" import type { ProcessConfiguration } from "./configuration.ts" import { Watcher } from "./watcher.ts" -import type { TelemetryData } from "../../telemetry.ts" +import type { ApiTelemetryData } from "@pup/api-definitions" import { Cron } from "@hexagon/croner" import { delay } from "@std/async" -/** - * Represents the state of a process in Pup. - * - * NEVER change or delete any existing mapping, - * just add new ones. - */ -enum ProcessState { - CREATED = 0, - STARTING = 100, - RUNNING = 200, - STOPPING = 250, - FINISHED = 300, - ERRORED = 400, - EXHAUSTED = 450, - MIXED = 500, // Used for clusters with instances of varying modes -} +import { ApiProcessState } from "@pup/api-definitions" interface ProcessStateChangedEvent { - old?: ProcessState - new?: ProcessState + old?: ApiProcessState + new?: ApiProcessState status: ProcessInformation } interface ProcessScheduledEvent { - next?: ProcessState + next?: ApiProcessState status: ProcessInformation } @@ -50,7 +35,7 @@ interface ProcessWatchEvent { interface ProcessInformation { id: string - status: ProcessState + status: ApiProcessState code?: number signal?: string pid?: number @@ -60,7 +45,7 @@ interface ProcessInformation { restarts?: number updated: Date pendingRestartReason?: string - telemetry?: TelemetryData + telemetry?: ApiTelemetryData type: "cluster" | "process" | "worker" } @@ -81,7 +66,7 @@ class Process { private cronTerminateJob?: Cron // Status - private status: ProcessState = ProcessState.CREATED + private status: ApiProcessState = ApiProcessState.CREATED private pid?: number private code?: number private signal?: string @@ -90,7 +75,7 @@ class Process { private restarts = 0 private updated: Date = new Date() private pendingRestartReason?: string - private telemetry?: TelemetryData + private telemetry?: ApiTelemetryData private watcher?: Watcher constructor(pup: Pup, config: ProcessConfiguration) { @@ -98,11 +83,11 @@ class Process { this.pup = pup } - public setTelemetry(t: TelemetryData) { + public setTelemetry(t: ApiTelemetryData) { this.telemetry = t } - private setStatus(s: ProcessState) { + private setStatus(s: ApiProcessState) { const oldVal = this.status this.status = s this.updated = new Date() @@ -176,7 +161,7 @@ class Process { } // Do not start if running and overrun isn't enabled - if (this.status === ProcessState.RUNNING && !this.config.overrun) { + if (this.status === ApiProcessState.RUNNING && !this.config.overrun) { logger.log("blocked", `Process still running, refusing to start`, this.config) return } @@ -184,14 +169,14 @@ class Process { // Do not restart if maximum number of restarts are exhausted and reason is restart if (this.restarts >= (this.config.restartLimit ?? Infinity) && restart) { logger.log("exhausted", `Maximum number of starts exhausted, refusing to start`, this.config) - this.setStatus(ProcessState.EXHAUSTED) + this.setStatus(ApiProcessState.EXHAUSTED) return } logger.log("starting", `Process starting, reason: ${reason}`, this.config) // Update status - this.setStatus(ProcessState.STARTING) + this.setStatus(ApiProcessState.STARTING) this.pid = undefined this.code = undefined this.signal = undefined @@ -221,7 +206,7 @@ class Process { this.pendingRestartReason = undefined const result = await this.runner.run((pid?: number) => { // Process started - this.setStatus(ProcessState.RUNNING) + this.setStatus(ApiProcessState.RUNNING) this.pid = pid this.started = new Date() }) @@ -233,14 +218,14 @@ class Process { * Exited - Update status */ if (result.code === 0) { - this.setStatus(ProcessState.FINISHED) + this.setStatus(ApiProcessState.FINISHED) logger.log("finished", `Process finished with code ${result.code}`, this.config) /** * Forcefully stopped */ } else if (result.code === 124) { - this.setStatus(ProcessState.FINISHED) + this.setStatus(ApiProcessState.FINISHED) logger.log("finished", `Process manually stopped with code ${result.code}`, this.config) /** @@ -249,13 +234,13 @@ class Process { * Treat all exit codes except 0 as errors */ } else { - this.setStatus(ProcessState.ERRORED) + this.setStatus(ApiProcessState.ERRORED) logger.log("errored", `Process exited with code ${result.code}`, this.config) } } catch (e) { this.code = 1 this.signal = undefined - this.setStatus(ProcessState.ERRORED) + this.setStatus(ApiProcessState.ERRORED) logger.log("errored", `Process exited with error: ${e}`, this.config) } @@ -279,7 +264,7 @@ class Process { return false } - this.setStatus(ProcessState.STOPPING) + this.setStatus(ApiProcessState.STOPPING) const abortTimers = new AbortController() // Stop process after `terminateGracePeriod` @@ -313,7 +298,7 @@ class Process { // Using `any` because event payload is not typed yet // deno-lint-ignore no-explicit-any const onFinish = (ev: any) => { - if (ev.status?.pid == this.getStatus().pid && [ProcessState.FINISHED, ProcessState.EXHAUSTED, ProcessState.ERRORED].includes(this.status)) { + if (ev.status?.pid == this.getStatus().pid && [ApiProcessState.FINISHED, ApiProcessState.EXHAUSTED, ApiProcessState.ERRORED].includes(this.status)) { abortTimers.abort() this.pup.events.off("process_status_changed", onFinish) // ToDo, resolve to whatever `killRunner()` returns, which is currently unavailable inside the `process_status_changed` event, so it's fixed to `true` by now @@ -336,7 +321,7 @@ class Process { if (this.runner) { this.runner.kill(signal) this.pup.logger.log("stop", `Process stopped, reason: ${reason}`, this.config) - this.setStatus(ProcessState.STOPPING) + this.setStatus(ApiProcessState.STOPPING) this.restarts = 0 return true } @@ -447,5 +432,5 @@ class Process { public cleanup = () => {} } -export { Process, ProcessState } +export { ApiProcessState, Process } export type { ProcessInformation, ProcessScheduledEvent, ProcessStateChangedEvent, ProcessWatchEvent } diff --git a/lib/core/pup.ts b/lib/core/pup.ts index 2ba8734..dafe0fa 100644 --- a/lib/core/pup.ts +++ b/lib/core/pup.ts @@ -16,16 +16,17 @@ import { WATCHDOG_INTERVAL_MS, } from "./configuration.ts" import { Logger } from "./logger.ts" -import { Process, ProcessState } from "./process.ts" +import { Process } from "./process.ts" +import { ApiProcessState } from "@pup/api-definitions" import { Status } from "./status.ts" import { Cluster } from "./cluster.ts" import { RestApi } from "./rest.ts" -import { EventEmitter } from "../common/eventemitter.ts" -import { toPersistentPath, toResolvedAbsolutePath, toTempPath } from "../common/utils.ts" +import { EventEmitter } from "@pup/common/eventemitter" +import { toPersistentPath, toResolvedAbsolutePath, toTempPath } from "@pup/common/path" import { Prop } from "../common/prop.ts" -import { TelemetryData } from "../../telemetry.ts" +import type { ApiTelemetryData } from "@pup/api-definitions" import { rm } from "@cross/fs" -import { findFreePort } from "../common/port.ts" +import { findFreePort } from "./port.ts" interface InstructionResponse { success: boolean @@ -186,7 +187,7 @@ class Pup { * * @private */ - public telemetry(data: TelemetryData): boolean { + public telemetry(data: ApiTelemetryData): boolean { let success = false if (data.sender && typeof data.sender === "string") { const cleanedId = data.sender.trim().toLocaleLowerCase() @@ -216,17 +217,17 @@ class Pup { const config = process.getConfig() // Handle initial starts - if (config.autostart && status.status === ProcessState.CREATED) { + if (config.autostart && status.status === ApiProcessState.CREATED) { process.start("autostart") } // Handle pending restart - if (status.status !== ProcessState.STOPPING && process.isPendingRestart()) { + if (status.status !== ApiProcessState.STOPPING && process.isPendingRestart()) { process.start(process["pendingRestartReason"]) } // Handle restarts - if (status.status === ProcessState.FINISHED || status.status === ProcessState.ERRORED) { + if (status.status === ApiProcessState.FINISHED || status.status === ApiProcessState.ERRORED) { const msSinceExited = status.exited ? (new Date().getTime() - status.exited?.getTime()) : Infinity // Default restart delay to 10000ms, except when watching @@ -240,10 +241,10 @@ class Pup { if (restartPolicy === "always") { process.start("restart", true) - /* Restart on error if ProcessState is ERRORED */ + /* Restart on error if ApiProcessState is ERRORED */ } else if ( restartPolicy === "error" && - status.status === ProcessState.ERRORED + status.status === ApiProcessState.ERRORED ) { process.start("restart", true) } @@ -251,7 +252,7 @@ class Pup { } // Handle timeouts - if (status.status === ProcessState.RUNNING && config.timeout && status.started) { + if (status.status === ApiProcessState.RUNNING && config.timeout && status.started) { const secondsSinceStart = (new Date().getTime() - status.started.getTime()) / 1000 if (secondsSinceStart > config.timeout) { process.stop("timeout") @@ -291,7 +292,7 @@ class Pup { const secret = await this.secret?.load() if (!secret) return - const port = await this.port?.loadOrGenerate(async () => { + const port = await this.port?.generate(async () => { const resultingPort = this.configuration.api?.port || await findFreePort() return resultingPort.toString() }) diff --git a/lib/core/rest.ts b/lib/core/rest.ts index 1f06b5d..41a70d5 100644 --- a/lib/core/rest.ts +++ b/lib/core/rest.ts @@ -1,3 +1,9 @@ +/** + * Guidelines: + * - All routes should be wrapped in try/catch + * - All communication with Pup should go through the programmatic api (PupApi) + */ + import { Application, Context, Router, Status } from "@oak/oak" import { PupApi } from "./api.ts" import { Pup } from "./pup.ts" @@ -7,6 +13,11 @@ import { ValidateToken } from "../common/token.ts" const ALLOWED_SEVERITIES = ["log", "info", "warn", "error"] +export interface ApiResponseBody { + error?: string + data?: unknown +} + function isAllowedSeverity(severity: string): boolean { return ALLOWED_SEVERITIES.includes(severity.toLowerCase()) } @@ -17,13 +28,13 @@ const generateAuthMiddleware = (key: CryptoKey, revoked?: string[]) => { const authorization = headers.get("Authorization") if (!authorization) { ctx.response.status = Status.Unauthorized - ctx.response.body = { message: "Authorization header required" } + ctx.response.body = { error: "Authorization header required" } return } const parts = authorization.split(" ") if (parts.length !== 2 || parts[0] !== "Bearer") { ctx.response.status = Status.Unauthorized - ctx.response.body = { message: "Invalid authorization format" } + ctx.response.body = { error: "Invalid authorization format" } return } const token = parts[1] @@ -33,21 +44,21 @@ const generateAuthMiddleware = (key: CryptoKey, revoked?: string[]) => { if (payload.data?.consumer) { if (revoked && revoked.find((r) => r.toLowerCase().trim() === payload.data?.consumer.toLowerCase().trim())) { ctx.response.status = Status.Unauthorized - ctx.response.body = { message: "Invalid token" } + ctx.response.body = { error: "Invalid token" } } else { await next() } } else { ctx.response.status = Status.Unauthorized - ctx.response.body = { message: "Invalid token" } + ctx.response.body = { error: "Invalid token" } } } else { ctx.response.status = Status.Unauthorized - ctx.response.body = { message: "Invalid/expired token" } + ctx.response.body = { error: "Invalid/expired token" } } } catch (_err) { ctx.response.status = Status.Unauthorized - ctx.response.body = { message: "Invalid token" } + ctx.response.body = { error: "Invalid token" } } } } @@ -91,9 +102,6 @@ export class RestApi { ws.send(JSON.stringify({ t: "log", d: d })) } this.pupApi.events.on("log", proxyFn) - /*ws.onopen = () => { - ws.send("Hello from server!") - }*/ ws.onmessage = (m) => { ws.send(m.data as string) } @@ -105,25 +113,51 @@ export class RestApi { // Process related routes this.router .get("/processes", (ctx) => { - ctx.response.body = this.pupApi.allProcessStates() + try { + ctx.response.body = { + data: this.pupApi.allProcessStates(), + } + ctx.response.status = Status.OK + } catch (err) { + ctx.response.status = Status.InternalServerError + ctx.response.body = { error: err.message } + } }) .get("/state", (ctx) => { - ctx.response.body = this.pupApi.applicationState() + try { + ctx.response.body = { + data: this.pupApi.applicationState(), + } + ctx.response.status = Status.OK + } catch (err) { + ctx.response.status = Status.InternalServerError + ctx.response.body = { error: err.message } + } }) .post("/processes/:id/start", (ctx) => { const id = ctx.params.id try { - this.pupApi.start(id, "REST API request") + if (this.pupApi.start(id, "REST API request")) { + ctx.response.status = Status.OK + } else { + ctx.response.status = Status.InternalServerError + ctx.response.body = { error: "Action could not be carried out, check the arguments." } + } ctx.response.status = Status.OK } catch (err) { ctx.response.status = Status.InternalServerError ctx.response.body = { error: err.message } } }) - .post("/processes/:id/stop", (ctx) => { + .post("/processes/:id/stop", async (ctx) => { const id = ctx.params.id try { - this.pupApi.stop(id, "REST API request") + if (await this.pupApi.stop(id, "REST API request")) { + ctx.response.status = Status.OK + } else { + ctx.response.status = Status.InternalServerError + ctx.response.body = { error: "Action could not be carried out, check the arguments." } + } ctx.response.status = Status.OK } catch (err) { ctx.response.status = Status.InternalServerError @@ -133,7 +167,12 @@ export class RestApi { .post("/processes/:id/restart", (ctx) => { const id = ctx.params.id try { - this.pupApi.restart(id, "REST API request") + if (this.pupApi.restart(id, "REST API request")) { + ctx.response.status = Status.OK + } else { + ctx.response.status = Status.InternalServerError + ctx.response.body = { error: "Action could not be carried out, check the arguments." } + } ctx.response.status = Status.OK } catch (err) { ctx.response.status = Status.InternalServerError @@ -143,7 +182,12 @@ export class RestApi { .post("/processes/:id/block", (ctx) => { const id = ctx.params.id try { - this.pupApi.block(id, "REST API request") + if (this.pupApi.block(id, "REST API request")) { + ctx.response.status = Status.OK + } else { + ctx.response.status = Status.InternalServerError + ctx.response.body = { error: "Action could not be carried out, check the arguments." } + } ctx.response.status = Status.OK } catch (err) { ctx.response.status = Status.InternalServerError @@ -153,7 +197,12 @@ export class RestApi { .post("/processes/:id/unblock", (ctx) => { const id = ctx.params.id try { - this.pupApi.unblock(id, "REST API request") + if (this.pupApi.unblock(id, "REST API request")) { + ctx.response.status = Status.OK + } else { + ctx.response.status = Status.InternalServerError + ctx.response.body = { error: "Action could not be carried out, check the arguments." } + } ctx.response.status = Status.OK } catch (err) { ctx.response.status = Status.InternalServerError @@ -178,15 +227,13 @@ export class RestApi { } }) .post("/terminate", (ctx) => { - // Add logic to read forceQuitMs from the request body if needed - const forceQuitMs = 3000 // Example value try { + this.pupApi.terminate(30000) ctx.response.status = Status.OK } catch (err) { ctx.response.status = Status.InternalServerError ctx.response.body = { error: err.message } } - this.pupApi.terminate(forceQuitMs) }) .post("/log", async (ctx) => { try { @@ -264,7 +311,7 @@ export class RestApi { logContents = logContents.filter((log) => log.severity.toLowerCase() === severityLower) } - context.response.body = logContents + context.response.body = { data: logContents } } catch (error) { context.response.status = 500 context.response.body = { error: "Internal Server Error", message: error.message } @@ -273,14 +320,13 @@ export class RestApi { } public async start(): Promise { - const port = this.port this.app.use(generateAuthMiddleware(await this.setupKey(), this.pupApi.getConfiguration().api?.revoked)) this.app.use(this.router.routes()) this.app.use(this.router.allowedMethods()) - this.pupApi.log("info", "rest", `Starting the Rest API on ${this.hostname}:${this.port}`) - await this.app.listen({ port, hostname: this.hostname, signal: this.appAbortController.signal }) - this.pupApi.log("info", "rest", `Rest API listening on port ${this.port}`) - return port + this.pupApi.log("info", "rest", `Starting the Rest API`) + await this.app.listen({ port: this.port, hostname: this.hostname, signal: this.appAbortController.signal }) + this.pupApi.log("info", "rest", `Rest API running, available on ${this.hostname}:${this.port}`) + return this.port } public terminate() { diff --git a/lib/core/status.ts b/lib/core/status.ts index 9962780..bf45609 100644 --- a/lib/core/status.ts +++ b/lib/core/status.ts @@ -8,7 +8,9 @@ import { Application } from "../../application.meta.ts" import type { Cluster } from "./cluster.ts" import { APPLICATION_STATE_WRITE_LIMIT_MS } from "./configuration.ts" -import { type Process, type ProcessInformation, ProcessState } from "./process.ts" +import { type Process, type ProcessInformation } from "./process.ts" +import { ApiProcessState } from "@pup/api-definitions" + import { Prop } from "../common/prop.ts" const started = new Date() @@ -141,7 +143,7 @@ class Status { return { pid: Deno.pid, version: Application.version, - status: ProcessState[ProcessState.RUNNING], + status: ApiProcessState[ApiProcessState.RUNNING], updated: new Date().toISOString(), started: started.toISOString(), memory: Deno.memoryUsage(), diff --git a/telemetry.ts b/telemetry.ts deleted file mode 100644 index c7b0b2e..0000000 --- a/telemetry.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Optional entrypoint for Pup client processes written in Deno, which periodically sends - * memory usage and current working directory to the main Pup process. - * - * Usage: - * - * // Early in your application entrypoint - pin to a specific version similar - * // to what your main process use, like pup@1.0.0-alpha-28 - * import { PupTelemetry } from "jsr:@pup/pup/telemetry.ts" - * const telemetry = PupTelemetry() - * - * // The rest of your application - * console.log("Hello World!") - * - * // As PupTelemetry uses the singleton pattern, you can now use the same instance - * // anywhere in your application - * const telemetry = PupTelemetry() - * - * // To receive messages from another process, use - * telemetry.on('event_name', (event) => { console.log(event) }); - * - * // To send messages to another process, use - * telemetry.emit('target-process-id', 'event_name', { any: { event: "data" }} ); - * - * // To stop the telemetry and allow the process to exit, use the following: - * telemetry.stop() - * - * @file telemetry.ts - * @license MIT - */ - -import { EventEmitter, type EventHandler } from "./lib/common/eventemitter.ts" -import { FileIPC } from "./lib/common/ipc.ts" -import { isDir } from "@cross/fs" -import { getEnv } from "@cross/env" -import { RestClient } from "./lib/common/restclient.ts" - -export interface TelemetryData { - sender?: string - memory: Deno.MemoryUsage - sent: string - cwd: string -} - -export class PupTelemetry { - private static instance: PupTelemetry - - private events: EventEmitter = new EventEmitter() - - private intervalSeconds = 15 - - private timer?: number - private aborted = false - private ipc?: FileIPC - - /** - * PupTelemetry singleton instance. - * The `new` keyword is optional. - * @param intervalSeconds - The interval in seconds between telemetry data transmissions (default: 15). - * Value is clamped between 1 and 180 seconds. - */ - constructor(intervalSeconds = 5) { - // Use as a factory if called without the keyword `new` - if (!(this instanceof PupTelemetry)) { - return new PupTelemetry(intervalSeconds) - } - - // Re-use existing instance (singleton pattern) - if (PupTelemetry.instance) { - return PupTelemetry.instance - } - - // Set instance to the newly created object (singleton pattern) - PupTelemetry.instance = this - - // Clamp intervalSeconds between 1 and 180 seconds before storing - if (!intervalSeconds || intervalSeconds < 1) intervalSeconds = 1 - if (intervalSeconds > 180) intervalSeconds = 180 - this.intervalSeconds = intervalSeconds - // Start the watchdog - this.telemetryWatchdog() - // Start the IPC - this.checkIpc() - } - - /** - * Main telemetry data is sent back to the main process using the rest API - */ - private sendMainTelemetry() { - const pupProcessId = getEnv("PUP_PROCESS_ID") - if (pupProcessId) { - const data: TelemetryData = { - sender: pupProcessId, - memory: Deno.memoryUsage(), - sent: new Date().toISOString(), - cwd: Deno.cwd(), - } - this.emit("main", "telemetry", data) - } else { - // Ignore, process not run by Pup? - } - } - - private async checkIpc() { - const pupTempPath = getEnv("PUP_TEMP_STORAGE") - const pupProcessId = getEnv("PUP_PROCESS_ID") - - if (pupTempPath && (await isDir(pupTempPath)) && pupProcessId) { - const ipcPath = `${pupTempPath}/.${pupProcessId}.ipc` // Process-specific IPC path - // Break out of the loop if aborted - if (!this.aborted) { - this.ipc = new FileIPC(ipcPath) - - // Read incoming messages - for await (const messages of this.ipc.receiveData()) { - // Break out of the loop if aborted - if (this.aborted) break - - if (messages.length > 0) { - // Process messages and emit events - for (const message of messages) { - try { - if (message.data) { - const parsedMessage = JSON.parse(message.data) - this.events.emit(parsedMessage.event, parsedMessage.eventData) - } - } catch (_e) { - // Ignore errors in message parsing and processing - } - } - } - } - } - } - } - - /** - * The watchdog is guarded by a try/catch block and recursed by a unrefed - * timer to prevent the watchdog from keeping a process alive. - */ - private async telemetryWatchdog() { - try { - await this.sendMainTelemetry() - } catch (_e) { - // Ignore errors - } finally { - clearTimeout(this.timer) - if (!this.aborted) { - this.timer = setTimeout( - () => this.telemetryWatchdog(), - this.intervalSeconds * 1000, - ) - Deno.unrefTimer(this.timer) - } - } - } - - on(event: string, fn: EventHandler) { - this.events.on(event, fn) - } - - off(event: string, fn: EventHandler) { - this.events.off(event, fn) - } - - async emit(targetProcessId: string, event: string, eventData?: T) { - // If target is main (pup host process, use the secure rest api), for child-process to child-process - // use the file based bus - if (targetProcessId === "main") { - try { - const pupApiHostname = getEnv("PUP_API_HOSTNAME") - const pupApiPort = getEnv("PUP_API_PORT") - const pupApiToken = getEnv("PUP_API_TOKEN") - if (pupApiHostname && pupApiPort && pupApiToken) { - // Send api request - const apiBaseUrl = `http://${pupApiHostname}:${pupApiPort}` - const client = new RestClient(apiBaseUrl, pupApiToken) - client.post("/telemetry", eventData) - } - } catch (_e) { - console.error(_e) - } - } else { - const pupTempPath = getEnv("PUP_TEMP_STORAGE") - if (pupTempPath && (await isDir(pupTempPath)) && targetProcessId) { - const ipcPath = `${pupTempPath}/.${targetProcessId}.ipc` // Target process IPC path - - // Create a temporary IPC to send the message - const ipc = new FileIPC(ipcPath) - - // Create the message with event and eventData - const message = { event, eventData } - - // Send the message to the target process - try { - await ipc.sendData(JSON.stringify(message)) - } finally { - // Close the temporary IPC - ipc.close(true) - } - } else { - // Ignore, process not run by Pup? - } - } - } - - close() { - this.aborted = true - - if (this.timer) { - clearTimeout(this.timer) - } - - if (this.ipc) { - this.ipc.close() - } - - this.events.close() - } -} diff --git a/test/cli/args.test.ts b/test/cli/args.test.ts index e8b0dd0..076c09d 100644 --- a/test/cli/args.test.ts +++ b/test/cli/args.test.ts @@ -1,8 +1,5 @@ import { checkArguments, parseArguments } from "../../lib/cli/args.ts" import { assertEquals, assertThrows } from "@std/assert" -/*import { spy } from "@std/testing/mock" -import { Application } from "../../application.meta.ts" -import { printHeader, printUsage } from "../../lib/cli/output.ts"*/ import { ArgsParser } from "@cross/utils" import { test } from "@cross/test" @@ -145,221 +142,3 @@ test("checkArguments should throw error when both --cmd and -- is specified", () "'--cmd', '--worker' and '--' cannot be used at the same time.", ) }) - -/* -test("checkArguments should throw error when id argument is missing with init or append argument", async () => { - const args = { _: ["init"], cmd: "command" } - await assertThrows( - () => { - checkArguments(args) - }, - Error, - "Arguments 'init', 'append', and 'remove' require '--id'", - ) -}) - -test("checkArguments should throw error when id argument is missing with init or append argument", async () => { - const args = { _: ["append"], cmd: "command" } - await assertThrows( - () => { - checkArguments(args) - }, - Error, - "Arguments 'init', 'append', and 'remove' require '--id'", - ) -}) - -test("checkArguments should throw error when id argument is missing with init or remove argument", async () => { - const args = { _: ["remove"], cmd: "command" } - await assertThrows( - () => { - checkArguments(args) - }, - Error, - "Arguments 'init', 'append', and 'remove' require '--id'", - ) -}) - -test("printHeader should output the name, version, and repository of the application", () => { - const expectedName = "pup" - const expectedRepository = "https://github.com/hexagon/pup" - const consoleSpy = spy(console, "log") - printHeader() - assertEquals( - consoleSpy.calls[0].args[0], - `${expectedName} ${Application.version}\n${expectedRepository}`, - ) - consoleSpy.restore() -}) - -test("printUsage should output the usage of the application", () => { - const consoleSpy = spy(console, "log") - printUsage() - const expectedOutput = `Usage: ${Application.name} [OPTIONS...]` - assertEquals(consoleSpy.calls[0].args[0], expectedOutput) - consoleSpy.restore() -}) - -test("checkArguments should return the provided arguments when they are valid", () => { - const expectedArgs = { - _: ["init"], - cmd: "command", - id: "taskId", - } - const result = checkArguments(expectedArgs) - assertEquals(result, expectedArgs) -}) - -test("checkArguments should throw error when --env argument is provided without service install", async () => { - const args = { _: [], env: "NODE_ENV=production" } - await assertThrows( - () => { - checkArguments(args) - }, - Error, - "Argument '--env' can only be used with 'service install' base argument", - ) -}) - -test("checkArguments should return the provided arguments when service install and --env are used together", () => { - const expectedArgs = { - _: ["install"], - env: "NODE_ENV=production", - } - const result = checkArguments(expectedArgs) - assertEquals(result, expectedArgs) -}) - -test("Collect env arguments formatted as KEY=VALUE", () => { - const inputArgs = [ - "--env", - "KEY1=VALUE1", - "--env", - "KEY2=VALUE2", - ] - const parsedArgs = parseArguments(inputArgs) - const expectedArgs = { - env: ["KEY1=VALUE1", "KEY2=VALUE2"], - e: ["KEY1=VALUE1", "KEY2=VALUE2"], - - // All boolean options will be included in output too - help: false, - h: false, - autostart: false, - A: false, - "dry-run": false, - setup: false, - upgrade: false, - update: false, - - // Unspecified string options will not be included - _: [], - "--": [], - } - assertEquals(parsedArgs, expectedArgs) -}) - -test("checkArguments should throw error when both --cmd and --worker are specified", async () => { - const args = { _: [], cmd: "command", worker: "worker_script", init: true, id: "test" } - await assertThrows( - () => { - checkArguments(args) - }, - Error, - "'--cmd', '--worker' and '--' cannot be used at the same time.", - ) -}) - -test("checkArguments should allow both --cwd and --id when used together", () => { - const args = { _: ["init"], cmd: "command", id: "test", cwd: "cwd" } - const result = checkArguments(args) - assertEquals(result, args) -}) - -test("checkArguments should allow --terminate when used with --worker", () => { - const args = { _: ["init"], worker: "worker_script", id: "test", terminate: "terminate" } - const result = checkArguments(args) - assertEquals(result, args) -}) - -test("checkArguments should allow --watch when used with --worker", () => { - const args = { _: ["init"], worker: "worker_script", id: "test", watch: "watched.ts" } - const result = checkArguments(args) - assertEquals(result, args) -}) - -test("checkArguments should allow --instances when used with init", () => { - const args = { _: ["init"], cmd: "command", id: "test", instances: 2 } - const result = checkArguments(args) - assertEquals(result, args) -}) - -test("checkArguments should allow --instances when used with append", () => { - const args = { _: ["append"], cmd: "command", id: "test", instances: 2 } - const result = checkArguments(args) - assertEquals(result, args) -}) - -test("checkArguments should allow --instances when used with run", () => { - const args = { _: ["run"], cmd: "command", instances: 2 } - const result = checkArguments(args) - assertEquals(result, args) -}) - -test("checkArguments should allow --start-port when used with init", () => { - const args = { _: ["init"], cmd: "command", id: "test", startPort: 3000 } - const result = checkArguments(args) - assertEquals(result, args) -}) - -test("checkArguments should allow --start-port when used with append", () => { - const args = { _: ["append"], cmd: "command", id: "test", startPort: 3000 } - const result = checkArguments(args) - assertEquals(result, args) -}) - -test("checkArguments should allow --start-port when used with run", () => { - const args = { _: ["run"], cmd: "command", startPort: 3000 } - const result = checkArguments(args) - assertEquals(result, args) -}) - -test("checkArguments should allow --instances and --start-port when used together", () => { - const args = { _: ["init"], cmd: "command", id: "test", instances: 2, startPort: 3000 } - const result = checkArguments(args) - assertEquals(result, args) -}) - -test("checkArguments should throw error when --start-port value is not a number", async () => { - const args = { _: ["init"], cmd: "command", id: "test", "start-port": "invalid" } - await assertThrows( - () => { - checkArguments(args) - }, - Error, - "Argument '--start-port' must be a numeric value", - ) -}) - -test("checkArguments should throw error when --instances value is not a number", async () => { - const args = { _: ["init"], cmd: "command", id: "test", instances: "invalid" } - await assertThrows( - () => { - checkArguments(args) - }, - Error, - "Argument '--instances' must be a numeric value", - ) -}) - -test("checkArguments should throw error when --common-port value is not a number", async () => { - const args = { _: ["init"], cmd: "command", id: "test", "common-port": "invalid" } - await assertThrows( - () => { - checkArguments(args) - }, - Error, - "Argument '--common-port' must be a numeric value", - ) -}) -*/ diff --git a/test/common/eventemitter.test.ts b/test/common/eventemitter.test.ts deleted file mode 100644 index 38f1b87..0000000 --- a/test/common/eventemitter.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { EventEmitter } from "../../lib/common/eventemitter.ts" -import { assert, assertEquals } from "@std/assert" -import { test } from "@cross/test" - -test("EventEmitter - Add and trigger event listener", () => { - const eventEmitter = new EventEmitter() - let called = false - - eventEmitter.on("test", () => { - called = true - }) - - eventEmitter.emit("test") - assert(called, "Event listener should be called") -}) - -test("EventEmitter - Trigger event listener with data", () => { - const eventEmitter = new EventEmitter() - let receivedData: string | undefined - - eventEmitter.on("test", (data) => { - receivedData = data - }) - - eventEmitter.emit("test", "Hello, World!") - assertEquals(receivedData, "Hello, World!", "Event listener should receive data") -}) - -test("EventEmitter - Remove event listener", () => { - const eventEmitter = new EventEmitter() - let called = false - - const listener = () => { - called = true - } - - eventEmitter.on("test", listener) - eventEmitter.off("test", listener) - eventEmitter.emit("test") - - assert(!called, "Event listener should not be called after being removed") -}) - -test("EventEmitter - Multiple listeners for same event", () => { - const eventEmitter = new EventEmitter() - let listener1Called = false - let listener2Called = false - - eventEmitter - .on("test", () => { - listener1Called = true - }) - eventEmitter - .on("test", () => { - listener2Called = true - }) - - eventEmitter.emit("test") - - assert(listener1Called, "Listener 1 should be called") - assert(listener2Called, "Listener 2 should be called") -}) - -test("EventEmitter - Multiple events with different listeners", () => { - const eventEmitter = new EventEmitter() - let testEventCalled = false - let anotherEventCalled = false - - eventEmitter - .on("test", () => { - testEventCalled = true - }) - eventEmitter - .on("another", () => { - anotherEventCalled = true - }) - - eventEmitter.emit("test") - eventEmitter.emit("another") - - assert(testEventCalled, "Test event listener should be called") - assert(anotherEventCalled, "Another event listener should be called") -}) diff --git a/test/common/ipc.test.ts b/test/common/ipc.test.ts deleted file mode 100644 index ff06b12..0000000 --- a/test/common/ipc.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { assertEquals } from "@std/assert" -import { FileIPC } from "../../lib/common/ipc.ts" -import { exists } from "@cross/fs" -import { test } from "@cross/test" - -const TEST_FILE_PATH = "./test_data_FileIPC.ipctest" -const TEST_STALE_LIMIT = 2000 - -test("FileIPC - sendData writes data to file", async () => { - const fileIPC = new FileIPC(TEST_FILE_PATH) - await fileIPC.sendData("test data") - const fileExistsResult = await exists(TEST_FILE_PATH) - assertEquals(fileExistsResult, true) - await fileIPC.close() -}) - -test("FileIPC - receiveData returns an array of ValidatedMessages", async () => { - const fileIPC = new FileIPC(TEST_FILE_PATH) - await fileIPC.sendData("test data") - for await (const receivedMessages of fileIPC.receiveData()) { - assertEquals(receivedMessages.length, 1) - assertEquals(receivedMessages[0].pid, Deno.pid) - assertEquals(receivedMessages[0].data, "test data") - assertEquals(receivedMessages[0].errors.length, 0) - await fileIPC.close() - } -}) - -test("FileIPC - receiveData removes stale messages", async () => { - const fileIPC = new FileIPC(TEST_FILE_PATH, TEST_STALE_LIMIT) - await fileIPC.sendData("test data 1") - await new Promise((resolve) => setTimeout(resolve, TEST_STALE_LIMIT + 100)) - await fileIPC.sendData("test data 2") - for await (const receivedMessages of fileIPC.receiveData()) { - assertEquals(receivedMessages.length, 2) - assertEquals(receivedMessages[0].pid, Deno.pid) - assertEquals(receivedMessages[0].data, null) - assertEquals(receivedMessages[0].errors.length, 1) - assertEquals(receivedMessages[0].errors[0], "Invalid data received: stale") - assertEquals(receivedMessages[1].pid, Deno.pid) - assertEquals(receivedMessages[1].data, "test data 2") - assertEquals(receivedMessages[1].errors.length, 0) - await fileIPC.close() - } -}) - -test("FileIPC - receiveData handles invalid messages", async () => { - const fileIPC = new FileIPC(TEST_FILE_PATH) - await fileIPC.sendData("test data") - await fileIPC.sendData("a".repeat(fileIPC.MAX_DATA_LENGTH + 1)) - for await (const receivedMessages of fileIPC.receiveData()) { - assertEquals(receivedMessages.length, 2) - assertEquals(receivedMessages[0].pid, Deno.pid) - assertEquals(receivedMessages[0].data, "test data") - assertEquals(receivedMessages[0].errors.length, 0) - assertEquals(receivedMessages[1].pid, Deno.pid) - assertEquals(receivedMessages[1].data, null) - assertEquals(receivedMessages[1].errors.length, 1) - assertEquals(receivedMessages[1].errors[0], "Invalid data received: too long") - await fileIPC.close() - } -}) - -test("FileIPC - close removes IPC file", async () => { - const fileIPC = new FileIPC(TEST_FILE_PATH) - await fileIPC.sendData("test data") - await fileIPC.close() - const fileExistsResult = await exists(TEST_FILE_PATH) - assertEquals(fileExistsResult, false) -}) - -test("FileIPC - close leaves IPC file when leaveFile option is true", async () => { - const fileIPC = new FileIPC(TEST_FILE_PATH) - await fileIPC.sendData("test data") - await fileIPC.close(true) - const fileExistsResult = await exists(TEST_FILE_PATH) - assertEquals(fileExistsResult, true) - await Deno.remove(TEST_FILE_PATH) -}) - -test("FileIPC - close leaves IPC file when leaveFile option is true", async () => { - const fileIPC = new FileIPC(TEST_FILE_PATH) - await fileIPC.sendData("test data") - await fileIPC.close(true) - const fileExistsResult = await exists(TEST_FILE_PATH) - assertEquals(fileExistsResult, true) - await Deno.remove(TEST_FILE_PATH) -}) - -test("FileIPC - close leaves IPC file when leaveFile option is true", async () => { - const fileIPC = new FileIPC(TEST_FILE_PATH) - await fileIPC.sendData("test data") - await fileIPC.close(true) - const fileExistsResult = await exists(TEST_FILE_PATH) - assertEquals(fileExistsResult, true) - await Deno.remove(TEST_FILE_PATH) -}) diff --git a/test/core/pup.test.ts b/test/core/pup.test.ts index 09c7231..aae4eed 100644 --- a/test/core/pup.test.ts +++ b/test/core/pup.test.ts @@ -5,7 +5,7 @@ */ import type { Configuration } from "../../lib/core/configuration.ts" -import { ProcessState } from "../../lib/core/process.ts" +import { ApiProcessState } from "@pup/api-definitions" import { Pup } from "../../lib/core/pup.ts" import { assertEquals, assertNotEquals } from "@std/assert" import { test } from "@cross/test" @@ -28,17 +28,17 @@ test("Create test process. Test start, block, stop, start, unblock, start in seq // Find process, assert existance const testProcess = pup.processes.findLast((p) => p.getConfig().id === TEST_PROCESS_ID) assertNotEquals(testProcess, undefined) - assertEquals(testProcess?.getStatus().status, ProcessState.CREATED) + assertEquals(testProcess?.getStatus().status, ApiProcessState.CREATED) // Start process, assert started const startResult = pup.start(TEST_PROCESS_ID, "test") assertEquals(startResult, true) - assertEquals(testProcess?.getStatus().status, ProcessState.STARTING) + assertEquals(testProcess?.getStatus().status, ApiProcessState.STARTING) // Stop process, assert stopped const stopResult = await pup.stop(TEST_PROCESS_ID, "test") assertEquals(stopResult, true) - assertEquals(testProcess?.getStatus().status, ProcessState.ERRORED) + assertEquals(testProcess?.getStatus().status, ApiProcessState.ERRORED) // Block process, assert blocked const blockResult = pup.block(TEST_PROCESS_ID, "test") @@ -48,7 +48,7 @@ test("Create test process. Test start, block, stop, start, unblock, start in seq // Start process, assert failed const startResult2 = pup.start(TEST_PROCESS_ID, "test") assertEquals(startResult2, false) - assertEquals(testProcess?.getStatus().status, ProcessState.ERRORED) + assertEquals(testProcess?.getStatus().status, ApiProcessState.ERRORED) // Unblock process, assert unblocked const unblockResult = pup.unblock(TEST_PROCESS_ID, "test") @@ -58,7 +58,7 @@ test("Create test process. Test start, block, stop, start, unblock, start in seq // Start process, assert started const startResult3 = pup.start(TEST_PROCESS_ID, "test") assertEquals(startResult3, true) - assertEquals(testProcess?.getStatus().status, ProcessState.STARTING) + assertEquals(testProcess?.getStatus().status, ApiProcessState.STARTING) // Terminate pup instantly await pup.terminate(0) @@ -85,17 +85,17 @@ test("Create test cluster. Test start, block, stop, start, unblock, start in seq // Find process, assert existance const testProcess = pup.processes.findLast((p) => p.getConfig().id === TEST_PROCESS_ID) assertNotEquals(testProcess, undefined) - assertEquals(testProcess?.getStatus().status, ProcessState.CREATED) + assertEquals(testProcess?.getStatus().status, ApiProcessState.CREATED) // Start process, assert started const startResult = pup.start(TEST_PROCESS_ID, "test") assertEquals(startResult, true) - assertEquals(testProcess?.getStatus().status, ProcessState.STARTING) + assertEquals(testProcess?.getStatus().status, ApiProcessState.STARTING) // Stop process, assert finished const stopResult = await pup.stop(TEST_PROCESS_ID, "test") assertEquals(stopResult, true) - assertEquals(testProcess?.getStatus().status, ProcessState.ERRORED) + assertEquals(testProcess?.getStatus().status, ApiProcessState.ERRORED) // Block process, assert blocked const blockResult = pup.block(TEST_PROCESS_ID, "test") @@ -105,7 +105,7 @@ test("Create test cluster. Test start, block, stop, start, unblock, start in seq // Start process, assert failed const startResult2 = pup.start(TEST_PROCESS_ID, "test") assertEquals(startResult2, false) - assertEquals(testProcess?.getStatus().status, ProcessState.ERRORED) + assertEquals(testProcess?.getStatus().status, ApiProcessState.ERRORED) // Unblock process, assert unblocked const unblockResult = pup.unblock(TEST_PROCESS_ID, "test") @@ -115,7 +115,7 @@ test("Create test cluster. Test start, block, stop, start, unblock, start in seq // Start process, assert started const startResult3 = pup.start(TEST_PROCESS_ID, "test") assertEquals(startResult3, true) - assertEquals(testProcess?.getStatus().status, ProcessState.STARTING) + assertEquals(testProcess?.getStatus().status, ApiProcessState.STARTING) // Terminate pup instantly await pup.terminate(0) diff --git a/test/main.test.ts b/test/main.test.ts deleted file mode 100644 index 4e7d291..0000000 --- a/test/main.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* -import { main } from "../lib/main.ts" -import { assertSpyCall, spy } from "@std/testing/mock" - -test("main: exit with --version flag", async () => { - const exitSpy = spy(Deno, "exit") - const args = ["--version"] - - await main(args) - - assertSpyCall(exitSpy, 0) - - exitSpy.restore() -}) - -test("main: exit with --help flag", async () => { - const exitSpy = spy(Deno, "exit") - const args = ["--help"] - - await main(args) - - assertSpyCall(exitSpy, 0) - - exitSpy.restore() -}) - -test("main: exit when no configuration file found", async () => { - const exitSpy = spy(Deno, "exit") - const originalFileExists = Deno.stat - - Deno.stat = () => { - throw new Deno.errors.NotFound("File not found") - } - const args: string[] = [] - - await main(args) - - assertSpyCall(exitSpy, 1) - - Deno.stat = originalFileExists -}) -*/ diff --git a/test/telemetry.test.ts b/test/telemetry.test.ts deleted file mode 100644 index 6a72a6e..0000000 --- a/test/telemetry.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -// deno-lint-ignore-file -import { assertEquals } from "@std/assert" -import { PupTelemetry } from "../telemetry.ts" -import { test } from "@cross/test" - -test("PupTelemetry - Singleton pattern", () => { - const telemetry1 = new PupTelemetry() - const telemetry2 = new PupTelemetry() - const telemetry3 = new PupTelemetry() - - assertEquals(telemetry1, telemetry2) - assertEquals(telemetry1, telemetry3) - - telemetry1.close() - telemetry2.close() - telemetry3.close() -}) - -// deno-lint-ignore require-await -test("PupTelemetry - Emitting messages", async () => { - const telemetry = new PupTelemetry() - const eventData = { test: "data" } - - // Mock FileIPC - class MockFileIPC { - // deno-lint-ignore require-await - async sendData(message: string) { - const parsedMessage = JSON.parse(message) - assertEquals(parsedMessage.testEvent, eventData) - } - } - - const originalFileIPC = (telemetry as any).FileIPC // deno-lint-ignore no-explicit-any - ;(telemetry as any).FileIPC = MockFileIPC - - telemetry.emit("main", "testEvent", eventData) // deno-lint-ignore no-explicit-any - ;(telemetry as any).FileIPC = originalFileIPC - - telemetry.close() -}) diff --git a/versions.json b/versions.json index 54fe2b6..55fade8 100644 --- a/versions.json +++ b/versions.json @@ -2,6 +2,20 @@ "canary_url": "https://raw.githubusercontent.com/Hexagon/pup/main/pup.ts", "stable": [], "prerelease": [ + { + "version": "1.0.0-rc.32", + "url": "jsr:@pup/pup@1.0.0-rc.32", + "deno": null, + "deno_unstable": "1.42.0", + "default_permissions": [ + "--allow-env", + "--allow-read", + "--allow-write", + "--allow-sys=loadavg,systemMemoryInfo,osUptime,osRelease,uid,gid", + "--allow-net", + "--allow-run" + ] + }, { "version": "1.0.0-rc.31", "url": "jsr:@pup/pup@1.0.0-rc.31",