• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

apowers313 / servherd / 20961701317

13 Jan 2026 03:07PM UTC coverage: 82.727% (+1.2%) from 81.563%
20961701317

push

github

apowers313
test: improved test coverage to 81.56%

901 of 1027 branches covered (87.73%)

Branch coverage included in aggregate %.

3601 of 4415 relevant lines covered (81.56%)

13.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.0
/src/services/process.service.ts
1
import { homedir } from "node:os";
1✔
2
import { join } from "node:path";
3
import type { PM2ProcessDescription, PM2StartOptions, PM2Process } from "../types/pm2.js";
4
import type { ServerStatus } from "../types/registry.js";
5
import { logger } from "../utils/logger.js";
6

7
// Set PM2_HOME to servherd's own directory BEFORE PM2 is imported.
8
// This ensures servherd uses its own PM2 daemon, isolated from other projects.
9
// Only set if not already explicitly configured by the user.
10
if (!process.env.PM2_HOME) {
1✔
11
  process.env.PM2_HOME = join(homedir(), ".servherd", "pm2");
23✔
12
}
23✔
13

14
// Now import PM2 - it will pick up our PM2_HOME setting
15
const pm2 = (await import("pm2")).default;
1✔
16

17
const SERVHERD_PREFIX = "servherd-";
1✔
18

19
/**
20
 * Service for managing processes via PM2
21
 */
