-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement systemd-notify directly
Dependency on sd-notify removed. sd-notify is wrapping libsystemd, which requires installing libsystemd-dev for building, and loads libsystemd.so at runtime. Since we only need the sd_notify part of the lib, and the protocol is trivial to implement, do so. We also now bail out early in case a watchdog timeout is set and the notification module could not be loaded, as in this case systemd would constantly kill and restart zigbee2mqtt, resulting in unstable operation that might not be obvious at first sight.
- Loading branch information
Showing
7 changed files
with
165 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
declare module 'unix-dgram' { | ||
import {EventEmitter} from 'events'; | ||
import {Buffer} from 'buffer'; | ||
|
||
export class UnixDgramSocket extends EventEmitter { | ||
send(buf: Buffer, callback?: (err?: Error) => void): void; | ||
send(buf: Buffer, offset: number, length: number, path: string, callback?: (err?: Error) => void): void; | ||
bind(path: string): void; | ||
connect(remotePath: string): void; | ||
close(): void; | ||
} | ||
|
||
export function createSocket(type: 'unix_dgram', listener?: (msg: Buffer) => void): UnixDgramSocket; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import type {UnixDgramSocket} from 'unix-dgram'; | ||
|
||
import logger from './logger'; | ||
|
||
// Handle sd_notify protocol, see https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html | ||
// All methods in here will be no-ops if run on unsupported platforms or without Type=notify | ||
|
||
let socket: UnixDgramSocket | undefined; | ||
let watchdog: NodeJS.Timeout | undefined; | ||
|
||
function sendToSystemd(msg: string): void { | ||
if (!socket) return; | ||
const buffer = Buffer.from(msg); | ||
socket.send(buffer, 0, buffer.byteLength, process.env.NOTIFY_SOCKET!, (err: Error | undefined) => { | ||
/* v8 ignore start */ | ||
if (err) { | ||
logger.warning(`Failed to send "${msg}" to systemd: ${err.message}`); | ||
} | ||
/* v8 ignore stop */ | ||
}); | ||
} | ||
|
||
export async function init(): Promise<void> { | ||
if (!process.env.NOTIFY_SOCKET) return; | ||
try { | ||
const {createSocket} = await import('unix-dgram'); | ||
socket = createSocket('unix_dgram'); | ||
/* v8 ignore start */ | ||
} catch (error) { | ||
// Ignore error on Windows if not running on WSL, as UNIX sockets don't exist | ||
// on Windows. Unlikely that NOTIFY_SOCKET is set anyways but better be safe. | ||
if (process.platform === 'win32' && !process.env.WSL_DISTRO_NAME) return; | ||
// Otherwise, pass on exception, so main process can bail out immediately | ||
throw error; | ||
} | ||
/* v8 ignore start */ | ||
} | ||
|
||
export function started(): void { | ||
sendToSystemd('READY=1'); | ||
if (!socket || !process.env.WATCHDOG_USEC || watchdog) return; | ||
const num = Math.max(0, parseInt(process.env.WATCHDOG_USEC, 10)); | ||
if (!num) { | ||
logger.warning(`WATCHDOG_USEC invalid: "${process.env.WATCHDOG_USEC}", parsed to "${num}"`); | ||
return; | ||
} | ||
// Convert us to ms, send twice as frequently as the timeout | ||
const interval = num / 1000 / 2; | ||
watchdog = setInterval(() => sendToSystemd('WATCHDOG=1'), interval); | ||
} | ||
|
||
export function stopping(): void { | ||
sendToSystemd('STOPPING=1'); | ||
} | ||
|
||
export function stopped(): void { | ||
if (!watchdog) return; | ||
clearInterval(watchdog); | ||
watchdog = undefined; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import {mockLogger} from './mocks/logger'; | ||
|
||
function expectMessage(nth: number, message: string): void { | ||
expect(sendMock).toHaveBeenNthCalledWith(nth, Buffer.from(message), 0, expect.any(Number), 'mocked', expect.any(Function)); | ||
} | ||
|
||
const sendMock = vi.fn(); | ||
|
||
vi.mock('unix-dgram', () => { | ||
const mockUnixDgramSocket = { | ||
send: sendMock, | ||
}; | ||
return { | ||
createSocket: vi.fn(() => mockUnixDgramSocket), | ||
}; | ||
}); | ||
|
||
async function runTest(): Promise<void> { | ||
const sd = await import('../lib/util/sd-notify'); | ||
await sd.init(); | ||
sd.started(); | ||
vi.advanceTimersByTime(6000); | ||
sd.stopping(); | ||
sd.stopped(); | ||
} | ||
|
||
const mocksClear = [mockLogger.log, mockLogger.debug, mockLogger.info, mockLogger.warning, mockLogger.error, sendMock]; | ||
|
||
describe('sd-notify', () => { | ||
beforeAll(async () => { | ||
vi.useFakeTimers(); | ||
}); | ||
|
||
afterAll(async () => { | ||
vi.useRealTimers(); | ||
delete process.env.NOTIFY_SOCKET; | ||
delete process.env.WATCHDOG_USEC; | ||
}); | ||
|
||
beforeEach(() => { | ||
vi.resetModules(); | ||
mocksClear.forEach((m) => m.mockClear()); | ||
}); | ||
|
||
it('No socket', async () => { | ||
delete process.env.NOTIFY_SOCKET; | ||
delete process.env.WATCHDOG_USEC; | ||
await runTest(); | ||
expect(sendMock).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
it('Socket only', async () => { | ||
process.env.NOTIFY_SOCKET = 'mocked'; | ||
delete process.env.WATCHDOG_USEC; | ||
await runTest(); | ||
expect(sendMock).toHaveBeenCalledTimes(2); | ||
expectMessage(1, 'READY=1'); | ||
expectMessage(2, 'STOPPING=1'); | ||
}); | ||
|
||
it('Socket and watchdog', async () => { | ||
process.env.NOTIFY_SOCKET = 'mocked'; | ||
process.env.WATCHDOG_USEC = '10000000'; | ||
await runTest(); | ||
expect(sendMock).toHaveBeenCalledTimes(3); | ||
expectMessage(1, 'READY=1'); | ||
expectMessage(2, 'WATCHDOG=1'); | ||
expectMessage(3, 'STOPPING=1'); | ||
}); | ||
|
||
it('Invalid watchdog timeout', async () => { | ||
process.env.NOTIFY_SOCKET = 'mocked'; | ||
process.env.WATCHDOG_USEC = 'mocked'; | ||
await runTest(); | ||
expect(sendMock).toHaveBeenCalledTimes(2); | ||
expectMessage(1, 'READY=1'); | ||
expectMessage(2, 'STOPPING=1'); | ||
}); | ||
}); |