feat: SIGHUP hot reload for headless config updates
Add signal handler that calls rehash() on SIGHUP, logging results instead of sending to a client. Useful for systemd and container environments where no IRC client is attached. Update docs with channel key config, hot reload section, and roadmap checkoffs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,8 +33,8 @@
|
|||||||
## v0.3.0
|
## v0.3.0
|
||||||
|
|
||||||
- [x] Client-side TLS (accept TLS from clients)
|
- [x] Client-side TLS (accept TLS from clients)
|
||||||
- [ ] Channel key support (JOIN #channel key)
|
- [x] Channel key support (JOIN #channel key)
|
||||||
- [ ] Hot config reload (SIGHUP)
|
- [x] Hot config reload (SIGHUP)
|
||||||
- [ ] Systemd service file
|
- [ ] Systemd service file
|
||||||
|
|
||||||
## v0.4.0
|
## v0.4.0
|
||||||
|
|||||||
3
TASKS.md
3
TASKS.md
@@ -26,6 +26,7 @@
|
|||||||
## Next
|
## Next
|
||||||
|
|
||||||
- [x] P2: Client-side TLS support
|
- [x] P2: Client-side TLS support
|
||||||
- [ ] P2: Channel key support
|
- [x] P2: Channel key support
|
||||||
|
- [x] P2: Hot config reload (SIGHUP + REHASH refactor)
|
||||||
- [ ] P3: Systemd service file
|
- [ ] P3: Systemd service file
|
||||||
- [ ] P3: Containerfile for podman deployment
|
- [ ] P3: Containerfile for podman deployment
|
||||||
|
|||||||
2
TODO.md
2
TODO.md
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [ ] Channel key support (JOIN #channel key)
|
|
||||||
- [ ] Hot config reload on SIGHUP
|
|
||||||
- [ ] Web status dashboard
|
- [ ] Web status dashboard
|
||||||
- [ ] DCC passthrough
|
- [ ] DCC passthrough
|
||||||
- [ ] Per-client backlog tracking (multi-user)
|
- [ ] Per-client backlog tracking (multi-user)
|
||||||
|
|||||||
@@ -74,8 +74,9 @@ PASS <password> # authenticate (all networks)
|
|||||||
/msg *bouncer REHASH # reload config file
|
/msg *bouncer REHASH # reload config file
|
||||||
/msg *bouncer ADDNETWORK name host=h port=N tls=yes nick=n channels=#a,#b
|
/msg *bouncer ADDNETWORK name host=h port=N tls=yes nick=n channels=#a,#b
|
||||||
/msg *bouncer DELNETWORK name # remove network
|
/msg *bouncer DELNETWORK name # remove network
|
||||||
/msg *bouncer AUTOJOIN net +#chan # add to autojoin
|
/msg *bouncer AUTOJOIN net +#chan # add to autojoin
|
||||||
/msg *bouncer AUTOJOIN net -#chan # remove from autojoin
|
/msg *bouncer AUTOJOIN net +#chan key # add with channel key
|
||||||
|
/msg *bouncer AUTOJOIN net -#chan # remove from autojoin
|
||||||
```
|
```
|
||||||
|
|
||||||
### NickServ
|
### NickServ
|
||||||
@@ -174,6 +175,13 @@ notify_proxy = false # use SOCKS5 for notifications
|
|||||||
|
|
||||||
Only fires when no clients are attached.
|
Only fires when no clients are attached.
|
||||||
|
|
||||||
|
## Hot Reload
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kill -HUP $(pidof bouncer) # reload config via signal
|
||||||
|
/msg *bouncer REHASH # reload config via command
|
||||||
|
```
|
||||||
|
|
||||||
## Config Skeleton
|
## Config Skeleton
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
@@ -201,6 +209,7 @@ host / port
|
|||||||
[networks.<name>] # repeatable
|
[networks.<name>] # repeatable
|
||||||
host / port / tls
|
host / port / tls
|
||||||
nick / channels / autojoin
|
nick / channels / autojoin
|
||||||
|
channel_keys # keys for +k channels
|
||||||
password # optional, IRC server PASS
|
password # optional, IRC server PASS
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ port = 6697 # server port (default: 6697 if tls, 6667 otherwise)
|
|||||||
tls = true # use TLS for server connection
|
tls = true # use TLS for server connection
|
||||||
nick = "mynick" # desired IRC nick (set after probation)
|
nick = "mynick" # desired IRC nick (set after probation)
|
||||||
channels = ["#test"] # channels to join (after probation)
|
channels = ["#test"] # channels to join (after probation)
|
||||||
|
channel_keys = { "#secret" = "hunter2" } # keys for +k channels (optional)
|
||||||
autojoin = true # auto-join channels on ready (default: true)
|
autojoin = true # auto-join channels on ready (default: true)
|
||||||
password = "" # IRC server password (optional, for PASS command)
|
password = "" # IRC server password (optional, for PASS command)
|
||||||
```
|
```
|
||||||
@@ -488,10 +489,11 @@ Responses arrive as NOTICE messages from `*bouncer`.
|
|||||||
| `REHASH` | Reload config file, add/remove/reconnect networks |
|
| `REHASH` | Reload config file, add/remove/reconnect networks |
|
||||||
| `ADDNETWORK <name> key=val ...` | Create a network at runtime |
|
| `ADDNETWORK <name> key=val ...` | Create a network at runtime |
|
||||||
| `DELNETWORK <name>` | Stop and remove a network |
|
| `DELNETWORK <name>` | Stop and remove a network |
|
||||||
| `AUTOJOIN <network> +/-#channel` | Add or remove channel from autojoin list |
|
| `AUTOJOIN <network> +#channel [key]` | Add channel (with optional key for +k channels) |
|
||||||
|
| `AUTOJOIN <network> -#channel` | Remove channel from autojoin list |
|
||||||
|
|
||||||
**ADDNETWORK keys:** `host` (required), `port`, `tls` (yes/no), `nick`,
|
**ADDNETWORK keys:** `host` (required), `port`, `tls` (yes/no), `nick`,
|
||||||
`channels` (comma-separated), `password`.
|
`channels` (comma-separated), `channel_keys` (`#chan=key,...`), `password`.
|
||||||
|
|
||||||
### NickServ
|
### NickServ
|
||||||
|
|
||||||
@@ -536,6 +538,7 @@ Responses arrive as NOTICE messages from `*bouncer`.
|
|||||||
/msg *bouncer ADDNETWORK oftc host=irc.oftc.net port=6697 tls=yes channels=#test
|
/msg *bouncer ADDNETWORK oftc host=irc.oftc.net port=6697 tls=yes channels=#test
|
||||||
/msg *bouncer DELNETWORK oftc
|
/msg *bouncer DELNETWORK oftc
|
||||||
/msg *bouncer AUTOJOIN libera +#newchannel
|
/msg *bouncer AUTOJOIN libera +#newchannel
|
||||||
|
/msg *bouncer AUTOJOIN libera +#secret hunter2
|
||||||
/msg *bouncer AUTOJOIN libera -#oldchannel
|
/msg *bouncer AUTOJOIN libera -#oldchannel
|
||||||
/msg *bouncer IDENTIFY libera
|
/msg *bouncer IDENTIFY libera
|
||||||
/msg *bouncer REGISTER libera
|
/msg *bouncer REGISTER libera
|
||||||
@@ -625,6 +628,62 @@ farm_interval = 3600 # seconds between attempts per network
|
|||||||
farm_max_accounts = 10 # stop farming when this many verified accounts exist
|
farm_max_accounts = 10 # stop farming when this many verified accounts exist
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Channel Keys
|
||||||
|
|
||||||
|
Channels with mode `+k` require a key to join. Configure keys in TOML:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[networks.libera]
|
||||||
|
channels = ["#secret", "#public"]
|
||||||
|
channel_keys = { "#secret" = "hunter2" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Keys are used automatically during autojoin and KICK rejoin. To add a keyed
|
||||||
|
channel at runtime:
|
||||||
|
|
||||||
|
```
|
||||||
|
/msg *bouncer AUTOJOIN libera +#secret hunter2
|
||||||
|
```
|
||||||
|
|
||||||
|
Removing a channel also clears its key:
|
||||||
|
|
||||||
|
```
|
||||||
|
/msg *bouncer AUTOJOIN libera -#secret
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hot Reload
|
||||||
|
|
||||||
|
The bouncer reloads its config file on `SIGHUP` or via the `REHASH` command.
|
||||||
|
Both use the same logic: re-read TOML, diff networks (add/remove/reconnect),
|
||||||
|
and update mutable fields (channels, channel_keys, nick, password).
|
||||||
|
|
||||||
|
### SIGHUP
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kill -HUP $(pidof bouncer)
|
||||||
|
```
|
||||||
|
|
||||||
|
Results are logged (no client connection needed). Useful for headless
|
||||||
|
operation (systemd, containers).
|
||||||
|
|
||||||
|
### REHASH command
|
||||||
|
|
||||||
|
```
|
||||||
|
/msg *bouncer REHASH
|
||||||
|
```
|
||||||
|
|
||||||
|
Results are sent back as NOTICE messages.
|
||||||
|
|
||||||
|
### What changes on reload
|
||||||
|
|
||||||
|
| Field | Effect |
|
||||||
|
|-------|--------|
|
||||||
|
| Network host/port/tls/proxy | Network reconnected |
|
||||||
|
| channels, channel_keys, nick, password | Updated in-place |
|
||||||
|
| notify_url, notify_cooldown, etc. | Notifier recreated |
|
||||||
|
| farm_enabled, farm_interval, etc. | Farm started/stopped |
|
||||||
|
| bind, port, password, client_tls | Warning logged (restart required) |
|
||||||
|
|
||||||
## Stopping
|
## Stopping
|
||||||
|
|
||||||
Press `Ctrl+C` or send `SIGTERM`. The bouncer shuts down gracefully, closing
|
Press `Ctrl+C` or send `SIGTERM`. The bouncer shuts down gracefully, closing
|
||||||
|
|||||||
@@ -90,6 +90,21 @@ async def _run(config_path: Path, verbose: bool) -> None:
|
|||||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
loop.add_signal_handler(sig, _signal_handler)
|
loop.add_signal_handler(sig, _signal_handler)
|
||||||
|
|
||||||
|
# Hot reload on SIGHUP
|
||||||
|
async def _sighup_rehash() -> None:
|
||||||
|
try:
|
||||||
|
lines = await commands.rehash(router, config_path)
|
||||||
|
for line in lines:
|
||||||
|
log.info("REHASH: %s", line)
|
||||||
|
except Exception:
|
||||||
|
log.exception("SIGHUP rehash failed")
|
||||||
|
|
||||||
|
def _sighup_handler() -> None:
|
||||||
|
log.info("SIGHUP received, reloading config...")
|
||||||
|
asyncio.create_task(_sighup_rehash())
|
||||||
|
|
||||||
|
loop.add_signal_handler(signal.SIGHUP, _sighup_handler)
|
||||||
|
|
||||||
await stop_event.wait()
|
await stop_event.wait()
|
||||||
|
|
||||||
server.close()
|
server.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user