koishi-plugin-pm2 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .log-list[data-v-95c3a303]{color:var(--terminal-fg);background-color:var(--terminal-bg)}.log-list[data-v-95c3a303] .el-scrollbar__view{padding:1rem}.log-list .line.start[data-v-95c3a303]{margin-top:1rem}.log-list .line.start[data-v-95c3a303]:before{content:"";position:absolute;left:0;right:0;top:-.5rem;border-top:1px solid var(--terminal-separator)}.log-list .line[data-v-95c3a303]:first-child{margin-top:0}.log-list .line[data-v-95c3a303]:first-child:before{display:none}.log-list .line[data-v-95c3a303]{padding:0 .5rem;border-radius:2px;font-size:14px;line-height:20px;white-space:pre-wrap;word-break:break-all;position:relative}.log-list .line[data-v-95c3a303]:hover{color:var(--terminal-fg-hover);background-color:var(--terminal-bg-hover)}.log-list .line[data-v-95c3a303] ::selection{background-color:var(--terminal-bg-selection)}.alert-dialog-actions{margin-top:20px;display:flex;justify-content:center}.action-dialog-loading[data-v-fe53df72]{padding:24px 0}.action-result-list[data-v-fe53df72]{display:flex;flex-direction:column;gap:12px}.action-result-scrollbar[data-v-fe53df72]{margin:0 -4px}.action-result-row[data-v-fe53df72]{border:var(--fg3) 1px solid;border-radius:4px;padding:12px 14px;margin:2px}.action-result-meta[data-v-fe53df72]{font-size:12px;color:var(--fg2);margin-bottom:6px}.action-result-payload[data-v-fe53df72]{margin:0;font-family:Fira Code,SFMono-Regular,Consolas,monospace;font-size:13px;white-space:pre-wrap;word-break:break-word}.section-empty[data-v-fe53df72]{margin:10px 0 0;color:var(--fg2);font-size:13px}.expand-section[data-v-4d6fd500]{padding:10px 20px;border-top:var(--k-status-divider, var(--k-color-divider-dark)) 1px solid}.expand-section[data-v-4d6fd500]:first-of-type{border-top:none}.expand-title[data-v-4d6fd500]{margin:4px 0;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.05em}.expand-header[data-v-4d6fd500]{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px}.expand-actions[data-v-4d6fd500]{display:flex;gap:8px}.section-empty[data-v-4d6fd500]{margin:10px 0 0;color:var(--fg2);font-size:13px}.metrics-container[data-v-4d6fd500]{display:flex;flex-wrap:wrap;gap:10px}.metric-card[data-v-4d6fd500]{border:var(--k-status-divider, var(--k-color-divider-dark)) 1px solid;border-radius:4px;padding:10px;flex:1 1 150px;box-sizing:border-box;text-align:center;max-width:200px}.metric-name[data-v-4d6fd500]{font-size:12px;color:var(--fg2)}.metric-value[data-v-4d6fd500]{font-size:16px;font-weight:700;display:flex;justify-content:center;gap:4px}.metric-unit[data-v-4d6fd500]{font-size:16px;color:var(--fg2)}.actions-container[data-v-4d6fd500]{display:flex;flex-wrap:wrap;gap:8px}[data-v-4d6fd500] .actions-container .el-button+.el-button{margin-left:0}.action-chip[data-v-4d6fd500]{border:var(--k-status-divider, var(--k-color-divider-dark)) 1px solid;display:inline-flex;align-items:center;gap:6px}.action-name[data-v-4d6fd500]{font-weight:600;margin-right:6px}.status-cell[data-v-4d6fd500]{display:flex;align-items:center;gap:8px}.status-badges[data-v-4d6fd500]{display:inline-flex;align-items:center;gap:4px}.status-badge[data-v-4d6fd500]{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border:var(--fg4, var(--k-color-border)) 1px solid;border-radius:999px;font-size:14px;color:var(--fg2)}.status-badge.auto[data-v-4d6fd500],.status-badge.cron[data-v-4d6fd500]{color:var(--k-color-success, var(--k-color-primary));border-color:currentColor}.alert-row-actions[data-v-4d6fd500]{display:inline-flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:6px;width:100%}.alert-row-actions[data-v-4d6fd500] .el-button{min-width:32px;padding:4px 6px}.alert-table[data-v-4d6fd500]{width:100%}.alert-message[data-v-4d6fd500]{display:inline-block;max-width:100%;white-space:pre-wrap;word-break:break-word}.alert-message.placeholder[data-v-4d6fd500]{color:var(--fg2);font-style:italic}.log-view[data-v-4d6fd500]{padding:1rem;max-height:60vh;overflow-y:auto;white-space:pre-wrap;word-break:break-all}.process-scroll[data-v-4d6fd500] .el-scrollbar__wrap{overflow-x:auto;overflow-y:auto;-webkit-overflow-scrolling:touch}.el-button--success[data-v-4d6fd500]{--el-button-hover-text-color: var(--k-color-success);--el-button-hover-border-color: var(--k-color-success)}.el-button--info[data-v-4d6fd500]{--el-button-hover-text-color: var(--k-color-info);--el-button-hover-border-color: var(--k-color-info)}.el-button--danger[data-v-4d6fd500]{--el-button-hover-text-color: var(--k-color-danger);--el-button-hover-border-color: var(--k-color-danger)}
1
+ .log-list[data-v-8d10b629]{color:var(--terminal-fg);background-color:var(--terminal-bg)}.log-list[data-v-8d10b629] .el-scrollbar__view{padding:1rem}.log-list .line.start[data-v-8d10b629]{margin-top:1rem}.log-list .line.start[data-v-8d10b629]:before{content:"";position:absolute;left:0;right:0;top:-.5rem;border-top:1px solid var(--terminal-separator)}.log-list .line[data-v-8d10b629]:first-child{margin-top:0}.log-list .line[data-v-8d10b629]:first-child:before{display:none}.log-list .line[data-v-8d10b629]{padding:0 .5rem;border-radius:2px;font-size:14px;line-height:20px;white-space:pre-wrap;word-break:break-all;position:relative}.log-list .line[data-v-8d10b629]:hover{color:var(--terminal-fg-hover);background-color:var(--terminal-bg-hover)}.log-list .line[data-v-8d10b629] ::selection{background-color:var(--terminal-bg-selection)}.alert-dialog-actions{margin-top:20px;display:flex;justify-content:center}.action-dialog-loading[data-v-fe53df72]{padding:24px 0}.action-result-list[data-v-fe53df72]{display:flex;flex-direction:column;gap:12px}.action-result-scrollbar[data-v-fe53df72]{margin:0 -4px}.action-result-row[data-v-fe53df72]{border:var(--fg3) 1px solid;border-radius:4px;padding:12px 14px;margin:2px}.action-result-meta[data-v-fe53df72]{font-size:12px;color:var(--fg2);margin-bottom:6px}.action-result-payload[data-v-fe53df72]{margin:0;font-family:Fira Code,SFMono-Regular,Consolas,monospace;font-size:13px;white-space:pre-wrap;word-break:break-word}.section-empty[data-v-fe53df72]{margin:10px 0 0;color:var(--fg2);font-size:13px}.expand-section[data-v-41921f53]{padding:10px 20px;border-top:var(--k-status-divider, var(--k-color-divider-dark)) 1px solid}.expand-section[data-v-41921f53]:first-of-type{border-top:none}[data-v-41921f53] .el-table__expanded-cell{padding:0!important}.expand-title[data-v-41921f53]{margin:4px 0;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.05em}.expand-header[data-v-41921f53]{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px}.expand-actions[data-v-41921f53]{display:flex;gap:8px}.section-empty[data-v-41921f53]{margin:10px 0 0;color:var(--fg2);font-size:13px}.namespace-name[data-v-41921f53]{display:inline-flex;align-items:center;font-weight:600;margin-left:-12px}[data-v-41921f53] .namespace-row td{padding-top:4px;padding-bottom:4px;border-bottom:0!important}[data-v-41921f53] .namespace-row+.el-table__row td{border-top:0!important}[data-v-41921f53] .namespace-row+.el-table__expanded-row{display:none!important;height:0!important;line-height:0!important;font-size:0!important}[data-v-41921f53] .namespace-row+.el-table__expanded-row td,[data-v-41921f53] .namespace-row+.el-table__expanded-row .cell{padding:0!important;border:0!important;height:0!important;line-height:0!important;font-size:0!important}[data-v-41921f53] .cluster-row+.el-table__expanded-row{display:none!important;height:0!important;line-height:0!important;font-size:0!important}.cluster-name[data-v-41921f53]{display:inline-flex;align-items:baseline;gap:8px;line-height:1.2}.cluster-title[data-v-41921f53]{font-weight:600}.cluster-count[data-v-41921f53]{font-size:12px;color:var(--fg2)}[data-v-41921f53] .cluster-row.cluster-expanded td{border-bottom:0!important}[data-v-41921f53] .cluster-row.cluster-expanded+.el-table__expanded-row+.el-table__row td{border-top:0!important}[data-v-41921f53] .cluster-row td{padding-top:4px;padding-bottom:4px}[data-v-41921f53] .cluster-row+.el-table__expanded-row td,[data-v-41921f53] .cluster-row+.el-table__expanded-row .cell{padding:0!important;border:0!important;height:0!important;line-height:0!important;font-size:0!important}.process-name[data-v-41921f53]{display:inline-flex;align-items:baseline;gap:6px;line-height:1.2}.process-name.cluster-process[data-v-41921f53]{padding-left:12px}.process-id[data-v-41921f53]{font-size:12px;color:var(--fg2)}.metrics-container[data-v-41921f53]{display:flex;flex-wrap:wrap;gap:10px}.metric-card[data-v-41921f53]{border:var(--k-status-divider, var(--k-color-divider-dark)) 1px solid;border-radius:4px;padding:10px;flex:1 1 150px;box-sizing:border-box;text-align:center;max-width:200px}.metric-name[data-v-41921f53]{font-size:12px;color:var(--fg2)}.metric-value[data-v-41921f53]{font-size:16px;font-weight:700;display:flex;justify-content:center;gap:4px}.metric-unit[data-v-41921f53]{font-size:16px;color:var(--fg2)}.metric-cell[data-v-41921f53]{position:relative;padding:4px 6px;border-radius:6px;background-repeat:no-repeat;background-position:center;background-size:100% 100%}.metric-text[data-v-41921f53]{position:relative;z-index:1}.actions-container[data-v-41921f53]{display:flex;flex-wrap:wrap;gap:8px}[data-v-41921f53] .actions-container .el-button+.el-button{margin-left:0}.action-chip[data-v-41921f53]{border:var(--k-status-divider, var(--k-color-divider-dark)) 1px solid;display:inline-flex;align-items:center;gap:6px}.action-name[data-v-41921f53]{font-weight:600;margin-right:6px}.status-cell[data-v-41921f53]{display:flex;align-items:center;gap:8px;overflow:visible}[data-v-41921f53] .status-column,[data-v-41921f53] .status-column .cell{overflow:visible}.status-wrap[data-v-41921f53]{position:relative;display:inline-flex;overflow:visible;padding-top:4px;padding-right:4px}.status-badges[data-v-41921f53]{position:absolute;top:-4px;right:-4px;display:inline-flex;align-items:center;gap:2px}.status-badge[data-v-41921f53]{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border:var(--fg4, var(--k-color-border)) 1px solid;border-radius:999px;font-size:10px;font-weight:700;color:var(--fg2);background:var(--bg1);box-shadow:0 0 0 1px var(--bg2)}.status-badge.auto[data-v-41921f53],.status-badge.cron[data-v-41921f53]{color:var(--k-color-success, var(--k-color-primary));border-color:currentColor}.alert-table[data-v-41921f53]{width:100%}.alert-row-actions[data-v-41921f53]{display:inline-flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:2px;width:100%}.alert-row-actions[data-v-41921f53] .el-button{min-width:32px;padding:2px 4px}.alert-row-actions[data-v-41921f53] .el-button+.el-button{margin-left:0}.alert-message[data-v-41921f53]{display:inline-block;max-width:100%;white-space:pre-wrap;word-break:break-word}.alert-message.placeholder[data-v-41921f53]{color:var(--fg2);font-style:italic}.log-view[data-v-41921f53]{padding:1rem;max-height:60vh;overflow-y:auto;white-space:pre-wrap;word-break:break-all}.process-scroll[data-v-41921f53] .el-scrollbar__wrap{overflow-x:auto;overflow-y:auto;-webkit-overflow-scrolling:touch}.el-button--success[data-v-41921f53]{--el-button-hover-text-color: var(--k-color-success);--el-button-hover-border-color: var(--k-color-success)}.el-button--info[data-v-41921f53]{--el-button-hover-text-color: var(--k-color-info);--el-button-hover-border-color: var(--k-color-info)}.el-button--danger[data-v-41921f53]{--el-button-hover-text-color: var(--k-color-danger);--el-button-hover-border-color: var(--k-color-danger)}
package/lib/index.d.ts CHANGED
@@ -14,6 +14,7 @@ declare module '@koishijs/console' {
14
14
  'pm2/test-alert'(alert: PM2.Alert): Promise<void>;
15
15
  'pm2/remove-alert'(alert: PM2.Alert): Promise<void>;
16
16
  'pm2/clear-alerts'(name: string, events?: string[]): Promise<void>;
17
+ 'pm2/save'(): Promise<void>;
17
18
  'pm2/patch-log'(id: number | string, logs: string[]): void;
18
19
  }
