aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--logs/npx ts-node-20965-Wed, 11 Dec 2019 03:38:32 GMT0
-rw-r--r--package.json3
-rw-r--r--src/server/ActionUtilities.ts13
-rw-r--r--src/server/ProcessManager.ts45
-rw-r--r--src/server/daemon/persistence_daemon.ts (renamed from src/server/persistence_daemon.ts)60
-rw-r--r--src/server/index.ts13
6 files changed, 106 insertions, 28 deletions
diff --git a/logs/npx ts-node-20965-Wed, 11 Dec 2019 03:38:32 GMT b/logs/npx ts-node-20965-Wed, 11 Dec 2019 03:38:32 GMT
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/logs/npx ts-node-20965-Wed, 11 Dec 2019 03:38:32 GMT
diff --git a/package.json b/package.json
index 1689194db..97563e137 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,8 @@
"main": "index.js",
"scripts": {
"start-release": "cross-env RELEASE=true NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts",
- "start": "cross-env LOCATION=http://localhost NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts",
+ "start-spawn": "cross-env SPAWNED=true NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts",
+ "start": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts",
"debug": "cross-env NODE_OPTIONS=--max_old_space_size=8192 ts-node-dev --inspect -- src/server/index.ts",
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 webpack --env production",
"test": "mocha -r ts-node/register test/**/*.ts",
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