///|
/// 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()
  let log_directory = resolve_runtime_app_path(self, AppPathKind::Logs)
  if self.create_logs_path {
    ensure_log_directory(log_directory)
  }
  start_runtime_logging(log_directory)
  let failure_log = RuntimeFailureLog::{ primary_logged: false, }
  try {
    @xlog.info(category="proton.runtime")  raise ConfigurationError(error)
    }
    let application_session = ApplicationSession::{
      partition: resolved.session_partition,
      data_path: browser_data_dir,
    }
    let locale_preferences = resolve_locale_preferences(
      resolved.explicit_locale,
    )
    let application_executable = executable_path() catch {
      error =>
        raise RuntimeOperationFailed(
          action="resolve application executable",
          status=-1,
          detail=error.message(),
        )
    }
    let process_arguments = @env.args()
    let application_arguments = if process_arguments.length() > 1 {
      process_arguments[1:].to_owned()
    } else {
      []
    }
    let app_instance = match acquire_app_instance(resolved) {
      Some(@native.AppInstanceAcquire::Forwarded) => {
        @xlog.info(category="proton.runtime")  Some(instance)
      None => None
    }
    errdefer (match app_instance {
      Some(instance) => destroy_app_instance(instance) catch { _ => () }
      None => ()
    })
    if resolved.update_channel is Some(_) {
      @native.update_validate_running_image() catch {
        error => raise native_run_error("validate running application", error)
      }
    }
    let environment = RuntimeEnvironment::{
      locale_preferences,
      application_session,
      application_executable,
      application_arguments,
    }
    let outcome = run_manifest(
      resolved,
      environment,
      app_instance~,
      failure_log~,
    )
    defer (if outcome.update is Some(update) {
      update.discard() catch {
        _ => ()
      }
    })
    if outcome.update is Some(update) {
      try {
        ignore(update.install())
        let installer_handoff = match app_instance {
          Some(instance) => instance.prepare_update_handoff()
          None => false
        }
        if !installer_handoff {
          match app_instance {
            Some(instance) => destroy_app_instance(instance)
            None => ()
          }
        }
        @updater.relaunch()
        if installer_handoff {
          match app_instance {
            Some(instance) => destroy_app_instance(instance)
            None => ()
          }
        }
      } catch {
        error =>
          raise RuntimeOperationFailed(
            action="install and restart application",
            status=-1,
            detail=@debug.render(Repr(error)),
          )
      }
      @native.process_exit(0)
    }
    match app_instance {
      Some(instance) => destroy_app_instance(instance)
      None => ()
    }
    @native.process_run_relaunches() catch {
      error => raise native_run_error("relaunch application", error)
    }
    @xlog.info(category="proton.runtime")  raise failure_log.logged_remaining(error)
  }
}

///|
fn AccessibilityMode::to_native(
  self : AccessibilityMode,
) -> @native.AccessibilityMode {
  match self {
    Automatic => @native.AccessibilityMode::Automatic
    AlwaysEnabled => @native.AccessibilityMode::AlwaysEnabled
  }
}

