From 8c3d7118a73f8d2613e795e0c244cd469ad17402 Mon Sep 17 00:00:00 2001 From: Chris Dillard Date: Fri, 24 Jan 2025 16:30:18 -0700 Subject: [PATCH] NR-359272: Fix setState signature and add NRMAHandledRequest associated object to track handled URLSessionTasks --- .../NSURLSession/NRMAURLSessionOverride.m | 41 ++++++++----- .../NSURLSession/NRMAURLSessionTaskOverride.h | 2 +- .../NSURLSession/NRMAURLSessionTaskOverride.m | 37 ++++++++---- Agent/Public/NRConstants.h | 2 + .../NRTestApp/NRTestApp/AppDelegate.swift | 3 + .../NRTestApp/NRTestApp/Helpers/ApodURL.swift | 9 +++ .../ViewControllers/ViewController.swift | 18 +++++- .../NRTestApp/ViewModels/ApodViewModel.swift | 47 +++++++++++++++ .../NRTestApp/ViewModels/UtilViewModel.swift | 16 +++++ .../NRMAURLSessionAsyncTests.swift | 58 ++++++++++++++++++- 10 files changed, 203 insertions(+), 30 deletions(-) diff --git a/Agent/Instrumentation/NSURLSession/NRMAURLSessionOverride.m b/Agent/Instrumentation/NSURLSession/NRMAURLSessionOverride.m index 09cc07e9..b76de3e6 100644 --- a/Agent/Instrumentation/NSURLSession/NRMAURLSessionOverride.m +++ b/Agent/Instrumentation/NSURLSession/NRMAURLSessionOverride.m @@ -216,10 +216,11 @@ + (void)swizzleURLSessionTask NSMutableURLRequest* mutableRequest = [NRMAHTTPUtilities addCrossProcessIdentifier:request]; NSURLSessionTask* task = ((id(*)(id,SEL,NSURLRequest*))originalImp)(self,_cmd,request); + objc_setAssociatedObject(task, NRMAHandledRequestKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - NRMAPayloadContainer* payload = [NRMAHTTPUtilities addConnectivityHeader:mutableRequest]; - [NRMAHTTPUtilities attachPayload:payload - to:task.originalRequest]; + NRMAPayloadContainer* payload = [NRMAHTTPUtilities addConnectivityHeader:mutableRequest]; + [NRMAHTTPUtilities attachPayload:payload + to:task.originalRequest]; // Try to override the methods of the private class that is returned by this method. [NRMAURLSessionTaskOverride instrumentConcreteClass:[task class]]; @@ -256,6 +257,7 @@ + (void)swizzleURLSessionTask if (completionHandler == nil) { task = ((id(*)(id,SEL,NSURLRequest*,void(^)(NSData*,NSURLResponse*,NSError*)))originalImp)(self,_cmd,mutableRequest,completionHandler); + objc_setAssociatedObject(task, NRMAHandledRequestKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [NRMAHTTPUtilities attachPayload:payloadHolder.cppPayload to:task.originalRequest]; @@ -267,11 +269,14 @@ + (void)swizzleURLSessionTask [NRMAHTTPUtilities attachPayload:payloadHolder.cppPayload to:task.originalRequest]; + // NSLog(@"NRMA__recordTask called from NRMAOverride__dataTaskWithRequest_completionHandler"); + NRMA__recordTask(task,data,response,error); completionHandler(data,response,error); }); + objc_setAssociatedObject(task, NRMAHandledRequestKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // Try to override the methods of the private class that is returned by this method. [NRMAURLSessionTaskOverride instrumentConcreteClass:[task class]]; @@ -291,7 +296,8 @@ + (void)swizzleURLSessionTask } NSURLSessionTask* task = ((id(*)(id,SEL,NSURL*))originalImp)(self,_cmd,url); - + objc_setAssociatedObject(task, NRMAHandledRequestKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + // Try to override the methods of the private class that is returned by this method. [NRMAURLSessionTaskOverride instrumentConcreteClass:[task class]]; return task; @@ -312,10 +318,11 @@ + (void)swizzleURLSessionTask NSMutableURLRequest* mutableRequest = [NRMAHTTPUtilities addCrossProcessIdentifier:request]; NSURLSessionTask* task = ((NSURLSessionTask*(*)(id,SEL,NSURLRequest*,NSURL*))originalImp)(self,_cmd,mutableRequest,fileURL); + objc_setAssociatedObject(task, NRMAHandledRequestKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - NRMAPayloadContainer* payload = [NRMAHTTPUtilities addConnectivityHeader:mutableRequest]; - [NRMAHTTPUtilities attachPayload:payload - to:task.originalRequest]; + NRMAPayloadContainer* payload = [NRMAHTTPUtilities addConnectivityHeader:mutableRequest]; + [NRMAHTTPUtilities attachPayload:payload + to:task.originalRequest]; [NRMAURLSessionTaskOverride instrumentConcreteClass:[task class]]; @@ -335,6 +342,7 @@ + (void)swizzleURLSessionTask NSMutableURLRequest* mutableRequest = [NRMAHTTPUtilities addCrossProcessIdentifier:request]; NSURLSessionTask* task = ((NSURLSessionTask*(*)(id,SEL,NSURLRequest*,NSData*))originalImp)(self, _cmd, mutableRequest, data); + objc_setAssociatedObject(task, NRMAHandledRequestKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); NRMAPayloadContainer* payload = [NRMAHTTPUtilities addConnectivityHeader:mutableRequest]; [NRMAHTTPUtilities attachPayload:payload to:task.originalRequest]; @@ -356,7 +364,8 @@ + (void)swizzleURLSessionTask } NSURLSessionTask* task = ((NSURLSessionTask*(*)(id,SEL,NSURLRequest*))originalImp)(self, _cmd,request); - + objc_setAssociatedObject(task, NRMAHandledRequestKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [NRMAURLSessionTaskOverride instrumentConcreteClass:[task class]]; return task; @@ -381,8 +390,7 @@ + (void)swizzleURLSessionTask if (completionHandler == nil) { task = ((NSURLSessionUploadTask*(*)(id,SEL,NSURLRequest*,NSURL*,void(^)(NSData*,NSURLResponse*,NSError*)))originalIMP)(self,_cmd,mutableRequest,fileURL,completionHandler); - - [NRMAHTTPUtilities attachPayload:payloadHolder.cppPayload to:task.originalRequest]; + [NRMAHTTPUtilities attachPayload:payloadHolder.cppPayload to:task.originalRequest]; [NRMAURLSessionTaskOverride instrumentConcreteClass:[task class]]; return task; @@ -391,13 +399,15 @@ + (void)swizzleURLSessionTask task = ((NSURLSessionUploadTask*(*)(id,SEL,NSURLRequest*,NSURL*,void(^)(NSData*,NSURLResponse*,NSError*)))originalIMP)(self,_cmd,mutableRequest,fileURL,^(NSData* data, NSURLResponse* response, NSError* error){ + [NRMAHTTPUtilities attachPayload:payloadHolder.cppPayload to:task.originalRequest]; - [NRMAHTTPUtilities attachPayload:payloadHolder.cppPayload to:task.originalRequest]; + // NSLog(@"NRMA__recordTask called from NRMAOverride__uploadTaskWithRequest_fromFile_completionHandler"); NRMA__recordTask(task,data,response,error); completionHandler(data,response,error); }); + objc_setAssociatedObject(task, NRMAHandledRequestKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // Try to override the methods of the private class that is returned by this method. [NRMAURLSessionTaskOverride instrumentConcreteClass:[task class]]; @@ -425,8 +435,7 @@ + (void)swizzleURLSessionTask if (completionHandler == nil) { task = ((NSURLSessionUploadTask*(*)(id,SEL,NSURLRequest*,NSData*,void(^)(NSData*,NSURLResponse*,NSError*)))originalIMP)(self,_cmd,mutableRequest,bodyData,completionHandler); - - [NRMAHTTPUtilities attachPayload:payloadHolder.cppPayload to:task.originalRequest]; + [NRMAHTTPUtilities attachPayload:payloadHolder.cppPayload to:task.originalRequest]; [NRMAURLSessionTaskOverride instrumentConcreteClass:[task class]]; return task; @@ -434,13 +443,15 @@ + (void)swizzleURLSessionTask task = ((NSURLSessionUploadTask*(*)(id,SEL,NSURLRequest*,NSData*,void(^)(NSData*,NSURLResponse*,NSError*)))originalIMP)(self,_cmd,mutableRequest,bodyData,^(NSData* data, NSURLResponse* response, NSError* error){ - - [NRMAHTTPUtilities attachPayload:payloadHolder.cppPayload to:task.originalRequest]; + [NRMAHTTPUtilities attachPayload:payloadHolder.cppPayload to:task.originalRequest]; + + // NSLog(@"NRMA__recordTask called from NRMAOverride__uploadTaskWithRequest_fromFile_completionHandler"); NRMA__recordTask(task,data,response,error); completionHandler(data,response,error); }); + objc_setAssociatedObject(task, NRMAHandledRequestKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // Try to override the methods of the private class that is returned by this method. [NRMAURLSessionTaskOverride instrumentConcreteClass:[task class]]; diff --git a/Agent/Instrumentation/NSURLSession/NRMAURLSessionTaskOverride.h b/Agent/Instrumentation/NSURLSession/NRMAURLSessionTaskOverride.h index 4f607bf0..3610324c 100644 --- a/Agent/Instrumentation/NSURLSession/NRMAURLSessionTaskOverride.h +++ b/Agent/Instrumentation/NSURLSession/NRMAURLSessionTaskOverride.h @@ -9,7 +9,7 @@ #import #import "NRTimer.h" void NRMAOverride__resume(id self, SEL _cmd); -void NRMAOverride__urlSessionTask_SetState(NSURLSessionTask *sessionTask, SEL _cmd, NSURLSessionTaskState *newState); +void NRMAOverride__urlSessionTask_SetState(NSURLSessionTask *sessionTask, SEL _cmd, NSURLSessionTaskState newState); @interface NRMAURLSessionTaskOverride : NSObject diff --git a/Agent/Instrumentation/NSURLSession/NRMAURLSessionTaskOverride.m b/Agent/Instrumentation/NSURLSession/NRMAURLSessionTaskOverride.m index b70e9d91..01347378 100644 --- a/Agent/Instrumentation/NSURLSession/NRMAURLSessionTaskOverride.m +++ b/Agent/Instrumentation/NSURLSession/NRMAURLSessionTaskOverride.m @@ -15,6 +15,7 @@ #import "NRMAHTTPUtilities.h" #import "NRMANetworkFacade.h" #import "NRMAFlags.h" +#import "NRConstants.h" static IMP NRMAOriginal__resume; static IMP NRMAOriginal__urlSessionTask_SetState; @@ -108,40 +109,56 @@ void NRMAOverride__resume(id self, SEL _cmd) } // This is the only way we have right now to record an swift async await web request. -void NRMAOverride__urlSessionTask_SetState(NSURLSessionTask* task, SEL _cmd, NSURLSessionTaskState *newState) +void NRMAOverride__urlSessionTask_SetState(NSURLSessionTask* task, SEL _cmd, NSURLSessionTaskState newState) { @synchronized(lock) { @synchronized(task) { if ([NRMAURLSessionTaskOverride isSupportedTaskType: task]) { - // Checking for NEW_RELIC_CROSS_PROCESS_ID_HEADER_KEY in the headers here. The data usually isn't link to the task yet here so, if that header exists we are handling the task elsewhere and have a better chance of getting the data so we don't need to record it here. + + NSNumber *isHandled = objc_getAssociatedObject(task, NRMAHandledRequestKey); + + if (isHandled != nil && [isHandled boolValue]) { + if (NRMAOriginal__urlSessionTask_SetState!= nil) { + // Call original setState function. + ((void(*)(NSURLSessionTask *,SEL,NSURLSessionTaskState))NRMAOriginal__urlSessionTask_SetState)(task, _cmd, newState); + } + return; + } + + NSURLRequest *currentRequest = task.currentRequest; - if(currentRequest != nil && [currentRequest valueForHTTPHeaderField:NEW_RELIC_CROSS_PROCESS_ID_HEADER_KEY] != nil) { + if(currentRequest == nil) { return; } NSURL *url = [currentRequest URL]; - if (url != nil && - task.state == NSURLSessionTaskStateRunning) { - + if (url != nil) { // Added this section to add Distributed Tracing traceId\trace.id, guid,id and payload. //1 NSMutableURLRequest* mutableRequest = [NRMAHTTPUtilities addCrossProcessIdentifier:currentRequest]; mutableRequest = [NRMAHTTPUtilities addConnectivityHeaderAndPayload:mutableRequest]; NRMAPayloadContainer* payload = [NRMAHTTPUtilities addConnectivityHeader:mutableRequest]; - [NRMAHTTPUtilities attachPayload:payload - to:task.originalRequest]; + [NRMAHTTPUtilities attachPayload:payload to:task.originalRequest]; NSData *data = NRMA__getDataForSessionTask(task); - NRMA__recordTask(task, data, task.response, task.error); + + // log the task and data that we will record + //NSLog(@"NRMAOverride__urlSessionTask_SetState newState: %ld, taskState:%ld task: %@ data: %@", (long) newState, (long)task.state, task, data); + + if (newState == NSURLSessionTaskStateCompleted) { + // NSLog(@"NRMAOverride NRMA__recordTask called because newState == NSURLSessionTaskStateCompleted newState: %ld, taskState:%ld task: %@ data: %@", (long) newState, (long)task.state, task, data); + + NRMA__recordTask(task, data, task.response, task.error); + } } } } } if (NRMAOriginal__urlSessionTask_SetState!= nil) { // Call original setState function. - ((void(*)(NSURLSessionTask *,SEL,NSURLSessionTaskState *))NRMAOriginal__urlSessionTask_SetState)(task, _cmd, newState); + ((void(*)(NSURLSessionTask *,SEL,NSURLSessionTaskState))NRMAOriginal__urlSessionTask_SetState)(task, _cmd, newState); } } diff --git a/Agent/Public/NRConstants.h b/Agent/Public/NRConstants.h index 98912126..0bb7c8b1 100644 --- a/Agent/Public/NRConstants.h +++ b/Agent/Public/NRConstants.h @@ -159,6 +159,8 @@ typedef NSString NRMetricUnit; #define NRMA_METRIC_APP_LAUNCH_COLD @"AppLaunch/Cold" #define NRMA_METRIC_APP_LAUNCH_RESUME @"AppLaunch/Hot" +#define NRMAHandledRequestKey @"NRMAHandledRequest" + // Network Failure Codes enum NRNetworkFailureCode { NRURLErrorUnknown = -1, diff --git a/Test Harness/NRTestApp/NRTestApp/AppDelegate.swift b/Test Harness/NRTestApp/NRTestApp/AppDelegate.swift index c219925a..5949be51 100644 --- a/Test Harness/NRTestApp/NRTestApp/AppDelegate.swift +++ b/Test Harness/NRTestApp/NRTestApp/AppDelegate.swift @@ -30,6 +30,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { NewRelic.enableFeatures(NRMAFeatureFlags.NRFeatureFlag_SwiftInteractionTracing) #endif + NewRelic.enableFeatures(NRMAFeatureFlags.NRFeatureFlag_SwiftAsyncURLSessionSupport) + + // Generate your own api key to see data get sent to your app's New Relic web services. Also be sure to put your key in the `Run New Relic dSYM Upload Tool` build phase. guard let apiKey = plistHelper.objectFor(key: "NRAPIKey", plist: "NRAPI-Info") as? String else {return true} diff --git a/Test Harness/NRTestApp/NRTestApp/Helpers/ApodURL.swift b/Test Harness/NRTestApp/NRTestApp/Helpers/ApodURL.swift index 5ba91da9..feb17039 100644 --- a/Test Harness/NRTestApp/NRTestApp/Helpers/ApodURL.swift +++ b/Test Harness/NRTestApp/NRTestApp/Helpers/ApodURL.swift @@ -15,3 +15,12 @@ struct ApodURL { self.url = "https://api.nasa.gov/planetary/apod?api_key=L9fVBfet3ldADKiogWO5EZyOOOHczSE45du4FhXT&date=\(date)" } } + +struct ApodURLBroke { + + let url: String + + init(date:String) { + self.url = "https://api.nasa.gov/planetary/apod?date=\(date)" + } +} diff --git a/Test Harness/NRTestApp/NRTestApp/ViewControllers/ViewController.swift b/Test Harness/NRTestApp/NRTestApp/ViewControllers/ViewController.swift index 77161bfe..62b05e03 100644 --- a/Test Harness/NRTestApp/NRTestApp/ViewControllers/ViewController.swift +++ b/Test Harness/NRTestApp/NRTestApp/ViewControllers/ViewController.swift @@ -118,6 +118,11 @@ class ViewController: UIViewController { options.append(UtilOption(title: "Change Image (Async)", handler: { [self] in refreshActionAsync()})) + options.append(UtilOption(title: "Change Image Error", handler: { [self] in brokeRefreshAction()})) + + options.append(UtilOption(title: "Change Image Error (Async)", handler: { [self] in refreshActionAsync()})) + + } func utilitiesAction() { @@ -131,13 +136,22 @@ class ViewController: UIViewController { func refreshAction() { viewModel.loadApodData() } + func brokeRefreshAction() { + viewModel.loadApodDataBrokeData() + } func refreshActionAsync() { Task { - await viewModel.loadApodDataAsync() + await viewModel.loadApodDataAsyncBrokeData() } } - + + func brokeRefreshActionAsync() { + Task { + await viewModel.loadApodDataAsyncBrokeData() + } + } + func makeButton(title: String) -> UIButton { let button = UIButton(type: .system) button.setTitle(title, for: .normal) diff --git a/Test Harness/NRTestApp/NRTestApp/ViewModels/ApodViewModel.swift b/Test Harness/NRTestApp/NRTestApp/ViewModels/ApodViewModel.swift index 51a5a3a7..bfb8b651 100644 --- a/Test Harness/NRTestApp/NRTestApp/ViewModels/ApodViewModel.swift +++ b/Test Harness/NRTestApp/NRTestApp/ViewModels/ApodViewModel.swift @@ -58,4 +58,51 @@ class ApodViewModel { self.error.value = error } } + + // Broke Data + + func loadApodDataBrokeData() { + let nasaUrl = ApodURLBroke(date: Date.randomBetween(start: "2015-10-31", end: Date().dateString())) + service.getApod(nasaURL: URL(string: nasaUrl.url)!, completion: { [weak self] result in + switch result { + case .success(let response): + // We do not want a video, so if we get one try again + if response.media_type == "video"{ + self?.loadApodData() + return + } + NewRelic.logInfo("ApodViewModel loadApodData finished.") + + self?.apodResponse.value = response + case .failure(let error): + NewRelic.logError("ApodViewModel loadApodData encountered error=error=\(error.localizedDescription).") + + self?.error.value = error + } + }) + } + + func loadApodDataAsyncBrokeData() async { + do { + let nasaUrl = ApodURLBroke(date: Date.randomBetween(start: "2015-10-31", end: Date().dateString())) + guard let url = URL(string: nasaUrl.url) else { return } + + let request = URLRequest(url: url) + let (data, _) = try await URLSession.shared.data(for: request) + + let decoded = try JSONDecoder().decode(ApodResult.self, from: data) + + if decoded.media_type == "video" { + return await loadApodDataAsync() + } + NewRelic.logInfo("ApodViewModel loadApodDataAsync finished.") + + self.apodResponse.value = decoded + } catch { + + NewRelic.logError("ApodViewModel loadApodDataAsync encountered error=\(error.localizedDescription).") + + self.error.value = error + } + } } diff --git a/Test Harness/NRTestApp/NRTestApp/ViewModels/UtilViewModel.swift b/Test Harness/NRTestApp/NRTestApp/ViewModels/UtilViewModel.swift index 2911ddab..8fd6b06b 100644 --- a/Test Harness/NRTestApp/NRTestApp/ViewModels/UtilViewModel.swift +++ b/Test Harness/NRTestApp/NRTestApp/ViewModels/UtilViewModel.swift @@ -48,6 +48,11 @@ class UtilViewModel { try await doAsyncDataTask() } })) + options.append(UtilOption(title: "Async URLSession dataTask Fail", handler: { [self] in + Task { + try await doAsyncDataTaskFail() + } + })) options.append(UtilOption(title: "Shut down New Relic Agent", handler: { [self] in shutDown()})) } @@ -175,6 +180,17 @@ class UtilViewModel { print("Data: \(data)") } + func doAsyncDataTaskFail() async throws { + let urlSession = URLSession(configuration: URLSession.shared.configuration, delegate: taskProcessor, delegateQueue: nil) + + guard let url = URL(string: "https://www.goo3gle.c3om") else { return } + + let request = URLRequest(url: url) + let (data, _) = try await urlSession.data(for: request) + + print("Data: \(data)") + } + func shutDown() { NewRelic.shutdown() } diff --git a/Tests/Unit-Tests/NewRelicAgentTests/NSURLSession-Tests/NRMAURLSessionAsyncTests.swift b/Tests/Unit-Tests/NewRelicAgentTests/NSURLSession-Tests/NRMAURLSessionAsyncTests.swift index e1c8c11b..fee26760 100644 --- a/Tests/Unit-Tests/NewRelicAgentTests/NSURLSession-Tests/NRMAURLSessionAsyncTests.swift +++ b/Tests/Unit-Tests/NewRelicAgentTests/NSURLSession-Tests/NRMAURLSessionAsyncTests.swift @@ -18,7 +18,7 @@ class NRMAURLSessionAsyncTests: XCTestCase { override func setUp() async throws { try super.setUpWithError() - + NewRelic.enableFeatures([NRMAFeatureFlags.NRFeatureFlag_SwiftAsyncURLSessionSupport]) NRMAURLSessionOverride.beginInstrumentation() @@ -27,7 +27,7 @@ class NRMAURLSessionAsyncTests: XCTestCase { NRMAMeasurements.initializeMeasurements() NRMAMeasurements.addMeasurementConsumer(helper) } - + override func tearDown() async throws { NRMAURLSessionOverride.deinstrument() @@ -40,6 +40,8 @@ class NRMAURLSessionAsyncTests: XCTestCase { try super.tearDownWithError() } + // built-in + func testAsyncURLSessionDataForRequest() async throws { let request = URLRequest(url: URL(string: "http://www.google.com")!) let (_, _) = try await URLSession.shared.data(for: request) @@ -71,4 +73,56 @@ class NRMAURLSessionAsyncTests: XCTestCase { XCTAssertEqual(result?.url, "http://www.google.com") } + + // ephemeral + + func testAsyncURLSessionDataForRequestCustomURLSession() async throws { + let request = URLRequest(url: URL(string: "http://www.google.com")!) + + let urlSession = URLSession(configuration: .ephemeral) + let (_, _) = try await urlSession.data(for: request) + + sleep(1) + + let result = helper?.result as? NRMAHTTPTransactionMeasurement + + XCTAssertEqual(result?.url, "http://www.google.com") + } + + func testAsyncURLSessionDataForURLCustomURLSession() async throws { + + let urlSession = URLSession(configuration: .ephemeral) + let (_, _) = try await urlSession.data(from: URL(string: "http://www.google.com")!) + + sleep(1) + + let result = helper?.result as? NRMAHTTPTransactionMeasurement + + XCTAssertEqual(result?.url, "http://www.google.com") + } + // default + func testAsyncURLSessionDataForRequestCustomURLSessionDefault() async throws { + let request = URLRequest(url: URL(string: "http://www.google.com")!) + + let urlSession = URLSession(configuration: .default) + let (_, _) = try await urlSession.data(for: request) + + sleep(1) + + let result = helper?.result as? NRMAHTTPTransactionMeasurement + + XCTAssertEqual(result?.url, "http://www.google.com") + } + + func testAsyncURLSessionDataForURLCustomURLSessionDefault() async throws { + + let urlSession = URLSession(configuration: .default) + let (_, _) = try await urlSession.data(from: URL(string: "http://www.google.com")!) + + sleep(1) + + let result = helper?.result as? NRMAHTTPTransactionMeasurement + + XCTAssertEqual(result?.url, "http://www.google.com") + } }