cordova.plugins.diagnostic 7.2.8 → 7.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ {
2
+ "workspace_id": "3d27-7072-f841-12e1",
3
+ "workspace_id_at": "2026-02-24T14:03:38.792Z",
4
+ "project_name": "cordova-diagnostic-plugin",
5
+ "cloud_sync": false,
6
+ "git_id": "303b-c6e9-a865-86a3",
7
+ "git_id_at": "2026-02-24T14:03:38.810Z"
8
+ }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ **v7.2.9**
4
+ (ios) crashfix: Refactored local-network permission flow to use one deterministic start path with guarded callback handling:
5
+ - Consolidated duplicated browse/publish start logic into a single helper used by both status and authorisation APIs.
6
+ - Added uniform exception guards and fallback completion for asynchronous callback and delegate execution paths.
7
+ - Added synchronised request-state transitions and consistent timeout handling to reduce overlapping or stuck requests.
8
+
9
+
3
10
  **v7.2.8**
4
11
  (ios) bugfix: Avoid reporting false-positive Local Network Permission Denied by returning indeterminate result, instead of denied, if NSNetService publish fails with an error, as it can fail for reasons other than permission denied.
5
12
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "7.2.8",
2
+ "version": "7.2.9",
3
3
  "name": "cordova.plugins.diagnostic",
4
4
  "cordova_name": "Diagnostic",
5
5
  "description": "Cordova/Phonegap plugin to check the state of Location/WiFi/Camera/Bluetooth device settings.",
package/plugin.xml CHANGED
@@ -2,7 +2,7 @@
2
2
  <plugin xmlns="http://www.phonegap.com/ns/plugins/1.0"
3
3
  xmlns:android="http://schemas.android.com/apk/res/android"
4
4
  id="cordova.plugins.diagnostic"
5
- version="7.2.8">
5
+ version="7.2.9">
6
6
 
7
7
  <name>Diagnostic</name>
8
8
  <description>Cordova/Phonegap plugin to check the state of Location/WiFi/Camera/Bluetooth device settings.</description>
@@ -44,7 +44,12 @@ static Diagnostic* diagnostic;
44
44
 
45
45
  // Internal constants
46
46
  static NSString*const LOG_TAG = @"Diagnostic_Wifi[native]";
47
- static NSTimeInterval const kLocalNetworkDefaultTimeoutSeconds = 2.0;
47
+ static NSTimeInterval const kLocalNetworkDefaultTimeoutSeconds = 30.0; // Default timeout for local network permission flow, after which we'll return indeterminate if we haven't received a response. This is needed to prevent hanging requests in cases where delegate callbacks are not fired (e.g. due to iOS bugs or edge-case network conditions).
48
+ static const char* kLocalNetworkBonjourServiceTypeBrowse = "_lnp._tcp";
49
+ static NSString*const kLocalNetworkBonjourServiceTypePublish = @"_lnp._tcp.";
50
+ static NSString*const kLocalNetworkBonjourServiceDomain = @"local.";
51
+ static NSString*const kLocalNetworkBonjourServiceName = @"LocalNetworkPrivacy";
52
+ static NSInteger const kLocalNetworkBonjourServicePort = 1100;
48
53
 
49
54
  - (void)pluginInitialize {
50
55
 
@@ -81,7 +86,17 @@ static NSTimeInterval const kLocalNetworkDefaultTimeoutSeconds = 2.0;
81
86
  [self->_localNetworkCommands addObject:command];
82
87
  }
83
88
 