///|
fn log_runtime_failure(error : AppRunError) -> Unit {
  @xlog.error(category="proton.runtime")  Unit {
  log_runtime_failure(error)
  self.primary_logged = true
}

///|
fn RuntimeFailureLog::logged_remaining(
  self : RuntimeFailureLog,
  error : AppRunError,
) -> AppRunError {
  if !self.primary_logged {
    log_runtime_failure(error)
  } else if error is CleanupFailed(failures~, ..) {
    log_runtime_failure(CleanupFailed(primary=None, failures~))
  }
  error
}

///|
fn app_run_error_log_code(error : AppRunError) -> String {
  match error {
    EventLoopError(_) => "event_loop_error"
    ConfigurationError(_) => "configuration_error"
    LoggingInitializationFailed(..) => "logging_initialization_failed"
    UnsupportedNativeFeature(..) => "unsupported_native_feature"
    RuntimeOperationFailed(..) => "runtime_operation_failed"
    CommandExtensionLifecycleError(_) => "command_extension_lifecycle_error"
    LifecycleHookError(_) => "lifecycle_hook_error"
    EntryLoadError(_) => "entry_load_error"
    BridgeStartupError(diagnostic) => diagnostic.code
    BridgeRuntimeError(diagnostic) => diagnostic.code
    CleanupFailed(..) => "cleanup_failed"
    UnexpectedTaskFailure(..) => "unexpected_task_failure"
  }
}

///|
/// 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(
  config : ResolvedAppConfig,
  environment : RuntimeEnvironment,
  app_instance? : @native.AppInstance? = None,
  failure_log? : RuntimeFailureLog = RuntimeFailureLog::{
    primary_logged: false,
  },
) -> ApplicationExit raise AppRunError {
  let plans = planned_windows(config.manifest) catch {
    error => raise ConfigurationError(error)
  }
  if config.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 config.headless && config.menu is Some(_) {
    raise ConfigurationError(
      InvalidSetting(
        name="headless",
        message="native menus are not supported in headless mode",
      ),
    )
  }
  let lifecycle = AppLifecycle()
  lifecycle.begin_starting()
  // Context menus are created after startup through WindowHandle, so windowed
  // applications need menu command events even without an application menu.
  let forward_menu_events = !config.headless
  let catalog = framework_catalog(environment.locale_preferences)
  let remote_debugging = remote_debugging(config.manifest.debug) catch {
    error => raise ConfigurationError(error)
  }
  let runtime = @native.Runtime(
    config=@native.RuntimeConfig::bundled(
      cache_dir?=environment.application_session.data_path,
      proxy_server?=config.proxy_server,
      proxy_bypass?=config.proxy_bypass,
      locale=environment.locale_preferences.chromium_locale(),
      accept_languages=environment.locale_preferences.accept_languages(),
      remote_debugging~,
      headless=config.headless,
      accessibility_mode=config.accessibility_mode.to_native(),
    ).with_framework_dialog_labels(catalog.dialog_ok, catalog.dialog_cancel),
  ) catch {
    error => raise native_run_error("create runtime", error)
  }
  let wakeup = RuntimeWakeup()
  let dialog_completions = DialogCompletionStore()
  let cookie_completions = CookieCompletionStore()
  defer cookie_completions.close()
  let pdf_completions = PdfCompletionStore()
  defer pdf_completions.fail(None, None, "application has ended")
  let event_pump = RuntimeEventPump(
    runtime, dialog_completions, cookie_completions, pdf_completions,
  )
  let mut command_host : CommandHostRuntime? = None
  let windows : Array[RunningWindow] = []
  let lifecycle_failures : Array[AppCleanupError] = []
  let cleanup_started = Ref(false)
  // Cancellation bypasses catch. Keep native teardown after the task groups
  // unwind, and protect it until CEF has finished destroying its browsers.
  errdefer (if !cleanup_started.val {
    cleanup_started.val = true
    let failures = try! @async.protect_from_cancel(async fn() noraise {
      app_cleanup(
        lifecycle,
        runtime,
        windows,
        command_host,
        wakeup,
        lifecycle_failures~,
      )
    })
    if cleanup_run_error(None, failures) is Some(error) {
      log_runtime_failure(error)
    }
  })
  match app_instance {
    Some(instance) =>
      instance.attach_runtime(runtime) catch {
        error => {
          let attach_error = native_run_error("attach app instance", error)
          cleanup_started.val = true
          let failure = cleanup_run_error(
            Some(attach_error),
            try! @async.protect_from_cancel(async fn() noraise {
              app_cleanup(lifecycle, runtime, [], None, wakeup)
            }),
          )
          match failure {
            Some(cleanup_error) => raise cleanup_error
            None => raise attach_error
          }
        }
      }
    None => ()
  }
  let close_coordinator = CloseCoordinator()
  let updates = ApplicationUpdates(config.update_channel, can_download=() => {
    close_coordinator.accepts_download()
  })
  active_application_updates.val = Some(updates)
  defer {
    updates.discard()
    updates.channel = None
    active_application_updates.val = None
  }
  let outcome = Ref(ApplicationExit::{
    exit_code: 0,
    forced: false,
    update: None,
  })
  try {
    if !config.headless {
      let platform = @native.runtime_info().platform catch {
          error => raise native_run_error("query runtime platform", error)
        }
      match
        resolve_runtime_menu(
          config.menu,
          environment.locale_preferences,
          config.application_name,
          platform~,
        ) {
        Some(menu) =>
          runtime.set_menu(menu) catch {
            error => raise native_run_error("set menu", error)
          }
        None => ()
      }
    }
    validate_renderer_capability_targets(
      config.manifest,
      config.command_capabilities,
      config.extension_capabilities,
    ) catch {
      error => raise ConfigurationError(error)
    }
    let extension_catalog = command_extension_catalog(
      config.extension_capabilities,
    ) catch {
      error => raise ConfigurationError(error)
    }
    let titlebar_area_targets : Array[RendererTarget] = []
    for plan in plans {
      if plan.window.titlebar_style == @manifest.TitlebarStyle::Overlay {
        titlebar_area_targets.push(RendererTarget::entry(window=plan.id))
      }
    }
    let command_build = build_command_host(
      config.command_capabilities,
      extension_catalog,
      titlebar_area_targets~,
    )
    command_host = command_build.runtime
    let command_extensions = command_build.extensions
    let resolved_extension_capabilities = resolve_extension_capabilities(
      config.extension_capabilities,
      config.permission_base_path,
      command_extensions,
    ) catch {
      error => raise ConfigurationError(error)
    }
    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 permission_policies : Array[WindowPermissionPolicy] = []
    for index, plan in plans {
      permission_policies.push(
        resolve_window_permission_policy(
          plan.id,
          policies[index],
          command_build.app_capabilities,
          resolved_extension_capabilities,
        ) catch {
          error => raise ConfigurationError(error)
        },
      )
    }
    let bridge_enabled = requires_renderer_bridge(
      command_host,
      forward_menu_events,
      config.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(
      navigation=if config.navigation_handler is Some(_) {
        @native.BrowserPolicyMode::Ask
      } else {
        @native.BrowserPolicyMode::Allow
      },
      new_window=if config.new_window_handler is Some(_) {
        @native.BrowserPolicyMode::Ask
      } else {
        @native.BrowserPolicyMode::Deny
      },
      download=if config.download_handler is Some(_) {
        @native.BrowserPolicyMode::Ask
      } else {
        @native.BrowserPolicyMode::Deny
      },
      certificate=if config.certificate_handler is Some(_) {
        @native.BrowserPolicyMode::Ask
      } else {
        @native.BrowserPolicyMode::Deny
      },
      media=if config.media_handler is Some(_) {
        @native.BrowserPolicyMode::Ask
      } else {
        @native.BrowserPolicyMode::Deny
      },
      devtools=config.manifest.debug > 0,
    )
    for index, plan in plans {
      let entry = prepare_entry(plan.entry) catch {
        error => raise EntryLoadError(error)
      }
      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,
        entry,
        bridge,
        permissions: permission_policies[index],
        browser_policy,
        views: config.window_views.get(plan.id).unwrap_or([]),
      })
    }
    @async.with_task_group(application_tasks => {
      if command_host is Some(host) {
        for start in host.start_hooks {
          start(application_tasks)
        }
      }
      @async.with_task_group(window_tasks => {
        let session = RuntimeSession(
          config, environment, updates, close_coordinator, runtime, event_pump, definitions,
          windows, command_host, wakeup, forward_menu_events, bridge_enabled, app_instance,
          application_tasks, window_tasks, lifecycle_failures,
        )
        let application_context = session.application_context()
        // Electron reports the session as soon as it exists, before the
        // application start hooks create any window of their own.
        session.emit_session_created()
        let application_start = application_tasks.spawn(
          () => {
            defer session.wakeup.signal.notify()
            start_application_lifecycle_hooks(
              config.application_lifecycle_hooks,
              application_context,
              application_tasks,
              lifecycle_failures,
            )
          },
          no_wait=true,
          allow_failure=true,
        )
        let startup = session.drive_until_complete(application_start)
        if startup is Completed {
          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(config.bridge_startup_timeout_ms)
            if !session.is_quitting() {
              @xlog.info(category="proton.bridge")  {
              defer session.wakeup.signal.notify()
              for running in initial_windows {
                if !running.is_closed() {
                  session.activate_window(running)
                }
              }
            },
            no_wait=true,
            allow_failure=true,
          )
          if session.drive_until_complete(activation) is Completed {
            lifecycle.begin_running()
            cleanup_previous_update()
            start_update_check(
              updates,
              config.update_handlers,
              application_tasks,
            )
          }
        }
        session.run()
        outcome.val = session.resolved_exit()
        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)
      failure_log.report_primary(primary)
      if !config.headless {
        show_app_run_failure(
          runtime,
          event_pump,
          preferred_active_window(windows).map(window => window.window),
          wakeup,
          primary,
          environment.locale_preferences,
          lifecycle.has_reached_running(),
        ) catch {
          _ => ()
        }
      }
      cleanup_started.val = true
      let failure = cleanup_run_error(
        Some(primary),
        try! @async.protect_from_cancel(async fn() noraise {
          app_cleanup(
            lifecycle,
            runtime,
            windows,
            command_host,
            wakeup,
            lifecycle_failures~,
          )
        }),
      )
      match failure {
        Some(cleanup_error) => raise cleanup_error
        None => raise primary
      }
    }
  }
  cleanup_started.val = true
  match
    cleanup_run_error(
      None,
      try! @async.protect_from_cancel(async fn() noraise {
        app_cleanup(
          lifecycle,
          runtime,
          windows,
          command_host,
          wakeup,
          lifecycle_failures~,
        )
      }),
    ) {
    Some(error) => raise error
    None => ()
  }
  let update = if close_coordinator.purpose == InstallUpdate {
    let prepared = updates.prepared
    updates.prepared = None
    prepared
  } else {
    None
  }
  { ..outcome.val, update, }
}

///|
fn requires_renderer_bridge(
  command_host : CommandHostRuntime?,
  forward_menu_events : Bool,
  has_window_lifecycle_hooks : Bool,
) -> Bool {
  command_host is Some(_) || forward_menu_events || has_window_lifecycle_hooks
}

///|
/// 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(
  app : App,
  config : ResolvedAppConfig,
) -> String? raise AppConfigurationError {
  guard config.instance_identifier is Some(_) else { return None }
  let path = app.session_data_path(config.application_identifier) catch {
    error =>
      raise InvalidSetting(name="paths.sessionData", message=error.message())
  }
  match config.session_partition {
    None => Some(path)
    Some(partition) => {
      guard session_partition_is_valid(partition) else {
        raise InvalidSetting(
          name="session_partition",
          message="must contain only letters, digits, '.', '-', or '_'",
        )
      }
      Some(@mbpath.Path(path).join(partition).to_string())
    }
  }
}