22
export class ProcessService {
1✔
23
  private connected = false;
181✔
24

25
  /**
26
   * Connect to PM2 daemon
27
   */
28
  async connect(): Promise<void> {
181✔
29
    return new Promise((resolve, reject) => {
171✔
30
      pm2.connect((err) => {
171✔
31
        if (err) {
171✔
32
          logger.error({ error: err }, "Failed to connect to PM2");
1✔
33
          reject(err);
1✔
34
          return;
1✔
35
        }
1✔
36
        this.connected = true;
170✔
37
        logger.debug("Connected to PM2");
170✔
38
        resolve();
170✔
39
      });
171✔
40
    });
171✔
41
  }
171✔
42

43
  /**
44
   * Disconnect from PM2 daemon
45
   */
46
  disconnect(): void {
181✔
47
    pm2.disconnect();
153✔
48
    this.connected = false;
153✔
49
    logger.debug("Disconnected from PM2");
153✔
50
  }
153✔
51

52
  /**
53
   * Ensure connected to PM2
54
   */
55
  private ensureConnected(): void {
181✔
56
    if (!this.connected) {
227✔
57
      throw new Error("Not connected to PM2. Call connect() first.");
3✔
58
    }
3✔
59
  }
227✔
60

61
  /**
62
   * Start a new process
63
   */
64
  async start(options: PM2StartOptions): Promise<PM2Process[]> {
181✔
65
    this.ensureConnected();
46✔
66

67
    return new Promise((resolve, reject) => {
46✔
68
      pm2.start(
45✔
69
        {
45✔
70
          name: options.name,
45✔
71
          script: options.script,
45✔
72
          args: options.args,
45✔
73
          cwd: options.cwd,
45✔
74
          env: options.env,
45✔
75
          instances: options.instances,
45✔
76
          autorestart: options.autorestart ?? false,
45✔
77
          watch: options.watch ?? false,
45✔
78
          max_memory_restart: options.max_memory_restart,
45✔
79
          output: options.output,
45✔
80
          error: options.error,
45✔
81
          // Enable ISO timestamps in logs by default
82
          log_date_format: options.log_date_format ?? "YYYY-MM-DDTHH:mm:ss.SSSZ",
45✔
83
        },
45✔
84
        (err, proc) => {
45✔
85
          if (err) {
45!
86
            logger.error({ error: err, name: options.name }, "Failed to start process");
×
87
            reject(err);
×
88
            return;
×
89
          }
×
90
          logger.info({ name: options.name }, "Process started");
45✔
91
          resolve(proc as PM2Process[]);
45✔
92
        },
45✔
93
      );
45✔
94
    });
46✔
95
  }
46✔
96

97
  /**
98
   * Stop a process by name
99
   */
100
  async stop(name: string): Promise<PM2Process[]> {
181✔
101
    this.ensureConnected();
16✔
102

103
    return new Promise((resolve, reject) => {
16✔
104
      pm2.stop(name, (err, proc) => {
16✔
105
        if (err) {
16✔
106
          logger.error({ error: err, name }, "Failed to stop process");
3✔
107
          reject(err);
3✔
108
          return;
3✔
109
        }
3✔
110
        logger.info({ name }, "Process stopped");
13✔
111
        resolve(proc as PM2Process[]);
13✔
112
      });
16✔
113
    });
16✔
114
  }
16✔
115

116
  /**
117
   * Restart a process by name
118
   */
119
  async restart(name: string): Promise<PM2Process[]> {
181✔
120
    this.ensureConnected();
15✔
121

122
    return new Promise((resolve, reject) => {
15✔
123
      pm2.restart(name, (err, proc) => {
15✔
124
        if (err) {
15✔
125
          logger.error({ error: err, name }, "Failed to restart process");
1✔
126
          reject(err);
1✔
127
          return;
1✔
128
        }
1✔
129
        logger.info({ name }, "Process restarted");
14✔
130
        resolve(proc as PM2Process[]);
14✔
131
      });
15✔
132
    });
15✔
133
  }
15✔
134

135
  /**
136
   * Delete a process by name
137
   */
138
  async delete(name: string): Promise<PM2Process[]> {
181✔
139
    this.ensureConnected();
37✔
140

141
    return new Promise((resolve, reject) => {
37✔
142
      pm2.delete(name, (err, proc) => {
37✔
143
        if (err) {
37✔
144
          logger.error({ error: err, name }, "Failed to delete process");
5✔
145
          reject(err);
5✔
146
          return;
5✔
147
        }
5✔
148
        logger.info({ name }, "Process deleted");
32✔
149
        resolve(proc as PM2Process[]);
32✔
150
      });
37✔
151
    });
37✔
152
  }
37✔
153

154
  /**
155
   * Get process description
156
   * Returns undefined for "not found" errors, re-throws other errors
157
   */
158
  async describe(name: string): Promise<PM2ProcessDescription | undefined> {
181✔
159
    this.ensureConnected();
96✔
160

161
    try {
96✔
162
      return await new Promise<PM2ProcessDescription | undefined>((resolve, reject) => {
96✔
163
        pm2.describe(name, (err, procDesc) => {
96✔
164
          if (err) {
96✔
165
            reject(err);
5✔
166
            return;
5✔
167
          }
5✔
168

169
          const desc = procDesc as PM2ProcessDescription[] | undefined;
91✔
170
          if (!desc || desc.length === 0) {
96✔
171
            resolve(undefined);
7✔
172
            return;
7✔
173
          }
7✔
174

175
          resolve(desc[0]);
84✔
176
        });
96✔
177
      });
96✔
178
    } catch (error) {
96✔
179
      const message = error instanceof Error ? error.message : String(error);
5!
180
      // Only suppress "not found" errors - these are expected when querying non-existent processes
181
      if (message.includes("not found") || message.includes("process name not found")) {
5✔
182
        return undefined;
2✔
183
      }
2✔
184
      // Re-throw all other errors (connection errors, IPC errors, etc.)
185
      logger.error({ error, name }, "Failed to describe process");
3✔
186
      throw error;
3✔
187
    }
3✔
188
  }
96✔
189

190
  /**
191
   * List all PM2 processes
192
   */
193
  async list(): Promise<PM2ProcessDescription[]> {
181✔
194
    this.ensureConnected();
6✔
195

196
    return new Promise((resolve, reject) => {
6✔
197
      pm2.list((err, procList) => {
6✔
198
        if (err) {
6!
199
          logger.error({ error: err }, "Failed to list processes");
×
200
          reject(err);
×
201
          return;
×
202
        }
×
203
        resolve((procList as PM2ProcessDescription[]) || []);
6!
204
      });
6✔
205
    });
6✔
206
  }
6✔
207

208
  /**
209
   * List only servherd-managed processes
210
   */
211
  async listServherdProcesses(): Promise<PM2ProcessDescription[]> {
181✔
212
    const allProcesses = await this.list();
4✔
213
    return allProcesses.filter((p) => p.name.startsWith(SERVHERD_PREFIX));
4✔
214
  }
4✔
215

216
  /**
217
   * Get the status of a process
218
   */
219
  async getStatus(name: string): Promise<ServerStatus> {
181✔
220
    try {
65✔
221
      const proc = await this.describe(name);
65✔
222
      if (!proc) {
65✔
223
        return "unknown";
2✔
224
      }
2✔
225

226
      const status = proc.pm2_env.status;
63✔
227

228
      // Map PM2 status to ServerStatus
229
      switch (status) {
63✔
230
        case "online":
65✔
231
          return "online";
56✔
232
        case "stopped":
65✔
233
        case "stopping":
65✔
234
          return "stopped";
7✔
235
        case "errored":
65!
236
          return "errored";
×
237
        default:
65!
238
          return "unknown";
×
239
      }
65✔
240
    } catch {
65!
241
      return "unknown";
×
242
    }
×
243
  }
65✔
244

245
  /**
246
   * Check if connected to PM2
247
   */
248
  isConnected(): boolean {
181✔
249
    return this.connected;
×
250
  }
×
251

252
  /**
253
   * Flush (clear) logs for a specific process.
254
   * @param name - Process name to flush logs for (required)
255
   */
256
  async flush(name: string): Promise<void> {
181✔
257
    this.ensureConnected();
7✔
258

259
    return new Promise((resolve, reject) => {
7✔
260
      pm2.flush(name, (err) => {
6✔
261
        if (err) {
6!
262
          logger.error({ error: err, name }, "Failed to flush logs");
×
263
          reject(err);
×
264
          return;
×
265
        }
×
266
        logger.info({ name }, "Logs flushed");
6✔
267
        resolve();
6✔
268
      });
6✔
269
    });
7✔
270
  }
7✔
271

272
  /**
273
   * Flush (clear) logs for all servherd-managed processes.
274
   * This only affects processes with the servherd prefix, not other PM2 processes.
275
   */
276
  async flushAll(): Promise<void> {
181✔
277
    this.ensureConnected();
4✔
278

279
    const processes = await this.listServherdProcesses();
4✔
280
    for (const proc of processes) {
3✔
281
      await this.flush(proc.name);
3✔
282
    }
3✔
283
    logger.info({ count: processes.length }, "Flushed logs for all servherd processes");
3✔
284
  }
4✔
285
}
181✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc