225 lines
6.4 KiB
TypeScript
225 lines
6.4 KiB
TypeScript
import express from 'express';
|
|
import path from 'path';
|
|
import http from 'http';
|
|
import cors from 'cors';
|
|
import dotenv from 'dotenv';
|
|
dotenv.config();
|
|
import { record, workflow, storage, auth, integration, proxy, webhook } from './routes';
|
|
import { BrowserPool } from "./browser-management/classes/BrowserPool";
|
|
import logger from './logger';
|
|
import { connectDB, syncDB } from './storage/db'
|
|
import cookieParser from 'cookie-parser';
|
|
import { SERVER_PORT } from "./constants/config";
|
|
import { Server } from "socket.io";
|
|
import { readdirSync } from "fs"
|
|
import { fork } from 'child_process';
|
|
import { capture } from "./utils/analytics";
|
|
import swaggerUi from 'swagger-ui-express';
|
|
import swaggerSpec from './swagger/config';
|
|
import connectPgSimple from 'connect-pg-simple';
|
|
import pg from 'pg';
|
|
import session from 'express-session';
|
|
import Run from './models/Run';
|
|
import { processQueuedRuns } from './routes/storage';
|
|
|
|
const app = express();
|
|
app.use(cors({
|
|
origin: process.env.PUBLIC_URL ? process.env.PUBLIC_URL : 'http://localhost:5173',
|
|
credentials: true,
|
|
}));
|
|
app.use(express.json());
|
|
|
|
const { Pool } = pg;
|
|
const pool = new Pool({
|
|
user: process.env.DB_USER,
|
|
host: process.env.DB_HOST,
|
|
database: process.env.DB_NAME,
|
|
password: process.env.DB_PASSWORD,
|
|
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : undefined,
|
|
});
|
|
|
|
const PgSession = connectPgSimple(session);
|
|
|
|
interface PgStoreOptions {
|
|
pool: pg.Pool;
|
|
tableName: string;
|
|
createTableIfMissing?: boolean;
|
|
pruneSessionInterval?: number;
|
|
errorLog?: (err: Error) => void;
|
|
}
|
|
|
|
const sessionStore = new PgSession({
|
|
pool: pool,
|
|
tableName: 'session',
|
|
createTableIfMissing: true,
|
|
pruneSessionInterval: 15 * 60,
|
|
errorLog: (err: Error) => {
|
|
logger.log('error', `Session store error: ${err.message}`);
|
|
},
|
|
} as PgStoreOptions);
|
|
|
|
app.use(
|
|
session({
|
|
store: sessionStore,
|
|
secret: process.env.SESSION_SECRET || 'mx-session',
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
secure: process.env.NODE_ENV === 'production',
|
|
maxAge: 24 * 60 * 60 * 1000,
|
|
}
|
|
})
|
|
);
|
|
|
|
const server = http.createServer(app);
|
|
|
|
/**
|
|
* Globally exported singleton instance of socket.io for socket communication with the client.
|
|
* @type {Server}
|
|
*/
|
|
export const io = new Server(server);
|
|
|
|
/**
|
|
* {@link BrowserPool} globally exported singleton instance for managing browsers.
|
|
*/
|
|
export const browserPool = new BrowserPool();
|
|
|
|
app.use(cookieParser())
|
|
|
|
app.use('/webhook', webhook);
|
|
app.use('/record', record);
|
|
app.use('/workflow', workflow);
|
|
app.use('/storage', storage);
|
|
app.use('/auth', auth);
|
|
app.use('/integration', integration);
|
|
app.use('/proxy', proxy);
|
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
|
|
|
readdirSync(path.join(__dirname, 'api')).forEach((r) => {
|
|
const route = require(path.join(__dirname, 'api', r));
|
|
const router = route.default || route;
|
|
if (typeof router === 'function') {
|
|
app.use('/api', router);
|
|
} else {
|
|
console.error(`Error: ${r} does not export a valid router`);
|
|
}
|
|
});
|
|
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
const workerPath = path.resolve(__dirname, isProduction ? './schedule-worker.js' : './schedule-worker.ts');
|
|
const recordingWorkerPath = path.resolve(__dirname, isProduction ? './pgboss-worker.js' : './pgboss-worker.ts');
|
|
|
|
let workerProcess: any;
|
|
let recordingWorkerProcess: any;
|
|
|
|
if (!isProduction) {
|
|
workerProcess = fork(workerPath, [], {
|
|
execArgv: ['--inspect=5859'],
|
|
});
|
|
workerProcess.on('message', (message: any) => {
|
|
console.log(`Message from worker: ${message}`);
|
|
});
|
|
workerProcess.on('error', (error: any) => {
|
|
console.error(`Error in worker: ${error}`);
|
|
});
|
|
workerProcess.on('exit', (code: any) => {
|
|
console.log(`Worker exited with code: ${code}`);
|
|
});
|
|
|
|
recordingWorkerProcess = fork(recordingWorkerPath, [], {
|
|
execArgv: ['--inspect=5860'],
|
|
});
|
|
recordingWorkerProcess.on('message', (message: any) => {
|
|
console.log(`Message from recording worker: ${message}`);
|
|
});
|
|
recordingWorkerProcess.on('error', (error: any) => {
|
|
console.error(`Error in recording worker: ${error}`);
|
|
});
|
|
recordingWorkerProcess.on('exit', (code: any) => {
|
|
console.log(`Recording worker exited with code: ${code}`);
|
|
});
|
|
}
|
|
|
|
app.get('/', function (req, res) {
|
|
capture(
|
|
'maxun-oss-server-run', {
|
|
event: 'server_started',
|
|
}
|
|
);
|
|
return res.send('Maxun server started 🚀');
|
|
});
|
|
|
|
app.use((req, res, next) => {
|
|
res.header('Access-Control-Allow-Origin', process.env.PUBLIC_URL || 'http://localhost:5173');
|
|
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
|
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
res.header('Access-Control-Allow-Credentials', 'true');
|
|
if (req.method === 'OPTIONS') {
|
|
return res.sendStatus(200);
|
|
}
|
|
next();
|
|
});
|
|
|
|
io.of('/queued-run').on('connection', (socket) => {
|
|
const userId = socket.handshake.query.userId as string;
|
|
|
|
if (userId) {
|
|
socket.join(`user-${userId}`);
|
|
logger.log('info', `Client joined queued-run namespace for user: ${userId}, socket: ${socket.id}`);
|
|
|
|
socket.on('disconnect', () => {
|
|
logger.log('info', `Client disconnected from queued-run namespace: ${socket.id}`);
|
|
});
|
|
} else {
|
|
logger.log('warn', `Client connected to queued-run namespace without userId: ${socket.id}`);
|
|
socket.disconnect();
|
|
}
|
|
});
|
|
|
|
setInterval(() => {
|
|
processQueuedRuns();
|
|
}, 5000);
|
|
|
|
|
|
server.listen(SERVER_PORT, '0.0.0.0', async () => {
|
|
try {
|
|
await connectDB();
|
|
await syncDB();
|
|
logger.log('info', `Server listening on port ${SERVER_PORT}`);
|
|
} catch (error: any) {
|
|
logger.log('error', `Failed to connect to the database: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
process.on('SIGINT', async () => {
|
|
console.log('Main app shutting down...');
|
|
try {
|
|
await Run.update(
|
|
{
|
|
status: 'failed',
|
|
finishedAt: new Date().toLocaleString(),
|
|
log: 'Process interrupted during execution - worker shutdown'
|
|
},
|
|
{
|
|
where: { status: 'running' }
|
|
}
|
|
);
|
|
} catch (error: any) {
|
|
console.error('Error updating runs:', error);
|
|
}
|
|
|
|
try {
|
|
console.log('Closing PostgreSQL connection pool...');
|
|
await pool.end();
|
|
console.log('PostgreSQL connection pool closed');
|
|
} catch (error) {
|
|
console.error('Error closing PostgreSQL connection pool:', error);
|
|
}
|
|
|
|
if (!isProduction) {
|
|
if (workerProcess) workerProcess.kill();
|
|
if (recordingWorkerProcess) recordingWorkerProcess.kill();
|
|
}
|
|
process.exit();
|
|
}); |