2022-10-12 10:14:59 +00:00
/*
* Copyright ( c ) 2022 , Florent Castelli < florent . castelli @ gmail . com >
* Copyright ( c ) 2022 , Sam Atkins < atkinssj @ serenityos . org >
2022-10-15 12:08:07 +00:00
* Copyright ( c ) 2022 , Tobias Christiansen < tobyase @ serenityos . org >
2022-10-19 14:50:16 +00:00
* Copyright ( c ) 2022 , Linus Groh < linusg @ serenityos . org >
2024-09-29 13:40:17 +00:00
* Copyright ( c ) 2022 - 2024 , Tim Flynn < trflynn89 @ ladybird . org >
2022-10-12 10:14:59 +00:00
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include "Session.h"
# include "Client.h"
2023-03-05 22:57:36 +00:00
# include <AK/JsonObject.h>
2022-10-12 10:14:59 +00:00
# include <LibCore/LocalServer.h>
2022-12-15 12:36:17 +00:00
# include <LibCore/StandardPaths.h>
2022-10-12 10:14:59 +00:00
# include <LibCore/System.h>
2024-11-04 13:37:16 +00:00
# include <LibWeb/WebDriver/TimeoutsConfiguration.h>
2022-10-12 10:14:59 +00:00
# include <unistd.h>
namespace WebDriver {
2022-11-22 12:38:07 +00:00
Session : : Session ( unsigned session_id , NonnullRefPtr < Client > client , Web : : WebDriver : : LadybirdOptions options )
2022-10-12 10:14:59 +00:00
: m_client ( move ( client ) )
2022-11-22 12:38:07 +00:00
, m_options ( move ( options ) )
2022-10-12 10:14:59 +00:00
, m_id ( session_id )
{
}
2023-03-19 10:36:32 +00:00
// https://w3c.github.io/webdriver/#dfn-close-the-session
2022-10-12 10:14:59 +00:00
Session : : ~ Session ( )
{
2023-03-19 10:36:32 +00:00
if ( ! m_started )
return ;
// 1. Perform the following substeps based on the remote end’ s type:
// NOTE: We perform the "Remote end is an endpoint node" steps in the WebContent process.
2023-03-19 10:52:14 +00:00
for ( auto & it : m_windows ) {
it . value . web_content_connection - > close_session ( ) ;
}
2023-03-19 10:36:32 +00:00
// 2. Remove the current session from active sessions.
// NOTE: We are in a session destruction which means it is already removed
// from active sessions
// 3. Perform any implementation-specific cleanup steps.
2024-11-17 15:11:13 +00:00
if ( m_browser_process . has_value ( ) )
MUST ( Core : : System : : kill ( m_browser_process - > pid ( ) , SIGTERM ) ) ;
2023-03-19 10:36:32 +00:00
if ( m_web_content_socket_path . has_value ( ) ) {
MUST ( Core : : System : : unlink ( * m_web_content_socket_path ) ) ;
m_web_content_socket_path = { } ;
}
2022-10-20 13:17:23 +00:00
}
2024-11-04 13:37:16 +00:00
// Step 12 of https://w3c.github.io/webdriver/#dfn-new-sessions
void Session : : initialize_from_capabilities ( JsonObject & capabilities )
{
auto & connection = web_content_connection ( ) ;
// 1. Let strategy be the result of getting property "pageLoadStrategy" from capabilities.
auto strategy = capabilities . get_byte_string ( " pageLoadStrategy " sv ) ;
// 2. If strategy is a string, set the current session’ s page loading strategy to strategy. Otherwise, set the page loading strategy to normal and set a property of capabilities with name "pageLoadStrategy" and value "normal".
if ( strategy . has_value ( ) ) {
m_page_load_strategy = Web : : WebDriver : : page_load_strategy_from_string ( * strategy ) ;
connection . async_set_page_load_strategy ( m_page_load_strategy ) ;
} else {
capabilities . set ( " pageLoadStrategy " sv , " normal " sv ) ;
}
// 3. Let strictFileInteractability be the result of getting property "strictFileInteractability" from capabilities.
auto strict_file_interactiblity = capabilities . get_bool ( " strictFileInteractability " sv ) ;
// 4. If strictFileInteractability is a boolean, set the current session’ s strict file interactability to strictFileInteractability. Otherwise set the current session’ s strict file interactability to false.
if ( strict_file_interactiblity . has_value ( ) ) {
m_strict_file_interactiblity = * strict_file_interactiblity ;
connection . async_set_strict_file_interactability ( m_strict_file_interactiblity ) ;
} else {
capabilities . set ( " strictFileInteractability " sv , false ) ;
}
// FIXME: 5. Let proxy be the result of getting property "proxy" from capabilities and run the substeps of the first matching statement:
// FIXME: proxy is a proxy configuration object
// FIXME: Take implementation-defined steps to set the user agent proxy using the extracted proxy configuration. If the defined proxy cannot be configured return error with error code session not created.
// FIXME: Otherwise
// FIXME: Set a property of capabilities with name "proxy" and a value that is a new JSON Object.
// 6. If capabilities has a property with the key "timeouts":
if ( auto timeouts = capabilities . get_object ( " timeouts " sv ) ; timeouts . has_value ( ) ) {
// a. Let timeouts be the result of trying to JSON deserialize as a timeouts configuration the value of the "timeouts" property.
// NOTE: This happens on the remote end.
// b. Make the session timeouts the new timeouts.
MUST ( set_timeouts ( * timeouts ) ) ;
} else {
// 7. Set a property on capabilities with name "timeouts" and value that of the JSON deserialization of the session timeouts.
capabilities . set ( " timeouts " sv , Web : : WebDriver : : timeouts_object ( { } ) ) ;
}
// 8. Apply changes to the user agent for any implementation-defined capabilities selected during the capabilities processing step.
if ( auto behavior = capabilities . get_byte_string ( " unhandledPromptBehavior " sv ) ; behavior . has_value ( ) ) {
m_unhandled_prompt_behavior = Web : : WebDriver : : unhandled_prompt_behavior_from_string ( * behavior ) ;
connection . async_set_unhandled_prompt_behavior ( m_unhandled_prompt_behavior ) ;
} else {
capabilities . set ( " unhandledPromptBehavior " sv , " dismiss and notify " sv ) ;
}
}
2022-12-15 15:40:11 +00:00
ErrorOr < NonnullRefPtr < Core : : LocalServer > > Session : : create_server ( NonnullRefPtr < ServerPromise > promise )
2022-10-12 10:14:59 +00:00
{
2024-10-22 21:47:33 +00:00
static_assert ( IsSame < IPC : : Transport , IPC : : TransportSocket > , " Need to handle other IPC transports here " ) ;
2022-12-15 15:40:11 +00:00
dbgln ( " Listening for WebDriver connection on {} " , * m_web_content_socket_path ) ;
2022-10-12 10:14:59 +00:00
2023-03-23 14:33:29 +00:00
( void ) Core : : System : : unlink ( * m_web_content_socket_path ) ;
2022-11-08 15:18:11 +00:00
auto server = TRY ( Core : : LocalServer : : try_create ( ) ) ;
2022-12-15 15:40:11 +00:00
server - > listen ( * m_web_content_socket_path ) ;
2022-11-08 15:18:11 +00:00
2022-11-19 01:09:53 +00:00
server - > on_accept = [ this , promise ] ( auto client_socket ) {
2024-10-22 21:47:33 +00:00
auto maybe_connection = adopt_nonnull_ref_or_enomem ( new ( nothrow ) WebContentConnection ( IPC : : Transport ( move ( client_socket ) ) ) ) ;
2022-11-11 19:14:58 +00:00
if ( maybe_connection . is_error ( ) ) {
2023-07-07 22:00:27 +00:00
promise - > resolve ( maybe_connection . release_error ( ) ) ;
2022-11-11 19:14:58 +00:00
return ;
2022-11-08 15:18:11 +00:00
}
2022-11-11 19:14:58 +00:00
dbgln ( " WebDriver is connected to WebContent socket " ) ;
2023-03-05 22:57:36 +00:00
auto web_content_connection = maybe_connection . release_value ( ) ;
2024-10-05 22:42:50 +00:00
auto maybe_window_handle = web_content_connection - > get_window_handle ( ) ;
if ( maybe_window_handle . is_error ( ) ) {
promise - > reject ( Error : : from_string_literal ( " Window was closed immediately " ) ) ;
return ;
}
auto window_handle = MUST ( String : : from_byte_string ( maybe_window_handle . value ( ) . as_string ( ) ) ) ;
2023-03-19 11:08:52 +00:00
web_content_connection - > on_close = [ this , window_handle ] ( ) {
dbgln_if ( WEBDRIVER_DEBUG , " Window {} was closed remotely. " , window_handle ) ;
m_windows . remove ( window_handle ) ;
if ( m_windows . is_empty ( ) )
m_client - > close_session ( session_id ( ) ) ;
} ;
2024-11-04 21:18:20 +00:00
web_content_connection - > async_set_page_load_strategy ( m_page_load_strategy ) ;
web_content_connection - > async_set_strict_file_interactability ( m_strict_file_interactiblity ) ;
web_content_connection - > async_set_unhandled_prompt_behavior ( m_unhandled_prompt_behavior ) ;
if ( m_timeouts_configuration . has_value ( ) )
web_content_connection - > async_set_timeouts ( * m_timeouts_configuration ) ;
2023-03-13 14:45:34 +00:00
m_windows . set ( window_handle , Session : : Window { window_handle , move ( web_content_connection ) } ) ;
2023-03-05 22:57:36 +00:00
if ( m_current_window_handle . is_empty ( ) )
2023-03-13 14:45:34 +00:00
m_current_window_handle = window_handle ;
2022-11-08 15:18:11 +00:00
2023-07-07 22:00:27 +00:00
promise - > resolve ( { } ) ;
2022-11-08 15:18:11 +00:00
} ;
2022-11-19 01:09:53 +00:00
server - > on_accept_error = [ promise ] ( auto error ) {
2023-07-07 22:00:27 +00:00
promise - > resolve ( move ( error ) ) ;
2022-11-08 15:18:11 +00:00
} ;
return server ;
}
2022-12-15 13:46:01 +00:00
ErrorOr < void > Session : : start ( LaunchBrowserCallbacks const & callbacks )
2022-11-08 15:18:11 +00:00
{
auto promise = TRY ( ServerPromise : : try_create ( ) ) ;
2023-12-16 14:19:34 +00:00
m_web_content_socket_path = ByteString : : formatted ( " {}/webdriver/session_{}_{} " , TRY ( Core : : StandardPaths : : runtime_directory ( ) ) , getpid ( ) , m_id ) ;
2023-03-05 22:45:30 +00:00
m_web_content_server = TRY ( create_server ( promise ) ) ;
2022-10-12 10:14:59 +00:00
2022-12-15 13:46:01 +00:00
if ( m_options . headless )
2024-11-17 15:11:13 +00:00
m_browser_process = TRY ( callbacks . launch_headless_browser ( * m_web_content_socket_path ) ) ;
2022-12-15 13:46:01 +00:00
else
2024-11-17 15:11:13 +00:00
m_browser_process = TRY ( callbacks . launch_browser ( * m_web_content_socket_path ) ) ;
2022-10-12 10:14:59 +00:00
2022-11-09 15:56:12 +00:00
// FIXME: Allow this to be more asynchronous. For now, this at least allows us to propagate
2022-11-08 15:18:11 +00:00
// errors received while accepting the Browser and WebContent sockets.
2022-12-29 13:53:05 +00:00
TRY ( TRY ( promise - > await ( ) ) ) ;
2022-10-12 10:14:59 +00:00
m_started = true ;
return { } ;
}
2024-11-04 13:37:16 +00:00
Web : : WebDriver : : Response Session : : set_timeouts ( JsonValue payload )
{
m_timeouts_configuration = TRY ( web_content_connection ( ) . set_timeouts ( move ( payload ) ) ) ;
return JsonValue { } ;
}
2023-03-05 22:57:36 +00:00
// 11.2 Close Window, https://w3c.github.io/webdriver/#dfn-close-window
Web : : WebDriver : : Response Session : : close_window ( )
{
2024-11-05 12:47:51 +00:00
// 3. Close the current top-level browsing context.
TRY ( perform_async_action ( [ & ] ( auto & connection ) {
return connection . close_window ( ) ;
} ) ) ;
2023-03-07 14:16:01 +00:00
{
// Defer removing the window handle from this session until after we know we are done with its connection.
2024-02-13 00:37:42 +00:00
ScopeGuard guard { [ this ] { m_windows . remove ( m_current_window_handle ) ; m_current_window_handle = " NoSuchWindowPleaseSelectANewOne " _string ; } } ;
2023-03-05 22:57:36 +00:00
2023-03-07 14:16:01 +00:00
// 4. If there are no more open top-level browsing contexts, then close the session.
if ( m_windows . size ( ) = = 1 )
2023-03-19 10:36:32 +00:00
m_client - > close_session ( session_id ( ) ) ;
2023-03-07 14:16:01 +00:00
}
2023-03-05 22:57:36 +00:00
// 5. Return the result of running the remote end steps for the Get Window Handles command.
return get_window_handles ( ) ;
}
// 11.3 Switch to Window, https://w3c.github.io/webdriver/#dfn-switch-to-window
Web : : WebDriver : : Response Session : : switch_to_window ( StringView handle )
{
// 4. If handle is equal to the associated window handle for some top-level browsing context in the
// current session, let context be the that browsing context, and set the current top-level
// browsing context with context.
// Otherwise, return error with error code no such window.
if ( auto it = m_windows . find ( handle ) ; it ! = m_windows . end ( ) )
m_current_window_handle = it - > key ;
else
return Web : : WebDriver : : Error : : from_code ( Web : : WebDriver : : ErrorCode : : NoSuchWindow , " Window not found " ) ;
2023-03-20 23:53:15 +00:00
// 5. Update any implementation-specific state that would result from the user selecting the current
// browsing context for interaction, without altering OS-level focus.
2024-09-14 17:02:44 +00:00
TRY ( web_content_connection ( ) . switch_to_window ( m_current_window_handle ) ) ;
2023-03-05 22:57:36 +00:00
// 6. Return success with data null.
return JsonValue { } ;
}
// 11.4 Get Window Handles, https://w3c.github.io/webdriver/#dfn-get-window-handles
Web : : WebDriver : : Response Session : : get_window_handles ( ) const
{
// 1. Let handles be a JSON List.
JsonArray handles { } ;
// 2. For each top-level browsing context in the remote end, push the associated window handle onto handles.
for ( auto const & window_handle : m_windows . keys ( ) ) {
2023-04-17 05:38:27 +00:00
handles . must_append ( JsonValue ( window_handle ) ) ;
2023-03-05 22:57:36 +00:00
}
// 3. Return success with data handles.
return JsonValue { move ( handles ) } ;
}
2024-09-27 20:53:29 +00:00
ErrorOr < void , Web : : WebDriver : : Error > Session : : ensure_current_window_handle_is_valid ( ) const
{
if ( auto current_window = m_windows . get ( m_current_window_handle ) ; current_window . has_value ( ) )
return { } ;
return Web : : WebDriver : : Error : : from_code ( Web : : WebDriver : : ErrorCode : : NoSuchWindow , " Window not found " sv ) ;
}
2022-10-12 10:14:59 +00:00
}