peertube-plugin-livechat/server/lib/prosody/ctl.ts

407 lines
12 KiB
TypeScript
Raw Normal View History

import type { RegisterServerOptions } from '@peertube/peertube-types'
import { getProsodyConfig, getProsodyFilePaths, writeProsodyConfig } from './config'
2021-05-12 12:59:52 +00:00
import { startProsodyLogRotate, stopProsodyLogRotate } from './logrotate'
import {
ensureProsodyCertificates, startProsodyCertificatesRenewCheck, stopProsodyCertificatesRenewCheck
} from './certificates'
import { disableProxyRoute, enableProxyRoute } from '../routers/webchat'
import { fixRoomSubject } from './fix-room-subject'
2021-04-14 13:26:00 +00:00
import * as fs from 'fs'
2021-04-14 14:14:56 +00:00
import * as child_process from 'child_process'
2021-04-13 15:13:41 +00:00
async function _ensureWorkingDir (
options: RegisterServerOptions,
workingDir: string,
dataDir: string,
certsDir: string | undefined,
certsDirIsCustom: boolean,
appImageExtractPath: string
): Promise<string> {
const logger = options.peertubeHelpers.logger
logger.debug('Calling _ensureworkingDir')
if (!fs.existsSync(workingDir)) {
logger.info(`The working dir ${workingDir} does not exists, trying to create it`)
await fs.promises.mkdir(workingDir)
logger.debug(`Working dir ${workingDir} was created`)
}
logger.debug(`Testing write access on ${workingDir}`)
await fs.promises.access(workingDir, fs.constants.W_OK) // will throw an error if no access
logger.debug(`Write access ok on ${workingDir}`)
if (!fs.existsSync(dataDir)) {
logger.info(`The data dir ${dataDir} does not exists, trying to create it`)
await fs.promises.mkdir(dataDir)
logger.debug(`data dir ${dataDir} was created`)
}
if (certsDir && !certsDirIsCustom && !fs.existsSync(certsDir)) {
// Certificates dir for Prosody.
// Note: not used yet, but we create the directory to avoid errors in prosody logs.
logger.info(`The certs dir ${certsDir} does not exists, trying to create it`)
await fs.promises.mkdir(certsDir)
logger.debug(`certs dir ${certsDir} was created`)
}
if (!fs.existsSync(appImageExtractPath)) {
logger.info(`The appImageExtractPath dir ${appImageExtractPath} does not exists, trying to create it`)
await fs.promises.mkdir(appImageExtractPath)
logger.debug(`appImageExtractPath dir ${appImageExtractPath} was created`)
}
return workingDir
}
/**
* This function prepares:
* - the Prosody working dir
* - the binaries for the embeded Prosody (if needed).
* @param options
*/
async function prepareProsody (options: RegisterServerOptions): Promise<void> {
const logger = options.peertubeHelpers.logger
const filePaths = await getProsodyFilePaths(options)
logger.debug('Ensuring that the working dir exists')
await _ensureWorkingDir(
options,
filePaths.dir,
filePaths.data,
filePaths.certs,
filePaths.certsDirIsCustom,
filePaths.appImageExtractPath
)
try {
await fixRoomSubject(options, filePaths)
} catch (err) {
logger.error(err)
}
const appImageToExtract = filePaths.appImageToExtract
if (!appImageToExtract) {
return
}
return new Promise((resolve, reject) => {
const spawned = child_process.spawn(appImageToExtract, ['--appimage-extract'], {
cwd: filePaths.appImageExtractPath,
env: {
...process.env
}
})
spawned.stdout.on('data', (data) => {
logger.debug(`AppImage extract printed: ${data as string}`)
})
spawned.stderr.on('data', (data) => {
logger.error(`AppImage extract has errors: ${data as string}`)
})
spawned.on('error', reject)
spawned.on('close', (_code) => { // 'close' and not 'exit', to be sure it is finished.
resolve()
})
})
}
2021-04-14 16:15:43 +00:00
interface ProsodyCtlResult {
code: number | null
stdout: string
sterr: string
message: string
2021-04-13 16:00:45 +00:00
}
interface ProsodyCtlOptions {
additionalArgs?: string[]
yesMode?: boolean
stdErrFilter?: (data: string) => boolean
}
async function prosodyCtl (
options: RegisterServerOptions, command: string, prosodyCtlOptions?: ProsodyCtlOptions
): Promise<ProsodyCtlResult> {
2021-04-14 15:10:22 +00:00
const logger = options.peertubeHelpers.logger
logger.debug('Calling prosodyCtl with command ' + command)
2021-04-14 14:14:56 +00:00
const filePaths = await getProsodyFilePaths(options)
if (!/^\w+$/.test(command)) {
throw new Error(`Invalid prosodyctl command '${command}'`)
}
return new Promise((resolve, reject) => {
if (!filePaths.execCtl) {
reject(new Error('Missing prosodyctl command executable'))
return
}
2021-04-14 14:14:56 +00:00
let d: string = ''
let e: string = ''
2021-04-14 16:15:43 +00:00
let m: string = ''
const cmdArgs = [
...filePaths.execCtlArgs,
2021-04-14 14:14:56 +00:00
'--config',
filePaths.config,
command
]
prosodyCtlOptions?.additionalArgs?.forEach(arg => {
// No need to check for code injection, child_process.spawn will escape args correctly.
cmdArgs.push(arg)
})
const spawned = child_process.spawn(filePaths.execCtl, cmdArgs, {
2021-04-14 14:14:56 +00:00
cwd: filePaths.dir,
env: {
...process.env,
PROSODY_CONFIG: filePaths.config
}
})
let yesModeInterval: NodeJS.Timer
if (prosodyCtlOptions?.yesMode) {
yesModeInterval = setInterval(() => {
options.peertubeHelpers.logger.debug('ProsodyCtl was called in yesMode, writing to standard input.')
spawned.stdin.write('\n')
}, 10)
}
2021-04-14 14:14:56 +00:00
spawned.stdout.on('data', (data) => {
d += data as string
2021-04-14 16:15:43 +00:00
m += data as string
2021-04-14 14:14:56 +00:00
})
spawned.stderr.on('data', (data) => {
if (prosodyCtlOptions?.stdErrFilter) {
if (!prosodyCtlOptions.stdErrFilter('' + (data as string))) {
return
}
}
2021-04-14 14:14:56 +00:00
options.peertubeHelpers.logger.error(`Spawned command ${command} has errors: ${data as string}`)
e += data as string
2021-04-14 16:15:43 +00:00
m += data as string
2021-04-14 14:14:56 +00:00
})
spawned.on('error', reject)
// on 'close' and not 'exit', to be sure everything is done
// (else it can cause trouble by cleaning AppImage extract too soon)
spawned.on('close', (code) => {
if (yesModeInterval) { clearInterval(yesModeInterval) }
2021-04-14 16:15:43 +00:00
resolve({
code: code,
stdout: d,
sterr: e,
message: m
})
2021-04-14 14:14:56 +00:00
})
})
}
async function getProsodyAbout (options: RegisterServerOptions): Promise<string> {
2021-04-14 16:15:43 +00:00
const ctl = await prosodyCtl(options, 'about')
return ctl.message
2021-04-14 14:14:56 +00:00
}
2021-05-12 12:59:52 +00:00
async function reloadProsody (options: RegisterServerOptions): Promise<boolean> {
const reload = await prosodyCtl(options, 'reload')
if (reload.code) {
options.peertubeHelpers.logger.error('reloadProsody failed: ' + JSON.stringify(reload))
return false
}
return true
}
async function checkProsody (options: RegisterServerOptions): Promise<string> {
const ctl = await prosodyCtl(options, 'check')
return ctl.message
}
2021-04-14 16:15:43 +00:00
interface ProsodyRunning {
ok: boolean
messages: string[]
}
2021-04-14 13:26:00 +00:00
async function testProsodyRunning (options: RegisterServerOptions): Promise<ProsodyRunning> {
2021-04-13 15:13:41 +00:00
const { peertubeHelpers } = options
2021-04-14 15:10:22 +00:00
const logger = peertubeHelpers.logger
logger.info('Checking if Prosody is running')
2021-04-14 13:26:00 +00:00
const result: ProsodyRunning = {
2021-04-13 16:00:45 +00:00
ok: false,
messages: []
}
2021-04-14 13:26:00 +00:00
const filePaths = await getProsodyFilePaths(options)
try {
2021-04-14 15:10:22 +00:00
logger.debug('Trying to access the pid file')
2021-04-14 13:26:00 +00:00
await fs.promises.access(filePaths.pid, fs.constants.R_OK)
2021-04-14 14:14:56 +00:00
result.messages.push(`Pid file ${filePaths.pid} found`)
2021-04-14 13:26:00 +00:00
} catch (error) {
2021-04-14 15:10:22 +00:00
logger.debug(`Failed to access pid file: ${error as string}`)
2021-04-14 13:26:00 +00:00
result.messages.push(`Pid file ${filePaths.pid} not found`)
return result
}
2021-04-14 16:15:43 +00:00
const status = await prosodyCtl(options, 'status')
result.messages.push('Prosodyctl status: ' + status.message)
if (status.code) {
return result
}
2021-04-14 13:26:00 +00:00
result.ok = true
return result
}
async function testProsodyCorrectlyRunning (options: RegisterServerOptions): Promise<ProsodyRunning> {
const { peertubeHelpers } = options
peertubeHelpers.logger.info('Checking if Prosody is correctly running')
const result = await testProsodyRunning(options)
if (!result.ok) { return result }
result.ok = false // more tests to come
2021-04-13 15:13:41 +00:00
2021-04-14 16:15:43 +00:00
try {
const wantedConfig = await getProsodyConfig(options)
const filePath = wantedConfig.paths.config
2021-04-14 16:15:43 +00:00
await fs.promises.access(filePath, fs.constants.R_OK) // throw an error if file does not exist.
result.messages.push(`The prosody configuration file (${filePath}) exists`)
const actualContent = await fs.promises.readFile(filePath, {
encoding: 'utf-8'
})
const wantedContent = wantedConfig.content
if (actualContent === wantedContent) {
result.messages.push('Prosody configuration file content is correct.')
} else {
result.messages.push('Prosody configuration file content is not correct.')
return result
2021-04-14 16:15:43 +00:00
}
} catch (error) {
result.messages.push('Error when requiring the prosody config file: ' + (error as string))
return result
}
result.ok = true
2021-04-13 16:00:45 +00:00
return result
2021-04-13 15:13:41 +00:00
}
async function ensureProsodyRunning (options: RegisterServerOptions): Promise<void> {
const { peertubeHelpers } = options
2021-04-13 15:13:41 +00:00
const logger = peertubeHelpers.logger
2021-04-14 15:10:22 +00:00
logger.debug('Calling ensureProsodyRunning')
2021-04-13 15:13:41 +00:00
const r = await testProsodyCorrectlyRunning(options)
2021-04-13 16:00:45 +00:00
if (r.ok) {
2021-04-14 13:26:00 +00:00
r.messages.forEach(m => logger.debug(m))
2021-04-13 15:13:41 +00:00
logger.info('Prosody is already running correctly')
2021-04-14 15:10:22 +00:00
// Stop here. Nothing to change.
2021-04-13 15:13:41 +00:00
return
}
2021-04-14 13:26:00 +00:00
logger.info('Prosody is not running correctly: ')
r.messages.forEach(m => logger.info(m))
2021-04-13 15:13:41 +00:00
// Shutting down...
2021-04-14 15:10:22 +00:00
logger.debug('Shutting down prosody')
2021-04-13 15:13:41 +00:00
await ensureProsodyNotRunning(options)
// writing the configuration file
2021-04-14 15:10:22 +00:00
logger.debug('Writing the configuration file')
const config = await writeProsodyConfig(options)
2021-04-13 15:13:41 +00:00
const filePaths = config.paths
2021-04-14 13:26:00 +00:00
if (!filePaths.exec) {
logger.info('No Prosody executable, cant run.')
return
}
// Check certicates if needed.
await ensureProsodyCertificates(options, config)
2021-04-14 13:26:00 +00:00
// launch prosody
const execCmd = filePaths.exec + (filePaths.execArgs.length ? ' ' + filePaths.execArgs.join(' ') : '')
logger.info('Going to launch prosody (' + execCmd + ')')
const prosody = child_process.exec(execCmd, {
2021-04-14 13:26:00 +00:00
cwd: filePaths.dir,
env: {
...process.env,
PROSODY_CONFIG: filePaths.config
}
})
2021-04-14 15:55:26 +00:00
prosody.stdout?.on('data', (data) => {
logger.debug(`Prosody stdout: ${data as string}`)
})
prosody.stderr?.on('data', (data) => {
logger.error(`Prosody stderr: ${data as string}`)
})
prosody.on('error', (error) => {
logger.error(`Prosody exec error: ${JSON.stringify(error)}`)
})
2021-04-14 15:55:26 +00:00
prosody.on('close', (code) => {
logger.info(`Prosody process closed all stdio with code ${code ?? 'null'}`)
})
prosody.on('exit', (code) => {
logger.info(`Prosody process exited with code ${code ?? 'null'}`)
})
2021-04-14 15:48:46 +00:00
// Set the http-bind route.
2022-08-24 09:03:29 +00:00
await enableProxyRoute(options, {
host: config.host,
port: config.port
})
2021-04-14 15:48:46 +00:00
async function sleep (ms: number): Promise<any> {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
logger.info('Waiting for the prosody process to launch')
let count: number = 0
let processStarted: boolean = false
while (!processStarted && count < 5) {
count++
await sleep(500)
logger.info('Verifying prosody is launched')
2021-04-14 16:15:43 +00:00
const status = await prosodyCtl(options, 'status')
if (!status.code) {
logger.info(`Prosody status: ${status.stdout}`)
2021-04-14 15:48:46 +00:00
processStarted = true
2021-04-14 16:15:43 +00:00
} else {
logger.warn(`Prosody status: ${status.message}`)
2021-04-14 15:48:46 +00:00
}
}
if (!processStarted) {
logger.error('It seems that the Prosody process is not up')
2021-05-12 12:59:52 +00:00
return
2021-04-14 15:48:46 +00:00
}
2021-05-12 12:59:52 +00:00
logger.info('Prosody is running')
await startProsodyLogRotate(options, filePaths)
await startProsodyCertificatesRenewCheck(options, config)
2021-04-13 15:13:41 +00:00
}
async function ensureProsodyNotRunning (options: RegisterServerOptions): Promise<void> {
const { peertubeHelpers } = options
2021-04-14 15:10:22 +00:00
const logger = peertubeHelpers.logger
logger.info('Checking if Prosody is running, and shutting it down if so')
2021-05-12 12:59:52 +00:00
stopProsodyLogRotate(options)
stopProsodyCertificatesRenewCheck(options)
2021-05-12 12:59:52 +00:00
2021-04-14 15:10:22 +00:00
// NB: this function is called on plugin unregister, even if prosody is not used
// so we must avoid creating the working dir now
const filePaths = await getProsodyFilePaths(options)
if (!fs.existsSync(filePaths.dir)) {
logger.info(`The working dir ${filePaths.dir} does not exist, assuming there is no prosody on this server`)
return
}
2021-04-13 15:13:41 +00:00
logger.debug('Removing proxy route')
await disableProxyRoute(options)
2021-04-14 15:10:22 +00:00
logger.debug('Calling prosodyctl to stop the process')
2021-04-14 16:15:43 +00:00
const status = await prosodyCtl(options, 'stop')
logger.info(`ProsodyCtl command returned: ${status.message}`)
2021-04-13 15:13:41 +00:00
}
export {
2021-04-14 14:14:56 +00:00
getProsodyAbout,
checkProsody,
2021-04-14 13:26:00 +00:00
testProsodyRunning,
2021-04-13 15:13:41 +00:00
testProsodyCorrectlyRunning,
prepareProsody,
2021-04-13 15:13:41 +00:00
ensureProsodyRunning,
ensureProsodyNotRunning,
prosodyCtl,
reloadProsody
2021-04-13 15:13:41 +00:00
}