Skip to content

Commit

Permalink
Various fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Hexagon committed Apr 21, 2024
1 parent 4d4f482 commit 56717ea
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 114 deletions.
5 changes: 4 additions & 1 deletion docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ All notable changes to this project will be documented in this section.
- feat(cli): Add cli command `monitor` for streaming logs
- change(plugins): Remove programmatic api and bundled plugins. Plugins will now be entirely separate from pup, and communicate through the Rest API.
- chore(core): Internal refactor getting closer to being runtime agnostic
- feat(cli): Add cli command `
- feat(cli): Add cli command `token`to generate new API tokens
- change(api): Expose configuration to the API
- change(core): Expose port in the application status
- fix(core): Fix `enable-service` in Windows by updating dependency `@cross/service`

## [1.0.0-rc.25] - 2023-04-17

Expand Down
25 changes: 18 additions & 7 deletions lib/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

// Import core dependencies
import { Pup } from "../core/pup.ts"
import { type Configuration, DEFAULT_REST_API_HOSTNAME, DEFAULT_REST_API_PORT, generateConfiguration, validateConfiguration } from "../core/configuration.ts"
import { type Configuration, DEFAULT_REST_API_HOSTNAME, generateConfiguration, validateConfiguration } from "../core/configuration.ts"

// Import CLI utilities
import { printFlags, printHeader, printUsage } from "./output.ts"
Expand All @@ -17,7 +17,7 @@ import { printStatus } from "./status.ts"
import { upgrade } from "./upgrade.ts"

// Import common utilities
import { toPersistentPath, toResolvedAbsolutePath } from "../common/utils.ts"
import { toPersistentPath, toResolvedAbsolutePath, toTempPath } from "../common/utils.ts"
import { exists, readFile } from "@cross/fs"

// Import external dependencies
Expand All @@ -30,11 +30,11 @@ import { args } from "@cross/utils/args"
import { installService, uninstallService } from "@cross/service"
import { Colors, exit } from "@cross/utils"
import { chdir, cwd } from "@cross/fs"
import { Secret } from "../core/secret.ts"
import { GenerateToken } from "../common/token.ts"
import { RestClient } from "../common/restclient.ts"
import { ApiApplicationState } from "../core/api.ts"
import { CurrentRuntime, Runtime } from "@cross/runtime"
import { Prop } from "../common/prop.ts"

/**
* Define the main entry point of the CLI application
Expand Down Expand Up @@ -170,16 +170,27 @@ async function main() {
checkedArgs.get("name"),
)
}
// Prepare API port
let port = configuration.api?.port
const portFile = `${await toTempPath(configFile as string)}/.main.port`
const portFileObj = new Prop(portFile)
if (!port) {
try {
const filePort = await portFileObj.load()
port = parseInt(filePort)
} catch (_e) { /* That's ok, there is no running instance. */ }
}

// Prepare secret file
let client
let token
let secret
if (useConfigFile) {
const secretFile = `${await toPersistentPath(configFile as string)}/.main.secret`
// Get secret
const secretInstance = new Secret(secretFile)
const secretInstance = new Prop(secretFile)
try {
secret = await secretInstance.loadOrGenerate()
secret = await secretInstance.load()
} catch (_e) {
console.error("Could not connect to instance, secret could not be read.")
return exit(1)
Expand All @@ -189,7 +200,7 @@ async function main() {
token = await GenerateToken(secret, { consumer: "cli" }, new Date().getTime() + 120_000)

// Send api request
const apiBaseUrl = `http://${configuration.api?.hostname || DEFAULT_REST_API_HOSTNAME}:${configuration.api?.port || DEFAULT_REST_API_PORT}`
const apiBaseUrl = `http://${configuration.api?.hostname || DEFAULT_REST_API_HOSTNAME}:${port}`
client = new RestClient(apiBaseUrl, token!)
}