84
- if(self->_isRequesting){
89
+ BOOL requestInProgress = NO;
90
+ @synchronized(self) {
91
+ if (self->_isRequesting) {
92
+ requestInProgress = YES;
93
+ } else {
94
+ self->_isRequesting = YES;
95
+ self->_isPublishing = NO;
96
+ }
97
+ }
98
+
99
+ if(requestInProgress){
85
100
  // A request is already in progress so await the result
86
101
  [diagnostic logDebug:@"A request is already in progress, will return result when done"];
87
102
  return;
@@ -89,74 +104,12 @@ static NSTimeInterval const kLocalNetworkDefaultTimeoutSeconds = 2.0;
89
104
 
90
105
  NSTimeInterval timeoutSeconds = [self resolveLocalNetworkTimeoutFromCommand:command];
91
106
 
92
- // Create parameters, and allow browsing over peer-to-peer link.
93
107
  if (@available(iOS 14.0, *)) {
94
- // Create parameters, and allow browsing over peer-to-peer link.
95
- nw_parameters_t parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION);
96
- nw_parameters_set_include_peer_to_peer(parameters, true);
97
-
98
- // Browse for a custom service type.
99
- nw_browse_descriptor_t descriptor =
100
- nw_browse_descriptor_create_bonjour_service("_bonjour._tcp", NULL);
101
- self->_browser = nw_browser_create(descriptor, parameters);
102
-
103
- nw_browser_set_queue(self->_browser, dispatch_get_main_queue());
104
-
105
- self->_netService = [[NSNetService alloc] initWithDomain:@"local." type:@"_lnp._tcp." name:@"LocalNetworkPrivacy" port:1100];
106
-
107
- self->_isRequesting = YES;
108
- self->_isPublishing = NO;
109
-
110
- [diagnostic logDebug:[NSString stringWithFormat:@"Starting local network permission status check (timeout %.2fs)", timeoutSeconds]];
111
- // Start the browsing/publish flow on the main queue immediately and create a single-shot timeout.
112
- dispatch_async(dispatch_get_main_queue(), ^{
113
- __weak __typeof__(self) weakSelf = self;
114
-
115
- __strong __typeof__(weakSelf) strongSelf = weakSelf;
116
- if (!strongSelf) return;
117
-
118
- // Ensure we only start once for this check
119
- if (strongSelf->_isPublishing) {
120
- [diagnostic logDebug:@"Local network permission request already publishing, skipping start"];
121
- return;
122
- }
123
-
124
- strongSelf->_isPublishing = YES;
125
- strongSelf->_netService.delegate = strongSelf;
126
-
127
- // Install a state handler so the browser emits state changes (silences the warning about missing handlers)
128
- if (strongSelf->_browser) {
129
- nw_browser_set_state_changed_handler(strongSelf->_browser, ^(nw_browser_state_t newState, nw_error_t error) {
130
- __strong __typeof__(weakSelf) innerSelf = weakSelf;
131
- if (!innerSelf) {
132
- return;
133
- }
134
- [innerSelf handleBrowserState:newState error:error context:@"status check"];
135
- });
136
-
137
- nw_browser_start(strongSelf->_browser);
138
- } else {
139
- [diagnostic logDebug:@"Attempted to start browser but browser is null"];
140
- }
141
-
142
- [strongSelf->_netService publish];
143
- [strongSelf->_netService scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
144
-
145
- if (timeoutSeconds > 0) {
146
- strongSelf->_localNetworkTimer = [NSTimer scheduledTimerWithTimeInterval:timeoutSeconds
147
- repeats:NO
148
- block:^(NSTimer * _Nonnull timer) {
149
- __strong __typeof__(weakSelf) innerSelf = weakSelf;
150
- if (!innerSelf) return;
151
-
152
- [diagnostic logDebug:[NSString stringWithFormat:@"Local network permission status check timed out after %.2fs", timeoutSeconds]];
153
- [innerSelf completeLocalNetworkFlowWithState:LocalNetworkPermissionStateIndeterminate shouldCache:NO];
154
- }];
155
- }
156
- });
108
+ [diagnostic logDebug:[NSString stringWithFormat:@"Starting local network permission status check (timeout %.2fs)", timeoutSeconds]];
109
+ [self startLocalNetworkAuthorizationFlowWithTimeout:timeoutSeconds context:@"status check"];
157
110
  }else{
158
111
  [diagnostic logDebug:@"iOS version < 14.0, so local network permission is not required"];
159
- [self callLocalNetworkCallbacks:LocalNetworkPermissionStateGranted];
112
+ [self completeLocalNetworkFlowWithState:LocalNetworkPermissionStateGranted shouldCache:YES];
160
113
  }
161
114
  }
162
115
  @catch (NSException *exception) {
@@ -170,12 +123,21 @@ static NSTimeInterval const kLocalNetworkDefaultTimeoutSeconds = 2.0;
170
123
  {
171
124
  [self.commandDelegate runInBackground:^{
172
125
  @try {
173
- if(self->_isRequesting){
126
+ BOOL requestInProgress = NO;
127
+ @synchronized(self) {
128
+ if (self->_isRequesting) {
129
+ requestInProgress = YES;
130
+ } else {
131
+ self->_isRequesting = YES;
132
+ self->_isPublishing = NO;
133
+ }
134
+ }
135
+
136
+ if(requestInProgress){
174
137
  // A request is already in progress
175
138
  [diagnostic sendPluginError:@"A request is already in progress" :command];
176
139
  return;
177
140
  }
178
- self->_isRequesting = YES;
179
141
 
180
142
  // Store command so we can send the result later
181
143
  @synchronized(self->_localNetworkCommands) {
@@ -183,37 +145,11 @@ static NSTimeInterval const kLocalNetworkDefaultTimeoutSeconds = 2.0;
183
145
  }
184
146
 
185
147
  if (@available(iOS 14.0, *)) {
186
- // Create parameters, and allow browsing over peer-to-peer link.
187
- nw_parameters_t parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION);
188
- nw_parameters_set_include_peer_to_peer(parameters, true);
189
-
190
- // Browse for a custom service type.
191
- nw_browse_descriptor_t descriptor =
192
- nw_browse_descriptor_create_bonjour_service("_bonjour._tcp", NULL);
193
- self->_browser = nw_browser_create(descriptor, parameters);
194
-
195
- nw_browser_set_queue(self->_browser, dispatch_get_main_queue());
196
-
197
- __weak __typeof__(self) weakSelf = self;
198
- nw_browser_set_state_changed_handler(self->_browser, ^(nw_browser_state_t newState, nw_error_t error) {
199
- __strong __typeof__(weakSelf) strongSelf = weakSelf;
200
- if (!strongSelf) {
201
- return;
202
- }
203
- [strongSelf handleBrowserState:newState error:error context:@"authorization request"];
204
- });
205
-
206
- self->_netService = [[NSNetService alloc] initWithDomain:@"local." type:@"_lnp._tcp." name:@"LocalNetworkPrivacy" port:1100];
207
- self->_netService.delegate = self;
208
-
209
- // Start browsing on main queue
210
- nw_browser_start(self->_browser);
211
- [self->_netService publish];
212
- // the netService needs to be scheduled on a run loop, in this case the main runloop
213
- [self->_netService scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
148
+ [diagnostic logDebug:[NSString stringWithFormat:@"Starting local network authorization request (timeout %.2fs)", kLocalNetworkDefaultTimeoutSeconds]];
149
+ [self startLocalNetworkAuthorizationFlowWithTimeout:kLocalNetworkDefaultTimeoutSeconds context:@"authorization request"];
214
150
  }else{
215
151
  // iOS version < 14.0, so local network permission is not required
216
- [self callLocalNetworkCallbacks:LocalNetworkPermissionStateGranted];
152
+ [self completeLocalNetworkFlowWithState:LocalNetworkPermissionStateGranted shouldCache:YES];
217
153
  }
218
154
  } @catch (NSException *exception) {
219
155
  [diagnostic handlePluginException:exception :command];
@@ -259,12 +195,14 @@ static NSTimeInterval const kLocalNetworkDefaultTimeoutSeconds = 2.0;
259
195
  - (void)completeLocalNetworkFlowWithState:(LocalNetworkPermissionState)state shouldCache:(BOOL)shouldCache
260
196
  {
261
197
  dispatch_block_t completion = ^{
262
- [self resetLocalNetwork];
263
- if (shouldCache && (state == LocalNetworkPermissionStateGranted || state == LocalNetworkPermissionStateDenied)) {
264
- [[NSUserDefaults standardUserDefaults] setInteger:state forKey:kLocalNetworkPermissionKey];
265
- [[NSUserDefaults standardUserDefaults] synchronize];
266
- }
267
- [self callLocalNetworkCallbacks:state];
198
+ [self performLocalNetworkCallbackSafelyWithContext:@"flow completion" block:^{
199
+ [self resetLocalNetwork];
200
+ if (shouldCache && (state == LocalNetworkPermissionStateGranted || state == LocalNetworkPermissionStateDenied)) {
201
+ [[NSUserDefaults standardUserDefaults] setInteger:state forKey:kLocalNetworkPermissionKey];
202
+ [[NSUserDefaults standardUserDefaults] synchronize];
203
+ }
204
+ [self callLocalNetworkCallbacks:state];
205
+ }];
268
206
  };
269
207
 
270
208
  if ([NSThread isMainThread]) {
@@ -299,6 +237,136 @@ static NSTimeInterval const kLocalNetworkDefaultTimeoutSeconds = 2.0;
299
237
  return timeout;
300
238
  }
301
239
 
240
+ - (void)handleLocalNetworkCallbackException:(NSException *)exception context:(NSString *)context
241
+ {
242
+ NSString *resolvedContext = context ?: @"local network callback";
243
+ NSString *reason = exception.reason ?: @"No reason provided";
244
+ [diagnostic logDebug:[NSString stringWithFormat:@"Caught exception in %@: %@ (%@)", resolvedContext, exception.name, reason]];
245
+
246
+ [self resetLocalNetwork];
247
+
248
+ @try {
249
+ [self callLocalNetworkCallbacks:LocalNetworkPermissionStateIndeterminate];
250
+ }
251
+ @catch (NSException *fallbackException) {
252
+ NSString *fallbackReason = fallbackException.reason ?: @"No reason provided";
253
+ [diagnostic logDebug:[NSString stringWithFormat:@"Failed to send fallback local network callback after %@: %@ (%@)", resolvedContext, fallbackException.name, fallbackReason]];
254
+ }
255
+ }
256
+
257
+ - (void)performLocalNetworkCallbackSafelyWithContext:(NSString *)context block:(dispatch_block_t)block
258
+ {
259
+ @try {
260
+ if (block) {
261
+ block();
262
+ }
263
+ }
264
+ @catch (NSException *exception) {
265
+ [self handleLocalNetworkCallbackException:exception context:context];
266
+ }
267
+ }
268
+
269
+ - (void)startLocalNetworkAuthorizationFlowWithTimeout:(NSTimeInterval)timeoutSeconds context:(NSString *)context
270
+ {
271
+ dispatch_block_t startFlow = ^{
272
+ NSString *operationContext = context ?: @"local network authorisation";
273
+ [self performLocalNetworkCallbackSafelyWithContext:[NSString stringWithFormat:@"%@ setup", operationContext] block:^{
274
+ if (!self->_isRequesting) {
275
+ [diagnostic logDebug:[NSString stringWithFormat:@"Ignoring %@ start because there is no active request", operationContext]];
276
+ return;
277
+ }
278
+
279
+ if (self->_isPublishing) {
280
+ [diagnostic logDebug:@"Local network permission request already publishing, skipping start"];
281
+ return;
282
+ }
283
+
284
+ // Cancel any stale timer before starting a fresh flow.
285
+ if (self->_localNetworkTimer) {
286
+ [self->_localNetworkTimer invalidate];
287
+ self->_localNetworkTimer = nil;
288
+ }
289
+
290
+ nw_parameters_t parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION);
291
+ if (!parameters) {
292
+ [diagnostic logDebug:[NSString stringWithFormat:@"Failed to create network parameters for %@", operationContext]];
293
+ [self completeLocalNetworkFlowWithState:LocalNetworkPermissionStateIndeterminate shouldCache:NO];
294
+ return;
295
+ }
296
+
297
+ nw_parameters_set_include_peer_to_peer(parameters, true);
298
+
299
+ nw_browse_descriptor_t descriptor = nw_browse_descriptor_create_bonjour_service(kLocalNetworkBonjourServiceTypeBrowse, NULL);
300
+ if (!descriptor) {
301
+ [diagnostic logDebug:[NSString stringWithFormat:@"Failed to create browse descriptor for %@", operationContext]];
302
+ [self completeLocalNetworkFlowWithState:LocalNetworkPermissionStateIndeterminate shouldCache:NO];
303
+ return;
304
+ }
305
+
306
+ nw_browser_t browser = nw_browser_create(descriptor, parameters);
307
+ if (!browser) {
308
+ [diagnostic logDebug:[NSString stringWithFormat:@"Failed to create browser for %@", operationContext]];
309
+ [self completeLocalNetworkFlowWithState:LocalNetworkPermissionStateIndeterminate shouldCache:NO];
310
+ return;
311
+ }
312
+
313
+ NSNetService *netService = [[NSNetService alloc] initWithDomain:kLocalNetworkBonjourServiceDomain
314
+ type:kLocalNetworkBonjourServiceTypePublish
315
+ name:kLocalNetworkBonjourServiceName
316
+ port:(int)kLocalNetworkBonjourServicePort];
317
+ if (!netService) {
318
+ [diagnostic logDebug:[NSString stringWithFormat:@"Failed to create net service for %@", operationContext]];
319
+ [self completeLocalNetworkFlowWithState:LocalNetworkPermissionStateIndeterminate shouldCache:NO];
320
+ return;
321
+ }
322
+
323
+ self->_browser = browser;
324
+ self->_netService = netService;
325
+ self->_netService.delegate = self;
326
+ self->_isPublishing = YES;
327
+
328
+ __weak __typeof__(self) weakSelf = self;
329
+ nw_browser_set_queue(browser, dispatch_get_main_queue());
330
+ nw_browser_set_state_changed_handler(browser, ^(nw_browser_state_t newState, nw_error_t error) {
331
+ __strong __typeof__(weakSelf) strongSelf = weakSelf;
332
+ if (!strongSelf) {
333
+ return;
334
+ }
335
+
336
+ [strongSelf performLocalNetworkCallbackSafelyWithContext:[NSString stringWithFormat:@"%@ browser state", operationContext] block:^{
337
+ [strongSelf handleBrowserState:newState error:error context:operationContext];
338
+ }];
339
+ });
340
+
341
+ nw_browser_start(browser);
342
+ [netService publish];
343
+ [netService scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
344
+
345
+ if (timeoutSeconds > 0) {
346
+ self->_localNetworkTimer = [NSTimer scheduledTimerWithTimeInterval:timeoutSeconds
347
+ repeats:NO
348
+ block:^(NSTimer * _Nonnull timer) {
349
+ __strong __typeof__(weakSelf) strongSelf = weakSelf;
350
+ if (!strongSelf) {
351
+ return;
352
+ }
353
+
354
+ [strongSelf performLocalNetworkCallbackSafelyWithContext:[NSString stringWithFormat:@"%@ timeout", operationContext] block:^{
355
+ [diagnostic logDebug:[NSString stringWithFormat:@"Local network %@ timed out after %.2fs", operationContext, timeoutSeconds]];
356
+ [strongSelf completeLocalNetworkFlowWithState:LocalNetworkPermissionStateIndeterminate shouldCache:NO];
357
+ }];
358
+ }];
359
+ }
360
+ }];
361
+ };
362
+
363
+ if ([NSThread isMainThread]) {
364
+ startFlow();
365
+ } else {
366
+ dispatch_async(dispatch_get_main_queue(), startFlow);
367
+ }
368
+ }
369
+
302
370
  - (BOOL)isPermissionDeniedError:(nw_error_t)error
303
371
  {
304
372
  if (!error) {
@@ -426,18 +494,22 @@ static NSTimeInterval const kLocalNetworkDefaultTimeoutSeconds = 2.0;
426
494
  /********************************/
427
495
 
428
496
  - (void)netServiceDidPublish:(NSNetService *)sender {
429
- [diagnostic logDebug:@"netServiceDidPublish: Local network permission has been granted"];
430
- [self completeLocalNetworkFlowWithState:LocalNetworkPermissionStateGranted shouldCache:YES];
497
+ [self performLocalNetworkCallbackSafelyWithContext:@"netServiceDidPublish" block:^{
498
+ [diagnostic logDebug:@"netServiceDidPublish: Local network permission has been granted"];
499
+ [self completeLocalNetworkFlowWithState:LocalNetworkPermissionStateGranted shouldCache:YES];
500
+ }];
431
501
  }
432
502
 
433
503
  - (void)netService:(NSNetService *)sender didNotPublish:(NSDictionary<NSString *,NSNumber *> *)errorDict {
434
- NSNumber *errorDomain = errorDict[NSNetServicesErrorDomain];
435
- NSNumber *errorCode = errorDict[NSNetServicesErrorCode];
436
- [diagnostic logDebug:[NSString stringWithFormat:@"netService didNotPublish (domain=%@, code=%@)", errorDomain, errorCode]];
437
- // NSNetService can fail to publish for many reasons unrelated to permissions (network issues,
438
- // name collisions, configuration problems, etc.). We cannot reliably determine permission denial
439
- // from NSNetService error codes alone, so return indeterminate. The browser state handler in
440
- // handleBrowserState will catch actual permission denials via isPermissionDeniedError.
441
- [self completeLocalNetworkFlowWithState:LocalNetworkPermissionStateIndeterminate shouldCache:NO];
504
+ [self performLocalNetworkCallbackSafelyWithContext:@"netService didNotPublish" block:^{
505
+ NSNumber *errorDomain = errorDict[NSNetServicesErrorDomain];
506
+ NSNumber *errorCode = errorDict[NSNetServicesErrorCode];
507
+ [diagnostic logDebug:[NSString stringWithFormat:@"netService didNotPublish (domain=%@, code=%@)", errorDomain, errorCode]];
508
+ // NSNetService can fail to publish for many reasons unrelated to permissions (network issues,
509
+ // name collisions, configuration problems, etc.). We cannot reliably determine permission denial
510
+ // from NSNetService error codes alone, so return indeterminate. The browser state handler in
511
+ // handleBrowserState will catch actual permission denials via isPermissionDeniedError.
512
+ [self completeLocalNetworkFlowWithState:LocalNetworkPermissionStateIndeterminate shouldCache:NO];
513
+ }];
442
514
  }
443
515
  @end