Skip to content

Commit

Permalink
Plugin docs. Dependency update. Add restarts. Error handling.
Browse files Browse the repository at this point in the history
  • Loading branch information
Hexagon committed Apr 25, 2024
1 parent 7d1987a commit a336147
Show file tree
Hide file tree
Showing 14 changed files with 281 additions and 154 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected].34 setup --channel prerelease
deno run -Ar jsr:@pup/[email protected].35 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
Expand Down
2 changes: 1 addition & 1 deletion application.meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

const Application = {
name: "pup",
version: "1.0.0-rc.34",
version: "1.0.0-rc.35",
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) */
Expand Down
12 changes: 6 additions & 6 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pup/pup",
"version": "1.0.0-rc.34",
"version": "1.0.0-rc.35",

"exports": {
".": "./pup.ts",
Expand Down Expand Up @@ -42,16 +42,16 @@
"@cross/deepmerge": "jsr:@cross/deepmerge@^1.0.0",
"@cross/env": "jsr:@cross/env@^1.0.2",
"@cross/fs": "jsr:@cross/fs@^0.0.10",
"@cross/jwt": "jsr:@cross/jwt@^0.4.1",
"@cross/jwt": "jsr:@cross/jwt@^0.4.2",
"@cross/runtime": "jsr:@cross/runtime@^1.0.0",
"@cross/service": "jsr:@cross/service@^1.0.3",
"@cross/test": "jsr:@cross/test@^0.0.9",
"@cross/utils": "jsr:@cross/utils@^0.12.0",
"@hexagon/croner": "jsr:@hexagon/croner@^8.0.1",
"@hexagon/croner": "jsr:@hexagon/croner@^8.0.2",
"@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",
"@pup/api-client": "jsr:@pup/api-client@^1.0.1",
"@pup/api-definitions": "jsr:@pup/api-definitions@^1.0.1",
"@pup/common": "jsr:@pup/common@^1.0.2",
"@pup/plugin": "jsr:@pup/plugin@^1.0.0",
"@std/assert": "jsr:@std/assert@^0.223.0",
"@std/async": "jsr:@std/async@^0.223.0",
Expand Down
2 changes: 1 addition & 1 deletion docs/src/_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"description": "Universal Process Manager"
},
"substitute": {
"$PUP_VERSION": "1.0.0-rc.34"
"$PUP_VERSION": "1.0.0-rc.35"
},
"top_links": [
{
Expand Down
8 changes: 8 additions & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ nav_order: 13

All notable changes to this project will be documented in this section.

## [1.0.0-rc.35] - 2024-04-26

## Added

- fix(api): Make sure websocket is open before sending
- add(status): Add Restarts to `pup status`
- chore(docs): Update developer guide for writing plugins

## [1.0.0-rc.34] - 2024-04-25

## Added
Expand Down
2 changes: 1 addition & 1 deletion docs/src/examples/basic-webinterface/pup.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"plugins": [
{
// Use full uri to plugin, e.g. jsr:@pup/plugin-web-interface
"url": "jsr:@pup/plugin-web-interface",
"url": "@pup/plugin-web-interface",
"options": {
"port": 8002
}
Expand Down
181 changes: 92 additions & 89 deletions docs/src/examples/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,61 @@ nav_order: 2

# Plugin development

**Note! This is outdated, need to be updated asap.** Refer to <https://github.com/hexagon/pup-plugin> for up-to-date info.

---

### Getting started

To create a custom plugin, developers should extend the `PluginImplementation` class and override its methods as necessary. This class is the main entry point for plugins to interact with Pup and
provides various hooks and events to listen for.
To create a custom plugin, developers should extend the `PluginImplementation` class from `@pup/plugin` and override its methods as necessary. This class is the main entry point for plugins.

The plugin then communicates with Pup using `PupRestClient` from `@pup/api-client`. The url and credentials are passed to the plugin constructor.

```typescript
import { LogEvent, PluginApi, PluginConfiguration, PluginImplementation, PluginMetadata } from "jsr:@pup/pup@PUP_$VERSION/mod.ts"
import { PupRestClient } from "jsr:@pup/api-client"
import { type PluginConfiguration, PluginImplementation } from "jsr:@pup/plugin"

// Set up the expected configuration
interface Configuration {
threshold: string
}

// The main entrypoint of a Plugin is an exported class named PupPlugin
// which should always extend PluginImplementation
export class PupPlugin extends PluginImplementation {
constructor(pup: PluginApi, config: PluginConfiguration) {
super(pup, config)
this.meta = {
name: "MinimalPlugin",
version: "1.0.0",
api: "1",
repository: "https://github.com/hexagon/pup",
}
public meta = {
name: "ExamplePlugin",
version: "1.0.0",
api: "1.0.0",
repository: "https://github.com/user/my-repo",
}

private config: Configuration
private client: PupRestClient

constructor(
config: PluginConfiguration,
apiUrl: string,
apiToken: string,
) {
super(config, apiUrl, apiToken)

this.config = config.options as Configuration

// Set up the rest client
// - api url and a token is supplied by Pup
this.client = new PupRestClient(
`http://${apiUrl}`,
apiToken,
true,
)
}
}
```

Now, to communicate with pup, there are three concepts

## Hooks

Hooks are a way for plugins to intercept and modify the behavior of Pup. To use a hook, a plugin must implement the hook method and handle the relevant signals. Currently, only the `log` hook is
supported.
Now, to communicate with pup, there are two concepts

## Events

Events allow plugins to listen for specific occurrences within Pup. Plugins can subscribe to events using the events property of the PluginApi class.
Events allow plugins to listen for specific occurrences within Pup. Plugins can subscribe to events using the `.on` method of the Rest client

The following events are available:

Expand All @@ -53,81 +73,64 @@ The following events are available:
- **terminating:** Fired when Pup is terminating.
- **ipc:** Fired when an IPC message is received.

## PluginApi

The PluginApi class is the main API for interacting with Pup from a plugin. It exposes methods for managing processes and listening to events, the class also exposes some paths that may be useful for
plugins:

To use the PluginApi, access it through the pup parameter in your custom plugin's constructor:

```ts
class PupPlugin extends PluginImplementation {
constructor(pup: PluginApi, config: PluginConfiguration) {
// Use the pup PluginApi instance to interact with pup

// - Start, stop, restart, block, and unblock processes:
pup.start(id, reason)
pup.stop(id, reason)
pup.restart(id, reason)
pup.block(id, reason)
pup.unblock(id, reason)

// - Terminate Pup:
pup.terminate(forceQuitMs)

// - Get the status of all processes:
const ProcessStatees = pup.allProcessStatees()

// Listen to events using the pup.events property:
pup.events.on("process_status_changed", (eventData) => {
console.log("Process status changed:", eventData)
})

// Extract usable paths:
// - A path to temporary storage that can be used by the plugin.
const tempStoragePath = pup.paths.temporaryStorage
// - A path to persistent storage that can be used by the plugin to store data across Pup restarts.
const persistentStoragePath = pup.paths.persistentStorage //
// - The full path to Pup's current configuration file (usually pup.json).
const configFilePath = pup.paths.configFilePath
}
}
// Listen for log messages from the API
this.client.on("application_state", (pupState: unknown) => {
const tPupState = pupState as ApiApplicationState
})
```

## Custom logger plugin
## Rest Endpoints

To sum it up, and create a custom plugin that intercepts the logger through hooks, you need to extend the `PluginImplementation` class and override the `hook` method to handle the `log` signal. In
this example, the plugin will print all available log data when the log signal is received.
The Rest client exposes methods for managing processes.

```typescript
import { LogEvent, PluginApi, PluginConfiguration, PluginImplementation } from "jsr:@pup/pup@PUP_$VERSION/mod.ts"
To use the Rest endponts, access it through client instance:

export class PupPlugin extends PluginImplementation {
constructor(pup: PluginApi, config: PluginConfiguration) {
super(pup, config)
this.meta = {
name: "LoggerInterceptorPlugin",
version: "1.0.0",
api: "1",
repository: "https://github.com/hexagon/pup",
}
}

public hook(signal: string, parameters: unknown): boolean {
if (signal === "log") {
this.handleLog(parameters as LogEvent)
// Block further processing by other log plugins, or built in logger
return true
}
return false
}

private handleLog(p: LogEvent) {
console.log(p.severity, p.category, p.text)
}
}
```ts
try {
await this.client.sendLog(
severity,
"example-plugin",
message,
)
} catch (_e) { /* Could not send log */ }
```

Always wrap requests in try/catch, we do not want any unhandled errors in a Plugin.

### All endpoints

1. **`/processes`:**
- **Purpose:** List configured processes and their statues.

2. **Process action Endpoints (`/processes/:id/start`, etc.):**
- **Purpose:** Provide an interface for managing Pup processes.
- **Actions:**
- Retrieve process status (`/processes`) and application state (`/state`).
- Start, stop, restart, block, and unblock processes (`/processes/:id/...`).

3. **`/terminate`:**
- **Purpose:** Initiates a graceful termination of the Pup application.

4. **`/log`:**
- **Purpose:** Allows sending log messages to Pup.
- **Behavior:**
- Extracts severity, plugin, and message from the request body.
- Validates severity.
- Logs the message.

5. **`/logs`:**
- **Purpose:** Retrieves log entries from Pup.
- **Parameters:**
- `processId` (optional)
- `startTimeStamp` (optional)
- `endTimeStamp` (optional)
- `severity` (optional)
- `nRows` (optional)
- **Behavior:**
- Parses query parameters.
- Calls the `PupApi.getLogs` method to fetch logs based on provided criteria.

## End user configuration

The end user configuration for activating a plugin by `pup.json` is
Expand All @@ -139,14 +142,14 @@ The end user configuration for activating a plugin by `pup.json` is
"plugins": [
/* Remote plugin */
{
"url": "jsr:@scope/pup-example-plugin@0.0.1/mod.ts",
"url": "jsr:@scope/pup-example-plugin",
"options": {
/* Plugin specific configuration */
}
},
/* Local plugin */
{
"url": "./plugins/app-plugin.ts",
"url": "file:///full/path/to/plugin/plugin.ts",
"options": {
/* Plugin specific configuration */
}
Expand Down
75 changes: 75 additions & 0 deletions docs/src/examples/plugins/example-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { PupRestClient } from "jsr:@pup/api-client"
import { type PluginConfiguration, PluginImplementation } from "jsr:@pup/plugin"
import type { ApiApplicationState } from "jsr:@pup/api-definitions"

// Set up the expected configuration
interface Configuration {
threshold: string
}

// The main entrypoint of a Plugin is an exported class named PupPlugin
// which should always extend PluginImplementation
export class PupPlugin extends PluginImplementation {
public meta = {
name: "ExamplePlugin",
version: "1.0.0",
api: "1.0.0",
repository: "https://github.com/user/my-repo",
}

private config: Configuration

private client: PupRestClient

constructor(
config: PluginConfiguration,
apiUrl: string,
apiToken: string,
) {
super(config, apiUrl, apiToken)
this.config = config.options as Configuration

// Set up the rest client
// - api url and a token is supplied by Pup
this.client = new PupRestClient(
`http://${apiUrl}`,
apiToken,
true,
)

// Get threshold from config
const threshold = parseInt(this.config.threshold, 10)

// Send a little hello
this.sendLog("info", `Example plugin is initiated, memory usage threshold is set to ${threshold}%.`)

// Listen for log messages from the API
this.client.on("application_state", (pupState: unknown) => {
const tPupState = pupState as ApiApplicationState
const memoryUsage = Math.round(tPupState.systemMemory.free / tPupState.systemMemory.total * 100)
if (memoryUsage > threshold) {
this.sendLog("error", `Memory usage is critically high (${memoryUsage}%)...`)
} else {
this.sendLog("info", `Memory usage is fine (${memoryUsage}%)...`)
}
})
}

// Helper function to send logs via the Rest API
public async sendLog(severity: string, message: string) {
// Wrap all API calls in try/catch
try {
await this.client.sendLog(
severity,
"example-plugin",
message,
)
} catch (_e) { /* Could not send log */ }
}

// Keep things tidy
public async cleanup(): Promise<boolean> {
this.client.close()
return await true
}
}
Loading

0 comments on commit a336147

Please sign in to comment.