diff --git a/CHANGELOG.md b/CHANGELOG.md index 8185a06..ac2cf76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ bump may contain breaking changes, and each one is listed below. `-handleVoIPPushPayload:completion:` used to add a second nudge on top of the one `-prepareIncomingCallWithUUID:caller:` already performs, sending two `REGISTER`s per call. +- Starting the engine no longer aborts with PJLIB's "Calling pjlib from + unknown/external thread" when something else in the process has already + initialized PJLIB. `pjsua_create()` registers its caller only when it is the + one bringing PJLIB up, and a push-driven start runs on a transient SIP queue + worker; the stack is now initialized and the thread registered before + `pjsua_create()`, and the teardown balances that initialization. ## [0.7.1] - 2026-09-08 diff --git a/CallWaveKit/CallWaveClient.m b/CallWaveKit/CallWaveClient.m index 8f088f2..50bf141 100644 --- a/CallWaveKit/CallWaveClient.m +++ b/CallWaveKit/CallWaveClient.m @@ -1064,12 +1064,31 @@ - (pj_status_t)startEngineLocked { CallWaveEngineConfiguration *engine = self.engineConfiguration; if (!gPJSUACreated) { - pj_status_t status = pjsua_create(); + // pjsua_create() starts with pj_init(), but pj_init() registers the + // calling thread only when it is the one that brings PJLIB up. A host + // that already runs another PJSIP-based stack — or that called + // pj_init() itself — leaves this thread, a transient sipQueue worker, + // unknown to PJLIB, and pjsua_create() aborts on the first per-thread + // call it makes. Initializing PJLIB first makes the registration below + // legal; pjsua_destroy() undoes only its own pj_init(), so the + // teardown balances this one. + pj_status_t status = pj_init(); if (status != PJ_SUCCESS) { return status; } - gPJSUACreated = PJ_TRUE; gPJInitialized = PJ_TRUE; + if (!ensurePJThreadRegistered("CallWaveCreate")) { + pj_shutdown(); + gPJInitialized = PJ_FALSE; + return PJ_EUNKNOWN; + } + status = pjsua_create(); + if (status != PJ_SUCCESS) { + pj_shutdown(); + gPJInitialized = PJ_FALSE; + return status; + } + gPJSUACreated = PJ_TRUE; } if (!ensurePJThreadRegistered("CallWaveConfig")) { @@ -1495,6 +1514,12 @@ + (void)destroyRuntimeOnQueue:(dispatch_queue_t)queue { if (gPJSUACreated) { pjsua_destroy(); } + if (gPJInitialized) { + // Balances the pj_init() in -startEngineLocked. pjsua_destroy() + // releases only the reference pjsua_create() took, and PJLIB stays + // up for whoever else in the process initialized it. + pj_shutdown(); + } gPJSUAStarted = PJ_FALSE; gPJSUACreated = PJ_FALSE; gPJInitialized = PJ_FALSE; diff --git a/Tests/CallWaveKitRegistryTests/CallWavePushWakeRegistrationTests.m b/Tests/CallWaveKitRegistryTests/CallWavePushWakeRegistrationTests.m index a3ef100..990dccf 100644 --- a/Tests/CallWaveKitRegistryTests/CallWavePushWakeRegistrationTests.m +++ b/Tests/CallWaveKitRegistryTests/CallWavePushWakeRegistrationTests.m @@ -3,6 +3,12 @@ #import "CallWaveClient.h" #import "CallWaveLogInternal.h" +#if __has_include() +#import +#else +#import +#endif + // A push for a host that only learns its credentials from that push used to // run the full -startWithError: path, which has no configuration to register // and therefore failed with CallWaveErrorNotConfigured on every single call. @@ -123,4 +129,47 @@ - (void)testHostOwnedPushNudgesRegistrationOnce { XCTAssertEqual(client.wakeCount, 1u); } +- (CallWaveConfiguration *)configuration { + return [[CallWaveConfiguration alloc] initWithHost:@"127.0.0.1" + port:5099 + transport:CallWaveTransportUDP + username:@"test" + password:@"test" + includesCallsInRecents:NO]; +} + +- (void)testPushWithCredentialsStartsTheStack { + CallWaveClient *client = [[CallWaveClient alloc] initWithConfiguration:[self configuration] + options:CallWaveIntegrationOptionNone + provider:nil + engineConfiguration:[self engine]]; + + [client prepareIncomingCallWithUUID:[NSUUID UUID] caller:@"1001"]; + [client performSIPSync:^{}]; + + XCTAssertTrue(client.isRunning); + [client stop]; +} + +- (void)testEngineStartsWhenSomethingElseAlreadyInitializedPJLIB { + // A second PJSIP consumer in the process — the host itself, or another + // library — leaves PJLIB initialized. pjsua_create() then no longer + // registers the thread it runs on, which for a push-driven start is a + // transient sipQueue worker: every PJLIB call on it used to abort. + XCTAssertEqual(pj_init(), PJ_SUCCESS); + + CallWaveClient *client = [[CallWaveClient alloc] initWithConfiguration:[self configuration] + options:CallWaveIntegrationOptionNone + provider:nil + engineConfiguration:[self engine]]; + + [client prepareIncomingCallWithUUID:[NSUUID UUID] caller:@"1001"]; + [client performSIPSync:^{}]; + + XCTAssertTrue(client.isRunning); + [client stop]; + // The client's own pj_init()/pj_shutdown() pair leaves this one standing. + pj_shutdown(); +} + @end