///|
fn session_partition_is_valid(partition : String) -> Bool {
  guard partition.length() > 0 && partition.length() <= 96 else { return false }
  for character in partition {
    let code = character.to_int()
    if !((code >= 'a'.to_int() && code <= 'z'.to_int()) ||
      (code >= 'A'.to_int() && code <= 'Z'.to_int()) ||
      (code >= '0'.to_int() && code <= '9'.to_int()) ||
      character == '.' ||
      character == '-' ||
      character == '_') {
      return false
    }
  }
  true
}

///|
fn resolve_runtime_app_path(
  app : App,
  kind : AppPathKind,
) -> String raise AppRunError {
  app.path(kind) catch {
    error =>
      raise ConfigurationError(
        InvalidSetting(
          name="paths." + kind.electron_name(),
          message=error.message(),
        ),
      )
  }
}

///|
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,
  preferences : @locale.LocalePreferences,
  web_request_cancel_prefixes : Array[String],
  web_request_redirect_prefixes : Array[(String, String)],
  web_request_header_prefixes : Array[(String, String, String)],
) -> @native.WindowConfig {
  let catalog = framework_catalog(preferences)
  @native.WindowConfig(
    title=window.title,
    traffic_light_position?=window.traffic_light_position,
    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
    },
    theme=match window.theme_preference {
      @manifest.WindowThemePreference::System =>
        @native.WindowThemePreference::System
      @manifest.WindowThemePreference::Light =>
        @native.WindowThemePreference::Light
      @manifest.WindowThemePreference::Dark =>
        @native.WindowThemePreference::Dark
    },
    browser~,
    bridge?,
    web_request_cancel_prefixes~,
    web_request_redirect_prefixes~,
    web_request_header_prefixes~,
  ).with_framework_titlebar_labels(
    catalog.titlebar_minimize,
    catalog.titlebar_maximize,
    catalog.titlebar_restore,
    catalog.titlebar_close,
  )
}