Expand Down Expand Up @@ -344,7 +355,7 @@ async function main() {
*/
if (baseArgument === "monitor") {
const apiHostname = configuration.api?.hostname || DEFAULT_REST_API_HOSTNAME
const apiPort = configuration.api?.port || DEFAULT_REST_API_PORT
const apiPort = port
const wsUrl = `ws://${apiHostname}:${apiPort}/wss`
const wss = new WebSocketStream(wsUrl, {
headers: {
Expand Down
3 changes: 2 additions & 1 deletion lib/cli/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Colors } from "@cross/utils"
import { filesize } from "filesize"
import { blockedFormatter, codeFormatter, naFormatter, statusFormatter } from "./formatters/strings.ts"
import { timeagoFormatter } from "./formatters/times.ts"
import { Configuration } from "../core/configuration.ts"
import { Configuration, DEFAULT_REST_API_HOSTNAME } from "../core/configuration.ts"
import { resolve } from "@std/path"
import { ApiApplicationState } from "../core/api.ts"

Expand All @@ -31,6 +31,7 @@ export function printStatus(configFile: string, configuration: Configuration, cw
console.log(Colors.bold("Configuration:") + "\t" + resolve(configFile))
console.log(Colors.bold("Working dir:") + "\t" + cwd || "Not set (default: pup)")
console.log(Colors.bold("Instance name:") + "\t" + (configuration.name || "Not set"))
console.log(Colors.bold("Rest API URL:") + "\thttp://" + (configuration.api?.hostname || DEFAULT_REST_API_HOSTNAME) + ":" + status.port)

const taskTable: Row[] = []

Expand Down
59 changes: 59 additions & 0 deletions lib/common/port.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Provides functions for finding available network ports within a specified range.
*
* @file lib/common/port.ts
* @license MIT
*/

import { createServer } from "node:net"

interface FindFreePortOptions {
/**
* The starting port number to begin the search.
* @default 49152
*/
startPort?: number

/**
* The ending port number (inclusive) for the search.
* @default 65535
*/
endPort?: number
}

/**
* Asynchronously checks if a given port is available.
*
* @param port - The port number to check.
* @returns A promise that resolves to `true` if the port is available, `false` otherwise.
*/
// 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)
})
})
}

/**
* Asynchronously finds an available port within a specified range.
*
* @param options - Options for the port search.
* @returns A promise that resolves to the first available free port.
* @throws An error if no free ports are found within the range.
*/
export async function findFreePort(options: FindFreePortOptions = {}): Promise<number> {
const { startPort = 49152, endPort = 65535 } = options

for (let port = startPort; port <= endPort; port++) {
if (await isPortAvailable(port)) {
return port
}
}

throw new Error("No free ports found in the specified range.")
}
91 changes: 91 additions & 0 deletions lib/common/prop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Provides a mechanism for loading or generating properties stored in files,
* ensuring secure file permissions.
*
* @file lib/common/prop.ts
* @license MIT
*/

import { exists, readFile, writeFile } from "@cross/fs"
import { DEFAULT_PROP_FILE_PERMISSIONS } from "../core/configuration.ts"

export type PropGenerator = () => Promise<string>

