Shutdown clean async scripts

Set parent current script
This commit is contained in:
Karl Seguin
2025-08-04 17:56:52 +08:00
parent 7831aabe5a
commit 7f9e309ae8
4 changed files with 44 additions and 41 deletions

View File

@@ -37,16 +37,16 @@ page: *Page,
// Only once this is true can deferred scripts be run // Only once this is true can deferred scripts be run
static_scripts_done: bool, static_scripts_done: bool,
// when async_count == 0 and static_script_done == true, the document is completed // List of async scripts. We don't care about the execution order of these, but
// loading (i.e. page.documentIsComplete should be called). // on shutdown/abort, we need to co cleanup any pending ones.
async_count: usize, asyncs: OrderList,
// Normal scripts (non-deffered & non-async). These must be executed ni order // Normal scripts (non-deffered & non-async). These must be executed ni order
scripts: OrderList, scripts: OrderList,
// List of deferred scripts. These must be executed in order, but only once // List of deferred scripts. These must be executed in order, but only once
// dom_loaded == true, // dom_loaded == true,
deferred: OrderList, deferreds: OrderList,
client: *HttpClient, client: *HttpClient,
allocator: Allocator, allocator: Allocator,
@@ -60,9 +60,9 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager {
const allocator = browser.allocator; const allocator = browser.allocator;
return .{ return .{
.page = page, .page = page,
.asyncs = .{},
.scripts = .{}, .scripts = .{},
.deferred = .{}, .deferreds = .{},
.async_count = 0,
.allocator = allocator, .allocator = allocator,
.client = browser.http_client, .client = browser.http_client,
.static_scripts_done = false, .static_scripts_done = false,
@@ -77,8 +77,9 @@ pub fn deinit(self: *ScriptManager) void {
} }
pub fn reset(self: *ScriptManager) void { pub fn reset(self: *ScriptManager) void {
self.clearList(&self.asyncs);
self.clearList(&self.scripts); self.clearList(&self.scripts);
self.clearList(&self.deferred); self.clearList(&self.deferreds);
self.static_scripts_done = false; self.static_scripts_done = false;
} }
@@ -200,15 +201,9 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
return; return;
} }
if (self.getList(&pending_script.script)) |list| { const list = self.getList(&pending_script.script);
pending_script.node = .{.data = pending_script}; pending_script.node = .{ .data = pending_script };
list.append(&pending_script.node); list.append(&pending_script.node);
} else {
// async scripts don't get added to a list, because we can execute
// them in any order
std.debug.assert(script.is_async);
self.async_count += 1;
}
errdefer pending_script.deinit(); errdefer pending_script.deinit();
@@ -282,7 +277,7 @@ fn evaluate(self: *ScriptManager) void {
return; return;
} }
while (self.deferred.first) |n| { while (self.deferreds.first) |n| {
var pending_script = n.data; var pending_script = n.data;
if (pending_script.complete == false) { if (pending_script.complete == false) {
return; return;
@@ -295,7 +290,7 @@ fn evaluate(self: *ScriptManager) void {
// state changes (this ultimately triggers the DOMContentLoaded event) // state changes (this ultimately triggers the DOMContentLoaded event)
page.documentIsLoaded(); page.documentIsLoaded();
if (self.async_count == 0) { if (self.asyncs.first == null) {
// if we're here, then its like `asyncDone` // if we're here, then its like `asyncDone`
// 1 - there are no async scripts pending // 1 - there are no async scripts pending
// 2 - we checkecked static_scripts_done == true above // 2 - we checkecked static_scripts_done == true above
@@ -306,28 +301,26 @@ fn evaluate(self: *ScriptManager) void {
} }
pub fn isDone(self: *const ScriptManager) bool { pub fn isDone(self: *const ScriptManager) bool {
return self.async_count == 0 and // there are no more async scripts return self.asyncs.first == null and // there are no more async scripts
self.static_scripts_done and // and we've finished parsing the HTML to queue all <scripts> self.static_scripts_done and // and we've finished parsing the HTML to queue all <scripts>
self.scripts.first == null and // and there are no more <script src=> to wait for self.scripts.first == null and // and there are no more <script src=> to wait for
self.deferred.first == null; // and there are no more <script defer src=> to wait for self.deferreds.first == null; // and there are no more <script defer src=> to wait for
} }
fn asyncDone(self: *ScriptManager) void { fn asyncDone(self: *ScriptManager) void {
self.async_count -= 1;
if (self.isDone()) { if (self.isDone()) {
// then the document is considered complete // then the document is considered complete
self.page.documentIsComplete(); self.page.documentIsComplete();
} }
} }
fn getList(self: *ScriptManager, script: *const Script) ?*OrderList { fn getList(self: *ScriptManager, script: *const Script) *OrderList {
if (script.is_defer) { if (script.is_defer) {
return &self.deferred; return &self.deferreds;
} }
if (script.is_async) { if (script.is_async) {
// async don't need to execute in order. return &self.asyncs;
return null;
} }
return &self.scripts; return &self.scripts;
@@ -389,12 +382,7 @@ const PendingScript = struct {
manager.buffer_pool.release(script.source.remote); manager.buffer_pool.release(script.source.remote);
} }
if (manager.getList(script)) |list| { manager.getList(script).remove(&self.node);
list.remove(&self.node);
} else {
std.debug.assert(script.is_async);
manager.asyncDone();
}
} }
fn startCallback(self: *PendingScript, transfer: *HttpClient.Transfer) !void { fn startCallback(self: *PendingScript, transfer: *HttpClient.Transfer) !void {
@@ -429,7 +417,6 @@ const PendingScript = struct {
// @newhttp TODO: max-length enforcement ?? // @newhttp TODO: max-length enforcement ??
try self.script.source.remote.appendSlice(self.manager.allocator, data); try self.script.source.remote.appendSlice(self.manager.allocator, data);
} }
fn doneCallback(self: *PendingScript) void { fn doneCallback(self: *PendingScript) void {
@@ -438,8 +425,9 @@ const PendingScript = struct {
const manager = self.manager; const manager = self.manager;
if (self.script.is_async) { if (self.script.is_async) {
// async script can be evaluated immediately // async script can be evaluated immediately
defer self.deinit();
self.script.eval(self.manager.page); self.script.eval(self.manager.page);
self.deinit();
manager.asyncDone();
} else { } else {
self.complete = true; self.complete = true;
manager.evaluate(); manager.evaluate();
@@ -448,10 +436,10 @@ const PendingScript = struct {
fn errorCallback(self: *PendingScript, err: anyerror) void { fn errorCallback(self: *PendingScript, err: anyerror) void {
log.warn(.http, "script fetch error", .{ .req = self.script.url, .err = err }); log.warn(.http, "script fetch error", .{ .req = self.script.url, .err = err });
defer self.deinit(); const manager = self.manager;
self.deinit();
// this script might have been blocking others; manager.evaluate();
self.manager.evaluate();
} }
}; };
@@ -488,6 +476,14 @@ const Script = struct {
}; };
fn eval(self: *Script, page: *Page) void { fn eval(self: *Script, page: *Page) void {
page.setCurrentScript(@ptrCast(self.element)) catch |err| {
log.err(.browser, "set document script", .{ .err = err });
return;
};
defer page.setCurrentScript(null) catch |err| {
log.err(.browser, "clear document script", .{ .err = err });
};
// inline scripts aren't cached. remote ones are. // inline scripts aren't cached. remote ones are.
const cacheable = self.source == .remote; const cacheable = self.source == .remote;
@@ -643,7 +639,7 @@ const BufferPool = struct {
}; };
b.clearRetainingCapacity(); b.clearRetainingCapacity();
node.data = b; node.* = .{ .data = b };
self.count += 1; self.count += 1;
self.available.append(node); self.available.append(node);
} }

View File

@@ -398,6 +398,11 @@ pub const Page = struct {
}); });
} }
pub fn setCurrentScript(self: *Page, script: ?*parser.Script) !void {
const html_doc = self.window.document;
try parser.documentHTMLSetCurrentScript(html_doc, script);
}
pub fn documentIsLoaded(self: *Page) void { pub fn documentIsLoaded(self: *Page) void {
if (self.load_state != .parsing) { if (self.load_state != .parsing) {
// Ideally, documentIsLoaded would only be called once, but if a // Ideally, documentIsLoaded would only be called once, but if a
@@ -909,6 +914,7 @@ pub export fn scriptAddedCallback(ctx: ?*anyopaque, element: ?*parser.Element) c
// if we're planning on navigating to another page, don't run this script // if we're planning on navigating to another page, don't run this script
return; return;
} }
self.script_manager.addFromElement(element.?) catch |err| { self.script_manager.addFromElement(element.?) catch |err| {
log.warn(.browser, "dynamcic script", .{ .err = err }); log.warn(.browser, "dynamcic script", .{ .err = err });
}; };

View File

@@ -100,7 +100,7 @@ pub const Connection = struct {
const easy = c.curl_easy_init() orelse return error.FailedToInitializeEasy; const easy = c.curl_easy_init() orelse return error.FailedToInitializeEasy;
errdefer _ = c.curl_easy_cleanup(easy); errdefer _ = c.curl_easy_cleanup(easy);
// timeouts // timeouts
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_TIMEOUT_MS, @as(c_long, @intCast(opts.timeout_ms)))); try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_TIMEOUT_MS, @as(c_long, @intCast(opts.timeout_ms))));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CONNECTTIMEOUT_MS, @as(c_long, @intCast(opts.connect_timeout_ms)))); try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CONNECTTIMEOUT_MS, @as(c_long, @intCast(opts.connect_timeout_ms))));
@@ -121,7 +121,7 @@ pub const Connection = struct {
// Note, this can be difference for the proxy and for the main // Note, this can be difference for the proxy and for the main
// request. Might be something worth exposting as command // request. Might be something worth exposting as command
// line arguments at some point. // line arguments at some point.
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_CAINFO_BLOB , ca_blob)); try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_CAINFO_BLOB, ca_blob));
} }
} else { } else {
std.debug.assert(opts.tls_verify_host == false); std.debug.assert(opts.tls_verify_host == false);
@@ -136,7 +136,7 @@ pub const Connection = struct {
// request. Might be something worth exposting as command // request. Might be something worth exposting as command
// line arguments at some point. // line arguments at some point.
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 0))); try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 0)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER , @as(c_long, 0))); try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 0)));
} }
} }
@@ -204,7 +204,6 @@ pub const Connection = struct {
} }
}; };
pub fn errorCheck(code: c.CURLcode) errors.Error!void { pub fn errorCheck(code: c.CURLcode) errors.Error!void {
if (code == c.CURLE_OK) { if (code == c.CURLE_OK) {
return; return;

View File

@@ -231,6 +231,8 @@ const Command = struct {
\\ disabling host verification. \\ disabling host verification.
\\ \\
\\--http_proxy The HTTP proxy to use for all HTTP requests. \\--http_proxy The HTTP proxy to use for all HTTP requests.
\\ A username:password can be included to use basic
\\ authentication.
\\ Defaults to none. \\ Defaults to none.
\\ \\
\\--proxy_bearer_token \\--proxy_bearer_token