///|
fn native_bridge_config(
  grants : Array[@native.BridgeGrantConfig],
) -> @native.BridgeConfig {
  @native.BridgeConfig(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(source_origin, ops=[]))
  }
}

///|
fn native_run_error(
  action : String,
  error : @native.NativeError,
) -> AppRunError {
  runtime_operation_failed(action, error)
}

///|
fn normalize_async_run_error(error : Error) -> AppRunError {
  match error {
    EventLoopError(message) => EventLoopError(message)
    ConfigurationError(error) => ConfigurationError(error)
    LoggingInitializationFailed(detail~) => LoggingInitializationFailed(detail~)
    RuntimeOperationFailed(action~, status~, detail~) =>
      RuntimeOperationFailed(action~, status~, detail~)
    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 [] }
  stop_command_extension_event_sources(self.event_sources)
  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::{
    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 = running.window.bridge_lifecycle_state() catch {
        error =>
          raise native_run_error(
            "read bridge startup state for " + running.id,
            error,
          )
      }
      guard pending is Some(_) else {
        raise EntryLoadError(ClosedDuringStartup)
      }
    }
  }
  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,
      )
  }
  guard state is Some(state) else { raise EntryLoadError(ClosedDuringStartup) }
  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(BridgeDiagnostic::from_native(diagnostic))
      }
    "ineligible" =>
      raise BridgeStartupError(
        BridgeDiagnostic::from_native(
          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()
    LoggingInitializationFailed(detail~) => detail
    UnsupportedNativeFeature(feature~) =>
      "The active Proton native runtime is incompatible with this application.\n\nMissing feature: " +
      feature
    RuntimeOperationFailed(action~, status~, detail~) =>
      action + " failed (" + status.to_string() + "): " + detail
    CommandExtensionLifecycleError(error) => error.message()
    LifecycleHookError(error) => error.message()
    UnexpectedTaskFailure(detail~) => detail
  }
}

