Skip to content

Commit

Permalink
Merge pull request #207 from matrix-org/hs/watch-config
Browse files Browse the repository at this point in the history
Add ability to watch config for changes
  • Loading branch information
Half-Shot authored Sep 1, 2020
2 parents e95f31e + ee7b75c commit 8b920c1
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 11 deletions.
2 changes: 2 additions & 0 deletions changelog.d/207.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The bridge can now optionally reload the config file on a `SIGHUP` signal. Developers should define the `onConfigChanged` callback
when constructing `Cli` to make use of this feature.
107 changes: 107 additions & 0 deletions spec/integ/cli.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
const fs = require("fs").promises;
const os = require("os");
const { Cli } = require("../..");
const { Logging } = require("../..");
const path = require('path');
const { defer } = require("../../lib/utils/promiseutil");
Logging.configure();

let tempDir;

const registrationFileContent = {
id: "cli-test",
url: "http://127.0.0.1:1234",
as_token: "a_as_token",
hs_token: "a_hs_token",
sender_localpart: "the_sender",
namespaces: {
users: [{
exclusive: true,
regex: "@_the_bridge.*",
}],
aliases: [{
exclusive: true,
regex: "@_the_bridge.*",
}],
rooms: [],
},
rate_limited: false,
protocols: [],
};
async function writeRegistrationFile(content=registrationFileContent, filename="registration.yaml") {
const filePath = path.join(tempDir, filename);
await fs.writeFile(filePath, JSON.stringify(content), "utf-8");
return filePath;
}

async function writeConfigFile(content={}) {
const filePath = path.join(tempDir, "config.yaml");
await fs.writeFile(filePath, JSON.stringify(content), "utf-8");
return filePath;
}

describe("Cli", () => {
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "bridge-test"));
});
afterEach(async () => {
await fs.rmdir(tempDir, {recursive: true});
});

it("should be able to start the bridge with just a registration file", async () => {
let runCalledWith = false;
const cli = new Cli({
enableRegistration: false,
registrationPath: await writeRegistrationFile({}, "reg.yml"),
run: (...args) => { runCalledWith = args; }
});
cli.run();
expect(runCalledWith[0]).toEqual(Cli.DEFAULT_PORT);
});

it("should be able to start the bridge with a custom port", async () => {
const port = 1234;
let runCalledWith = null;
const cli = new Cli({
enableRegistration: false,
registrationPath: await writeRegistrationFile(),
run: (...args) => { runCalledWith = args; }
});
cli.run({port});
expect(runCalledWith[0]).toEqual(port);
});

it("should be able to start the bridge with a registration file and config file", async () => {
const configData = {"a": "var", "b": true};
const configFile = await writeConfigFile(configData);
let runCalledWith = null;
const cli = new Cli({
enableRegistration: false,
registrationPath: await writeRegistrationFile(),
bridgeConfig: {},
run: (...args) => { runCalledWith = args; }
});
cli.run({config: configFile});
expect(runCalledWith[0]).toEqual(Cli.DEFAULT_PORT);
expect(runCalledWith[1]).toEqual(configData);
expect(runCalledWith[2].getOutput()).toEqual(registrationFileContent);
});

it("should reload config on SIGHUP", async () => {
const newConfigData = {"b": "var", "c": false};
const configFile = await writeConfigFile({"a": "var", "b": true});
const configDefer = defer();
const cli = new Cli({
enableRegistration: false,
registrationPath: await writeRegistrationFile(),
bridgeConfig: { watchConfig: true, watchInterval: 100 },
onConfigChanged: (config) => { configDefer.resolve(config); },
run: (...args) => { }
});
cli.run({config: configFile});
await writeConfigFile(newConfigData);
// Send ourselves a signal
process.kill(process.pid, "SIGHUP");
expect(await configDefer.promise).toEqual(newConfigData);
});
})
42 changes: 31 additions & 11 deletions src/components/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ import { AppServiceRegistration } from "matrix-appservice";
import { ConfigValidator } from "./config-validator";
import * as logging from "./logging";