export class Prop {
/**
* The file path where the property is stored.
*/
path: string

/**
* An in-memory cache of the loaded or generated property.
*/
cache: string | undefined

/**
* File permissions to use when creating the property file.
*/
filePermissions: number

/**
* Creates a new instance of the Prop class.
*
* @param secretFilePath - The path to the file where the property is stored.
* @param filePermissions - File permissions to use when creating the property file (default: 0o600).
*/
constructor(secretFilePath: string, filePermissions = DEFAULT_PROP_FILE_PERMISSIONS) {
this.path = secretFilePath
this.filePermissions = filePermissions
}

/**
* Generates a new property and stores it in the file.
*
* @throws On error
* @returns The newly generated property.
*/
async generate(generatorFn: PropGenerator): Promise<string> {
const resultString = await generatorFn()
await writeFile(this.path, resultString, { mode: this.filePermissions })
return resultString
}

/**
* Loads the property from the file system.
*
* @returns The property from the file.
* @throws If an error occurs while reading the file.
*/
async load(): Promise<string> {
this.cache = await readFile(this.path, "utf-8")
return this.cache!
}

/**
* Loads the property from the cache
*
* @returns The property from the file.
* @throws If an error occurs while reading the file.
*/
fromCache(): string | undefined {
return this.cache
}

/**
* Loads the property if it exists in the file, or generates a new one if it doesn't.
* The property is cached for subsequent calls. This is the primary method for using the class.
*
* @throws On any error
* @returns The loaded or generated property.
*/
async loadOrGenerate(generatorFn: PropGenerator): Promise<string> {
if (!this.cache) {
if (await exists(this.path)) {
this.cache = await this.load()
} else {
this.cache = await this.generate(generatorFn)
}
}
return this.cache
}
}
7 changes: 7 additions & 0 deletions lib/common/restclient.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* 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
Expand Down
7 changes: 7 additions & 0 deletions lib/common/token.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* Utilities for generating and validating Pup Rest API Tokens
*
* @file lib/common/token.ts
* @license MIT
*/

import { createJWT, generateKey, JWTPayload, validateJWT } from "@cross/jwt"
import { DEFAULT_SECRET_KEY_ALGORITHM } from "../core/configuration.ts"

Expand Down
8 changes: 6 additions & 2 deletions lib/core/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import type { EventEmitter } from "../common/eventemitter.ts"
import type { LogEventData } from "./logger.ts"
import type { Pup } from "./pup.ts"
import type { ProcessLoggerConfiguration } from "./configuration.ts"
import type { Configuration, ProcessLoggerConfiguration } from "./configuration.ts"
import type { ProcessState } from "./process.ts"
import { TelemetryData } from "../../telemetry.ts"

Expand Down Expand Up @@ -78,6 +78,7 @@ export interface ApiApplicationState {
updated: string
started: string
memory: Deno.MemoryUsage
port: number
systemMemory: Deno.SystemMemoryInfo
loadAvg: number[]
osUptime: number
Expand All @@ -103,6 +104,9 @@ export class PupApi {
configFilePath: pup.configFilePath,
}
}
public getConfiguration(): Configuration {
return this._pup.configuration
}
public allProcessStates(): ApiProcessData[] {
const statuses: ApiProcessData[] = this._pup.allProcesses().map((p) => {
return {
Expand All @@ -113,7 +117,7 @@ export class PupApi {
return statuses
}
public applicationState(): ApiApplicationState {
return this._pup.status.applicationState(this._pup.allProcesses())
return this._pup.status.applicationState(this._pup.allProcesses(), this._pup.port)
}
public terminate(forceQuitMs: number) {
this._pup.terminate(forceQuitMs)
Expand Down
6 changes: 5 additions & 1 deletion lib/core/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export const DEFAULT_SECRET_LENGTH_BYTES = 64
export const DEFAULT_SECRET_FILE_PERMISSIONS = 0o600
export const DEFAULT_SECRET_KEY_ALGORITHM = "HS512"
export const DEFAULT_REST_API_HOSTNAME = "127.0.0.1"
export const DEFAULT_REST_API_PORT = 16441

// Prop file constants
export const DEFAULT_PROP_FILE_PERMISSIONS = 0o600

interface Configuration {
name?: string
Expand All @@ -37,6 +39,7 @@ interface Configuration {
interface ApiConfiguration {
hostname?: string
port?: number
revoked?: string[]
}

interface _BaseLoggerConfiguration {
Expand Down Expand Up @@ -102,6 +105,7 @@ const ConfigurationSchema = z.object({
z.object({
hostname: z.optional(z.string()),
port: z.optional(z.number().int()),
revoked: z.optional(z.array(z.string())),
}),
),
logger: z.optional(
Expand Down
Loading

0 comments on commit 56717ea

Please sign in to comment.