From db5d98d9d60cd46eef506fcde6149ebcf43d1245 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 9 May 2023 02:02:21 -0400 Subject: [PATCH] fix(striker-ui-api): add timeout to get server screenshot --- striker-ui-api/src/lib/consts/ENV.ts | 10 +++ .../server/getServerDetail.ts | 87 +++++++++++++++---- 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/striker-ui-api/src/lib/consts/ENV.ts b/striker-ui-api/src/lib/consts/ENV.ts index 2bf7036f..43f12c91 100644 --- a/striker-ui-api/src/lib/consts/ENV.ts +++ b/striker-ui-api/src/lib/consts/ENV.ts @@ -33,3 +33,13 @@ export const PUID = resolveUid(process.env.PUID ?? 'striker-ui-api'); * @default PUID */ export const PGID = resolveGid(process.env.PGID ?? PUID); + +/** + * Get server screenshot job timeout in milliseconds. The job will be + * forced to progress 100 if it doesn't start within this time limit. + * + * @default 30000 + */ +export const GET_SERVER_SCREENSHOT_TIMEOUT = Number.parseInt( + process.env.GET_SERVER_SCREENSHOT_TIMEOUT ?? '30000', +); diff --git a/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts b/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts index faa16053..0a86ca75 100644 --- a/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts @@ -1,11 +1,15 @@ import assert from 'assert'; import { RequestHandler } from 'express'; -import { createReadStream } from 'fs'; +import { ReadStream, createReadStream, writeFileSync } from 'fs'; import path from 'path'; -import { REP_UUID, SERVER_PATHS } from '../../consts'; +import { + GET_SERVER_SCREENSHOT_TIMEOUT, + REP_UUID, + SERVER_PATHS, +} from '../../consts'; -import { getLocalHostUUID, job, query } from '../../accessModule'; +import { getLocalHostUUID, job, query, write } from '../../accessModule'; import { sanitize } from '../../sanitize'; import { mkfifo, rm, stderr, stdout } from '../../shell'; @@ -13,7 +17,7 @@ const rmfifo = (path: string) => { try { rm(path); } catch (rmfifoError) { - stderr(`Failed to clean up named pipe; CAUSE: ${rmfifoError}`); + stderr(`Failed to clean up FIFO; CAUSE: ${rmfifoError}`); } }; @@ -74,32 +78,35 @@ export const getServerDetail: RequestHandler< stdout(`serverHostUUID=[${serverHostUUID}]`); - const imageFileName = `${serverUUID}_screenshot_${epoch}`; - const imageFilePath = path.join(SERVER_PATHS.tmp.self, imageFileName); + const imageFifoName = `${serverUUID}_screenshot_${epoch}`; + const imageFifoPath = path.join(SERVER_PATHS.tmp.self, imageFifoName); + + let fifoReadStream: ReadStream; try { - mkfifo(imageFilePath); + mkfifo(imageFifoPath); - const namedPipeReadStream = createReadStream(imageFilePath, { + fifoReadStream = createReadStream(imageFifoPath, { autoClose: true, + emitClose: true, encoding: 'utf-8', }); let imageData = ''; - namedPipeReadStream.once('error', (readError) => { - stderr(`Failed to read from named pipe; CAUSE: ${readError}`); + fifoReadStream.once('error', (readError) => { + stderr(`Failed to read from FIFO; CAUSE: ${readError}`); }); - namedPipeReadStream.once('close', () => { - stdout(`On close; removing named pipe at ${imageFilePath}.`); + fifoReadStream.once('close', () => { + stdout(`On close; removing FIFO at ${imageFifoPath}.`); - rmfifo(imageFilePath); + rmfifo(imageFifoPath); return response.status(200).send({ screenshot: imageData }); }); - namedPipeReadStream.on('data', (data) => { + fifoReadStream.on('data', (data) => { const imageChunk = data.toString().trim(); const peekLength = 10; @@ -120,10 +127,10 @@ export const getServerDetail: RequestHandler< }); } catch (prepPipeError) { stderr( - `Failed to prepare named pipe and/or receive image data; CAUSE: ${prepPipeError}`, + `Failed to prepare FIFO and/or receive image data; CAUSE: ${prepPipeError}`, ); - rmfifo(imageFilePath); + rmfifo(imageFifoPath); return response.status(500).send(); } @@ -134,8 +141,10 @@ export const getServerDetail: RequestHandler< resizeArgs = ''; } + let jobUuid: string; + try { - await job({ + jobUuid = await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-get-server-screenshot'].self, job_data: `server-uuid=${serverUUID} @@ -152,6 +161,50 @@ out-file-id=${epoch}`, return response.status(500).send(); } + + const timeoutId: NodeJS.Timeout = setTimeout<[string, string]>( + async (uuid, fpath) => { + const [[isNotInProgress]]: [[number]] = await query( + `SELECT + CASE + WHEN job_progress IN (0, 100) + THEN CAST(1 AS BOOLEAN) + ELSE CAST(0 AS BOOLEAN) + END AS is_job_started + FROM jobs + WHERE job_uuid = '${uuid}';`, + ); + + if (isNotInProgress) { + stdout( + `Discard job ${uuid} because it's not-in-progress after timeout`, + ); + + try { + const wcode = await write( + `UPDATE jobs SET job_progress = 100 WHERE job_uuid = '${uuid}';`, + ); + + assert(wcode === 0, `Write exited with code ${wcode}`); + + writeFileSync(fpath, ''); + } catch (error) { + stderr(`Failed to discard job ${uuid} on timeout; CAUSE: ${error}`); + + return response.status(500).send(); + } + } + }, + GET_SERVER_SCREENSHOT_TIMEOUT, + jobUuid, + imageFifoPath, + ); + + fifoReadStream.once('data', () => { + stdout(`Receiving server screenshot image data; cancel timeout`); + + clearTimeout(timeoutId); + }); } else { // For getting sever detail data.