Source/WebKit:
[WebKit-https.git] / Tools / TestWebKitAPI / Tests / WebKitCocoa / WKHTTPCookieStore.mm
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27
28 #import "PlatformUtilities.h"
29 #import "TestNavigationDelegate.h"
30 #import <WebKit/WKFoundation.h>
31 #import <WebKit/WKHTTPCookieStore.h>
32 #import <WebKit/WKProcessPoolPrivate.h>
33 #import <WebKit/WKWebsiteDataStorePrivate.h>
34 #import <WebKit/_WKWebsiteDataStoreConfiguration.h>
35 #import <wtf/RetainPtr.h>
36 #import <wtf/text/WTFString.h>
37
38 #if WK_API_ENABLED
39
40 static bool gotFlag;
41 static uint64_t observerCallbacks;
42 static RetainPtr<WKHTTPCookieStore> globalCookieStore;
43
44 @interface CookieObserver : NSObject<WKHTTPCookieStoreObserver>
45 - (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore;
46 @end
47
48 @implementation CookieObserver
49
50 - (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore
51 {
52     ASSERT_EQ(cookieStore, globalCookieStore.get());
53     ++observerCallbacks;
54 }
55
56 @end
57
58 static void runTestWithWebsiteDataStore(WKWebsiteDataStore* dataStore)
59 {
60     observerCallbacks = 0;
61
62     auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
63     configuration.get().websiteDataStore = dataStore;
64     auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
65
66     [webView loadHTMLString:@"Oh hello" baseURL:[NSURL URLWithString:@"http://webkit.org"]];
67     [webView _test_waitForDidFinishNavigation];
68
69     [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:[] {
70         gotFlag = true;
71     }];
72
73     TestWebKitAPI::Util::run(&gotFlag);
74     gotFlag = false;
75
76     // Triggering removeData when we don't have plugin data to remove should not trigger the plugin process to launch.
77     id pool = [WKProcessPool _sharedProcessPool];
78     EXPECT_EQ([pool _pluginProcessCount], static_cast<size_t>(0));
79
80     globalCookieStore = dataStore.httpCookieStore;
81     RetainPtr<CookieObserver> observer1 = adoptNS([[CookieObserver alloc] init]);
82     RetainPtr<CookieObserver> observer2 = adoptNS([[CookieObserver alloc] init]);
83     [globalCookieStore addObserver:observer1.get()];
84     [globalCookieStore addObserver:observer2.get()];
85
86     NSArray<NSHTTPCookie *> *cookies = nil;
87     [globalCookieStore getAllCookies:[cookiesPtr = &cookies](NSArray<NSHTTPCookie *> *nsCookies) {
88         *cookiesPtr = [nsCookies retain];
89         gotFlag = true;
90     }];
91
92     TestWebKitAPI::Util::run(&gotFlag);
93
94     ASSERT_EQ(cookies.count, 0u);
95     [cookies release];
96
97     gotFlag = false;
98
99     RetainPtr<NSHTTPCookie> cookie1 = [NSHTTPCookie cookieWithProperties:@{
100         NSHTTPCookiePath: @"/",
101         NSHTTPCookieName: @"CookieName",
102         NSHTTPCookieValue: @"CookieValue",
103         NSHTTPCookieDomain: @".www.webkit.org",
104         NSHTTPCookieSecure: @"TRUE",
105         NSHTTPCookieDiscard: @"TRUE",
106         NSHTTPCookieMaximumAge: @"10000",
107     }];
108
109     RetainPtr<NSHTTPCookie> cookie2 = [NSHTTPCookie cookieWithProperties:@{
110         NSHTTPCookiePath: @"/path",
111         NSHTTPCookieName: @"OtherCookieName",
112         NSHTTPCookieValue: @"OtherCookieValue",
113         NSHTTPCookieDomain: @".www.w3c.org",
114         NSHTTPCookieMaximumAge: @"10000",
115     }];
116
117     [globalCookieStore setCookie:cookie1.get() completionHandler:[](){
118         gotFlag = true;
119     }];
120
121     TestWebKitAPI::Util::run(&gotFlag);
122     gotFlag = false;
123
124     [globalCookieStore setCookie:cookie2.get() completionHandler:[](){
125         gotFlag = true;
126     }];
127
128     TestWebKitAPI::Util::run(&gotFlag);
129     gotFlag = false;
130
131     [globalCookieStore getAllCookies:[cookiesPtr = &cookies](NSArray<NSHTTPCookie *> *nsCookies) {
132         *cookiesPtr = [nsCookies retain];
133         gotFlag = true;
134     }];
135
136     TestWebKitAPI::Util::run(&gotFlag);
137     gotFlag = false;
138
139     ASSERT_EQ(cookies.count, 2u);
140     ASSERT_EQ(observerCallbacks, 4u);
141
142     for (NSHTTPCookie *cookie : cookies) {
143         if ([cookie.name isEqual:@"CookieName"]) {
144             ASSERT_TRUE([cookie1.get().path isEqualToString:cookie.path]);
145             ASSERT_TRUE([cookie1.get().value isEqualToString:cookie.value]);
146             ASSERT_TRUE([cookie1.get().domain isEqualToString:cookie.domain]);
147             ASSERT_TRUE(cookie.secure);
148             ASSERT_TRUE(cookie.sessionOnly);
149         } else {
150             ASSERT_TRUE([cookie2.get().path isEqualToString:cookie.path]);
151             ASSERT_TRUE([cookie2.get().value isEqualToString:cookie.value]);
152             ASSERT_TRUE([cookie2.get().name isEqualToString:cookie.name]);
153             ASSERT_TRUE([cookie2.get().domain isEqualToString:cookie.domain]);
154             ASSERT_FALSE(cookie.secure);
155             ASSERT_FALSE(cookie.sessionOnly);
156         }
157     }
158     [cookies release];
159
160     [globalCookieStore deleteCookie:cookie2.get() completionHandler:[](){
161         gotFlag = true;
162     }];
163
164     TestWebKitAPI::Util::run(&gotFlag);
165     gotFlag = false;
166
167     [globalCookieStore getAllCookies:[cookiesPtr = &cookies](NSArray<NSHTTPCookie *> *nsCookies) {
168         *cookiesPtr = [nsCookies retain];
169         gotFlag = true;
170     }];
171
172     TestWebKitAPI::Util::run(&gotFlag);
173     gotFlag = false;
174
175     ASSERT_EQ(cookies.count, 1u);
176     ASSERT_EQ(observerCallbacks, 6u);
177
178     for (NSHTTPCookie *cookie : cookies) {
179         ASSERT_TRUE([cookie1.get().path isEqualToString:cookie.path]);
180         ASSERT_TRUE([cookie1.get().value isEqualToString:cookie.value]);
181         ASSERT_TRUE([cookie1.get().domain isEqualToString:cookie.domain]);
182         ASSERT_TRUE(cookie.secure);
183         ASSERT_TRUE(cookie.sessionOnly);
184     }
185     [cookies release];
186
187     [globalCookieStore removeObserver:observer1.get()];
188     [globalCookieStore removeObserver:observer2.get()];
189 }
190
191 TEST(WebKit, WKHTTPCookieStore)
192 {
193     runTestWithWebsiteDataStore([WKWebsiteDataStore defaultDataStore]);
194 }
195
196 TEST(WebKit, WKHTTPCookieStoreHttpOnly) 
197 {
198     WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore];
199
200     auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
201     configuration.get().websiteDataStore = dataStore;
202     auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
203
204     [webView loadHTMLString:@"WebKit Test" baseURL:[NSURL URLWithString:@"http://webkit.org"]];
205     [webView _test_waitForDidFinishNavigation];
206
207     [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:[] {
208         gotFlag = true;
209     }];
210     TestWebKitAPI::Util::run(&gotFlag);
211     gotFlag = false;
212
213     globalCookieStore = dataStore.httpCookieStore;
214
215     NSArray<NSHTTPCookie *> *cookies = nil;
216     [globalCookieStore getAllCookies:[cookiesPtr = &cookies](NSArray<NSHTTPCookie *> *nsCookies) {
217         *cookiesPtr = [nsCookies retain];
218         gotFlag = true;
219     }];
220     TestWebKitAPI::Util::run(&gotFlag);
221     gotFlag = false;
222     ASSERT_EQ(cookies.count, 0u);
223     [cookies release];
224
225     NSMutableDictionary *cookieProperties = [[NSMutableDictionary alloc] init];
226     [cookieProperties setObject:@"cookieName" forKey:NSHTTPCookieName];
227     [cookieProperties setObject:@"cookieValue" forKey:NSHTTPCookieValue];
228     [cookieProperties setObject:@".www.webkit.org" forKey:NSHTTPCookieDomain];
229     [cookieProperties setObject:@"/path" forKey:NSHTTPCookiePath];
230     [cookieProperties setObject:@YES forKey:@"HttpOnly"];
231     RetainPtr<NSHTTPCookie> httpOnlyCookie = [NSHTTPCookie cookieWithProperties:cookieProperties];
232     [cookieProperties setObject:@"cookieValue2" forKey:NSHTTPCookieValue];
233     RetainPtr<NSHTTPCookie> httpOnlyCookie2 = [NSHTTPCookie cookieWithProperties:cookieProperties];
234     [cookieProperties removeObjectForKey:@"HttpOnly"];
235     RetainPtr<NSHTTPCookie> notHttpOnlyCookie = [NSHTTPCookie cookieWithProperties:cookieProperties];
236
237     EXPECT_TRUE(httpOnlyCookie.get().HTTPOnly);
238     EXPECT_FALSE(notHttpOnlyCookie.get().HTTPOnly);
239
240     // Setting httpOnlyCookie should succeed.
241     [globalCookieStore setCookie:httpOnlyCookie.get() completionHandler:[]() {
242         gotFlag = true;
243     }];
244     TestWebKitAPI::Util::run(&gotFlag);
245     gotFlag = false;
246
247     // Setting httpOnlyCookie2 should succeed.
248     [globalCookieStore setCookie:httpOnlyCookie2.get() completionHandler:[]() {
249         gotFlag = true;
250     }];
251     TestWebKitAPI::Util::run(&gotFlag);
252     gotFlag = false;
253
254     [globalCookieStore getAllCookies:[cookiesPtr = &cookies](NSArray<NSHTTPCookie *> *nsCookies) {
255         *cookiesPtr = [nsCookies retain];
256         gotFlag = true;
257     }];
258     TestWebKitAPI::Util::run(&gotFlag);
259     gotFlag = false;
260     ASSERT_EQ(cookies.count, 1u);
261     EXPECT_TRUE([[[cookies objectAtIndex:0] value] isEqual:@"cookieValue2"]);
262     [cookies release];
263
264     // Setting notHttpOnlyCookie should fail because we cannot overwrite HTTPOnly property using public API.
265     [globalCookieStore setCookie:notHttpOnlyCookie.get() completionHandler:[]() {
266         gotFlag = true;
267     }];
268     TestWebKitAPI::Util::run(&gotFlag);
269     gotFlag = false;
270
271     [globalCookieStore getAllCookies:[cookiesPtr = &cookies](NSArray<NSHTTPCookie *> *nsCookies) {
272         *cookiesPtr = [nsCookies retain];
273         gotFlag = true;
274     }];
275     TestWebKitAPI::Util::run(&gotFlag);
276     gotFlag = false;
277     ASSERT_EQ(cookies.count, 1u);
278     EXPECT_TRUE([[cookies objectAtIndex:0] isHTTPOnly]);
279     [cookies release];
280
281     // Deleting notHttpOnlyCookie should fail because the cookie stored is HTTPOnly.
282     [globalCookieStore deleteCookie:notHttpOnlyCookie.get() completionHandler:[]() {
283         gotFlag = true;
284     }];
285     TestWebKitAPI::Util::run(&gotFlag);
286     gotFlag = false;
287
288     [globalCookieStore getAllCookies:[cookiesPtr = &cookies](NSArray<NSHTTPCookie *> *nsCookies) {
289         *cookiesPtr = [nsCookies retain];
290         gotFlag = true;
291     }];
292     TestWebKitAPI::Util::run(&gotFlag);
293     gotFlag = false;
294     ASSERT_EQ(cookies.count, 1u);
295     [cookies release];
296
297     // Deleting httpOnlyCookie should succeed. 
298     [globalCookieStore deleteCookie:httpOnlyCookie.get() completionHandler:[]() {
299         gotFlag = true;
300     }];
301     TestWebKitAPI::Util::run(&gotFlag);
302     gotFlag = false;
303
304     [globalCookieStore getAllCookies:[cookiesPtr = &cookies](NSArray<NSHTTPCookie *> *nsCookies) {
305         *cookiesPtr = [nsCookies retain];
306         gotFlag = true;
307     }];
308     TestWebKitAPI::Util::run(&gotFlag);
309     gotFlag = false;
310     ASSERT_EQ(cookies.count, 0u);
311     [cookies release];
312 }
313
314 // FIXME: This should be removed once <rdar://problem/35344202> is resolved and bots are updated.
315 #if (PLATFORM(MAC) && __MAC_OS_X_VERSION_MAX_ALLOWED <= 101301) || (PLATFORM(IOS) && __IPHONE_OS_VERSION_MAX_ALLOWED <= 110102)
316 TEST(WebKit, WKHTTPCookieStoreNonPersistent)
317 {
318     RetainPtr<WKWebsiteDataStore> nonPersistentDataStore;
319     @autoreleasepool {
320         nonPersistentDataStore = [WKWebsiteDataStore nonPersistentDataStore];
321     }
322
323     runTestWithWebsiteDataStore(nonPersistentDataStore.get());
324 }
325
326 TEST(WebKit, WKHTTPCookieStoreCustom)
327 {
328     NSURL *cookieStorageFile = [NSURL fileURLWithPath:[@"~/Library/WebKit/TestWebKitAPI/CustomWebsiteData/CookieStorage/Cookie.File" stringByExpandingTildeInPath] isDirectory:NO];
329     NSURL *idbPath = [NSURL fileURLWithPath:[@"~/Library/WebKit/TestWebKitAPI/CustomWebsiteData/IndexedDB/" stringByExpandingTildeInPath] isDirectory:YES];
330
331     [[NSFileManager defaultManager] removeItemAtURL:cookieStorageFile error:nil];
332     [[NSFileManager defaultManager] removeItemAtURL:idbPath error:nil];
333
334     EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:cookieStorageFile.path]);
335     EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:idbPath.path]);
336
337     auto websiteDataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]);
338     websiteDataStoreConfiguration.get()._indexedDBDatabaseDirectory = idbPath;
339     websiteDataStoreConfiguration.get()._cookieStorageFile = cookieStorageFile;
340
341     auto customDataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:websiteDataStoreConfiguration.get()]);
342     runTestWithWebsiteDataStore(customDataStore.get());
343 }
344 #endif // (PLATFORM(MAC) && __MAC_OS_X_VERSION_MAX_ALLOWED <= 101301) || (PLATFORM(IOS) && __IPHONE_OS_VERSION_MAX_ALLOWED <= 110102)
345
346 TEST(WebKit, CookieObserverCrash)
347 {
348     RetainPtr<WKWebsiteDataStore> nonPersistentDataStore;
349     @autoreleasepool {
350         nonPersistentDataStore = [WKWebsiteDataStore nonPersistentDataStore];
351     }
352
353     auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
354     configuration.get().websiteDataStore = nonPersistentDataStore.get();
355
356     auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
357
358     [webView loadHTMLString:@"Oh hello" baseURL:[NSURL URLWithString:@"http://webkit.org"]];
359     [webView _test_waitForDidFinishNavigation];
360
361     globalCookieStore = nonPersistentDataStore.get().httpCookieStore;
362     RetainPtr<CookieObserver> observer = adoptNS([[CookieObserver alloc] init]);
363     [globalCookieStore addObserver:observer.get()];
364
365     [globalCookieStore getAllCookies:[](NSArray<NSHTTPCookie *> *) {
366         gotFlag = true;
367     }];
368
369     TestWebKitAPI::Util::run(&gotFlag);
370 }
371
372 static bool finished;
373
374 @interface CookieUIDelegate : NSObject <WKUIDelegate>
375 @end
376
377 @implementation CookieUIDelegate
378 - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
379 {
380     auto cookies = String(message.UTF8String);
381     EXPECT_TRUE(cookies == "PersistentCookieName=CookieValue; SessionCookieName=CookieValue" || cookies == "SessionCookieName=CookieValue; PersistentCookieName=CookieValue");
382     finished = true;
383     completionHandler();
384 }
385 @end
386
387 TEST(WebKit, WKHTTPCookieStoreWithoutProcessPool)
388 {
389     RetainPtr<NSHTTPCookie> sessionCookie = [NSHTTPCookie cookieWithProperties:@{
390         NSHTTPCookiePath: @"/",
391         NSHTTPCookieName: @"SessionCookieName",
392         NSHTTPCookieValue: @"CookieValue",
393         NSHTTPCookieDomain: @"127.0.0.1",
394     }];
395     RetainPtr<NSHTTPCookie> persistentCookie = [NSHTTPCookie cookieWithProperties:@{
396         NSHTTPCookiePath: @"/",
397         NSHTTPCookieName: @"PersistentCookieName",
398         NSHTTPCookieValue: @"CookieValue",
399         NSHTTPCookieDomain: @"127.0.0.1",
400         NSHTTPCookieExpires: [NSDate distantFuture],
401     }];
402     NSString *alertCookieHTML = @"<script>alert(document.cookie);</script>";
403
404     // NonPersistentDataStore
405     RetainPtr<WKWebsiteDataStore> ephemeralStoreWithCookies = [WKWebsiteDataStore nonPersistentDataStore];
406
407     finished = false;
408     [ephemeralStoreWithCookies.get().httpCookieStore setCookie:persistentCookie.get() completionHandler:^{
409         WKWebsiteDataStore *ephemeralStoreWithIndependentCookieStorage = [WKWebsiteDataStore nonPersistentDataStore];
410         [ephemeralStoreWithIndependentCookieStorage.httpCookieStore getAllCookies:^(NSArray<NSHTTPCookie *> *cookies) {
411             ASSERT_EQ(0u, cookies.count);
412             finished = true;
413         }];
414     }];
415     TestWebKitAPI::Util::run(&finished);
416
417     finished = false;
418     [ephemeralStoreWithCookies.get().httpCookieStore setCookie:sessionCookie.get() completionHandler:^{
419         [ephemeralStoreWithCookies.get().httpCookieStore getAllCookies:^(NSArray<NSHTTPCookie *> *cookies) {
420             ASSERT_EQ(2u, cookies.count);
421             finished = true;
422         }];
423     }];
424     TestWebKitAPI::Util::run(&finished);
425
426     finished = false;
427     auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
428     configuration.get().websiteDataStore = ephemeralStoreWithCookies.get();
429     auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
430     auto delegate = adoptNS([[CookieUIDelegate alloc] init]);
431     webView.get().UIDelegate = delegate.get();
432     [webView loadHTMLString:alertCookieHTML baseURL:[NSURL URLWithString:@"http://127.0.0.1"]];
433     TestWebKitAPI::Util::run(&finished);
434
435     finished = false;
436     [ephemeralStoreWithCookies.get().httpCookieStore deleteCookie:sessionCookie.get() completionHandler:^{
437         [ephemeralStoreWithCookies.get().httpCookieStore getAllCookies:^(NSArray<NSHTTPCookie *> *cookies) {
438             ASSERT_EQ(1u, cookies.count);
439             finished = true;
440         }];
441     }];
442     TestWebKitAPI::Util::run(&finished);
443     
444     // DefaultDataStore
445     auto defaultStore = [WKWebsiteDataStore defaultDataStore];
446     finished = false;
447     [defaultStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:[] {
448         finished = true;
449     }];
450     TestWebKitAPI::Util::run(&finished);
451
452     finished = false;
453     [defaultStore.httpCookieStore setCookie:persistentCookie.get() completionHandler:^{
454         [defaultStore.httpCookieStore getAllCookies:^(NSArray<NSHTTPCookie *> *cookies) {
455             ASSERT_EQ(1u, cookies.count);
456             finished = true;
457         }];
458     }];
459     TestWebKitAPI::Util::run(&finished);
460
461     finished = false;
462     [defaultStore.httpCookieStore setCookie:sessionCookie.get() completionHandler:^{
463         [defaultStore.httpCookieStore getAllCookies:^(NSArray<NSHTTPCookie *> *cookies) {
464             ASSERT_EQ(2u, cookies.count);
465             finished = true;
466         }];
467     }];
468     TestWebKitAPI::Util::run(&finished);
469
470     finished = false;
471     configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
472     configuration.get().websiteDataStore = defaultStore;
473     webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
474     webView.get().UIDelegate = delegate.get();
475     [webView loadHTMLString:alertCookieHTML baseURL:[NSURL URLWithString:@"http://127.0.0.1"]];
476     TestWebKitAPI::Util::run(&finished);
477 }
478 #endif