19
20
  interface Client {
@@ -50,28 +51,41 @@ export declare class PM2 extends Service {
50
51
  api: API;
51
52
  bus: EventEmitter | null;
52
53
  logs: LogManager;
54
+ metricHistory: Map<string, PM2.ProcessHistory>;
53
55
  list: () => Promise<PM2.Process[]>;
54
56
  trigger: (id: number | string, actionName: string) => Promise<PM2.MonitorActionResult[]>;
55
57
  constructor(ctx: Context, config: PM2.Config);
56
58
  _list(): Promise<PM2.Process[]>;
59
+ private recordHistory;
57
60
  _trigger(id: number | string, actionName: string): Promise<any[]>;
58
- alert(alert: PM2.Alert, env: PM2.Env & {
59
- event: string;
60
- manually: boolean;
61
- }): Promise<string[]>;
61
+ alert(alert: PM2.Alert, env: PM2.AlertEnv): Promise<string[]>;
62
62
  }
63
63
  export declare namespace PM2 {
64
64
  const inject: {
65
65
  required: string[];
66
66
  optional: string[];
67
67
  };
68
+ const EventMap: {
69
+ readonly restart: "restart";
70
+ readonly delete: "delete";
71
+ readonly stop: "stop";
72
+ readonly reload: "reload";
73
+ readonly 'reload:graceful': "graceful reload";
74
+ readonly start: "start";
75
+ readonly exit: "exit";
76
+ readonly online: "online";
77
+ readonly 'restart:overlimit': "restart overlimit";
78
+ readonly 'exit:code': "exit";
79
+ readonly 'log:out': "";
80
+ readonly 'log:err': "";
81
+ };
82
+ type EventMap = typeof EventMap;
68
83
  interface Alert {
69
84
  name: string;
70
- event: 'restart' | 'delete' | 'stop' | 'reload' | 'graceful reload' | 'start' | 'exit' | 'online' | 'restart overlimit' | {
71
- type: 'exit';
72
- eq?: number;
73
- neq?: number;
74
- };
85
+ event: keyof EventMap;
86
+ eq?: number;
87
+ neq?: number;
88
+ regexp?: string;
75
89
  sid?: string;
76
90
  cid: string;
77
91
  message?: string;
@@ -81,6 +95,8 @@ export declare namespace PM2 {
81
95
  logSyncInterval: number;
82
96
  listSyncInterval: number;
83
97
  listSyncTimeout: number;
98
+ metricsInterval: number;
99
+ metricsHistorySize: number;
84
100
  actionTimeout: number;
85
101
  logTailLines: number;
86
102
  ignoreStoppingExit: boolean;
@@ -127,6 +143,15 @@ export declare namespace PM2 {
127
143
  };
128
144
  [key: string]: any;
129
145
  }
146
+ interface AlertEnv extends Env {
147
+ event: string;
148
+ /** Whether the event is triggered manually */
149
+ manually: boolean;
150
+ /** The type of process:event */
151
+ raw_event?: string;
152
+ /** Log data */
153
+ data?: string;
154
+ }
130
155
  interface Process {
131
156
  /** Process ID */
132
157
  pm_id: number;
@@ -148,6 +173,7 @@ export declare namespace PM2 {
148
173
  cpu: number;
149
174
  memory: number;
150
175
  };
176
+ history?: ProcessHistory;
151
177
  alerts?: Alert[];
152
178
  }
153
179
  interface MonitorActionResult {
@@ -163,5 +189,18 @@ export declare namespace PM2 {
163
189
  };
164
190
  at: number;
165
191
  }
192
+ interface MetricSample {
193
+ time: number;
194
+ cpu: number;
195
+ memory: number;
196
+ }
197
+ interface MonitorSample {
198
+ time: number;
199
+ value: number;
200
+ }
201
+ interface ProcessHistory {
202
+ samples: MetricSample[];
203
+ monitors: Record<string, MonitorSample[]>;
204
+ }
166
205
  }
167
206
  export default PM2;
package/lib/index.js CHANGED
@@ -157,7 +157,7 @@ function parsePlatform(target) {
157
157
  __name(parsePlatform, "parsePlatform");
158
158
 
159
159
  // src/locales/en-US.yml
160
- var en_US_default = { pm2: { alerts: { restart: "[PM2] Process {name} restarted.", delete: "[PM2] Process {name} deleted.", stop: "[PM2] Process {name} stopped.", reload: "[PM2] Process {name} reloaded.", "graceful reload": "[PM2] Process {name} gracefully reloaded.", start: "[PM2] Process {name} started.", exit: "[PM2] Process {name} exited.", online: "[PM2] Process {name} is online.", "restart-overlimit": "[PM2] Process {name} reached restart overlimit." } }, commands: { "pm2.list": { description: "Display the list of PM2 processes.", messages: { output: "- PM2 Process List\n{context.map(proc => '[' + String(proc.pm_id) + '] ' + proc.name + ' ' + proc.pm2_env.status + (proc.pm2_env.status === 'online' ? ' ' + String(Math.floor((Date.now() - proc.pm2_env.pm_uptime) / 1000 / 60 / 60 / 24)) + 'd' : '')).join('\\n')}\n" } }, "pm2.start": { description: "Start a PM2 process by id or name.", messages: { output: "Process {name} started.", "not-found": "Process {id} not found." } }, "pm2.restart": { description: "Restart a PM2 process by id or name.", messages: { output: "Process {name} restarted.", "not-found": "Process {id} not found." } }, "pm2.reload": { description: "Reload a PM2 process by id or name.", messages: { output: "Process {name} reloaded.", "not-found": "Process {id} not found." } }, "pm2.stop": { description: "Stop a PM2 process by id or name.", messages: { output: "Process {name} stopped.", "not-found": "Process {id} not found." } }, "pm2.delete": { description: "Delete a PM2 process by id or name.", messages: { output: "Process {name} deleted.", "not-found": "Process {id} not found." } } } };
160
+ var en_US_default = { pm2: { alerts: { restart: "[PM2] Process {name} restarted.", delete: "[PM2] Process {name} deleted.", stop: "[PM2] Process {name} stopped.", reload: "[PM2] Process {name} reloaded.", "reload:graceful": "[PM2] Process {name} gracefully reloaded.", start: "[PM2] Process {name} started.", exit: "[PM2] Process {name} exited.", online: "[PM2] Process {name} is online.", "restart:overlimit": "[PM2] Process {name} reached restart overlimit.", "exit:code": "[PM2] Process {name} exited with code {code}.", "log:out": "[PM2] Process {name} logged stdout:\n{data}\n", "log:err": "[PM2] Process {name} logged stderr:\n{data}\n" } }, commands: { "pm2.list": { description: "Display the list of PM2 processes.", messages: { output: "- PM2 Process List\n{context.map(proc => '[' + String(proc.pm_id) + '] ' + proc.name + ' ' + proc.pm2_env.status + (proc.pm2_env.status === 'online' ? ' ' + String(Math.floor((Date.now() - proc.pm2_env.pm_uptime) / 1000 / 60 / 60 / 24)) + 'd' : '')).join('\\n')}\n" } }, "pm2.save": { description: "Dump process list.", messages: { success: "Success.", failed: "Failed" } }, "pm2.start": { description: "Start a PM2 process by id or name.", messages: { output: "Process {name} started.", "not-found": "Process {0} not found." } }, "pm2.restart": { description: "Restart a PM2 process by id or name.", messages: { output: "Process {name} restarted.", "not-found": "Process {0} not found." } }, "pm2.reload": { description: "Reload a PM2 process by id or name.", messages: { output: "Process {name} reloaded.", "not-found": "Process {0} not found." } }, "pm2.stop": { description: "Stop a PM2 process by id or name.", messages: { output: "Process {name} stopped.", "not-found": "Process {0} not found." } }, "pm2.delete": { description: "Delete a PM2 process by id or name.", messages: { output: "Process {name} deleted.", "not-found": "Process {0} not found." } } } };
161
161
 
162
162
  // src/index.ts
163
163
  var LogManager = class {
@@ -174,14 +174,39 @@ var LogManager = class {
174
174
  async init() {
175
175
  const onLog = /* @__PURE__ */ __name((packet, type) => {
176
176
  const id = packet.process.pm_id ?? "PM2";
177
- if (!this.listeners[id]) return;
177
+ const targets = [String(id), packet.process.name, packet.process.namespace, "all"];
178
178
  const data = packet.data.replace(/\n$/, "");
179
- this.buffers[id].push(LogUtils.format({
179
+ if (type === "out" || type === "err") {
180
+ for (const alert of this.pm2.config.alerts) {
181
+ if ((alert.name === packet.process.name || alert.name === "*") && alert.event === `log:${type}`) {
182
+ const regexp = new RegExp(alert.regexp ?? "");
183
+ if (regexp.test(data)) {
184
+ this.pm2.list().then((list) => {
185
+ const proc = list.find((p) => p.pm_id === packet.process.pm_id && p.name === packet.process.name);
186
+ if (proc) {
187
+ return this.pm2.alert(alert, {
188
+ ...proc.pm2_env,
189
+ event: alert.event,
190
+ data,
191
+ manually: false
192
+ }).catch((err) => this.ctx.logger.warn("failed to deliver pm2 log alert:", err));
193
+ }
194
+ }, (err) => this.ctx.logger.warn("failed to fetch pm2 process for log alert:", err));
195
+ }
196
+ }
197
+ }
198
+ }
199
+ const formatted = LogUtils.format({
180
200
  app_name: packet.process.pm_id + "|" + packet.process.name,
181
201
  type,
182
202
  path: ""
183
- }, data));
184
- this.broadcasters[id]();
203
+ }, data);
204
+ for (const target of targets) {
205
+ if (!target) continue;
206
+ if (!this.listeners[target]) continue;
207
+ this.buffers[target].push(formatted);
208
+ this.broadcasters[target]();
209
+ }
185
210
  }, "onLog");
186
211
  this.pm2.bus.on("log:out", (packet) => onLog(packet, "out"));
187
212
  this.pm2.bus.on("log:err", (packet) => onLog(packet, "err"));
@@ -194,58 +219,57 @@ var LogManager = class {
194
219
  }
195
220
  }
196
221
  async start(id, cid, exclusive) {
197
- this.listeners[id] ||= /* @__PURE__ */ new Set();
198
- if (!this.listeners[id].size) {
199
- this.buffers[id] = [];
200
- this.broadcasters[id] = this.ctx.throttle(() => {
201
- if (!this.buffers[id].length) return;
202
- const buffer = this.buffers[id].splice(0);
222
+ const key = String(id);
223
+ this.listeners[key] ||= /* @__PURE__ */ new Set();
224
+ if (!this.listeners[key].size) {
225
+ this.buffers[key] = [];
226
+ this.broadcasters[key] = this.ctx.throttle(() => {
227
+ if (!this.buffers[key].length) return;
228
+ const buffer = this.buffers[key].splice(0);
203
229
  const toRemoved = [];
204
- Object.values(this.ctx.console.clients).filter((client) => this.listeners[id].has(client.id)).forEach((client) => {
230
+ Object.values(this.ctx.console.clients).filter((client) => this.listeners[key].has(client.id)).forEach((client) => {
205
231
  if (client.pm2?.lastHeartbeat && Date.now() - client.pm2.lastHeartbeat > this.pm2.config.listSyncTimeout) {
206
232
  toRemoved.push(client.id);
207
233
  return;
208
234
  }
209
235
  client.send({
210
236
  type: "pm2/patch-log",
211
- body: [id, buffer]
237
+ body: [key, buffer]
212
238
  });
213
239
  });
214
240
  toRemoved.forEach((cid2) => this.stopAll(cid2));
215
241
  }, 200);
216
242
  }
217
- this.listeners[id].add(cid);
243
+ this.listeners[key].add(cid);
218
244
  if (this.pm2.config.logTailLines) {
219
245
  const entries = [];
220
- if (id === "PM2" || id === "pm2") {
246
+ if (key === "PM2" || key === "pm2") {
221
247
  this.pushEntry(entries, {
222
248
  path: this.pm2.api.pm2_home + "/pm2.log",
223
249
  app_name: "PM2",
224
250
  type: "PM2"
225
251
  });
226
252
  } else {
227
- const procs = await new Promise((resolve2, reject) => {
228
- this.pm2.api.describe(id, (err, desc) => {
229
- if (err) return reject(err);
230
- resolve2(desc);
231
- });
232
- });
233
- for (const proc of procs) {
234
- if (proc.pm2_env && (id === "all" || proc.pm2_env.name === id || proc.pm2_env.pm_id === id)) {
235
- if (proc.pm2_env.pm_out_log_path && exclusive !== "err") {
236
- this.pushEntry(entries, {
237
- path: proc.pm2_env.pm_out_log_path,
238
- app_name: proc.pm2_env.pm_id + "|" + proc.pm2_env.name,
239
- type: "out"
240
- });
241
- }
242
- if (proc.pm2_env.pm_err_log_path && exclusive !== "out") {
243
- this.pushEntry(entries, {
244
- path: proc.pm2_env.pm_err_log_path,
245
- app_name: proc.pm2_env.pm_id + "|" + proc.pm2_env.name,
246
- type: "err"
247
- });
248
- }
253
+ const list = await this.pm2._list();
254
+ const isAll = key === "all";
255
+ const isNumeric = !isNaN(Number(key));
256
+ for (const proc of list) {
257
+ if (!proc?.pm2_env) continue;
258
+ const match = isAll || proc.pm2_env.name === key || proc.pm2_env.namespace === key || isNumeric && proc.pm2_env.pm_id === Number(key);
259
+ if (!match) continue;
260
+ if (proc.pm2_env.pm_out_log_path && exclusive !== "err") {
261
+ this.pushEntry(entries, {
262
+ path: proc.pm2_env.pm_out_log_path,
263
+ app_name: proc.pm2_env.pm_id + "|" + proc.pm2_env.name,
264
+ type: "out"
265
+ });
266
+ }
267
+ if (proc.pm2_env.pm_err_log_path && exclusive !== "out") {
268
+ this.pushEntry(entries, {
269
+ path: proc.pm2_env.pm_err_log_path,
270
+ app_name: proc.pm2_env.pm_id + "|" + proc.pm2_env.name,
271
+ type: "err"
272
+ });
249
273
  }
250
274
  }
251
275
  }
@@ -255,20 +279,21 @@ var LogManager = class {
255
279
  }
256
280
  }
257
281
  stop(id, cid) {
258
- this.listeners[id]?.delete(cid);
259
- if (this.listeners[id]?.size === 0) {
260
- delete this.listeners[id];
261
- delete this.buffers[id];
262
- delete this.broadcasters[id];
282
+ const key = String(id);
283
+ this.listeners[key]?.delete(cid);
284
+ if (this.listeners[key]?.size === 0) {
285
+ delete this.listeners[key];
286
+ delete this.buffers[key];
287
+ delete this.broadcasters[key];
263
288
  }
264
289
  }
265
290
  stopAll(cid) {
266
291
  for (const id in this.listeners) {
267
- this.stop(Number(id), cid);
292
+ this.stop(id, cid);
268
293
  }
269
294
  }
270
295
  };
271
- var PM2 = class extends import_koishi.Service {
296
+ var PM2 = class _PM2 extends import_koishi.Service {
272
297
  constructor(ctx, config) {
273
298
  super(ctx, "pm2");
274
299
  this.ctx = ctx;
@@ -298,6 +323,13 @@ var PM2 = class extends import_koishi.Service {
298
323
  });
299
324
  });
300
325
  await that.logs.init();
326
+ if (this.config.metricsInterval) {
327
+ this.ctx.setInterval(() => {
328
+ this.recordHistory().catch((err) => {
329
+ this.ctx.logger.warn("failed to record pm2 metrics:", err);
330
+ });
331
+ }, this.config.metricsInterval);
332
+ }
301
333
  this.bus.on("process:event", (packet) => {
302
334
  if (that.config.ignoreStoppingExit && packet.event === "exit" && packet.process.status === "stopping") return;
303
335
  if (that.config.ignoreStoppedExit && packet.event === "exit" && packet.process.status === "stopped") return;
@@ -311,12 +343,12 @@ var PM2 = class extends import_koishi.Service {
311
343
  ctx.on("dispose", () => this.api.close());
312
344
  ctx.on("pm2/process-event", async (event, env, manually) => {
313
345
  for (const alert of this.config.alerts) {
314
- if ((alert.name === env.name || alert.name === "*") && (alert.event["type"] ?? alert.event) === event) {
315
- if (event === "exit" && typeof alert.event === "object") {
316
- if (!(0, import_koishi.isNullable)(alert.event.eq) && env.exit_code !== alert.event.eq) continue;
317
- if (!(0, import_koishi.isNullable)(alert.event.neq) && env.exit_code === alert.event.neq) continue;
346
+ if ((alert.name === env.name || alert.name === "*") && _PM2.EventMap[alert.event] === event) {
347
+ if (alert.event === "exit:code" && event === "exit") {
348
+ if (!(0, import_koishi.isNullable)(alert.eq) && env.exit_code !== alert.eq) continue;
349
+ if (!(0, import_koishi.isNullable)(alert.neq) && env.exit_code === alert.neq) continue;
318
350
  }
319
- this.alert(alert, { ...env, event, manually }).catch((err) => {
351
+ this.alert(alert, { ...env, event: alert.event, raw_event: event, manually }).catch((err) => {
320
352
  ctx.logger.warn("failed to deliver pm2 alert:", err);
321
353
  });
322
354
  }
@@ -331,6 +363,7 @@ var PM2 = class extends import_koishi.Service {
331
363
  return that.list().then((list) => list.map((proc) => {
332
364
  delete proc.pm2_env.env;
333
365
  proc.alerts = (that.config.alerts || []).filter((alert) => alert.name === proc.name || alert.name === "*");
366
+ proc.history = that.metricHistory.get(proc.name);
334
367
  return proc;
335
368
  }));
336
369
  });
@@ -363,7 +396,7 @@ var PM2 = class extends import_koishi.Service {
363
396
  });
364
397
  addListener("pm2/test-alert", async (alert) => {
365
398
  const proc = await that.list().then((procs) => procs.find((p) => p.name === alert.name || alert.name === "*"));
366
- await that.alert(alert, { ...proc.pm2_env, event: alert.event["type"] ?? alert.event, manually: true });
399
+ await that.alert(alert, { ...proc.pm2_env, event: alert.event, manually: true });
367
400
  });
368
401
  addListener("pm2/remove-alert", async (alert) => {
369
402
  if (!alert?.name) return;
@@ -379,6 +412,14 @@ var PM2 = class extends import_koishi.Service {
379
412
  });
380
413
  that.ctx.scope.update(that.config, false);
381
414
  });
415
+ addListener("pm2/save", () => {
416
+ return new Promise((resolve2, reject) => {
417
+ that.api.dump((err) => {
418
+ if (err) return reject(err);
419
+ resolve2();
420
+ });
421
+ });
422
+ });
382
423
  ctx.on("console/connection", (client) => {
383
424
  if (!(client.id in ctx.console.clients)) {
384
425
  that.logs.stopAll(client.id);
@@ -389,10 +430,23 @@ var PM2 = class extends import_koishi.Service {
389
430
  const list = await that.list();
390
431
  return session.text(".output", list);
391
432
  });
433
+ ctx.command("pm2.save").action(async ({ session }) => {
434
+ try {
435
+ await new Promise((resolve2, reject) => {
436
+ this.api.dump((err) => {
437
+ if (err) return reject(err);
438
+ resolve2();
439
+ });
440
+ });
441
+ } catch {
442
+ return session.text(".failed");
443
+ }
444
+ return session.text(".success");
445
+ });
392
446
  for (const action of ["start", "restart", "reload", "stop", "delete"]) {
393
- ctx.command(`pm2.${action} <id>`).action(async ({ session }, id) => {
447
+ ctx.command(`pm2.${action} <key>`).action(async ({ session }, key) => {
394
448
  const procs = await new Promise((resolve2, reject) => {
395
- that.api[action](id, (err, res) => {
449
+ that.api[action](key, (err, res) => {
396
450
  if (err) return reject(err);
397
451
  resolve2(res);
398
452
  });
@@ -400,7 +454,7 @@ var PM2 = class extends import_koishi.Service {
400
454
  if (procs.length) {
401
455
  return session.text(".output", procs[0]);
402
456
  } else {
403
- return session.text(".not-found", { id });
457
+ return session.text(".not-found", [key]);
404
458
  }
405
459
  });
406
460
  }
@@ -409,6 +463,7 @@ var PM2 = class extends import_koishi.Service {
409
463
  api;
410
464
  bus = null;
411
465
  logs;
466
+ metricHistory = /* @__PURE__ */ new Map();
412
467
  list;
413
468
  trigger;
414
469
  async _list() {
@@ -419,6 +474,43 @@ var PM2 = class extends import_koishi.Service {
419
474
  });
420
475
  });
421
476
  }
477
+ async recordHistory() {
478
+ const list = await this._list();
479
+ const now = Date.now();
480
+ const maxSamples = Math.max(1, this.config.metricsHistorySize);
481
+ const seen = /* @__PURE__ */ new Set();
482
+ for (const proc of list) {
483
+ if (!proc?.name) continue;
484
+ const history = this.metricHistory.get(proc.name) ?? { samples: [], monitors: {} };
485
+ if (proc.monit) {
486
+ history.samples.push({
487
+ time: now,
488
+ cpu: proc.monit.cpu ?? 0,
489
+ memory: proc.monit.memory ?? 0
490
+ });
491
+ if (history.samples.length > maxSamples) {
492
+ history.samples.splice(0, history.samples.length - maxSamples);
493
+ }
494
+ }
495
+ const monitor = proc.pm2_env?.axm_monitor || {};
496
+ for (const [name, data] of Object.entries(monitor)) {
497
+ if (!data?.historic) continue;
498
+ const value = typeof data.value === "number" ? data.value : Number(data.value);
499
+ if (!Number.isFinite(value)) continue;
500
+ const monitorHistory = history.monitors[name] ?? [];
501
+ monitorHistory.push({ time: now, value });
502
+ if (monitorHistory.length > maxSamples) {
503
+ monitorHistory.splice(0, monitorHistory.length - maxSamples);
504
+ }
505
+ history.monitors[name] = monitorHistory;
506
+ }
507
+ this.metricHistory.set(proc.name, history);
508
+ seen.add(proc.name);
509
+ }
510
+ for (const key of this.metricHistory.keys()) {
511
+ if (!seen.has(key)) this.metricHistory.delete(key);
512
+ }
513
+ }
422
514
  async _trigger(id, actionName) {
423
515
  return new Promise((resolve2, reject) => {
424
516
  let counter = 0, processCount = 0, timeout = null;
@@ -440,7 +532,7 @@ var PM2 = class extends import_koishi.Service {
440
532
  }, this.config.actionTimeout);
441
533
  this.api.Client.executeRemote("msgProcess", {
442
534
  msg: actionName,
443
- ...isNaN(id) ? { name: id } : { id }
535
+ name: String(id)
444
536
  }, function(err, data) {
445
537
  if (err) return reject(err);
446
538
  if (!data?.process_count) reject(new Error("No process received command."));
@@ -452,7 +544,7 @@ var PM2 = class extends import_koishi.Service {
452
544
  const [platform, channelId] = parsePlatform(alert.cid);
453
545
  const bot = this.ctx.bots[alert.sid] ?? this.ctx.bots.find((bot2) => bot2.platform === platform);
454
546
  if (!bot) {
455
- this.ctx.logger.warn(`Cannot find bot for PM2 alert: ${alert.name}/${alert.event["type"] ?? alert.event} to ${alert.cid}`);
547
+ this.ctx.logger.warn(`Cannot find bot for PM2 alert: ${alert.name}/${alert.event} to ${alert.cid}`);
456
548
  return;
457
549
  }
458
550
  if (alert.message) {
@@ -462,7 +554,7 @@ var PM2 = class extends import_koishi.Service {
462
554
  if (this.ctx.get("database")) {
463
555
  locales.push(...await this.ctx.get("database").getChannel(platform, channelId, ["locales"]).then((channel) => channel?.locales ?? []));
464
556
  }
465
- const message = this.ctx.i18n.render(locales, [`pm2.alerts.${alert.event["type"] ?? alert.event}`], env);
557
+ const message = this.ctx.i18n.render(locales, [`pm2.alerts.${alert.event}`], env);
466
558
  return bot.sendMessage(channelId, message);
467
559
  }
468
560
  }
@@ -472,22 +564,53 @@ var PM2 = class extends import_koishi.Service {
472
564
  required: ["console"],
473
565
  optional: ["database", "console.services.status"]
474
566
  };
475
- PM22.Config = import_koishi.Schema.object({
476
- alerts: import_koishi.Schema.array(import_koishi.Schema.object({
567
+ PM22.EventMap = {
568
+ restart: "restart",
569
+ delete: "delete",
570
+ stop: "stop",
571
+ reload: "reload",
572
+ "reload:graceful": "graceful reload",
573
+ start: "start",
574
+ exit: "exit",
575
+ online: "online",
576
+ "restart:overlimit": "restart overlimit",
577
+ "exit:code": "exit",
578
+ "log:out": "",
579
+ "log:err": ""
580
+ };
581
+ const AlertSchema = import_koishi.Schema.intersect([
582
+ import_koishi.Schema.object({
477
583
  name: import_koishi.Schema.string().description("The name of the process. Use `*` to match all processes."),
478
- event: import_koishi.Schema.union(["restart", "delete", "stop", "reload", "graceful reload", "start", "exit", "online", "restart overlimit", import_koishi.Schema.object({
479
- type: import_koishi.Schema.const("exit").default("exit").hidden(),
584
+ event: import_koishi.Schema.union(Object.keys(PM22.EventMap)).default("exit").description("The type of process:event."),
585
+ sid: import_koishi.Schema.string().description("platform:selfId").required(false),
586
+ cid: import_koishi.Schema.string().description("platform:channelId").required(),
587
+ message: import_koishi.Schema.string().description("The custom i18n message to send with the notification.").required(false).role("textarea")
588
+ }),
589
+ import_koishi.Schema.union([
590
+ import_koishi.Schema.object({
591
+ event: import_koishi.Schema.const("exit:code").required(),
480
592
  eq: import_koishi.Schema.number().required(false),
481
593
  neq: import_koishi.Schema.number().required(false)
482
- }).description("exit(code)")]).description("The type of process:event."),
483
- sid: import_koishi.Schema.string().description("platform:selfId").required(false),
484
- cid: import_koishi.Schema.string().description("platform:channelId"),
485
- message: import_koishi.Schema.string().description("The custom i18n message to send with the notification.").required(false)
486
- })).description("PM2 process event notifications.").default([]).hidden(),
594
+ }),
595
+ import_koishi.Schema.object({
596
+ event: import_koishi.Schema.const("log:out").required(),
597
+ regexp: import_koishi.Schema.string().description("The regular expression to match the stdout log content.")
598
+ }),
599
+ import_koishi.Schema.object({
600
+ event: import_koishi.Schema.const("log:err").required(),
601
+ regexp: import_koishi.Schema.string().description("The regular expression to match the stderr log content.")
602
+ }),
603
+ import_koishi.Schema.object({})
604
+ ])
605
+ ]).description("PM2 process event alert configuration.");
606
+ PM22.Config = import_koishi.Schema.object({
607
+ alerts: import_koishi.Schema.array(AlertSchema).default([]).hidden(),
487
608
  logSyncInterval: import_koishi.Schema.number().description("The interval (in milliseconds) to sync logs from PM2.").default(200),
488
609
  listSyncInterval: import_koishi.Schema.number().description("The interval (in milliseconds) to sync process list from PM2.").default(1e3),
489
- listSyncTimeout: import_koishi.Schema.number().description("The timeout (in milliseconds) acts as heartbeat for clients requesting process list.").default(3e4),
490
- actionTimeout: import_koishi.Schema.number().description("The timeout (in milliseconds) for PM2 monitor actions.").default(1e4),
610
+ listSyncTimeout: import_koishi.Schema.number().description("The timeout (in milliseconds) acts as heartbeat for clients requesting process list.").default(1e3 * 30),
611
+ metricsInterval: import_koishi.Schema.number().description("The interval (in milliseconds) to sample CPU/memory metrics.").default(1e3 * 60),
612
+ metricsHistorySize: import_koishi.Schema.number().description("The number of historical metric samples to keep.").default(60),
613
+ actionTimeout: import_koishi.Schema.number().description("The timeout (in milliseconds) for PM2 monitor actions.").default(1e3 * 10),
491
614
  logTailLines: import_koishi.Schema.number().description("The number of log lines to tail when starting log streaming.").default(100),
492
615
  ignoreStoppingExit: import_koishi.Schema.boolean().description("Disable exit alerts when a process is stopping.").default(true),
493
616
  ignoreStoppedExit: import_koishi.Schema.boolean().description("Disable exit alerts when a process has stopped.").default(false)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-pm2",
3
3
  "description": "PM2 process manager dashboard",
4
- "version": "0.1.4",
4
+ "version": "0.3.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [