diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/server/ActionUtilities.ts | 13 | ||||
-rw-r--r-- | src/server/ProcessManager.ts | 45 | ||||
-rw-r--r-- | src/server/daemon/persistence_daemon.ts (renamed from src/server/persistence_daemon.ts) | 60 | ||||
-rw-r--r-- | src/server/index.ts | 13 |
4 files changed, 104 insertions, 27 deletions
diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts index bc978c982..2173f4369 100644 --- a/src/server/ActionUtilities.ts +++ b/src/server/ActionUtilities.ts @@ -1,12 +1,16 @@ import * as fs from 'fs'; import { ExecOptions } from 'shelljs'; -import { exec } from 'child_process'; +import { exec, spawn } from 'child_process'; import * as path from 'path'; import * as rimraf from "rimraf"; import { yellow, Color } from 'colors'; const projectRoot = path.resolve(__dirname, "../../"); +export function pathFromRoot(relative: string) { + return path.resolve(projectRoot, relative); +} + export const command_line = (command: string, fromDirectory?: string) => { return new Promise<string>((resolve, reject) => { const options: ExecOptions = {}; @@ -17,6 +21,13 @@ export const command_line = (command: string, fromDirectory?: string) => { }); }; +export async function spawn_detached_process(command: string, args?: readonly string[]) { + const out = path.resolve(projectRoot, `./logs/${command}-${process.pid}-${new Date().toUTCString()}`); + const child_out = fs.openSync(out, 'a'); + const child_error = fs.openSync(out, 'a'); + spawn(command, args, { detached: true, stdio: ["ignore", child_out, child_error] }).unref(); +} + export const read_text_file = (relativePath: string) => { const target = path.resolve(__dirname, relativePath); return new Promise<string>((resolve, reject) => { diff --git a/src/server/ProcessManager.ts b/src/server/ProcessManager.ts new file mode 100644 index 000000000..2237f9e1b --- /dev/null +++ b/src/server/ProcessManager.ts @@ -0,0 +1,45 @@ +import { writeFileSync, unlinkSync, existsSync, mkdirSync } from "fs"; +import { pathFromRoot, log_execution, spawn_detached_process } from './ActionUtilities'; +import { resolve } from "path"; +import { red, yellow } from "colors"; + +const daemonPath = pathFromRoot("./src/server/daemon/persistence_daemon.ts"); + +export namespace ProcessManager { + + export async function initialize() { + const logPath = pathFromRoot("./logs"); + const filePath = resolve(logPath, "./server_pids.txt"); + const exists = existsSync(logPath); + if (exists) { + unlinkSync(filePath); + } else { + mkdirSync(logPath); + } + const { pid } = process; + if (process.env.SPAWNED === "true") { + writeFileSync(filePath, `${pid} created at ${new Date().toUTCString()}\n`); + } + } + + let daemonInitialized = false; + export async function trySpawnDaemon() { + if (!daemonInitialized) { + daemonInitialized = true; + await log_execution({ + startMessage: "\ninitializing persistence daemon", + endMessage: ({ result, error }) => { + const success = error === null && result !== undefined; + if (!success) { + console.log(red("failed to initialize the persistance daemon")); + process.exit(0); + } + return "persistence daemon process closed"; + }, + action: () => spawn_detached_process("npx ts-node", [daemonPath]), + color: yellow + }); + } + } + +}
\ No newline at end of file diff --git a/src/server/persistence_daemon.ts b/src/server/daemon/persistence_daemon.ts index 2cb17456c..3eb17a9b4 100644 --- a/src/server/persistence_daemon.ts +++ b/src/server/daemon/persistence_daemon.ts @@ -1,14 +1,21 @@ import * as request from "request-promise"; -import { command_line, log_execution } from "./ActionUtilities"; +import { log_execution, spawn_detached_process } from "../ActionUtilities"; import { red, yellow, cyan, green } from "colors"; import * as nodemailer from "nodemailer"; import { MailOptions } from "nodemailer/lib/json-transport"; -import { Database } from "./database"; +import { writeFileSync } from "fs"; +import { resolve } from 'path'; const LOCATION = "http://localhost"; const recipient = "samuel_wilkins@brown.edu"; let restarting = false; +writeFileSync(resolve(__dirname, "./current_pid.txt"), process.pid); + +function timestamp() { + return `@ ${new Date().toISOString()}`; +} + async function listen() { if (!LOCATION) { console.log(red("No location specified for persistence daemon. Please include as a command line environment variable or in a .env file.")); @@ -16,36 +23,39 @@ async function listen() { } const heartbeat = `${LOCATION}:1050/serverHeartbeat`; // if this is on our remote server, the server must be run in release mode - const suffix = LOCATION.includes("localhost") ? "" : "-release"; + // const suffix = LOCATION.includes("localhost") ? "" : "-release"; setInterval(async () => { - let response: any; let error: any; try { - response = await request.get(heartbeat); + await request.get(heartbeat); } catch (e) { error = e; } finally { - if (!response && !restarting) { - restarting = true; - console.log(yellow("Detected a server crash!")); - await log_execution({ - startMessage: "Sending crash notification email", - endMessage: ({ error, result }) => { - const success = error === null && result === true; - return (success ? `Notification successfully sent to ` : `Failed to notify `) + recipient; - }, - action: async () => notify(error || "Hmm, no error to report..."), - color: cyan - }); - console.log(await log_execution({ - startMessage: "Initiating server restart", - endMessage: "Server successfully restarted", - action: () => command_line(`npm run start${suffix}`), - color: green - })); - restarting = false; + if (error) { + if (!restarting) { + restarting = true; + console.log(yellow("Detected a server crash!")); + await log_execution({ + startMessage: "Sending crash notification email", + endMessage: ({ error, result }) => { + const success = error === null && result === true; + return (success ? `Notification successfully sent to ` : `Failed to notify `) + recipient; + }, + action: async () => notify(error || "Hmm, no error to report..."), + color: cyan + }); + console.log(await log_execution({ + startMessage: "Initiating server restart", + endMessage: "Server successfully restarted", + action: () => spawn_detached_process(`npm run start-spawn`), + color: green + })); + restarting = false; + } else { + console.log(yellow(`Callback ignored because restarting already initiated ${timestamp()}`)); + } } else { - console.log(green(`No issues detected as of ${new Date().toISOString()}`)); + console.log(green(`No issues detected ${timestamp()}`)); } } }, 1000 * 10); diff --git a/src/server/index.ts b/src/server/index.ts index 7671936a2..795418b31 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -18,11 +18,12 @@ import { GoogleCredentialsLoader } from './credentials/CredentialsLoader'; import DeleteManager from "./ApiManagers/DeleteManager"; import PDFManager from "./ApiManagers/PDFManager"; import UploadManager from "./ApiManagers/UploadManager"; -import { log_execution, command_line } from "./ActionUtilities"; +import { log_execution } from "./ActionUtilities"; import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager"; import GooglePhotosManager from "./ApiManagers/GooglePhotosManager"; import { yellow, red } from "colors"; import { disconnect } from "../server/Initialization"; +import { ProcessManager } from "./ProcessManager"; export const publicDirectory = path.resolve(__dirname, "public"); export const filesDirectory = path.resolve(publicDirectory, "files"); @@ -35,6 +36,7 @@ export const ExitHandlers = new Array<() => void>(); * before clients can access the server should be run or awaited here. */ async function preliminaryFunctions() { + await ProcessManager.initialize(); await GoogleCredentialsLoader.loadCredentials(); GoogleApiServerUtils.processProjectCredentials(); await DashUploadUtils.buildFileDirectories(); @@ -119,6 +121,15 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: } }); + addSupervisedRoute({ + method: Method.GET, + subscription: "/persist", + onValidation: ({ res }) => { + ProcessManager.trySpawnDaemon(); + res.redirect("/home"); + } + }); + logRegistrationOutcome(); // initialize the web socket (bidirectional communication: if a user changes |