An interactive command-line interface (CLI) tool to help you install, use, and administer an AO instance.
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.
 
 
 

225 lines
7.9 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 './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
}