Skip to content

Commit 4a0959b

Browse files
committed
feat(ups): refactor UPS service to use execa for command execution and enhance error handling; update settings for additional test commands
1 parent 865e3df commit 4a0959b

File tree

7 files changed

+73
-19
lines changed

7 files changed

+73
-19
lines changed

.claude/settings.local.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
"Bash(pnpm tailwind:build:*)",
3737
"Bash(pnpm --filter ./api test src/unraid-api/graph/resolvers/ups)",
3838
"Bash(pnpm --filter @unraid/api test src/unraid-api/graph/resolvers/ups)",
39-
"Bash(pnpm --filter @unraid/api lint)"
39+
"Bash(pnpm --filter @unraid/api lint)",
40+
"Bash(pnpm --filter ./api test)",
41+
"Bash(pnpm --filter @unraid/api test)"
4042
]
4143
},
4244
"enableAllProjectMcpServers": false

api/src/unraid-api/graph/resolvers/ups/ups.service.ts

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { Injectable, Logger } from '@nestjs/common';
2-
import { exec } from 'child_process';
32
import { readFile, writeFile } from 'fs/promises';
4-
import { promisify } from 'util';
53

4+
import { execa, ExecaError } from 'execa';
65
import { z } from 'zod';
76

87
import { fileExistsSync } from '@app/core/utils/files/file-exists.js';
98
import { UPSConfigInput } from '@app/unraid-api/graph/resolvers/ups/ups.inputs.js';
109

11-
const execPromise = promisify(exec);
12-
1310
const UPSSchema = z.object({
1411
MODEL: z.string().optional(),
1512
STATUS: z.string().optional(),
@@ -48,7 +45,10 @@ export class UPSService {
4845

4946
async getUPSData(): Promise<UPSData> {
5047
try {
51-
const { stdout } = await execPromise('/sbin/apcaccess 2>/dev/null', { timeout: 10000 });
48+
const { stdout } = await execa('/sbin/apcaccess', [], {
49+
timeout: 10000,
50+
reject: false, // Handle errors manually
51+
});
5252
if (!stdout || stdout.trim().length === 0) {
5353
throw new Error('No UPS data returned from apcaccess');
5454
}
@@ -71,7 +71,7 @@ export class UPSService {
7171

7272
// Stop the UPS service before making changes
7373
try {
74-
await execPromise('/etc/rc.d/rc.apcupsd stop', { timeout: 10000 });
74+
await execa('/etc/rc.d/rc.apcupsd', ['stop'], { timeout: 10000 });
7575
} catch (error) {
7676
this.logger.warn('Failed to stop apcupsd service (may not be running):', error);
7777
}
@@ -122,21 +122,73 @@ export class UPSService {
122122
throw error;
123123
}
124124

125+
// Validate KILLUPS and SERVICE values
126+
const validKillUpsValues = ['yes', 'no'];
127+
const validServiceValues = ['enable', 'disable'];
128+
129+
if (config.KILLUPS && !validKillUpsValues.includes(config.KILLUPS)) {
130+
throw new Error(`Invalid KILLUPS value: ${config.KILLUPS}. Must be 'yes' or 'no'`);
131+
}
132+
133+
if (config.SERVICE && !validServiceValues.includes(config.SERVICE)) {
134+
throw new Error(
135+
`Invalid SERVICE value: ${config.SERVICE}. Must be 'enable' or 'disable'`
136+
);
137+
}
138+
125139
// Handle killpower configuration
126140
if (config.KILLUPS === 'yes' && config.SERVICE === 'enable') {
127-
await execPromise(
128-
`! grep -q apccontrol /etc/rc.d/rc.6 && sed -i -e 's:/sbin/poweroff:/etc/apcupsd/apccontrol killpower; /sbin/poweroff:' /etc/rc.d/rc.6`
129-
);
141+
try {
142+
// First check if apccontrol is already in rc.6
143+
const { exitCode } = await execa('grep', ['-q', 'apccontrol', '/etc/rc.d/rc.6'], {
144+
reject: false,
145+
});
146+
147+
// If not found (exitCode !== 0), add it
148+
if (exitCode !== 0) {
149+
await execa('sed', [
150+
'-i',
151+
'-e',
152+
's:/sbin/poweroff:/etc/apcupsd/apccontrol killpower; /sbin/poweroff:',
153+
'/etc/rc.d/rc.6',
154+
]);
155+
this.logger.debug('Added killpower to rc.6');
156+
}
157+
} catch (error) {
158+
this.logger.error('Failed to update rc.6 for killpower enable:', error);
159+
throw new Error(
160+
`Failed to enable killpower in /etc/rc.d/rc.6: ${error instanceof Error ? error.message : String(error)}`
161+
);
162+
}
130163
} else {
131-
await execPromise(
132-
`grep -q apccontrol /etc/rc.d/rc.6 && sed -i -e 's:/etc/apcupsd/apccontrol killpower; /sbin/poweroff:/sbin/poweroff:' /etc/rc.d/rc.6`
133-
);
164+
try {
165+
// Check if apccontrol is in rc.6
166+
const { exitCode } = await execa('grep', ['-q', 'apccontrol', '/etc/rc.d/rc.6'], {
167+
reject: false,
168+
});
169+
170+
// If found (exitCode === 0), remove it
171+
if (exitCode === 0) {
172+
await execa('sed', [
173+
'-i',
174+
'-e',
175+
's:/etc/apcupsd/apccontrol killpower; /sbin/poweroff:/sbin/poweroff:',
176+
'/etc/rc.d/rc.6',
177+
]);
178+
this.logger.debug('Removed killpower from rc.6');
179+
}
180+
} catch (error) {
181+
this.logger.error('Failed to update rc.6 for killpower disable:', error);
182+
throw new Error(
183+
`Failed to disable killpower in /etc/rc.d/rc.6: ${error instanceof Error ? error.message : String(error)}`
184+
);
185+
}
134186
}
135187

136188
// Start the service if enabled
137189
if (config.SERVICE === 'enable') {
138190
try {
139-
await execPromise('/etc/rc.d/rc.apcupsd start', { timeout: 10000 });
191+
await execa('/etc/rc.d/rc.apcupsd', ['start'], { timeout: 10000 });
140192
this.logger.debug('Successfully started apcupsd service');
141193
} catch (error) {
142194
this.logger.error('Failed to start apcupsd service:', error);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1752524464371
1+
1753191130466
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1752524464066
1+
1753191130119
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1752524464213
1+
1753191130299
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1752524464631
1+
1753191130595
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1752524464761
1+
1753191130864

0 commit comments

Comments
 (0)