diff --git a/src/client.ts b/src/client.ts index 20c7fd9..23aa374 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,7 +11,7 @@ import { checkSessionOptions, undeclaredInterfaceVersion } from './session-optio import { connect as netConnect } from 'node:net'; import { connect as tlsConnect } from 'node:tls'; import { defaultInterfaceVersion } from './defs/constants.ts'; -import { silentLog } from './log.ts'; +import { guardedLog } from './log.ts'; export type ClientOptions = { addressRange?: string; @@ -194,7 +194,7 @@ async function connect(options: ClientOptions, log: SmppLog): Promise> { - const log = options.log ?? silentLog; + const log = guardedLog(options.log); const opened = await connect(options, log); if (opened.err) return { err: opened.err }; diff --git a/src/log.ts b/src/log.ts index 61c65b3..547c421 100644 --- a/src/log.ts +++ b/src/log.ts @@ -20,3 +20,26 @@ export const silentLog: SmppLog = { verbose: noop, warn: noop, }; + +function contained(log: SmppLog, method: keyof SmppLog): LogMethod { + return (msg, metadata) => { + try { + log[method](msg, metadata); + } catch { + // Reporting a broken logger would need a logger, and hard rule 1 outranks the report. + } + }; +} + +/** The application's logger with a throw from it contained, or silence when it supplied none. */ +export function guardedLog(log?: SmppLog): SmppLog { + if (log === undefined) return silentLog; + + return { + debug: contained(log, 'debug'), + error: contained(log, 'error'), + info: contained(log, 'info'), + verbose: contained(log, 'verbose'), + warn: contained(log, 'warn'), + }; +} diff --git a/src/server.ts b/src/server.ts index 6708575..0c1d5c1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -12,7 +12,7 @@ import { createServer as createTlsServer } from 'node:tls'; import { defaultInterfaceVersion } from './defs/constants.ts'; import { errorFrom } from './error-from.ts'; import { paramText } from './defs/types.ts'; -import { silentLog } from './log.ts'; +import { guardedLog } from './log.ts'; export type AuthenticateResult = { userData?: unknown } | boolean; @@ -185,7 +185,7 @@ async function acceptBind( } async function onBind(session: Session, pduObj: PduObject, options: ServerOptions): Promise { - const log = options.log ?? silentLog; + const log = guardedLog(options.log); const identity = { system_id: options.systemId ?? defaults.systemId }; const systemId = paramText(pduObj.params.system_id); @@ -212,7 +212,7 @@ async function onRequest( if (session.loggedIn || pduObj.cmdName === 'unbind') return false; if (!bindCommands.includes(pduObj.cmdName)) { - const log = options.log ?? silentLog; + const log = guardedLog(options.log); log.debug('server - command before bind', { cmdName: pduObj.cmdName }); await session.sendReturn(pduObj, 'ESME_RINVBNDSTS'); @@ -226,7 +226,7 @@ async function onRequest( } function onConnection(sock: Socket, options: ServerOptions, server: SmppServer): void { - const log = options.log ?? silentLog; + const log = guardedLog(options.log); const session = new Session({ idleTimeout: options.idleTimeout ?? defaults.idleTimeout, log, @@ -304,7 +304,7 @@ function createListener( } function onListening(listener: NetServer, smpp: SmppServer, options: ServerOptions): void { - const log = options.log ?? silentLog; + const log = guardedLog(options.log); // Past startup, a listener error is a runtime event, not a failed start. listener.on('error', (err: Error) => { @@ -321,7 +321,7 @@ function onListening(listener: NetServer, smpp: SmppServer, options: ServerOptio /** Starts listening for SMPP connections. Resolves once the socket is bound. */ export function server(options: ServerOptions = {}): Promise> { - const log = options.log ?? silentLog; + const log = guardedLog(options.log); const port = options.port ?? defaults.port; const created = createListener(options, log, port); diff --git a/src/session.ts b/src/session.ts index 9097030..4385722 100644 --- a/src/session.ts +++ b/src/session.ts @@ -19,7 +19,7 @@ import { errorFrom } from './error-from.ts'; import { optionalParamsMinVersion } from './defs/constants.ts'; import { bindCarries, bindCommands, defaultSystemId, defaults } from './session-options.ts'; import { isResp, pduReturn } from './pdu.ts'; -import { silentLog } from './log.ts'; +import { guardedLog } from './log.ts'; import { submitSms, unsent } from './send-sms.ts'; import { ConcatReference } from './udh.ts'; @@ -106,7 +106,7 @@ export class Session extends EventEmitter { constructor(options: SessionOptions) { super({ captureRejections: true }); - this.log = options.log ?? silentLog; + this.log = guardedLog(options.log); this.options = options; this.dlrMerger = new DlrMerger({ log: this.log, diff --git a/test/session.test.ts b/test/session.test.ts index 2b7f7d8..034df1c 100644 --- a/test/session.test.ts +++ b/test/session.test.ts @@ -1329,6 +1329,24 @@ describe('application hooks that throw or reject', () => { assert.equal(smpp.sessions.size, 0); }); + test('sends on through an application logger that throws', async t => { + const thrower = (): void => { throw new Error('the logger exploded'); }; + const log: SmppLog = { debug: thrower, error: thrower, info: thrower, verbose: thrower, warn: thrower }; + const smpp = await startServer(t, { log }); + + smpp.on('session', session => { + session.on('sms', sms => { void sms.sendResp(); }); + }); + + const { session } = await connect(t, smpp, { log }); + + assert.ok(session); + + const sent = await session.sendSms({ from: '46701113311', message: 'logged', to: '46709771337' }); + + assert.equal(sent.err, undefined); + }); + test('refuses a send window that can never free a slot', async t => { const smpp = await startServer(t); const { err, session } = await connect(t, smpp, { maxOutstanding: 0 });