deicidus
3 years ago
11 changed files with 455 additions and 80 deletions
@ -0,0 +1,225 @@ |
|||||||
|
// 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.') |
||||||
|
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 |
||||||
|
} |
Loading…
Reference in new issue