feat(systemd-manager): Support sudo password and root detection in SystemdManager; add user/group support in services and templates; add tests and expand README
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartdaemon',
|
||||
version: '2.0.9',
|
||||
version: '2.1.0',
|
||||
description: 'Start scripts as long running daemons and manage them.'
|
||||
}
|
||||
|
@@ -1 +1,2 @@
|
||||
export * from './smartdaemon.classes.smartdaemon.js';
|
||||
export * from './smartdaemon.classes.service.js';
|
||||
|
@@ -8,6 +8,8 @@ export interface ISmartDaemonServiceConstructorOptions {
|
||||
command: string;
|
||||
workingDir: string;
|
||||
version: string;
|
||||
user?: string;
|
||||
group?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,6 +35,8 @@ export class SmartDaemonService implements ISmartDaemonServiceConstructorOptions
|
||||
public command: string;
|
||||
public workingDir: string;
|
||||
public description: string;
|
||||
public user?: string;
|
||||
public group?: string;
|
||||
|
||||
public smartdaemonRef: SmartDaemon;
|
||||
|
||||
|
@@ -6,13 +6,17 @@ import {
|
||||
} from './smartdaemon.classes.service.js';
|
||||
import { SmartDaemonSystemdManager } from './smartdaemon.classes.systemdmanager.js';
|
||||
|
||||
export interface ISmartDaemonOptions {
|
||||
sudoPassword?: string;
|
||||
}
|
||||
|
||||
export class SmartDaemon {
|
||||
public templateManager: SmartDaemonTemplateManager;
|
||||
public systemdManager: SmartDaemonSystemdManager;
|
||||
|
||||
constructor() {
|
||||
constructor(optionsArg?: ISmartDaemonOptions) {
|
||||
this.templateManager = new SmartDaemonTemplateManager(this);
|
||||
this.systemdManager = new SmartDaemonSystemdManager(this);
|
||||
this.systemdManager = new SmartDaemonSystemdManager(this, optionsArg?.sudoPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -31,13 +31,29 @@ export class SmartDaemonSystemdManager {
|
||||
public smartsystem: plugins.smartsystem.Smartsystem;
|
||||
|
||||
public shouldExecute: boolean = false;
|
||||
public isRoot: boolean = false;
|
||||
public sudoPassword?: string;
|
||||
|
||||
constructor(smartdaemonRefArg: SmartDaemon) {
|
||||
constructor(smartdaemonRefArg: SmartDaemon, sudoPasswordArg?: string) {
|
||||
this.smartdaemonRef = smartdaemonRefArg;
|
||||
this.smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
});
|
||||
this.smartsystem = new plugins.smartsystem.Smartsystem();
|
||||
this.sudoPassword = sudoPasswordArg;
|
||||
|
||||
// Check if we're running as root
|
||||
this.checkIsRoot().then(isRoot => {
|
||||
this.isRoot = isRoot;
|
||||
if (!isRoot) {
|
||||
console.log('Not running as root. Sudo will be used for privileged operations.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async checkIsRoot(): Promise<boolean> {
|
||||
const result = await this.smartshellInstance.exec('id -u');
|
||||
return result.stdout.trim() === '0';
|
||||
}
|
||||
|
||||
public async checkElegibility() {
|
||||
@@ -52,7 +68,28 @@ export class SmartDaemonSystemdManager {
|
||||
|
||||
public async execute(commandArg: string) {
|
||||
if (await this.checkElegibility()) {
|
||||
await this.smartshellInstance.exec(commandArg);
|
||||
// Only use sudo if we're not root and command doesn't already have sudo
|
||||
if (!this.isRoot && !commandArg.startsWith('sudo')) {
|
||||
commandArg = `sudo ${commandArg}`;
|
||||
|
||||
if (this.sudoPassword) {
|
||||
// Use interactive PTY mode for password input
|
||||
const interactiveExec = await this.smartshellInstance.execInteractiveControlPty(commandArg);
|
||||
await interactiveExec.sendLine(this.sudoPassword);
|
||||
const result = await interactiveExec.finalPromise;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute command (with or without sudo)
|
||||
try {
|
||||
await this.smartshellInstance.exec(commandArg);
|
||||
} catch (error) {
|
||||
if (!this.isRoot && error.message && error.message.includes('authentication')) {
|
||||
throw new Error('Sudo authentication failed. Please configure passwordless sudo or provide a sudo password.');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,18 +138,31 @@ export class SmartDaemonSystemdManager {
|
||||
|
||||
public async saveService(serviceArg: SmartDaemonService) {
|
||||
if (await this.checkElegibility()) {
|
||||
await plugins.smartfile.memory.toFs(
|
||||
this.smartdaemonRef.templateManager.generateUnitFileForService(serviceArg),
|
||||
SmartDaemonSystemdManager.createFilePathFromServiceName(serviceArg.name)
|
||||
);
|
||||
const content = this.smartdaemonRef.templateManager.generateUnitFileForService(serviceArg);
|
||||
const targetPath = SmartDaemonSystemdManager.createFilePathFromServiceName(serviceArg.name);
|
||||
|
||||
if (this.isRoot) {
|
||||
// Direct write when running as root
|
||||
await plugins.smartfile.memory.toFs(content, targetPath);
|
||||
} else {
|
||||
// Use sudo to write when not root
|
||||
const tempPath = `/tmp/smartdaemon_${serviceArg.name}.service`;
|
||||
await plugins.smartfile.memory.toFs(content, tempPath);
|
||||
await this.execute(`mv ${tempPath} ${targetPath}`); // execute() will add sudo
|
||||
await this.execute(`chmod 644 ${targetPath}`); // execute() will add sudo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteService(serviceArg: SmartDaemonService) {
|
||||
if (await this.checkElegibility()) {
|
||||
await plugins.smartfile.fs.remove(
|
||||
SmartDaemonSystemdManager.createServiceNameFromServiceName(serviceArg.name)
|
||||
);
|
||||
const filePath = SmartDaemonSystemdManager.createFilePathFromServiceName(serviceArg.name);
|
||||
|
||||
if (this.isRoot) {
|
||||
await plugins.smartfile.fs.remove(filePath);
|
||||
} else {
|
||||
await this.execute(`rm ${filePath}`); // execute() will add sudo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import * as plugins from './smartdaemon.plugins.js';
|
||||
import { SmartDaemon } from './smartdaemon.classes.smartdaemon.js';
|
||||
import { SmartDaemonService } from './smartdaemon.classes.service.js';
|
||||
|
||||
@@ -15,7 +14,9 @@ export class SmartDaemonTemplateManager {
|
||||
# version: ${serviceArg.version}
|
||||
# description: ${serviceArg.description}
|
||||
# command: ${serviceArg.command}
|
||||
# workingDir: ${serviceArg.workingDir}
|
||||
# workingDir: ${serviceArg.workingDir}${serviceArg.user ? `
|
||||
# user: ${serviceArg.user}` : ''}${serviceArg.group ? `
|
||||
# group: ${serviceArg.group}` : ''}
|
||||
# ---
|
||||
[Unit]
|
||||
Description=${serviceArg.description}
|
||||
@@ -23,7 +24,9 @@ Requires=network.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Type=simple${serviceArg.user ? `
|
||||
User=${serviceArg.user}` : ''}${serviceArg.group ? `
|
||||
Group=${serviceArg.group}` : ''}
|
||||
Environment=NODE_OPTIONS="--max_old_space_size=100"
|
||||
ExecStart=/bin/bash -c "cd ${serviceArg.workingDir} && ${serviceArg.command}"
|
||||
WorkingDirectory=${serviceArg.workingDir}
|
||||
|
Reference in New Issue
Block a user