///|
async fn show_runtime_failure_dialog(
  runtime : @native.Runtime,
  event_pump : RuntimeEventPump,
  wakeup : RuntimeWakeup,
  detail : String,
  preferences : @locale.LocalePreferences,
  application_started : Bool,
) -> Unit {
  let (title, summary) = if application_started {
    (
      application_error_title(preferences),
      application_error_summary(preferences),
    )
  } else {
    (startup_failure_title(preferences), startup_failure_summary(preferences))
  }
  let message = framework_failure_message(summary, detail)
  let dialog = runtime.begin_message_dialog(
    Some(title),
    message,
    @native.DialogLevel::Error,
  ) catch {
    _ => return
  }
  wait_for_failure_dialog(event_pump, wakeup, dialog) catch {
    _ => return
  }
}

///|
async fn show_window_failure_dialog(
  window : @native.WindowRef,
  event_pump : RuntimeEventPump,
  wakeup : RuntimeWakeup,
  detail : String,
  preferences : @locale.LocalePreferences,
) -> Bool {
  let message = framework_failure_message(
    application_error_summary(preferences),
    detail,
  )
  let dialog = window.begin_message_dialog(
    Some(application_error_title(preferences)),
    message,
    @native.DialogLevel::Error,
  ) catch {
    _ => return false
  }
  wait_for_failure_dialog(event_pump, wakeup, dialog) catch {
    _ => return false
  }
  true
}

///|
async fn show_app_run_failure(
  runtime : @native.Runtime,
  event_pump : RuntimeEventPump,
  window : @native.Window?,
  wakeup : RuntimeWakeup,
  error : AppRunError,
  preferences : @locale.LocalePreferences,
  application_started : Bool,
) -> Unit {
  let message = app_run_error_dialog_text(error)
  if application_started {
    match window {
      Some(window) =>
        if show_window_failure_dialog(
            window.as_ref(),
            event_pump,
            wakeup,
            message,
            preferences,
          ) {
          return
        }
      None => ()
    }
  }
  show_runtime_failure_dialog(
    runtime, event_pump, wakeup, message, preferences, application_started,
  )
}

///|
fn WindowLifetime::WindowLifetime() -> WindowLifetime {
  WindowLifetime::{
    task: None,
    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 {
  while !self.ready {
    self.ready_signal.wait()
  }
  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],
  locale_preferences : @locale.LocalePreferences,
) -> Unit {
  @async.with_task_group(tasks => {
    start_window_lifecycle_hooks(
      hooks,
      WindowContext(
        running.id,
        handle,
        windows,
        tasks,
        events,
        locale_preferences,
      ),
      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 => {
      if running.lifetime.ready {
        raise UnexpectedTaskFailure(
          detail="window " + running.id + ": " + @debug.render(Repr(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()
}