|
|
|
// 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
|
|
|
|
}
|