///|
/// Runs the configured app through the native Proton runtime.
pub async fn App::run(self : App) -> Unit raise AppRunError {
require_event_loop()
match self.validation_error() {
Some(error) => raise ConfigurationError(error)
None => ()
}
let resolved = self.resolved_app_config() catch {
error => raise ConfigurationError(error)
}
let browser_data_dir = resolve_browser_data_dir(resolved) catch {
error => raise ConfigurationError(error)
}
let app_instance = match acquire_app_instance(resolved) {
Some(@native.AppInstanceAcquire::Forwarded) => return
Some(@native.AppInstanceAcquire::Primary(instance)) => Some(instance)
None => None
}
run_manifest(
resolved.manifest,
resolved.permission_base_path,
self.command_extensions,
self.command_registrars,
self.application_lifecycle_hooks,
self.window_lifecycle_hooks,
self.menu,
self.bridge_startup_timeout_value_ms,
self.resolved_headless(),
browser_data_dir?,
app_instance~,
launch_input_handlers=self.launch_input_handlers,
planned_views=self.planned_views,
window_event_handlers=self.window_event_handlers,
view_event_handlers=self.view_event_handlers,
window_close_handler=self.window_close_handler,
navigation_handler=self.navigation_handler,
popup_handler=self.popup_handler,
download_handler=self.download_handler,
certificate_handler=self.certificate_handler,
media_handler=self.media_handler,
download_event_handlers=self.download_event_handlers,
update_channel=resolved.update_channel,
update_handlers=self.update_handlers,
) catch {
error => {
match app_instance {
Some(instance) => destroy_app_instance(instance) catch { _ => () }
None => ()
}
raise error
}
}
match app_instance {
Some(instance) => destroy_app_instance(instance)
None => ()
}
}
///|
/// Runs the configured app and aborts with the error message on failure.
pub async fn App::run_or_abort(self : App) -> Unit {
self.run() catch {
error => abort(error.message())
}
}
///|
async fn run_manifest(
manifest : @manifest.AppManifest,
permission_base_path : String,
command_extensions : Map[String, @proton_command.AppCommandExtensionSpec],
command_registrars : Array[(@proton_command.CommandRegistrar) -> Unit raise],
application_lifecycle_hooks : Array[ApplicationLifecycleHook],
window_lifecycle_hooks : Array[WindowLifecycleHook],
menu : @native.MenuBar?,
bridge_startup_timeout_ms : Int,
headless : Bool,
browser_data_dir? : String,
app_instance? : @native.AppInstance? = None,
planned_views? : Array[(String, @native.ViewConfig)] = [],
launch_input_handlers? : Array[async (RuntimeLaunchInput) -> Unit noraise] = [],
window_event_handlers? : Array[
async (WindowHandle, WindowEvent) -> Unit noraise,
] = [],
view_event_handlers? : Array[async (ViewHandle, ViewEvent) -> Unit noraise] = [],
window_close_handler? : (async (WindowHandle) -> WindowCloseDecision noraise)? = None,
navigation_handler? : (async (BrowserHandle, NavigationRequest) -> NavigationDecision noraise)? = None,
popup_handler? : (async (BrowserHandle, PopupRequest) -> PopupDecision noraise)? = None,
download_handler? : (async (BrowserHandle, DownloadRequest) -> DownloadDecision noraise)? = None,
certificate_handler? : (async (BrowserHandle, CertificateError) -> BrowserPermissionDecision noraise)? = None,
media_handler? : (async (BrowserHandle, MediaPermissionRequest) -> BrowserPermissionDecision noraise)? = None,
download_event_handlers? : Array[
async (BrowserHandle, DownloadEvent) -> Unit noraise,
] = [],
update_channel? : ResolvedUpdateChannel? = None,
update_handlers? : Array[async (PendingUpdate) -> Unit noraise] = [],
) -> Unit raise AppRunError {
let plans = planned_windows(manifest) catch {
error => raise ConfigurationError(error)
}
if headless {
for plan in plans {
if plan.window.titlebar_style == @manifest.TitlebarStyle::Overlay {
raise ConfigurationError(
InvalidSetting(
name="headless",
message="titlebar overlay is not supported in headless mode",
),
)
}
}
}
if headless && menu is Some(_) {
raise ConfigurationError(
InvalidSetting(
name="headless",
message="native menus are not supported in headless mode",
),
)
}
let lifecycle = AppLifecycle::new()
lifecycle.begin_starting()
let forward_menu_events = menu is Some(_)
let runtime = @native.Runtime::new(
config=@native.RuntimeConfig::bundled(
cache_dir?=browser_data_dir,
remote_debugging_port=debug_port(manifest.debug),
headless~,
),
) catch {
error => raise native_run_error("create runtime", error)
}
match app_instance {
Some(instance) =>
instance.attach_runtime(runtime) catch {
error => {
let attach_error = native_run_error("attach app instance", error)
let failure = cleanup_run_error(
Some(attach_error),
app_cleanup(lifecycle, runtime, [], None),
)
match failure {
Some(cleanup_error) => raise cleanup_error
None => raise attach_error
}
}
}
None => ()
}
let mut command_host : CommandHostRuntime? = None
let windows : Array[RunningWindow] = []
let lifecycle_failures : Array[AppCleanupError] = []
let wakeup = RuntimeWakeup::new()
try {
match menu {
Some(menu) =>
runtime.set_menu(menu) catch {
error => raise native_run_error("set menu", error)
}
None => ()
}
command_host = build_command_host(
manifest,
command_extensions,
command_registrars~,
)
let policies : Array[BridgePagePolicy] = []
for plan in plans {
let policy = bridge_page_policy_for_entry(plan.entry) catch {
error => raise ConfigurationError(error)
}
policies.push(policy)
}
let ops = match command_host {
Some(host) => host.host.registered_ops()
None => []
}
validate_registered_capabilities_have_grants(
manifest, command_extensions, ops,
) catch {
error => raise ConfigurationError(error)
}
let permission_policies : Array[WindowPermissionPolicy] = []
for index, plan in plans {
permission_policies.push(
resolve_window_permission_policy(
plan.id,
policies[index],
manifest,
permission_base_path,
command_extensions,
ops,
) catch {
error => raise ConfigurationError(error)
},
)
}
let bridge_enabled = ops.length() > 0 ||
forward_menu_events ||
window_lifecycle_hooks.length() > 0
if bridge_enabled && !@native.bridge_permission_grants_supported() {
raise UnsupportedNativeFeature(feature="bridge_permission_grants")
}
let definitions : Array[PreparedWindow] = []
let browser_policy = @native.BrowserPolicy::new(
navigation=if navigation_handler is Some(_) {
@native.BrowserPolicyMode::Ask
} else {
@native.BrowserPolicyMode::Allow
},
popup=if popup_handler is Some(_) {
@native.BrowserPolicyMode::Ask
} else {
@native.BrowserPolicyMode::Deny
},
download=if download_handler is Some(_) {
@native.BrowserPolicyMode::Ask
} else {
@native.BrowserPolicyMode::Deny
},
certificate=if certificate_handler is Some(_) {
@native.BrowserPolicyMode::Ask
} else {
@native.BrowserPolicyMode::Deny
},
media=if media_handler is Some(_) {
@native.BrowserPolicyMode::Ask
} else {
@native.BrowserPolicyMode::Deny
},
devtools=manifest.debug > 0,
)
for index, plan in plans {
let bridge = if bridge_enabled {
let grants = permission_policies[index].native_grants(
command_extensions,
) catch {
error => raise ConfigurationError(error)
}
ensure_entry_bridge_grant(grants, policies[index])
Some(native_bridge_config(grants))
} else {
None
}
definitions.push(PreparedWindow::{
plan,
bridge,
permissions: permission_policies[index],
browser_policy,
views: if plan.id == "main" {
planned_views
} else {
[]
},
})
}
@async.with_task_group(application_tasks => {
@async.with_task_group(window_tasks => {
let session = RuntimeSession::new(
runtime, definitions, windows, command_host, wakeup, forward_menu_events,
bridge_enabled, launch_input_handlers, window_event_handlers, window_tasks,
view_event_handlers, window_close_handler, window_lifecycle_hooks, lifecycle_failures,
bridge_startup_timeout_ms, navigation_handler, popup_handler, download_handler,
certificate_handler, media_handler, download_event_handlers,
)
let application_context = ApplicationContext::new(
application_tasks,
session.window_manager(),
)
let application_start = application_tasks.spawn(
() => {
defer session.wakeup.signal.notify()
start_application_lifecycle_hooks(
application_lifecycle_hooks, application_context, application_tasks,
lifecycle_failures,
)
},
no_wait=true,
allow_failure=true,
)
session.drive_until_complete(application_start)
active_update_channel.val = update_channel
for definition in definitions {
if definition.plan.open_on_start &&
session.find_active_window(definition.plan.id) is None {
ignore(session.create_window(definition))
}
}
if bridge_enabled {
session.wait_for_bridge_startup(bridge_startup_timeout_ms) catch {
error => raise normalize_async_run_error(error)
}
}
for running in windows {
if !running.is_closed() {
session.activate_window(running)
}
}
lifecycle.begin_running()
cleanup_previous_update()
start_update_check(update_channel, update_handlers, application_tasks)
session.run() catch {
error => raise normalize_async_run_error(error)
}
lifecycle.observe_window_closed()
window_tasks.return_immediately(())
})
application_tasks.return_immediately(())
}) catch {
error => raise normalize_async_run_error(error)
}
} catch {
error => {
let primary = normalize_async_run_error(error)
if !headless {
match windows.get(0) {
Some(window) =>
show_app_run_failure(runtime, window.window, wakeup, primary) catch {
_ => ()
}
None =>
show_runtime_failure_dialog(runtime, wakeup, primary.message()) catch {
_ => ()
}
}
}
let failure = cleanup_run_error(
Some(primary),
app_cleanup(
lifecycle,
runtime,
windows,
command_host,
lifecycle_failures~,
),
)
match failure {
Some(cleanup_error) => raise cleanup_error
None => raise primary
}
}
}
match
cleanup_run_error(
None,
app_cleanup(
lifecycle,
runtime,
windows,
command_host,
lifecycle_failures~,
),
) {
Some(error) => raise error
None => ()
}
}
///|
/// Persistent browser state is only safe when Proton owns the application's
/// single-instance route. Multi-instance runtimes receive native-owned
/// temporary profiles instead.
fn resolve_browser_data_dir(
config : ResolvedAppConfig,
) -> String? raise AppConfigurationError {
guard config.instance_identifier is Some(_) else { return None }
guard config.application_identifier is Some(identifier) else {
raise InvalidSetting(
name="identifier",
message="is required for persistent browser storage",
)
}
let path = browser_data_dir(identifier) catch {
error => raise InvalidSetting(name="identifier", message=error.message())
}
Some(path)
}
///|
fn planned_windows(
manifest : @manifest.AppManifest,
) -> Array[PlannedWindow] raise AppConfigurationError {
let plans = [
PlannedWindow::{
id: "main",
window: manifest.window,
entry: manifest.entry,
open_on_start: true,
},
]
let ids : Map[String, Unit] = { "main": () }
for window in manifest.windows {
let id = window.id.trim().to_owned()
guard id != "" else {
raise InvalidSetting(name="windows.id", message="must not be empty")
}
guard !ids.contains(id) else {
raise InvalidSetting(
name="windows.id",
message="duplicate window id: " + id,
)
}
ids[id] = ()
plans.push(PlannedWindow::{
id,
window: window.window,
entry: window.entry,
open_on_start: window.open_on_start,
})
}
plans
}
///|
fn native_window_config(
window : @manifest.WindowManifest,
bridge : @native.BridgeConfig?,
browser : @native.BrowserPolicy,
) -> @native.WindowConfig {
@native.WindowConfig::new(
title=window.title,
width=window.width,
height=window.height,
initial_url="about:blank",
size_hint=match window.size_hint {
@manifest.WindowSizeHint::None => @native.WindowSizeHint::Unconstrained
@manifest.WindowSizeHint::Fixed => @native.WindowSizeHint::Fixed
@manifest.WindowSizeHint::Min => @native.WindowSizeHint::Min
@manifest.WindowSizeHint::Max => @native.WindowSizeHint::Max
},
titlebar_style=match window.titlebar_style {
@manifest.TitlebarStyle::Default => @native.TitlebarStyle::Default
@manifest.TitlebarStyle::Overlay => @native.TitlebarStyle::Overlay
},
browser~,
bridge?,
)
}
///|
fn native_bridge_config(
grants : Array[@native.BridgeGrantConfig],
) -> @native.BridgeConfig {
@native.BridgeConfig::new(grants~)
}
///|
fn ensure_entry_bridge_grant(
grants : Array[@native.BridgeGrantConfig],
policy : BridgePagePolicy,
) -> Unit {
let source_origin = match policy.entry_origin {
Some(origin) => origin
None => "app"
}
if !grants.any(grant => grant.source_origin == source_origin) {
grants.push(@native.BridgeGrantConfig::new(source_origin, ops=[]))
}
}
///|
fn native_run_error(
action : String,
error : @native.NativeError,
) -> AppRunError {
NativeRuntimeError(action~, error~)
}
///|
fn normalize_async_run_error(error : Error) -> AppRunError {
match error {
EventLoopError(message) => EventLoopError(message)
ConfigurationError(error) => ConfigurationError(error)
NativeRuntimeError(action~, error~) => NativeRuntimeError(action~, error~)
CommandExtensionLifecycleError(error) =>
CommandExtensionLifecycleError(error)
LifecycleHookError(error) => LifecycleHookError(error)
EntryLoadError(error) => EntryLoadError(error)
BridgeStartupError(diagnostic) => BridgeStartupError(diagnostic)
BridgeRuntimeError(diagnostic) => BridgeRuntimeError(diagnostic)
CleanupFailed(primary~, failures~) => CleanupFailed(primary~, failures~)
UnexpectedTaskFailure(detail~) => UnexpectedTaskFailure(detail~)
_ => UnexpectedTaskFailure(detail=@debug.render(Repr(error)))
}
}
///|
fn close_command_host(
host : CommandHostRuntime?,
) -> Array[CommandExtensionLifecycleError] {
match host {
Some(value) => value.close()
None => []
}
}
///|
fn CommandHostRuntime::close(
self : CommandHostRuntime,
) -> Array[CommandExtensionLifecycleError] {
guard !self.closed else { return [] }
let failures = close_command_host_runtime(self.host, self.destroy_hooks)
self.closed = true
failures
}
///|
fn bridge_protocol_diagnostic(
state : @native.BridgeLifecycleState,
code : String,
message : String,
) -> @native.BridgeDiagnostic {
@native.BridgeDiagnostic::{
abi_version: 1,
stage: "prepare",
code,
message,
page_instance: state.page_instance,
url: state.url,
owner: None,
source_url: None,
source_line: None,
line: None,
column: None,
stack: None,
additional_failure_count: None,
details_truncated: false,
}
}
///|
fn bridge_failure(
window : @native.Window,
state : @native.BridgeLifecycleState,
) -> @native.BridgeDiagnostic raise @native.NativeError {
match window.take_bridge_failure() {
Some(diagnostic) => diagnostic
None =>
bridge_protocol_diagnostic(
state, "bridge_failure_missing_diagnostic", "renderer reported bridge failure without a diagnostic",
)
}
}
///|
fn refresh_bridge_startup_states(
windows : Array[RunningWindow],
) -> @native.BridgeLifecycleState? raise AppRunError {
let mut pending : @native.BridgeLifecycleState? = None
for running in windows {
if refresh_window_bridge_startup_state(running) {
continue
}
if pending is None {
pending = Some(
running.window.bridge_lifecycle_state() catch {
error =>
raise native_run_error(
"read bridge startup state for " + running.id,
error,
)
},
)
}
}
pending
}
///|
fn refresh_window_bridge_startup_state(
running : RunningWindow,
) -> Bool raise AppRunError {
if running.bridge_ready {
return true
}
if running.is_closed() {
raise EntryLoadError(ClosedDuringStartup)
}
let state = running.window.bridge_lifecycle_state() catch {
error =>
raise native_run_error(
"read bridge startup state for " + running.id,
error,
)
}
if state.url == "about:blank" {
return false
}
match state.outcome {
"ready" => {
running.bridge_ready = true
true
}
"failed" =>
try bridge_failure(running.window, state) catch {
error =>
raise native_run_error(
"read bridge startup failure for " + running.id,
error,
)
} noraise {
diagnostic => raise BridgeStartupError(diagnostic)
}
"ineligible" =>
raise BridgeStartupError(
bridge_protocol_diagnostic(
state, "bridge_entry_origin_ineligible", "the application entry is not eligible for the native bridge",
),
)
_ => false
}
}
///|
fn bridge_startup_timeout_diagnostic(
state : @native.BridgeLifecycleState,
timeout_ms : Int,
) -> @native.BridgeDiagnostic {
bridge_protocol_diagnostic(
state,
"bridge_startup_timeout",
"the native bridge did not become ready within " +
timeout_ms.to_string() +
" ms",
)
}
///|
fn app_run_error_dialog_text(error : AppRunError) -> String {
match error {
EventLoopError(message) => message
BridgeStartupError(diagnostic) =>
"The application could not finish starting its native bridge.\n\nError code: " +
diagnostic.code
BridgeRuntimeError(diagnostic) =>
"The application bridge stopped working.\n\nError code: " +
diagnostic.code
CleanupFailed(primary~, failures~) =>
cleanup_failure_message(primary, failures)
EntryLoadError(error) => error.message()
ConfigurationError(error) => error.message()
UnsupportedNativeFeature(feature~) =>
"The active Proton native runtime is incompatible with this application.\n\nMissing feature: " +
feature
NativeRuntimeError(action~, error~) =>
action + " failed: " + error.message()
CommandExtensionLifecycleError(error) => error.message()
LifecycleHookError(error) => error.message()
UnexpectedTaskFailure(detail~) => detail
}
}
///|
async fn show_runtime_failure_dialog(
runtime : @native.Runtime,
wakeup : RuntimeWakeup,
message : String,
) -> Unit {
let dialog = runtime.begin_message_dialog(
Some("Application Startup Failed"),
message,
@native.DialogLevel::Error,
) catch {
_ => return
}
while true {
let revision = wakeup.revision()
let completed = runtime.poll_message_dialog(dialog) catch { _ => return }
match completed {
true => return
false => wakeup.wait_after(revision) catch { _ => return }
}
}
}
///|
async fn show_window_failure_dialog(
window : @native.WindowRef,
wakeup : RuntimeWakeup,
message : String,
) -> Unit {
let dialog = window.begin_message_dialog(
Some("Application Error"),
message,
@native.DialogLevel::Error,
) catch {
_ => return
}
while true {
let revision = wakeup.revision()
let result = window.poll_dialog_result(dialog) catch { _ => return }
match result {
@native.DialogPollResult::Ready(_) => return
@native.DialogPollResult::Pending =>
wakeup.wait_after(revision) catch {
_ => return
}
}
}
}
///|
async fn show_app_run_failure(
runtime : @native.Runtime,
window : @native.Window,
wakeup : RuntimeWakeup,
error : AppRunError,
) -> Unit {
let message = app_run_error_dialog_text(error)
match error {
BridgeRuntimeError(_) =>
show_window_failure_dialog(window.as_ref(), wakeup, message)
_ => show_runtime_failure_dialog(runtime, wakeup, message)
}
}
///|
fn take_runtime_bridge_failure(
window : @native.Window,
) -> @native.BridgeDiagnostic? raise @native.NativeError {
let state = window.bridge_lifecycle_state()
if !state.failure_pending {
return None
}
Some(bridge_failure(window, state))
}
///|
fn WindowLifetime::new() -> WindowLifetime {
WindowLifetime::{
ready: false,
closed: false,
startup_error: None,
ready_signal: @async.CondVar::Cond(),
close_signal: @async.CondVar::Cond(),
}
}
///|
fn WindowLifetime::finish_start(
self : WindowLifetime,
error : AppRunError?,
) -> Unit {
if !self.ready {
self.startup_error = error
self.ready = true
self.ready_signal.broadcast()
}
}
///|
async fn WindowLifetime::wait_until_ready(
self : WindowLifetime,
) -> Unit raise AppRunError {
while !self.ready {
self.ready_signal.wait() catch {
error => raise normalize_async_run_error(error)
}
}
match self.startup_error {
Some(error) => raise error
None => ()
}
}
///|
fn WindowLifetime::close(self : WindowLifetime) -> Unit {
if !self.closed {
self.closed = true
self.close_signal.broadcast()
}
}
///|
async fn WindowLifetime::wait_until_closed(self : WindowLifetime) -> Unit {
while !self.closed {
self.close_signal.wait()
}
}
///|
async fn run_window_lifecycle_scope(
running : RunningWindow,
handle : WindowHandle,
windows : WindowManager,
events : WindowEventEmitter,
hooks : Array[WindowLifecycleHook],
lifecycle_failures : Array[AppCleanupError],
) -> Unit {
@async.with_task_group(tasks => {
start_window_lifecycle_hooks(
hooks,
WindowContext::new(
running.id,
running.window.as_ref(),
handle,
windows,
tasks,
events,
),
tasks,
lifecycle_failures,
) catch {
error => {
running.lifetime.finish_start(Some(normalize_async_run_error(error)))
tasks.return_immediately(())
return
}
}
running.lifetime.finish_start(None)
running.lifetime.wait_until_closed()
tasks.return_immediately(())
}) catch {
error =>
running.lifetime.finish_start(Some(normalize_async_run_error(error)))
}
}
///|
fn is_stale_bridge_response_error(error : @native.NativeError) -> Bool {
error.is_stale_bridge_response()
}