// Tools for managing system services with systemctl (and eventually other options) import { execSync } from 'child_process' import path from 'path' import fs from 'fs' import { aoEnv, setAoEnv } from './settings.js' import { isFile } from './files.js' import { askQuestionText } from './welcome.js' const SERVICE_FOLDER_PATH = '/etc/systemd/system' // Manages a single systemctl service. serviceName is required. Implements the same functions as an AO feature module. // serviceFileText is optional but if omitted, install(), uninstall(), and isInstalledToSpec() will not exist // The only thing in this world that needs to be 'managed' is systemctl export default class SystemServiceManager { constructor(serviceName, serviceFileText = false) { this.name = serviceName + '.service' this.serviceFileText = serviceFileText this.assertInitialized() if(this.serviceFileText && this.serviceFileText.length >= 1) { this.install = this.willBeInstall this.uninstall = this.willBeUninstall this.isInstalledToSpec = this.willBeIsInstalledToSpec } } // Throws an exception if the service name has not been set assertInitialized() { if(!this.name || this.name.length < ('.service'.length + 1)) { throw new Error('SystemServiceManager: Service name must be a string at least one character long.') } } // Throws an exception if the serviceFileText has not been set assertServiceFileText() { if(!this.serviceFileText || this.serviceFileText.length < 1) { return false //throw new Error('SystemServiceManager: Attempt to use a function that requires serviceFileText, but serviceFileText is blank.') } return true } // Returns the full path of the service file, based on the service name servicePath() { this.assertInitialized() return path.join(SERVICE_FOLDER_PATH, this.name) } status(verbose = true) { if(this.isInstalled(verbose)) { if(this.isRunning(verbose)) { return 'running' } return 'installed' } console.log('its off') return 'off' } // Returns true if the service file exists (does not check if it matches serviceFileText) isInstalled(verbose = true) { this.assertInitialized() let error try { const stdout = execSync('systemctl cat -- ' + this.name + ' 2>&1') if(stdout.length >= 1) { return true } } catch(error) { if(error.output.toString().includes('No files found for')) { return false } } if(verbose) console.log('Unknown status, assuming not installed' + (error ? error.output.toString() : '.')) return false } // Return true if the file at the service file path matches the serviceFileText // This function and isInstalled use different methods of getting this text but it should be identical, if not we will notice someday willBeIsInstalledToSpec(verbose = true) { console.log('isInstalled()') this.assertInitialized() this.assertServiceFileText() const serviceFileText = fs.readFileSync(this.servicePath()) if(serviceFileText === this.serviceFileText) { return true } else { if(verbose) console.log('Failed to write service file, sorry.') return false } } // Creates or overwrites the service file willBeInstall(force = false, verbose = true) { console.log('Creating service file. You may be asked for you sudo password.') this.assertInitialized() this.assertServiceFileText() try { const tempPath = path.join(process.env.HOME, '.ao/generatedservice.temp') fs.writeFileSync(tempPath, this.serviceFileText, force ? { flag: "wx" } : undefined) execSync('sudo mv ' + tempPath + ' ' + this.servicePath()) execSync('sudo systemctl daemon-reload') return true } catch(error) { if(verbose) console.log('Service file already exists, or other error:', error) } // Clean up the temp file try { if(isFile(tempPath)) execSync('sudo rm ' + tempPath + ' 2>&1') } catch(error) {} // If it failed, the temp file probably didn't exist return false } // Deletes the service file willBeUninstall(verbose = true) { this.assertInitialized() console.log('Deleting service file. You may be asked for you sudo password.') this.stop() try { execSync('sudo rm ' + this.servicePath()) if(verbose) console.log("Deleted service file.") execSync('sudo systemctl daemon-reload') return true } catch(error) { if(verbose) console.log('Failed to delete service file:', error) } return false } // Returns true if the service is currently running without errors isRunning(verbose = true) { this.assertInitialized() try { const stdout = execSync('systemctl status -- ' + this.name) if(stdout.includes('active (running)')) { return true } } catch(error) { error = error.output.toString() if(error.includes(' could not be found.')) { if(verbose) console.log('Warning: Checking whether a nonexistent service ' + this.name, 'is running. Check whether it is installed first.') return false } else if(error.includes('inactive (dead)')) { return false } else if(error.includes('failed')) { return false } else if(error.includes('stopped')) { return false } else { if(verbose) console.log('Unknown error checking ' + this.name + ' running status:', error) } } return false } // Attempts to start the service. Asks for sudo. start(verbose = true) { this.assertInitialized() try { const stdout = execSync('sudo systemctl start -- ' + this.name) // todo: check for errors including wrong sudo password here return true } catch(error) { if(verbose) console.log('Error starting service:', error) } return false } // Attempts to stop the service. Asks for sudo. stop(verbose = true) { this.assertInitialized() try { const stdout = execSync('sudo systemctl stop -- ' + this.name) // todo: check for errors including wrong sudo password here return true } catch(error) { if(verbose) console.log('Error stopping service:', error) } } } // Returns the saved list of custom services the user has saved in ao-cli's feature management interface export function getCustomServicesList() { let servicesList = aoEnv('AO_CLI_CUSTOM_SERVICES') if(!servicesList) { return [] } return servicesList.split(',') } // Adds the given service name to the custom list of services saved for ao-cli in .env, and returns a new SystemServiceManager to control it // Adding custom services is only for starting and stopping them, not for adding a service file (could add this but would need to make a UI) function addCustomService(serviceName) { const customServices = getCustomServicesList() customServices.push(serviceName) const serialized = customServices.join(',') setAoEnv('AO_CLI_CUSTOM_SERVICES', serialized) return new SystemServiceManager(serviceName) } export function removeCustomService(serviceName) { let customServices = getCustomServicesList() customServices = customServices.filter(service => service !== serviceName) const serialized = customServices.join(',') setAoEnv('AO_CLI_CUSTOM_SERVICES', serialized) } // Asks the user for a new service name and save it export async function addCustomServiceInteractive() { let serviceName = await askQuestionText('What is the name of the service?') if(!serviceName || serviceName.length < 1) { console.log('Nothing entered, going back.') return false } serviceName = serviceName.toLowerCase() if(getCustomServicesList().includes(serviceName)) { console.log('You have already add that service to the list.') return true } const service = new SystemServiceManager(serviceName) if(!service.isInstalled()) { console.log('That service does not exist, sorry.') return true } addCustomService(serviceName) return false }