mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
fix(cfnetwork): preserve shared sessions and delegate-queue completions
Ignore both invalidation methods on the shared session; deliver task completion through the owning session delegate queue. Extend the standalone probe to exercise singleton reuse and a suspended custom queue. Require stable old-client process identities and complete window data before optional recovery, and continue launch after shutdown failures.
This commit is contained in:
1 parent
0087384a02
commit
e07cf72a15
8 files changed
+171
-41
No files matched your search
+8
-3
@@ -31,7 +31,9 @@ The patch check uses temporary base indexes and does not reset or modify the sou
|
||||
|
||||
## NSURLSession lifecycle regression
|
||||
|
||||
After configuring and building the patched Darling source:
|
||||
After configuring, building and installing the patched Darling runtime (including
|
||||
CFNetwork), run the probe. Supply `--frameworks` and `--libraries` when using
|
||||
staged overlays instead of the installed runtime:
|
||||
|
||||
```sh
|
||||
python3 tools/build-riot-urlsession-check.py
|
||||
@@ -53,8 +55,11 @@ overlays, and keeps diagnostics in mode-0600 `build/urlsession-check.log`.
|
||||
Expected checks: five successful HTTP GETs with exact response bytes/status;
|
||||
connection refusal delivered as an error; repeated resume/cancel without
|
||||
duplicate completion; finish waiting for task completion; and session
|
||||
invalidation delivered once for both graceful finish and cancellation.
|
||||
Expected output includes `URLSESSION_PASS` and `LIFECYCLE_PASS`. The original
|
||||
invalidation delivered once for both graceful finish and cancellation; shared-session
|
||||
invalidation leaving later requests usable; and completion delivery honoring a
|
||||
suspended custom delegate queue.
|
||||
Expected output includes `URLSESSION_PASS`, `LIFECYCLE_PASS`,
|
||||
`SHARED_SESSION_PASS` and `QUEUE_PASS`. The original
|
||||
runtime aborts on missing data-task initialization or session invalidation.
|
||||
These checks do not establish full NSURLSession conformance or transport-level
|
||||
interruption of an already-running request.
|
||||
|
||||
@@ -412,3 +412,8 @@ If a black fullscreen game has only its normal Metal child and becomes visible a
|
||||
### Visible cursor while relative on Xwayland (PR #18)
|
||||
|
||||
When Xwayland does not honour warps, the virtual cursor follows real pointer motion and application warps leave it in place. Expected warp echoes are still discarded using their request serial and coordinates. This keeps clicks and camera edge panning aligned with the visible cursor on GNOME/gamescope. The contributor confirmed this path in a gamescope practice-tool session; plain GNOME and NVIDIA still require manual verification.
|
||||
|
||||
Recovery requires every matching client to be at least 60 seconds old and a
|
||||
complete Hyprland window record (`class`, `title`, `mapped`, `hidden`). A second
|
||||
process scan after window inspection must find the same client identities and
|
||||
no game. Missing information or shutdown failure leaves normal launch in charge.
|
||||
+26
-13
@@ -1,5 +1,5 @@
|
||||
diff --git a/include/CFNetwork/NSCFURLSession.h b/include/CFNetwork/NSCFURLSession.h
|
||||
index e2326a3..b7489da 100644
|
||||
index e2326a3..32efa89 100644
|
||||
--- a/include/CFNetwork/NSCFURLSession.h
|
||||
+++ b/include/CFNetwork/NSCFURLSession.h
|
||||
@@ -2,7 +2,7 @@
|
||||
@@ -11,12 +11,13 @@ index e2326a3..b7489da 100644
|
||||
|
||||
__attribute__((visibility("hidden")))
|
||||
@interface __NSCFURLSessionConfiguration : NSObject
|
||||
@@ -19,6 +19,8 @@ __attribute__((visibility("hidden")))
|
||||
@@ -19,6 +19,9 @@ __attribute__((visibility("hidden")))
|
||||
__NSCFSessionBridge *_connectionSession;
|
||||
__NSCFURLSession *_extraRetain;
|
||||
NSOperationQueue *_realDelegateQueue;
|
||||
+ NSMutableSet *_activeDataTasks;
|
||||
+ BOOL _invalidationNotified;
|
||||
+ BOOL _isSharedSession;
|
||||
}
|
||||
|
||||
@property(retain) NSOperationQueue *realDelegateQueue; // @synthesize realDelegateQueue=_realDelegateQueue;
|
||||
@@ -38,7 +39,7 @@ index 91c0d0f..d6e3daf 100644
|
||||
NULL, // equal
|
||||
NULL, // hash
|
||||
diff --git a/src/URL/NSCFURLSession.m b/src/URL/NSCFURLSession.m
|
||||
index 95a726b..f5ff824 100644
|
||||
index 95a726b..86291e8 100644
|
||||
--- a/src/URL/NSCFURLSession.m
|
||||
+++ b/src/URL/NSCFURLSession.m
|
||||
@@ -9,6 +9,8 @@
|
||||
@@ -50,12 +51,13 @@ index 95a726b..f5ff824 100644
|
||||
|
||||
@implementation __NSCFURLSessionConfiguration
|
||||
@end
|
||||
@@ -20,19 +22,19 @@
|
||||
@@ -20,19 +22,20 @@
|
||||
static dispatch_once_t once = 0L;
|
||||
static NSURLSession *sharedSession = nil;
|
||||
dispatch_once(&once, ^{
|
||||
- sharedSession = (NSURLSession *)[[__NSCFURLSession alloc] initWithConfiguration:(NSURLSessionConfiguration *)[__NSCFURLSessionConfiguration defaultSessionConfiguration] delegate:nil delegateQueue:nil];
|
||||
+ sharedSession = (NSURLSession *)[[__NSCFURLSession alloc] initWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:nil delegateQueue:nil];
|
||||
+ ((__NSCFURLSession *)sharedSession)->_isSharedSession = YES;
|
||||
});
|
||||
return sharedSession;
|
||||
}
|
||||
@@ -73,7 +75,7 @@ index 95a726b..f5ff824 100644
|
||||
}
|
||||
|
||||
+ (void)_releaseProcessAssertionForSessionIdentifier:(NSString *)identifier
|
||||
@@ -56,12 +58,62 @@
|
||||
@@ -56,12 +59,66 @@
|
||||
return nil;
|
||||
}
|
||||
_nsCFConfig = (__NSCFURLSessionConfiguration *)[configuration retain];
|
||||
@@ -109,7 +111,10 @@ index 95a726b..f5ff824 100644
|
||||
+
|
||||
+- (void)finishTasksAndInvalidate
|
||||
+{
|
||||
+ @synchronized(self) { _invalid = YES; }
|
||||
+ @synchronized(self) {
|
||||
+ if (_isSharedSession) return;
|
||||
+ _invalid = YES;
|
||||
+ }
|
||||
+ [self _notifyInvalidationIfReady];
|
||||
+}
|
||||
+
|
||||
@@ -117,6 +122,7 @@ index 95a726b..f5ff824 100644
|
||||
+{
|
||||
+ NSArray *tasks;
|
||||
+ @synchronized(self) {
|
||||
+ if (_isSharedSession) return;
|
||||
+ _invalid = YES;
|
||||
+ tasks = [[_activeDataTasks allObjects] copy];
|
||||
+ }
|
||||
@@ -138,7 +144,7 @@ index 95a726b..f5ff824 100644
|
||||
- (void)delegate_didFinishEventsForBackgroundURLSession
|
||||
{
|
||||
if ([self can_delegate_didFinishEventsForBackgroundURLSession])
|
||||
@@ -364,7 +416,13 @@
|
||||
@@ -364,7 +421,13 @@
|
||||
|
||||
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler
|
||||
{
|
||||
@@ -153,7 +159,7 @@ index 95a726b..f5ff824 100644
|
||||
}
|
||||
|
||||
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url
|
||||
@@ -374,9 +432,7 @@
|
||||
@@ -374,9 +437,7 @@
|
||||
|
||||
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler
|
||||
{
|
||||
@@ -165,10 +171,10 @@ index 95a726b..f5ff824 100644
|
||||
|
||||
- (NSURLSessionDataTask *)dataTaskWithHTTPGetRequest:(NSURL *)url
|
||||
diff --git a/src/URL/NSURLSession.m b/src/URL/NSURLSession.m
|
||||
index 39367a5..625e88d 100644
|
||||
index 39367a5..ef3fdbc 100644
|
||||
--- a/src/URL/NSURLSession.m
|
||||
+++ b/src/URL/NSURLSession.m
|
||||
@@ -7,6 +7,11 @@
|
||||
@@ -7,6 +7,12 @@
|
||||
|
||||
#import <Foundation/NSURLSession.h>
|
||||
#import <objc/runtime.h>
|
||||
@@ -176,11 +182,12 @@ index 39367a5..625e88d 100644
|
||||
+#import <Foundation/NSError.h>
|
||||
+#import <Foundation/NSURLError.h>
|
||||
+#import <Foundation/NSData.h>
|
||||
+#import <Foundation/NSOperation.h>
|
||||
+#import <dispatch/dispatch.h>
|
||||
|
||||
const int64_t NSURLSessionTransferSizeUnknown = -1LL;
|
||||
NSString* const NSURLSessionDownloadTaskResumeData = @"NSURLSessionDownloadTaskResumeData";
|
||||
@@ -28,7 +33,108 @@ NSString* const NSURLSessionDownloadTaskResumeData = @"NSURLSessionDownloadTaskR
|
||||
@@ -28,7 +34,114 @@ NSString* const NSURLSessionDownloadTaskResumeData = @"NSURLSessionDownloadTaskR
|
||||
@implementation NSURLSessionTask
|
||||
@end
|
||||
|
||||
@@ -227,12 +234,18 @@ index 39367a5..625e88d 100644
|
||||
+ session = _taskSession;
|
||||
+ _taskSession = nil;
|
||||
+ }
|
||||
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
+ void (^completion)(void) = ^{
|
||||
+ @autoreleasepool {
|
||||
+ if (handler) handler(data, response, error);
|
||||
+ [session _taskDidComplete:self];
|
||||
+ }
|
||||
+ });
|
||||
+ };
|
||||
+ // Completion handlers follow the session's delegate queue, including its
|
||||
+ // suspension and serialization. The queued block retains task/session/data.
|
||||
+ if (session)
|
||||
+ [[session delegateQueue] addOperationWithBlock:completion];
|
||||
+ else
|
||||
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), completion);
|
||||
+ [handler release];
|
||||
+ [session release];
|
||||
+}
|
||||
|
||||
@@ -21,6 +21,7 @@ class RecoveryTests(unittest.TestCase):
|
||||
self.root = Path(self.temp.name)
|
||||
self.proc = self.root / 'proc'
|
||||
self.proc.mkdir()
|
||||
(self.proc / "uptime").write_text("1000 0\n")
|
||||
self.home = self.root / 'home'
|
||||
self.home_patch = patch.object(Path, 'home', return_value=self.home)
|
||||
self.home_patch.start()
|
||||
@@ -29,11 +30,13 @@ class RecoveryTests(unittest.TestCase):
|
||||
self.env_patch.start()
|
||||
self.addCleanup(self.env_patch.stop)
|
||||
|
||||
def process(self, pid, executable, prefix=None):
|
||||
def process(self, pid, executable, prefix=None, age=120):
|
||||
folder = self.proc / str(pid)
|
||||
folder.mkdir()
|
||||
(folder / 'cmdline').write_bytes(executable.encode() + b'\0')
|
||||
(folder / 'environ').write_bytes(b'' if prefix is None else b'DPREFIX=' + os.fsencode(prefix) + b'\0')
|
||||
fields = ['S'] + ['0'] * 18 + [str(int((1000 - age) * os.sysconf('SC_CLK_TCK')))]
|
||||
(folder / 'stat').write_text(str(pid) + ' (Riot Client (worker)) ' + ' '.join(fields))
|
||||
return folder
|
||||
|
||||
def riot(self):
|
||||
@@ -59,7 +62,7 @@ class RecoveryTests(unittest.TestCase):
|
||||
def test_visible_client_is_preserved(self):
|
||||
self.riot()
|
||||
for identity in ('Riot Client', 'League of Legends'):
|
||||
result, calls = self.run_recovery([{'class': identity, 'mapped': True, 'hidden': False}])
|
||||
result, calls = self.run_recovery([{'class': identity, 'title': identity, 'mapped': True, 'hidden': False}])
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(len(calls), 1)
|
||||
|
||||
@@ -96,7 +99,8 @@ class RecoveryTests(unittest.TestCase):
|
||||
|
||||
def test_malformed_compositor_response_is_preserved(self):
|
||||
self.riot()
|
||||
for raw in ('invalid', '{}', '[null]'):
|
||||
for raw in ('invalid', '{}', '[null]', '[{}]', '[{"class":"editor"}]',
|
||||
'[{"class":"Riot","title":"Riot","mapped":"true","hidden":false}]'):
|
||||
result, calls = self.run_recovery(raw=raw)
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(len(calls), 1)
|
||||
@@ -111,6 +115,45 @@ class RecoveryTests(unittest.TestCase):
|
||||
with patch.object(Path, 'read_bytes', read):
|
||||
self.assertEqual(self.run_recovery(), (False, []))
|
||||
|
||||
def test_young_client_prevents_recovery_even_if_an_old_client_exists(self):
|
||||
self.riot()
|
||||
self.process(125, '/Applications/Riot Client.app/Contents/MacOS/RiotClientServices', age=5)
|
||||
self.assertEqual(self.run_recovery(), (False, []))
|
||||
|
||||
def test_missing_or_invalid_age_information_prevents_recovery(self):
|
||||
proc = self.riot()
|
||||
for value in ('garbage', '123 (worker) S'):
|
||||
(proc / 'stat').write_text(value)
|
||||
self.assertEqual(self.run_recovery(), (False, []))
|
||||
(proc / 'stat').unlink()
|
||||
self.assertEqual(self.run_recovery(), (False, []))
|
||||
|
||||
def test_shutdown_failures_are_reported_without_aborting_launch(self):
|
||||
self.riot()
|
||||
for error in (FileNotFoundError(), subprocess.TimeoutExpired('darling', 25),
|
||||
subprocess.CalledProcessError(1, 'darling')):
|
||||
def command(args, **kwargs):
|
||||
if args[0] == 'hyprctl':
|
||||
return subprocess.CompletedProcess(args, 0, stdout='[]')
|
||||
raise error
|
||||
with patch.object(recovery.subprocess, 'run', side_effect=command):
|
||||
self.assertFalse(recovery.recover(self.proc))
|
||||
|
||||
def test_client_started_during_window_inspection_is_preserved(self):
|
||||
self.riot()
|
||||
def command(args, **kwargs):
|
||||
self.assertEqual(args[0], 'hyprctl')
|
||||
self.process(125, '/Applications/Riot Client.app/Contents/MacOS/RiotClientServices', age=1)
|
||||
return subprocess.CompletedProcess(args, 0, stdout='[]')
|
||||
with patch.object(recovery.subprocess, 'run', side_effect=command):
|
||||
self.assertFalse(recovery.recover(self.proc))
|
||||
|
||||
def test_valid_unrelated_window_does_not_prevent_recovery(self):
|
||||
self.riot()
|
||||
result, calls = self.run_recovery([{'class':'editor', 'title':'file', 'mapped':True, 'hidden':False}])
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(calls[-1], ['darling', 'shutdown'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -32,6 +32,8 @@ else:
|
||||
raise AssertionError('reserved error endpoint unexpectedly accepted a connection')
|
||||
print('LIFECYCLE_PASS: fixture')
|
||||
print('URLSESSION_PASS: fixture')
|
||||
print('SHARED_SESSION_PASS: fixture')
|
||||
print('QUEUE_PASS: fixture')
|
||||
''')
|
||||
executable.chmod(0o755)
|
||||
probe = root / 'probe'
|
||||
|
||||
@@ -1,40 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Opt-in recovery of a windowless Riot session on Hyprland; fail closed elsewhere."""
|
||||
"""Opt-in recovery of an old windowless Riot session on Hyprland; fail closed."""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
MIN_AGE_SECONDS = 60
|
||||
|
||||
def recover(proc_root=Path('/proc')):
|
||||
home = Path.home()
|
||||
prefix = Path(os.environ.get('DPREFIX', str(home / '.darling'))).expanduser()
|
||||
if prefix != home / '.darling':
|
||||
return False # Custom prefixes are not validated by this desktop helper.
|
||||
riot_running = False
|
||||
|
||||
def recoverable_processes(proc_root, prefix):
|
||||
"""Return stable process identities, or None when shutdown is not justified."""
|
||||
candidates = {}
|
||||
try:
|
||||
uptime = float((proc_root / 'uptime').read_text().split()[0])
|
||||
ticks = os.sysconf('SC_CLK_TCK')
|
||||
if not math.isfinite(uptime) or uptime < 0 or ticks <= 0:
|
||||
return None
|
||||
for proc in proc_root.iterdir():
|
||||
if not proc.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
if proc.stat().st_uid != os.getuid():
|
||||
continue
|
||||
argv = (proc / 'cmdline').read_bytes().split(b'\0')
|
||||
executable = argv[0].decode(errors='replace')
|
||||
executable = (proc / 'cmdline').read_bytes().split(b'\0')[0].decode(errors='replace')
|
||||
if executable.endswith('/LeagueofLegends'):
|
||||
return False # Never restart a running game, even without a window.
|
||||
return None
|
||||
if '/Riot Client.app/' not in executable:
|
||||
continue
|
||||
environment = dict(item.split(b'=', 1) for item in
|
||||
(proc / 'environ').read_bytes().split(b'\0') if b'=' in item)
|
||||
process_prefix = Path(os.fsdecode(environment.get(b'DPREFIX', os.fsencode(home / '.darling'))))
|
||||
if process_prefix == prefix:
|
||||
riot_running = True
|
||||
process_prefix = Path(os.fsdecode(environment.get(b'DPREFIX', os.fsencode(Path.home() / '.darling'))))
|
||||
if process_prefix != prefix:
|
||||
continue
|
||||
# Field 22 follows a comm field that may contain spaces/parentheses.
|
||||
start_ticks = int((proc / 'stat').read_text().rsplit(')', 1)[1].split()[19])
|
||||
age = uptime - start_ticks / ticks
|
||||
if start_ticks < 0 or age < MIN_AGE_SECONDS:
|
||||
return None # One young client prevents restarting the entire prefix.
|
||||
candidates[proc.name] = start_ticks
|
||||
except FileNotFoundError:
|
||||
if proc.exists():
|
||||
return None
|
||||
continue
|
||||
except (OSError, ValueError):
|
||||
return False # Unknown ownership/visibility must not trigger shutdown.
|
||||
if not riot_running:
|
||||
except (OSError, ValueError, IndexError):
|
||||
return None
|
||||
return candidates or None
|
||||
|
||||
|
||||
def recover(proc_root=Path('/proc')):
|
||||
home = Path.home()
|
||||
prefix = Path(os.environ.get('DPREFIX', str(home / '.darling'))).expanduser()
|
||||
if prefix != home / '.darling':
|
||||
return False
|
||||
processes = recoverable_processes(proc_root, prefix)
|
||||
if not processes:
|
||||
return False
|
||||
try:
|
||||
result = subprocess.run(['hyprctl', 'clients', '-j'], check=True,
|
||||
@@ -43,13 +63,25 @@ def recover(proc_root=Path('/proc')):
|
||||
if not isinstance(windows, list):
|
||||
return False
|
||||
for window in windows:
|
||||
identity = (window.get('class', '') + ' ' + window.get('title', '')).lower()
|
||||
if any(name in identity for name in ('riot', 'league')) and window.get('mapped') and not window.get('hidden'):
|
||||
if (not isinstance(window, dict) or
|
||||
not all(isinstance(window.get(key), str) for key in ('class', 'title')) or
|
||||
not all(type(window.get(key)) is bool for key in ('mapped', 'hidden'))):
|
||||
return False
|
||||
except (OSError, ValueError, TypeError, AttributeError, subprocess.SubprocessError):
|
||||
identity = (window['class'] + ' ' + window['title']).lower()
|
||||
if any(name in identity for name in ('riot', 'league')) and window['mapped'] and not window['hidden']:
|
||||
return False
|
||||
except (OSError, ValueError, subprocess.SubprocessError):
|
||||
return False
|
||||
# Compositor inspection can take seconds: refuse if a game/client started,
|
||||
# a PID was reused, or any relevant process disappeared during that interval.
|
||||
if recoverable_processes(proc_root, prefix) != processes:
|
||||
return False
|
||||
print('Recovering a Riot session with no visible client window.', flush=True)
|
||||
subprocess.run(['darling', 'shutdown'], check=True, timeout=25)
|
||||
try:
|
||||
subprocess.run(['darling', 'shutdown'], check=True, timeout=25)
|
||||
except (OSError, subprocess.SubprocessError) as error:
|
||||
print(f'Riot session recovery failed: {error}', flush=True)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,29 @@ static int lifecycle_check(int cancel) {
|
||||
[session release]; [delegate release];
|
||||
return 0;
|
||||
}
|
||||
static int queue_check(void) {
|
||||
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
|
||||
[queue setMaxConcurrentOperationCount:1];
|
||||
[queue setSuspended:YES];
|
||||
NSURLSession *session = [[NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:nil delegateQueue:queue] retain];
|
||||
dispatch_semaphore_t completed = dispatch_semaphore_create(0);
|
||||
__block int callbacks = 0, correctQueue = 0;
|
||||
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:probeURL] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
correctQueue = [NSOperationQueue currentQueue] == queue;
|
||||
++callbacks;
|
||||
dispatch_semaphore_signal(completed);
|
||||
}];
|
||||
[task cancel];
|
||||
[session finishTasksAndInvalidate];
|
||||
// A suspended delegate queue must also suspend task completion delivery.
|
||||
if (!dispatch_semaphore_wait(completed, dispatch_time(DISPATCH_TIME_NOW, 100*NSEC_PER_MSEC))) return 20;
|
||||
[queue setSuspended:NO];
|
||||
if (dispatch_semaphore_wait(completed, dispatch_time(DISPATCH_TIME_NOW, 15*NSEC_PER_SEC))) return 21;
|
||||
if (callbacks != 1 || !correctQueue) return 22;
|
||||
[session release]; [queue release];
|
||||
puts("QUEUE_PASS: completion obeys delegate queue and suspension");
|
||||
return 0;
|
||||
}
|
||||
int main(int argc, const char **argv) { @autoreleasepool {
|
||||
if (argc != 3) { puts("Usage: probe <loopback-success-url> <loopback-error-url>"); return 64; }
|
||||
probeURL = [NSString stringWithUTF8String:argv[1]];
|
||||
@@ -60,6 +83,11 @@ int main(int argc, const char **argv) { @autoreleasepool {
|
||||
for (int i=0;i<5;i++) if (session_check(probeURL,0)) return 1;
|
||||
if (session_check(probeURL,1)) return 2;
|
||||
if (session_check(failureURL,2)) return 3;
|
||||
[[NSURLSession sharedSession] finishTasksAndInvalidate];
|
||||
[[NSURLSession sharedSession] invalidateAndCancel];
|
||||
if (session_check(probeURL,0)) { puts("SHARED_SESSION_FAIL"); return 5; }
|
||||
puts("SHARED_SESSION_PASS: invalidation leaves singleton usable");
|
||||
if (queue_check()) { puts("QUEUE_FAIL"); return 6; }
|
||||
if (lifecycle_check(0) || lifecycle_check(1)) { puts("LIFECYCLE_FAIL"); return 4; }
|
||||
puts("LIFECYCLE_PASS: finish waits for request; cancel delivers callback; invalidation once");
|
||||
puts("URLSESSION_PASS: 5 HTTP requests and cancel/resume, callbacks exactly once");
|
||||
|
||||
@@ -68,9 +68,11 @@ try:
|
||||
env=env, stdout=stream, stderr=subprocess.STDOUT, timeout=90)
|
||||
text = log.read_text(errors='replace')
|
||||
for line in text.splitlines():
|
||||
if line.startswith(('LIFECYCLE_PASS:', 'URLSESSION_PASS:', 'LIFECYCLE_FAIL', 'FAIL ', 'TIMEOUT')):
|
||||
if line.startswith(('LIFECYCLE_PASS:', 'URLSESSION_PASS:', 'SHARED_SESSION_PASS:', 'QUEUE_PASS:',
|
||||
'LIFECYCLE_FAIL', 'SHARED_SESSION_FAIL', 'QUEUE_FAIL', 'FAIL ', 'TIMEOUT')):
|
||||
print(line)
|
||||
if result.returncode or 'LIFECYCLE_PASS:' not in text or 'URLSESSION_PASS:' not in text:
|
||||
required = ('LIFECYCLE_PASS:', 'URLSESSION_PASS:', 'SHARED_SESSION_PASS:', 'QUEUE_PASS:')
|
||||
if result.returncode or not all(marker in text for marker in required):
|
||||
raise SystemExit(f'Probe failed (exit {result.returncode}); inspect private log {log}')
|
||||
finally:
|
||||
server.shutdown()
|
||||
|
||||
Reference in new issue
Block a user