diff --git a/config.schema.json b/config.schema.json index f252b2d..91454d0 100644 --- a/config.schema.json +++ b/config.schema.json @@ -2,8 +2,8 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { - "ttl": { - "type": "number" + "pidFile": { + "type": "string" }, "proxy": { "type": "object", diff --git a/src/cli.ts b/src/cli.ts index 8e7c4b5..7ad8c75 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,8 @@ #!/usr/bin/env node import { z } from "zod"; import minimist from "minimist"; +import { kill } from "node:process"; +import * as fsp from "node:fs/promises"; import { NestFactory } from "@nestjs/core"; import path, { dirname } from "node:path"; import { DynamicModule, Module } from "@nestjs/common"; @@ -37,6 +39,14 @@ async function runDaemon(config: string) { await daemon.init(); } +async function sendSignal(config: string, signal: "reload") { + const c = await ConfigService.readConfig(config); + + const content = await fsp.readFile(c.pidFile); + const pid = +content.toString(); + kill(pid, "SIGHUP"); +} + export async function main() { const argv = minimist(process.argv, { boolean: ["version", "v"], @@ -46,11 +56,16 @@ export async function main() { const schema = z.object({ config: z.string().default("moxy.json"), + signal: z.literal("reload").optional(), }); + const parsed = parseSchema(schema, { config: argv.config ?? argv.c, + signal: argv.signal ?? argv.s, }); + if (parsed.signal) return sendSignal(parsed.config, parsed.signal); + await runDaemon(parsed.config); } @@ -81,8 +96,9 @@ function help(): never { moxy - Distributed transparent proxy with traffic control facilities USAGE: - [-c moxy.json]` + [-c moxy.json] [-s reload]` ); + process.exit(1); } diff --git a/src/config.schema.ts b/src/config.schema.ts index c5512f5..33a9a81 100644 --- a/src/config.schema.ts +++ b/src/config.schema.ts @@ -60,7 +60,7 @@ const DatabaseConfigSchema = z .default({}); export const ConfigSchema = z.object({ - ttl: z.number().default(5 * 60_000), + pidFile: z.string().default("moxy.pid"), database: DatabaseConfigSchema, users: z.record(UserConfigSchema), }); diff --git a/src/config.ts b/src/config.ts index 4836855..2269453 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,11 +1,11 @@ import { DynamicModule, - Inject, Injectable, Logger, Module, OnApplicationBootstrap, } from "@nestjs/common"; +import * as fsp from "node:fs/promises"; import * as assert from "node:assert"; import { readFile } from "node:fs"; import { promisify } from "node:util"; @@ -13,6 +13,7 @@ import { promisify } from "node:util"; import { ConfigSchema } from "./config.schema"; import { EventModule, MoxyEventEmitter } from "./event"; import { UserNotFoundError } from "./errors"; +import { withErrorLogging } from "./utils"; export type ExpirationDate = "unlimit" | string; @@ -68,7 +69,7 @@ export type ProxyConfig = { }; export type Config = { - ttl: number; + pidFile: string; database: DatabaseConfig; users: Record; }; @@ -82,7 +83,12 @@ export class ConfigService { public constructor( private readonly file: string, private readonly eventEmitter: MoxyEventEmitter - ) {} + ) { + process.on("SIGHUP", () => { + this.logger.log("Reloading config"); + this.reloadCache().catch((err) => this.logger.error(err)); + }); + } public static async readConfig(file: string): Promise { const content = await promisify(readFile)(file); @@ -109,16 +115,16 @@ export class ConfigService { public async getConfig(): Promise { if (this.cache) return this.cache; - return this.refreshCache(); + return this.reloadCache(); } - public async refreshCache(): Promise { + private async reloadCache(): Promise { this.oldCache = this.cache; this.cache = await ConfigService.readConfig(this.file); await this.emitChangeEvents(this.cache, this.oldCache); - this.logger.log("Refreshed config cache"); + this.logger.log("Reloaded config"); return this.cache; } @@ -167,27 +173,20 @@ export class ConfigService { export class ConfigModule implements OnApplicationBootstrap { private readonly logger = new Logger("ConfigModule"); - public constructor( - private readonly configService: ConfigService, - @Inject("CacheTtl") - private readonly ttl: number - ) {} + public constructor(private readonly configService: ConfigService) {} onApplicationBootstrap() { - this.configService.refreshCache().catch((err) => this.logger.error(err)); + withErrorLogging(async () => { + const config = await this.configService.getConfig(); - setInterval(() => { - this.configService.refreshCache().catch((err) => this.logger.error(err)); - }, this.ttl); + await fsp.writeFile(config.pidFile, String(process.pid)); + }, this.logger); } public static async register(file: string): Promise { - const config = await ConfigService.readConfig(file); - return { module: ConfigModule, providers: [ - { provide: "CacheTtl", useValue: config.ttl }, { provide: ConfigService, inject: [MoxyEventEmitter],