deicidus
2 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