2024-06-29 22:24:01 -06:00
/*
* Copyright ( c ) 2024 , Andrew Kaster < akaster @ serenityos . org >
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include <AK/Debug.h>
2024-07-30 14:01:05 -04:00
# include <LibCore/ArgsParser.h>
2024-08-24 12:15:20 -04:00
# include <LibCore/Environment.h>
2024-08-28 15:29:51 -04:00
# include <LibCore/StandardPaths.h>
2024-10-07 13:36:17 -04:00
# include <LibCore/System.h>
2024-08-24 12:15:20 -04:00
# include <LibCore/TimeZoneWatcher.h>
2025-02-15 08:09:47 -05:00
# include <LibDevTools/DevToolsServer.h>
2024-08-28 15:29:51 -04:00
# include <LibFileSystem/FileSystem.h>
2024-06-29 22:24:01 -06:00
# include <LibImageDecoderClient/Client.h>
2025-02-15 08:09:47 -05:00
# include <LibWeb/CSS/PropertyID.h>
2024-06-29 22:24:01 -06:00
# include <LibWebView/Application.h>
2024-09-05 18:19:51 -04:00
# include <LibWebView/CookieJar.h>
# include <LibWebView/Database.h>
2024-11-13 15:33:02 -05:00
# include <LibWebView/HelperProcess.h>
2024-07-30 14:01:05 -04:00
# include <LibWebView/URL.h>
2024-08-28 10:26:11 -04:00
# include <LibWebView/UserAgent.h>
2024-06-29 22:24:01 -06:00
# include <LibWebView/WebContentClient.h>
namespace WebView {
Application * Application : : s_the = nullptr ;
2024-07-30 14:01:05 -04:00
Application : : Application ( )
2024-06-29 22:24:01 -06:00
{
VERIFY ( ! s_the ) ;
s_the = this ;
2024-08-24 12:15:20 -04:00
// No need to monitor the system time zone if the TZ environment variable is set, as it overrides system preferences.
if ( ! Core : : Environment : : has ( " TZ " sv ) ) {
if ( auto time_zone_watcher = Core : : TimeZoneWatcher : : create ( ) ; time_zone_watcher . is_error ( ) ) {
warnln ( " Unable to monitor system time zone: {} " , time_zone_watcher . error ( ) ) ;
} else {
m_time_zone_watcher = time_zone_watcher . release_value ( ) ;
m_time_zone_watcher - > on_time_zone_changed = [ ] ( ) {
WebContentClient : : for_each_client ( [ & ] ( WebView : : WebContentClient & client ) {
client . async_system_time_zone_changed ( ) ;
return IterationDecision : : Continue ;
} ) ;
} ;
}
}
2024-06-29 22:24:01 -06:00
m_process_manager . on_process_exited = [ this ] ( Process & & process ) {
process_did_exit ( move ( process ) ) ;
} ;
}
Application : : ~ Application ( )
{
s_the = nullptr ;
}
2024-07-30 14:01:05 -04:00
void Application : : initialize ( Main : : Arguments const & arguments , URL : : URL new_tab_page_url )
{
2024-10-07 13:36:17 -04:00
// Increase the open file limit, as the default limits on Linux cause us to run out of file descriptors with around 15 tabs open.
if ( auto result = Core : : System : : set_resource_limits ( RLIMIT_NOFILE , 8192 ) ; result . is_error ( ) )
warnln ( " Unable to increase open file limit: {} " , result . error ( ) ) ;
2024-07-30 14:01:05 -04:00
Vector < ByteString > raw_urls ;
Vector < ByteString > certificates ;
bool new_window = false ;
bool force_new_process = false ;
bool allow_popups = false ;
2024-09-04 07:13:40 +01:00
bool disable_scripting = false ;
2024-07-30 14:01:05 -04:00
bool disable_sql_database = false ;
2025-02-15 08:09:47 -05:00
Optional < u16 > devtools_port ;
2024-08-01 07:03:03 -04:00
Optional < StringView > debug_process ;
2024-08-01 07:13:09 -04:00
Optional < StringView > profile_process ;
2024-07-30 14:01:05 -04:00
Optional < StringView > webdriver_content_ipc_path ;
2024-08-28 10:26:11 -04:00
Optional < StringView > user_agent_preset ;
2024-11-01 23:53:43 +01:00
Optional < StringView > dns_server_address ;
Optional < u16 > dns_server_port ;
bool use_dns_over_tls = false ;
2024-07-30 14:01:05 -04:00
bool log_all_js_exceptions = false ;
bool enable_idl_tracing = false ;
bool enable_http_cache = false ;
2024-09-22 23:12:36 +01:00
bool enable_autoplay = false ;
2024-07-30 14:01:05 -04:00
bool expose_internals_object = false ;
2024-08-01 18:49:24 +01:00
bool force_cpu_painting = false ;
2024-08-19 20:35:49 +02:00
bool force_fontconfig = false ;
2024-10-31 11:06:01 -04:00
bool collect_garbage_on_every_allocation = false ;
2024-12-16 11:23:10 +00:00
bool disable_scrollbar_painting = false ;
2024-07-30 14:01:05 -04:00
Core : : ArgsParser args_parser ;
args_parser . set_general_help ( " The Ladybird web browser :^) " ) ;
args_parser . add_positional_argument ( raw_urls , " URLs to open " , " url " , Core : : ArgsParser : : Required : : No ) ;
args_parser . add_option ( certificates , " Path to a certificate file " , " certificate " , ' C ' , " certificate " ) ;
args_parser . add_option ( new_window , " Force opening in a new window " , " new-window " , ' n ' ) ;
args_parser . add_option ( force_new_process , " Force creation of new browser/chrome process " , " force-new-process " ) ;
args_parser . add_option ( allow_popups , " Disable popup blocking by default " , " allow-popups " ) ;
2024-09-04 07:13:40 +01:00
args_parser . add_option ( disable_scripting , " Disable scripting by default " , " disable-scripting " ) ;
2024-07-30 14:01:05 -04:00
args_parser . add_option ( disable_sql_database , " Disable SQL database " , " disable-sql-database " ) ;
2024-08-01 07:03:03 -04:00
args_parser . add_option ( debug_process , " Wait for a debugger to attach to the given process name (WebContent, RequestServer, etc.) " , " debug-process " , 0 , " process-name " ) ;
2024-08-01 07:13:09 -04:00
args_parser . add_option ( profile_process , " Enable callgrind profiling of the given process name (WebContent, RequestServer, etc.) " , " profile-process " , 0 , " process-name " ) ;
2024-07-30 14:01:05 -04:00
args_parser . add_option ( webdriver_content_ipc_path , " Path to WebDriver IPC for WebContent " , " webdriver-content-path " , 0 , " path " , Core : : ArgsParser : : OptionHideMode : : CommandLineAndMarkdown ) ;
2025-02-15 08:09:47 -05:00
args_parser . add_option ( devtools_port , " Set the Firefox DevTools port (EXPERIMENTAL) " , " devtools " , 0 , " port " ) ;
2024-07-30 14:01:05 -04:00
args_parser . add_option ( log_all_js_exceptions , " Log all JavaScript exceptions " , " log-all-js-exceptions " ) ;
args_parser . add_option ( enable_idl_tracing , " Enable IDL tracing " , " enable-idl-tracing " ) ;
args_parser . add_option ( enable_http_cache , " Enable HTTP cache " , " enable-http-cache " ) ;
2024-09-22 23:12:36 +01:00
args_parser . add_option ( enable_autoplay , " Enable multimedia autoplay " , " enable-autoplay " ) ;
2024-07-30 14:01:05 -04:00
args_parser . add_option ( expose_internals_object , " Expose internals object " , " expose-internals-object " ) ;
2024-08-01 18:49:24 +01:00
args_parser . add_option ( force_cpu_painting , " Force CPU painting " , " force-cpu-painting " ) ;
2024-08-19 20:35:49 +02:00
args_parser . add_option ( force_fontconfig , " Force using fontconfig for font loading " , " force-fontconfig " ) ;
2024-10-31 11:06:01 -04:00
args_parser . add_option ( collect_garbage_on_every_allocation , " Collect garbage after every JS heap allocation " , " collect-garbage-on-every-allocation " , ' g ' ) ;
2024-12-16 11:23:10 +00:00
args_parser . add_option ( disable_scrollbar_painting , " Don't paint horizontal or vertical scrollbars on the main viewport " , " disable-scrollbar-painting " ) ;
2024-11-01 23:53:43 +01:00
args_parser . add_option ( dns_server_address , " Set the DNS server address " , " dns-server " , 0 , " host|address " ) ;
args_parser . add_option ( dns_server_port , " Set the DNS server port " , " dns-port " , 0 , " port (default: 53 or 853 if --dot) " ) ;
args_parser . add_option ( use_dns_over_tls , " Use DNS over TLS " , " dot " ) ;
2024-08-28 10:26:11 -04:00
args_parser . add_option ( Core : : ArgsParser : : Option {
. argument_mode = Core : : ArgsParser : : OptionArgumentMode : : Required ,
. help_string = " Name of the User-Agent preset to use in place of the default User-Agent " ,
. long_name = " user-agent-preset " ,
. value_name = " name " ,
. accept_value = [ & ] ( StringView value ) {
user_agent_preset = normalize_user_agent_name ( value ) ;
return user_agent_preset . has_value ( ) ;
} ,
} ) ;
2024-07-30 14:01:05 -04:00
create_platform_arguments ( args_parser ) ;
args_parser . parse ( arguments ) ;
2024-09-18 13:36:45 -04:00
// Our persisted SQL storage assumes it runs in a singleton process. If we have multiple UI processes accessing
// the same underlying database, one of them is likely to fail.
if ( force_new_process )
disable_sql_database = true ;
2024-11-01 23:53:43 +01:00
if ( ! dns_server_port . has_value ( ) )
dns_server_port = use_dns_over_tls ? 853 : 53 ;
2024-08-01 07:03:03 -04:00
Optional < ProcessType > debug_process_type ;
2024-08-01 07:13:09 -04:00
Optional < ProcessType > profile_process_type ;
2024-08-01 07:03:03 -04:00
if ( debug_process . has_value ( ) )
debug_process_type = process_type_from_name ( * debug_process ) ;
2024-08-01 07:13:09 -04:00
if ( profile_process . has_value ( ) )
profile_process_type = process_type_from_name ( * profile_process ) ;
2024-08-01 07:03:03 -04:00
2024-07-30 14:01:05 -04:00
m_chrome_options = {
. urls = sanitize_urls ( raw_urls , new_tab_page_url ) ,
. raw_urls = move ( raw_urls ) ,
. new_tab_page_url = move ( new_tab_page_url ) ,
. certificates = move ( certificates ) ,
. new_window = new_window ? NewWindow : : Yes : NewWindow : : No ,
. force_new_process = force_new_process ? ForceNewProcess : : Yes : ForceNewProcess : : No ,
. allow_popups = allow_popups ? AllowPopups : : Yes : AllowPopups : : No ,
2024-09-04 07:13:40 +01:00
. disable_scripting = disable_scripting ? DisableScripting : : Yes : DisableScripting : : No ,
2024-07-30 14:01:05 -04:00
. disable_sql_database = disable_sql_database ? DisableSQLDatabase : : Yes : DisableSQLDatabase : : No ,
2024-08-01 07:03:03 -04:00
. debug_helper_process = move ( debug_process_type ) ,
2024-08-01 07:13:09 -04:00
. profile_helper_process = move ( profile_process_type ) ,
2024-11-01 23:53:43 +01:00
. dns_settings = ( dns_server_address . has_value ( )
? ( use_dns_over_tls
? DNSSettings ( DNSOverTLS ( dns_server_address . release_value ( ) , * dns_server_port ) )
: DNSSettings ( DNSOverUDP ( dns_server_address . release_value ( ) , * dns_server_port ) ) )
: SystemDNS { } ) ,
2025-02-15 08:09:47 -05:00
. devtools_port = devtools_port ,
2024-07-30 14:01:05 -04:00
} ;
if ( webdriver_content_ipc_path . has_value ( ) )
m_chrome_options . webdriver_content_ipc_path = * webdriver_content_ipc_path ;
m_web_content_options = {
. command_line = MUST ( String : : join ( ' ' , arguments . strings ) ) ,
. executable_path = MUST ( String : : from_byte_string ( MUST ( Core : : System : : current_executable_path ( ) ) ) ) ,
2024-08-28 10:26:11 -04:00
. user_agent_preset = move ( user_agent_preset ) ,
2024-07-30 14:01:05 -04:00
. log_all_js_exceptions = log_all_js_exceptions ? LogAllJSExceptions : : Yes : LogAllJSExceptions : : No ,
. enable_idl_tracing = enable_idl_tracing ? EnableIDLTracing : : Yes : EnableIDLTracing : : No ,
. enable_http_cache = enable_http_cache ? EnableHTTPCache : : Yes : EnableHTTPCache : : No ,
. expose_internals_object = expose_internals_object ? ExposeInternalsObject : : Yes : ExposeInternalsObject : : No ,
2024-08-01 18:49:24 +01:00
. force_cpu_painting = force_cpu_painting ? ForceCPUPainting : : Yes : ForceCPUPainting : : No ,
2024-08-19 20:35:49 +02:00
. force_fontconfig = force_fontconfig ? ForceFontconfig : : Yes : ForceFontconfig : : No ,
2024-09-22 23:12:36 +01:00
. enable_autoplay = enable_autoplay ? EnableAutoplay : : Yes : EnableAutoplay : : No ,
2024-10-31 11:06:01 -04:00
. collect_garbage_on_every_allocation = collect_garbage_on_every_allocation ? CollectGarbageOnEveryAllocation : : Yes : CollectGarbageOnEveryAllocation : : No ,
2024-12-16 11:23:10 +00:00
. paint_viewport_scrollbars = disable_scrollbar_painting ? PaintViewportScrollbars : : No : PaintViewportScrollbars : : Yes ,
2024-07-30 14:01:05 -04:00
} ;
create_platform_options ( m_chrome_options , m_web_content_options ) ;
2024-09-05 18:19:51 -04:00
if ( m_chrome_options . disable_sql_database = = DisableSQLDatabase : : No ) {
m_database = Database : : create ( ) . release_value_but_fixme_should_propagate_errors ( ) ;
m_cookie_jar = CookieJar : : create ( * m_database ) . release_value_but_fixme_should_propagate_errors ( ) ;
} else {
m_cookie_jar = CookieJar : : create ( ) ;
}
2024-07-30 14:01:05 -04:00
}
2024-11-13 15:33:02 -05:00
ErrorOr < void > Application : : launch_services ( )
{
TRY ( launch_request_server ( ) ) ;
TRY ( launch_image_decoder_server ( ) ) ;
2025-02-15 08:09:47 -05:00
TRY ( launch_devtools_server ( ) ) ;
2024-11-13 15:33:02 -05:00
return { } ;
}
ErrorOr < void > Application : : launch_request_server ( )
{
// FIXME: Create an abstraction to re-spawn the RequestServer and re-hook up its client hooks to each tab on crash
2024-11-13 16:04:33 -05:00
m_request_server_client = TRY ( launch_request_server_process ( ) ) ;
2024-11-13 15:33:02 -05:00
return { } ;
}
ErrorOr < void > Application : : launch_image_decoder_server ( )
{
2024-11-13 16:04:33 -05:00
m_image_decoder_client = TRY ( launch_image_decoder_process ( ) ) ;
2024-11-13 15:33:02 -05:00
m_image_decoder_client - > on_death = [ this ] ( ) {
m_image_decoder_client = nullptr ;
if ( auto result = launch_image_decoder_server ( ) ; result . is_error ( ) ) {
dbgln ( " Failed to restart image decoder: {} " , result . error ( ) ) ;
VERIFY_NOT_REACHED ( ) ;
}
auto client_count = WebContentClient : : client_count ( ) ;
auto new_sockets = m_image_decoder_client - > send_sync_but_allow_failure < Messages : : ImageDecoderServer : : ConnectNewClients > ( client_count ) ;
if ( ! new_sockets | | new_sockets - > sockets ( ) . is_empty ( ) ) {
dbgln ( " Failed to connect {} new clients to ImageDecoder " , client_count ) ;
VERIFY_NOT_REACHED ( ) ;
}
WebContentClient : : for_each_client ( [ sockets = new_sockets - > take_sockets ( ) ] ( WebContentClient & client ) mutable {
client . async_connect_to_image_decoder ( sockets . take_last ( ) ) ;
return IterationDecision : : Continue ;
} ) ;
} ;
return { } ;
}
2025-02-15 08:09:47 -05:00
ErrorOr < void > Application : : launch_devtools_server ( )
{
if ( m_chrome_options . devtools_port . has_value ( ) )
m_devtools = TRY ( DevTools : : DevToolsServer : : create ( * this , * m_chrome_options . devtools_port ) ) ;
return { } ;
}
2024-07-30 14:01:05 -04:00
int Application : : execute ( )
2024-06-29 22:24:01 -06:00
{
int ret = m_event_loop . exec ( ) ;
m_in_shutdown = true ;
return ret ;
}
void Application : : add_child_process ( WebView : : Process & & process )
{
m_process_manager . add_process ( move ( process ) ) ;
}
# if defined(AK_OS_MACH)
void Application : : set_process_mach_port ( pid_t pid , Core : : MachPort & & port )
{
m_process_manager . set_process_mach_port ( pid , move ( port ) ) ;
}
# endif
Optional < Process & > Application : : find_process ( pid_t pid )
{
return m_process_manager . find_process ( pid ) ;
}
void Application : : update_process_statistics ( )
{
m_process_manager . update_all_process_statistics ( ) ;
}
String Application : : generate_process_statistics_html ( )
{
return m_process_manager . generate_html ( ) ;
}
void Application : : process_did_exit ( Process & & process )
{
if ( m_in_shutdown )
return ;
dbgln_if ( WEBVIEW_PROCESS_DEBUG , " Process {} died, type: {} " , process . pid ( ) , process_name_from_type ( process . type ( ) ) ) ;
switch ( process . type ( ) ) {
case ProcessType : : ImageDecoder :
if ( auto client = process . client < ImageDecoderClient : : Client > ( ) ; client . has_value ( ) ) {
dbgln_if ( WEBVIEW_PROCESS_DEBUG , " Restart ImageDecoder process " ) ;
if ( auto on_death = move ( client - > on_death ) ) {
on_death ( ) ;
}
}
break ;
case ProcessType : : RequestServer :
dbgln_if ( WEBVIEW_PROCESS_DEBUG , " FIXME: Restart request server " ) ;
break ;
case ProcessType : : WebContent :
if ( auto client = process . client < WebContentClient > ( ) ; client . has_value ( ) ) {
dbgln_if ( WEBVIEW_PROCESS_DEBUG , " Restart WebContent process " ) ;
if ( auto on_web_content_process_crash = move ( client - > on_web_content_process_crash ) )
on_web_content_process_crash ( ) ;
}
break ;
case ProcessType : : WebWorker :
dbgln_if ( WEBVIEW_PROCESS_DEBUG , " WebWorker {} died, not sure what to do. " , process . pid ( ) ) ;
break ;
case ProcessType : : Chrome :
dbgln ( " Invalid process type to be dying: Chrome " ) ;
VERIFY_NOT_REACHED ( ) ;
}
}
2024-08-28 15:29:51 -04:00
ErrorOr < LexicalPath > Application : : path_for_downloaded_file ( StringView file ) const
{
auto downloads_directory = Core : : StandardPaths : : downloads_directory ( ) ;
if ( ! FileSystem : : is_directory ( downloads_directory ) ) {
auto maybe_downloads_directory = ask_user_for_download_folder ( ) ;
if ( ! maybe_downloads_directory . has_value ( ) )
return Error : : from_errno ( ECANCELED ) ;
downloads_directory = maybe_downloads_directory . release_value ( ) ;
}
if ( ! FileSystem : : is_directory ( downloads_directory ) )
return Error : : from_errno ( ENOENT ) ;
return LexicalPath : : join ( downloads_directory , file ) ;
}
2025-02-15 08:09:47 -05:00
void Application : : refresh_tab_list ( )
{
if ( ! m_devtools )
return ;
m_devtools - > refresh_tab_list ( ) ;
}
Vector < DevTools : : TabDescription > Application : : tab_list ( ) const
{
Vector < DevTools : : TabDescription > tabs ;
ViewImplementation : : for_each_view ( [ & ] ( ViewImplementation & view ) {
2025-02-19 09:28:02 -05:00
tabs . empend ( view . view_id ( ) , MUST ( String : : from_byte_string ( view . title ( ) ) ) , view . url ( ) . to_string ( ) ) ;
2025-02-15 08:09:47 -05:00
return IterationDecision : : Continue ;
} ) ;
return tabs ;
}
Vector < DevTools : : CSSProperty > Application : : css_property_list ( ) const
{
Vector < DevTools : : CSSProperty > property_list ;
for ( auto i = to_underlying ( Web : : CSS : : first_property_id ) ; i < = to_underlying ( Web : : CSS : : last_property_id ) ; + + i ) {
auto property_id = static_cast < Web : : CSS : : PropertyID > ( i ) ;
DevTools : : CSSProperty property ;
2025-02-19 09:28:02 -05:00
property . name = Web : : CSS : : string_from_property_id ( property_id ) . to_string ( ) ;
2025-02-15 08:09:47 -05:00
property . is_inherited = Web : : CSS : : is_inherited_property ( property_id ) ;
property_list . append ( move ( property ) ) ;
}
return property_list ;
}
2025-02-21 16:22:12 -05:00
void Application : : inspect_tab ( DevTools : : TabDescription const & description , OnTabInspectionComplete on_complete ) const
2025-02-15 08:09:47 -05:00
{
auto view = ViewImplementation : : find_view_by_id ( description . id ) ;
if ( ! view . has_value ( ) ) {
on_complete ( Error : : from_string_literal ( " Unable to locate tab " ) ) ;
return ;
}
2025-02-21 12:39:43 -05:00
view - > on_received_dom_tree = [ & view = * view , on_complete = move ( on_complete ) ] ( JsonObject dom_tree ) {
2025-02-15 08:09:47 -05:00
view . on_received_dom_tree = nullptr ;
2025-02-21 12:39:43 -05:00
on_complete ( move ( dom_tree ) ) ;
2025-02-15 08:09:47 -05:00
} ;
view - > inspect_dom_tree ( ) ;
}
2025-02-21 16:22:12 -05:00
void Application : : inspect_dom_node ( DevTools : : TabDescription const & description , Web : : UniqueNodeID node_id , Optional < Web : : CSS : : Selector : : PseudoElement : : Type > pseudo_element , OnDOMNodeInspectionComplete on_complete ) const
{
auto view = ViewImplementation : : find_view_by_id ( description . id ) ;
if ( ! view . has_value ( ) ) {
on_complete ( Error : : from_string_literal ( " Unable to locate tab " ) ) ;
return ;
}
view - > on_received_dom_node_properties = [ & view = * view , on_complete = move ( on_complete ) ] ( ViewImplementation : : DOMNodeProperties properties ) {
view . on_received_dom_node_properties = nullptr ;
2025-02-26 17:23:10 -05:00
on_complete ( DevTools : : DOMNodeProperties {
. computed_style = move ( properties . computed_style ) ,
. node_box_sizing = move ( properties . node_box_sizing ) ,
} ) ;
2025-02-21 16:22:12 -05:00
} ;
view - > inspect_dom_node ( node_id , pseudo_element ) ;
}
void Application : : clear_inspected_dom_node ( DevTools : : TabDescription const & description ) const
{
if ( auto view = ViewImplementation : : find_view_by_id ( description . id ) ; view . has_value ( ) )
view - > clear_inspected_dom_node ( ) ;
}
void Application : : highlight_dom_node ( DevTools : : TabDescription const & description , Web : : UniqueNodeID node_id , Optional < Web : : CSS : : Selector : : PseudoElement : : Type > pseudo_element ) const
{
if ( auto view = ViewImplementation : : find_view_by_id ( description . id ) ; view . has_value ( ) )
view - > highlight_dom_node ( node_id , pseudo_element ) ;
}
void Application : : clear_highlighted_dom_node ( DevTools : : TabDescription const & description ) const
{
if ( auto view = ViewImplementation : : find_view_by_id ( description . id ) ; view . has_value ( ) )
view - > clear_highlighted_dom_node ( ) ;
}
2025-02-24 11:57:33 -05:00
void Application : : evaluate_javascript ( DevTools : : TabDescription const & description , String script , OnScriptEvaluationComplete on_complete ) const
{
auto view = ViewImplementation : : find_view_by_id ( description . id ) ;
if ( ! view . has_value ( ) ) {
on_complete ( Error : : from_string_literal ( " Unable to locate tab " ) ) ;
return ;
}
view - > on_received_js_console_result = [ & view = * view , on_complete = move ( on_complete ) ] ( JsonValue result ) {
view . on_received_js_console_result = nullptr ;
on_complete ( move ( result ) ) ;
} ;
view - > js_console_input ( move ( script ) ) ;
}
2024-06-29 22:24:01 -06:00
}