const DEFAULT_PORT = 8090;
const DEFAULT_FILENAME = "registration.yaml";
const log = logging.get("cli");

interface CliOpts<ConfigType extends Record<string, unknown>> {
run: (port: number, config: ConfigType|null, registration: AppServiceRegistration|null) => void;
onConfigChanged?: (config: ConfigType) => void,
generateRegistration?: (reg: AppServiceRegistration, cb: (finalReg: AppServiceRegistration) => void) => void;
bridgeConfig?: {
affectsRegistration?: boolean;
Expand All @@ -51,6 +50,9 @@ interface CliArgs {
}

export class Cli<ConfigType extends Record<string, unknown>> {
public static DEFAULT_PORT = 8090;
public static DEFAULT_WATCH_INTERVAL = 2500;
public static DEFAULT_FILENAME = "registration.yaml";
private bridgeConfig: ConfigType|null = null;
private args: CliArgs|null = null;

Expand Down Expand Up @@ -92,8 +94,8 @@ export class Cli<ConfigType extends Record<string, unknown>> {
}
this.opts.enableLocalpart = Boolean(this.opts.enableLocalpart);

this.opts.registrationPath = this.opts.registrationPath || DEFAULT_FILENAME;
this.opts.port = this.opts.port || DEFAULT_PORT;
this.opts.registrationPath = this.opts.registrationPath || Cli.DEFAULT_FILENAME;
this.opts.port = this.opts.port || Cli.DEFAULT_PORT;
}
/**
* Get the parsed arguments. Only set after run is called and arguments parsed.
Expand Down Expand Up @@ -122,8 +124,8 @@ export class Cli<ConfigType extends Record<string, unknown>> {
/**
* Run the app from the command line. Will parse sys args.
*/
public run() {
this.args = nopt({
public run(args?: CliArgs) {
this.args = args || nopt({
"generate-registration": Boolean,
"config": path,
"url": String,
Expand Down Expand Up @@ -187,18 +189,20 @@ export class Cli<ConfigType extends Record<string, unknown>> {
this.opts.port = this.args.port;
}
this.assignConfigFile(this.args.config);
this.startWithConfig(this.bridgeConfig);
this.startWithConfig(this.bridgeConfig, this.args.config);
}

private assignConfigFile(configFilePath: string) {
const configFile = (this.opts.bridgeConfig && configFilePath) ? configFilePath : undefined;
if (!configFile) {
return;
}
const config = this.loadConfig(configFile);
this.bridgeConfig = config;
}

private loadConfig(filename?: string): ConfigType|null {
if (!filename) { return null; }
log.info("Loading config file %s", filename);
private loadConfig(filename: string): ConfigType {
log.info("Loading config file", filename);
const cfg = this.loadYaml(filename);
if (!cfg || typeof cfg === "string") {
throw Error("Config file " + filename + " isn't valid YAML.");
Expand Down Expand Up @@ -235,7 +239,23 @@ export class Cli<ConfigType extends Record<string, unknown>> {
});
}

private startWithConfig(config: ConfigType|null) {
private startWithConfig(config: ConfigType|null, configFilename: string) {
if (this.opts.onConfigChanged && this.opts.bridgeConfig) {
log.info("Will listen for SIGHUP");
process.on("SIGHUP",
() => {
log.info("Got SIGHUP, reloading config file");
try {
const newConfig = this.loadConfig(configFilename);
if (this.opts.onConfigChanged) {
this.opts.onConfigChanged(newConfig);
}
}
catch (ex) {
log.warn("Failed to reload config file:", ex);
}
});
}
this.opts.run(
this.opts.port, config,
AppServiceRegistration.fromObject(this.loadYaml(this.opts.registrationPath))
Expand Down

0 comments on commit 8b920c1

Please sign in to comment.