You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
249 lines
8.7 KiB
249 lines
8.7 KiB
// 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 '../ao-lib/settings.js' |
|
import { isFile } from './files.js' |
|
import { askQuestionText } from './welcome.js' |
|
|
|
const SERVICE_FOLDER_PATH = '/etc/systemd/system' |
|
const USER_SERVICE_FOLDER_PATH = '/usr/lib/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) |
|
} |
|
|
|
// Returns the full path to the service file that MIGHT exist in the /usr user-specific service file directory |
|
userServicePath() { |
|
this.assertInitialized() |
|
return path.join(USER_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() |
|
let deleted = false |
|
try { |
|
execSync('sudo rm ' + this.servicePath() + ' 2>&1') |
|
deleted = true |
|
} catch(error) { |
|
} |
|
try { |
|
execSync('sudo rm ' + this.userServicePath() + ' 2>&1') |
|
if(verbose) console.log(`Deleted service file in ${USER_SERVICE_FOLDER_PATH} directory.`) |
|
deleted = true |
|
} catch(err) {} // This one might not exist so we can ignore errors |
|
|
|
if(verbose && deleted) { |
|
console.log("Deleted service file.") |
|
} else if(verbose && !deleted) { |
|
console.log('Failed to delete service file.') |
|
} |
|
|
|
try { |
|
execSync('sudo systemctl daemon-reload') |
|
} catch(err) { |
|
console.log('Failed to reload services. Most likely, there are errors in your service files.') |
|
return false |
|
} |
|
return true |
|
} |
|
|
|
// 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 |
|
}
|
|
|