feat(systemd): support configurable service environment variables

This commit is contained in:
2026-06-01 08:08:01 +00:00
parent 704d8328ef
commit 142b69ab63
6 changed files with 1441 additions and 2969 deletions
+10
View File
@@ -1,5 +1,15 @@
# Changelog
## Pending
### Features
- support configurable service environment variables (systemd)
- Add optional environment configuration to SmartDaemon service options.
- Render validated and escaped systemd Environment entries from service definitions.
- Remove the hard-coded NODE_OPTIONS memory setting from generated unit files.
- Update development tooling dependencies.
## 2026-05-01 - 2.1.1 - fix(systemdmanager)
migrate systemd file operations to smartfs and tighten TypeScript safety
+4 -4
View File
@@ -32,10 +32,10 @@
},
"homepage": "https://code.foss.global/push.rocks/smartdaemon",
"devDependencies": {
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsrun": "^2.0.3",
"@git.zone/tstest": "^3.6.3",
"@types/node": "^25.6.0"
"@git.zone/tsbuild": "^4.4.2",
"@git.zone/tsrun": "^2.0.4",
"@git.zone/tstest": "^3.6.6",
"@types/node": "^25.9.1"
},
"dependencies": {
"@push.rocks/lik": "^6.4.1",
+1385 -2962
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -54,6 +54,29 @@ tap.test('should generate systemd unit file with User/Group directives', async (
expect(unitFileContent).toInclude('Group=www-data');
expect(unitFileContent).toInclude('# user: www-data');
expect(unitFileContent).toInclude('# group: www-data');
expect(unitFileContent).not.toInclude('max_old_space_size=100');
});
tap.test('should generate systemd unit file with configured environment', async () => {
const testService = await smartdaemon.SmartDaemonService.createFromOptions(
testSmartdaemon,
{
name: 'test-service-env',
description: 'A test service with environment',
command: 'node test.js',
workingDir: '/tmp',
version: '1.0.0',
environment: {
NODE_OPTIONS: '--max_old_space_size=1024',
CUSTOM_VALUE: 'quoted "value"',
},
}
);
const unitFileContent = testSmartdaemon.templateManager.generateUnitFileForService(testService);
expect(unitFileContent).toInclude('Environment="NODE_OPTIONS=--max_old_space_size=1024"');
expect(unitFileContent).toInclude('Environment="CUSTOM_VALUE=quoted \\"value\\""');
});
tap.test('should handle services without user/group', async () => {
+2
View File
@@ -10,6 +10,7 @@ export interface ISmartDaemonServiceConstructorOptions {
version: string;
user?: string;
group?: string;
environment?: Record<string, string>;
}
/**
@@ -36,6 +37,7 @@ export class SmartDaemonService implements ISmartDaemonServiceConstructorOptions
public description!: string;
public user?: string;
public group?: string;
public environment?: Record<string, string>;
public smartdaemonRef: SmartDaemon;
+16 -2
View File
@@ -8,7 +8,21 @@ export class SmartDaemonTemplateManager {
this.smartdaemonRef = smartdaemonRefArg;
}
private generateEnvironmentLines(serviceArg: SmartDaemonService): string {
const environment = serviceArg.environment || {};
return Object.entries(environment)
.map(([key, value]) => {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
throw new Error(`Invalid systemd environment key: ${key}`);
}
const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
return `Environment="${key}=${escapedValue}"`;
})
.join('\n');
}
public generateUnitFileForService = (serviceArg: SmartDaemonService) => {
const environmentLines = this.generateEnvironmentLines(serviceArg);
return `# ---
# name: ${serviceArg.name}
# version: ${serviceArg.version}
@@ -26,8 +40,8 @@ After=network.target
[Service]
Type=simple${serviceArg.user ? `
User=${serviceArg.user}` : ''}${serviceArg.group ? `
Group=${serviceArg.group}` : ''}
Environment=NODE_OPTIONS="--max_old_space_size=100"
Group=${serviceArg.group}` : ''}${environmentLines ? `
${environmentLines}` : ''}
ExecStart=/bin/bash -c "cd ${serviceArg.workingDir} && ${serviceArg.command}"
WorkingDirectory=${serviceArg.workingDir}
Restart=on-failure