diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 09009f99d40..752eeefda84 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -565,6 +565,7 @@ set(SOURCES Internals/InternalsBase.cpp Internals/Processes.cpp Internals/Settings.cpp + Internals/WebUI.cpp IntersectionObserver/IntersectionObserver.cpp IntersectionObserver/IntersectionObserverEntry.cpp Layout/AudioBox.cpp diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index d85324a960c..fc37f7e985f 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -625,6 +625,7 @@ class RequestList; namespace Web::Internals { class Internals; class Processes; +class WebUI; } namespace Web::IntersectionObserver { diff --git a/Libraries/LibWeb/Internals/WebUI.cpp b/Libraries/LibWeb/Internals/WebUI.cpp new file mode 100644 index 00000000000..6eafca240a7 --- /dev/null +++ b/Libraries/LibWeb/Internals/WebUI.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace Web::Internals { + +GC_DEFINE_ALLOCATOR(WebUI); + +WebUI::WebUI(JS::Realm& realm) + : InternalsBase(realm) +{ +} + +WebUI::~WebUI() = default; + +void WebUI::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + WEB_SET_PROTOTYPE_FOR_INTERFACE(WebUI); +} + +void WebUI::send_message(String const& name, JS::Value data) +{ + page().client().received_message_from_web_ui(name, data); +} + +} diff --git a/Libraries/LibWeb/Internals/WebUI.h b/Libraries/LibWeb/Internals/WebUI.h new file mode 100644 index 00000000000..d1b067bc6f8 --- /dev/null +++ b/Libraries/LibWeb/Internals/WebUI.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::Internals { + +class WebUI final : public InternalsBase { + WEB_PLATFORM_OBJECT(WebUI, InternalsBase); + GC_DECLARE_ALLOCATOR(WebUI); + +public: + virtual ~WebUI() override; + + void send_message(String const& name, JS::Value data); + +private: + explicit WebUI(JS::Realm&); + + virtual void initialize(JS::Realm&) override; +}; + +} diff --git a/Libraries/LibWeb/Internals/WebUI.idl b/Libraries/LibWeb/Internals/WebUI.idl new file mode 100644 index 00000000000..91c2383bd89 --- /dev/null +++ b/Libraries/LibWeb/Internals/WebUI.idl @@ -0,0 +1,4 @@ +[Exposed=Nobody] +interface WebUI { + undefined sendMessage(DOMString name, optional any data = null); +}; diff --git a/Libraries/LibWeb/Page/Page.h b/Libraries/LibWeb/Page/Page.h index e845ff43cc7..d08ba7e6ddd 100644 --- a/Libraries/LibWeb/Page/Page.h +++ b/Libraries/LibWeb/Page/Page.h @@ -400,6 +400,8 @@ public: virtual void page_did_mutate_dom([[maybe_unused]] FlyString const& type, [[maybe_unused]] DOM::Node const& target, [[maybe_unused]] DOM::NodeList& added_nodes, [[maybe_unused]] DOM::NodeList& removed_nodes, [[maybe_unused]] GC::Ptr previous_sibling, [[maybe_unused]] GC::Ptr next_sibling, [[maybe_unused]] Optional const& attribute_name) { } + virtual void received_message_from_web_ui([[maybe_unused]] String const& name, [[maybe_unused]] JS::Value data) { } + virtual void update_process_statistics() { } virtual void request_current_settings() { } diff --git a/Libraries/LibWeb/idl_files.cmake b/Libraries/LibWeb/idl_files.cmake index 6962eb5a474..33ca0e21bcd 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -268,6 +268,7 @@ libweb_js_bindings(Internals/InternalAnimationTimeline) libweb_js_bindings(Internals/Internals) libweb_js_bindings(Internals/Processes) libweb_js_bindings(Internals/Settings) +libweb_js_bindings(Internals/WebUI) libweb_js_bindings(IntersectionObserver/IntersectionObserver) libweb_js_bindings(IntersectionObserver/IntersectionObserverEntry) libweb_js_bindings(MathML/MathMLElement) diff --git a/Libraries/LibWebView/CMakeLists.txt b/Libraries/LibWebView/CMakeLists.txt index 60b7fd49658..c69ad93d447 100644 --- a/Libraries/LibWebView/CMakeLists.txt +++ b/Libraries/LibWebView/CMakeLists.txt @@ -24,6 +24,7 @@ set(SOURCES Utilities.cpp ViewImplementation.cpp WebContentClient.cpp + WebUI.cpp ) if (APPLE) @@ -69,6 +70,8 @@ set(GENERATED_SOURCES ../../Services/WebContent/WebContentServerEndpoint.h ../../Services/WebContent/WebDriverClientEndpoint.h ../../Services/WebContent/WebDriverServerEndpoint.h + ../../Services/WebContent/WebUIClientEndpoint.h + ../../Services/WebContent/WebUIServerEndpoint.h NativeStyleSheetSource.cpp UIProcessClientEndpoint.h UIProcessServerEndpoint.h diff --git a/Libraries/LibWebView/Forward.h b/Libraries/LibWebView/Forward.h index 860a7a65744..4b172e1a659 100644 --- a/Libraries/LibWebView/Forward.h +++ b/Libraries/LibWebView/Forward.h @@ -18,6 +18,7 @@ class ProcessManager; class Settings; class ViewImplementation; class WebContentClient; +class WebUI; struct Attribute; struct ConsoleOutput; diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index cd7d0e7bacb..ebe3e268599 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -4,12 +4,13 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include "WebContentClient.h" -#include "Application.h" -#include "ViewImplementation.h" #include +#include #include #include +#include +#include +#include namespace WebView { @@ -68,6 +69,11 @@ void WebContentClient::unregister_view(u64 page_id) } } +void WebContentClient::web_ui_disconnected(Badge) +{ + m_web_ui.clear(); +} + void WebContentClient::did_paint(u64 page_id, Gfx::IntRect rect, i32 bitmap_id) { if (auto view = view_for_page_id(page_id); view.has_value()) @@ -95,6 +101,13 @@ void WebContentClient::did_start_loading(u64 page_id, URL::URL url, bool is_redi void WebContentClient::did_finish_loading(u64 page_id, URL::URL url) { + if (url.scheme() == "about"sv && url.paths().size() == 1) { + if (auto web_ui = WebUI::create(*this, url.paths().first()); web_ui.is_error()) + warnln("Could not create WebUI for {}: {}", url, web_ui.error()); + else + m_web_ui = web_ui.release_value(); + } + if (auto view = view_for_page_id(page_id); view.has_value()) { view->set_url({}, url); diff --git a/Libraries/LibWebView/WebContentClient.h b/Libraries/LibWebView/WebContentClient.h index fcb738385c1..dba9f04b727 100644 --- a/Libraries/LibWebView/WebContentClient.h +++ b/Libraries/LibWebView/WebContentClient.h @@ -47,6 +47,8 @@ public: void register_view(u64 page_id, ViewImplementation&); void unregister_view(u64 page_id); + void web_ui_disconnected(Badge); + Function on_web_content_process_crash; pid_t pid() const { return m_process_handle.pid; } @@ -143,6 +145,8 @@ private: ProcessHandle m_process_handle; + RefPtr m_web_ui; + static HashTable s_clients; }; diff --git a/Libraries/LibWebView/WebUI.cpp b/Libraries/LibWebView/WebUI.cpp new file mode 100644 index 00000000000..9806b35fcd2 --- /dev/null +++ b/Libraries/LibWebView/WebUI.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace WebView { + +template +static ErrorOr> create_web_ui(WebContentClient& client, String host) +{ + Array socket_fds { 0, 0 }; + TRY(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, socket_fds.data())); + + auto client_socket = Core::LocalSocket::adopt_fd(socket_fds[0]); + if (client_socket.is_error()) { + close(socket_fds[0]); + close(socket_fds[1]); + + return client_socket.release_error(); + } + + auto web_ui = WebUIType::create(client, IPC::Transport { client_socket.release_value() }, move(host)); + client.async_connect_to_web_ui(0, IPC::File::adopt_fd(socket_fds[1])); + + return web_ui; +} + +ErrorOr> WebUI::create(WebContentClient&, String) +{ + RefPtr web_ui; + + if (web_ui) + web_ui->register_interfaces(); + + return web_ui; +} + +WebUI::WebUI(WebContentClient& client, IPC::Transport transport, String host) + : IPC::ConnectionToServer(*this, move(transport)) + , m_client(client) + , m_host(move(host)) +{ +} + +WebUI::~WebUI() = default; + +void WebUI::die() +{ + m_client.web_ui_disconnected({}); +} + +void WebUI::register_interface(StringView name, Interface interface) +{ + auto result = m_interfaces.set(name, move(interface)); + VERIFY(result == HashSetResult::InsertedNewEntry); +} + +void WebUI::received_message(String name, JsonValue data) +{ + auto interface = m_interfaces.get(name); + if (!interface.has_value()) { + warnln("Received message from WebUI for unrecognized interface: {}", name); + return; + } + + interface.value()(move(data)); +} + +} diff --git a/Libraries/LibWebView/WebUI.h b/Libraries/LibWebView/WebUI.h new file mode 100644 index 00000000000..4c312b2befe --- /dev/null +++ b/Libraries/LibWebView/WebUI.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebView { + +class WebUI + : public IPC::ConnectionToServer + , public WebUIClientEndpoint { +public: + static ErrorOr> create(WebContentClient&, String host); + virtual ~WebUI(); + + String const& host() const { return m_host; } + +protected: + WebUI(WebContentClient&, IPC::Transport, String host); + + using Interface = Function; + + virtual void register_interfaces() { } + void register_interface(StringView name, Interface); + +private: + virtual void die() override; + virtual void received_message(String name, JsonValue data) override; + + WebContentClient& m_client; + String m_host; + + HashMap m_interfaces; +}; + +#define WEB_UI(WebUIType) \ +public: \ + static NonnullRefPtr create(WebContentClient& client, IPC::Transport transport, String host) \ + { \ + return adopt_ref(*new WebUIType(client, move(transport), move(host))); \ + } \ + \ +private: \ + WebUIType(WebContentClient& client, IPC::Transport transport, String host) \ + : WebView::WebUI(client, move(transport), move(host)) \ + { \ + } + +} diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index 8e7dca3dc4d..101daeccd2d 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -433,6 +433,8 @@ compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebContentServer.ipc Se compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebContentClient.ipc Services/WebContent/WebContentClientEndpoint.h) compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebDriverClient.ipc Services/WebContent/WebDriverClientEndpoint.h) compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebDriverServer.ipc Services/WebContent/WebDriverServerEndpoint.h) +compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebUIClient.ipc Services/WebContent/WebUIClientEndpoint.h) +compile_ipc(${SERENITY_PROJECT_ROOT}/Services/WebContent/WebUIServer.ipc Services/WebContent/WebUIServerEndpoint.h) foreach(lib IN LISTS lagom_standard_libraries) add_serenity_subdirectory("Libraries/Lib${lib}") diff --git a/Services/WebContent/CMakeLists.txt b/Services/WebContent/CMakeLists.txt index 652ddc009bc..ed3215395f0 100644 --- a/Services/WebContent/CMakeLists.txt +++ b/Services/WebContent/CMakeLists.txt @@ -9,6 +9,7 @@ set(SOURCES PageHost.cpp WebContentConsoleClient.cpp WebDriverConnection.cpp + WebUIConnection.cpp ) if (ANDROID) diff --git a/Services/WebContent/ConnectionFromClient.cpp b/Services/WebContent/ConnectionFromClient.cpp index 4feb100e23a..1c1fffd176a 100644 --- a/Services/WebContent/ConnectionFromClient.cpp +++ b/Services/WebContent/ConnectionFromClient.cpp @@ -124,6 +124,15 @@ void ConnectionFromClient::connect_to_webdriver(u64 page_id, ByteString webdrive } } +void ConnectionFromClient::connect_to_web_ui(u64 page_id, IPC::File web_ui_socket) +{ + if (auto page = this->page(page_id); page.has_value()) { + // FIXME: Propagate this error back to the browser. + if (auto result = page->connect_to_web_ui(move(web_ui_socket)); result.is_error()) + dbgln("Unable to connect to the WebUI host: {}", result.error()); + } +} + void ConnectionFromClient::connect_to_image_decoder(IPC::File image_decoder_socket) { if (on_image_decoder_connection) diff --git a/Services/WebContent/ConnectionFromClient.h b/Services/WebContent/ConnectionFromClient.h index edc083edb89..d288a3040b0 100644 --- a/Services/WebContent/ConnectionFromClient.h +++ b/Services/WebContent/ConnectionFromClient.h @@ -61,6 +61,7 @@ private: virtual Messages::WebContentServer::GetWindowHandleResponse get_window_handle(u64 page_id) override; virtual void set_window_handle(u64 page_id, String handle) override; virtual void connect_to_webdriver(u64 page_id, ByteString webdriver_ipc_path) override; + virtual void connect_to_web_ui(u64 page_id, IPC::File web_ui_socket) override; virtual void connect_to_image_decoder(IPC::File image_decoder_socket) override; virtual void update_system_theme(u64 page_id, Core::AnonymousBuffer) override; virtual void update_screen_rects(u64 page_id, Vector, u32) override; diff --git a/Services/WebContent/Forward.h b/Services/WebContent/Forward.h index 215a41e60ef..5724e323b55 100644 --- a/Services/WebContent/Forward.h +++ b/Services/WebContent/Forward.h @@ -15,5 +15,6 @@ class PageHost; class PageClient; class WebContentConsoleClient; class WebDriverConnection; +class WebUIConnection; } diff --git a/Services/WebContent/PageClient.cpp b/Services/WebContent/PageClient.cpp index 044c979a9ee..17d1c6ce1da 100644 --- a/Services/WebContent/PageClient.cpp +++ b/Services/WebContent/PageClient.cpp @@ -32,6 +32,7 @@ #include #include #include +#include namespace WebContent { @@ -95,6 +96,8 @@ void PageClient::visit_edges(JS::Cell::Visitor& visitor) if (m_webdriver) m_webdriver->visit_edges(visitor); + if (m_web_ui) + m_web_ui->visit_edges(visitor); } ConnectionFromClient& PageClient::client() const @@ -372,6 +375,8 @@ void PageClient::page_did_change_active_document_in_top_level_browsing_context(W { auto& realm = document.realm(); + m_web_ui.clear(); + if (auto console_client = document.console_client()) { auto& web_content_console_client = as(*console_client); m_top_level_document_console_client = web_content_console_client; @@ -737,6 +742,24 @@ ErrorOr PageClient::connect_to_webdriver(ByteString const& webdriver_ipc_p return {}; } +ErrorOr PageClient::connect_to_web_ui(IPC::File web_ui_socket) +{ + auto* active_document = page().top_level_browsing_context().active_document(); + if (!active_document || !active_document->window()) + return {}; + + VERIFY(!m_web_ui); + m_web_ui = TRY(WebUIConnection::connect(move(web_ui_socket), *active_document)); + + return {}; +} + +void PageClient::received_message_from_web_ui(String const& name, JS::Value data) +{ + if (m_web_ui) + m_web_ui->received_message_from_web_ui(name, data); +} + void PageClient::initialize_js_console(Web::DOM::Document& document) { if (document.is_temporary_document_for_fragment_parsing()) @@ -915,4 +938,5 @@ void PageClient::queue_screenshot_task(Optional node_id) m_screenshot_tasks.enqueue({ node_id }); page().top_level_traversable()->set_needs_repaint(); } + } diff --git a/Services/WebContent/PageClient.h b/Services/WebContent/PageClient.h index decad41550f..33ef70925ab 100644 --- a/Services/WebContent/PageClient.h +++ b/Services/WebContent/PageClient.h @@ -44,6 +44,7 @@ public: virtual Web::Page const& page() const override { return *m_page; } ErrorOr connect_to_webdriver(ByteString const& webdriver_ipc_path); + ErrorOr connect_to_web_ui(IPC::File); virtual void paint_next_frame() override; virtual void process_screenshot_requests() override; @@ -174,6 +175,7 @@ private: virtual void page_did_allocate_backing_stores(i32 front_bitmap_id, Gfx::ShareableBitmap front_bitmap, i32 back_bitmap_id, Gfx::ShareableBitmap back_bitmap) override; virtual IPC::File request_worker_agent() override; virtual void page_did_mutate_dom(FlyString const& type, Web::DOM::Node const& target, Web::DOM::NodeList& added_nodes, Web::DOM::NodeList& removed_nodes, GC::Ptr previous_sibling, GC::Ptr next_sibling, Optional const& attribute_name) override; + virtual void received_message_from_web_ui(String const& name, JS::Value data) override; virtual void update_process_statistics() override; virtual void request_current_settings() override; virtual void restore_default_settings() override; @@ -212,6 +214,7 @@ private: Web::CSS::PreferredMotion m_preferred_motion { Web::CSS::PreferredMotion::NoPreference }; RefPtr m_webdriver; + RefPtr m_web_ui; BackingStoreManager m_backing_store_manager; diff --git a/Services/WebContent/WebContentServer.ipc b/Services/WebContent/WebContentServer.ipc index ee34cacd306..82637b6d83c 100644 --- a/Services/WebContent/WebContentServer.ipc +++ b/Services/WebContent/WebContentServer.ipc @@ -24,6 +24,7 @@ endpoint WebContentServer set_window_handle(u64 page_id, String handle) =| connect_to_webdriver(u64 page_id, ByteString webdriver_ipc_path) =| + connect_to_web_ui(u64 page_id, IPC::File socket_fd) =| connect_to_image_decoder(IPC::File socket_fd) =| update_system_theme(u64 page_id, Core::AnonymousBuffer theme_buffer) =| diff --git a/Services/WebContent/WebUIClient.ipc b/Services/WebContent/WebUIClient.ipc new file mode 100644 index 00000000000..e6ebad581a2 --- /dev/null +++ b/Services/WebContent/WebUIClient.ipc @@ -0,0 +1,3 @@ +endpoint WebUIClient { + received_message(String name, JsonValue data) =| +} diff --git a/Services/WebContent/WebUIConnection.cpp b/Services/WebContent/WebUIConnection.cpp new file mode 100644 index 00000000000..2e7513f9691 --- /dev/null +++ b/Services/WebContent/WebUIConnection.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebContent { + +static auto LADYBIRD_PROPERTY = JS::PropertyKey { "ladybird"_fly_string }; +static auto WEB_UI_LOADED_EVENT = "WebUILoaded"_fly_string; +static auto WEB_UI_MESSAGE_EVENT = "WebUIMessage"_fly_string; + +ErrorOr> WebUIConnection::connect(IPC::File web_ui_socket, Web::DOM::Document& document) +{ + auto socket = TRY(Core::LocalSocket::adopt_fd(web_ui_socket.take_fd())); + TRY(socket->set_blocking(true)); + + return adopt_ref(*new WebUIConnection(IPC::Transport { move(socket) }, document)); +} + +WebUIConnection::WebUIConnection(IPC::Transport transport, Web::DOM::Document& document) + : IPC::ConnectionFromClient(*this, move(transport), 1) + , m_document(document) +{ + auto& realm = m_document->realm(); + m_document->window()->define_direct_property(LADYBIRD_PROPERTY, realm.create(realm), JS::default_attributes); + + Web::HTML::queue_a_task(Web::HTML::Task::Source::Unspecified, nullptr, m_document, GC::create_function(realm.heap(), [&document = *m_document]() { + document.dispatch_event(Web::DOM::Event::create(document.realm(), WEB_UI_LOADED_EVENT)); + })); +} + +WebUIConnection::~WebUIConnection() +{ + if (!m_document->window()) + return; + + (void)m_document->window()->internal_delete(LADYBIRD_PROPERTY); +} + +void WebUIConnection::visit_edges(JS::Cell::Visitor& visitor) +{ + visitor.visit(m_document); +} + +void WebUIConnection::send_message(String name, JsonValue data) +{ + if (!m_document->browsing_context()) + return; + + JsonObject detail; + detail.set("name"sv, move(name)); + detail.set("data"sv, move(data)); + + auto& realm = m_document->realm(); + Web::HTML::TemporaryExecutionContext context { realm }; + + auto serialized_detail = Web::WebDriver::json_deserialize(*m_document->browsing_context(), detail); + if (serialized_detail.is_error()) { + warnln("Unable to serialize JSON data from browser: {}", serialized_detail.error()); + return; + } + + Web::DOM::CustomEventInit event_init {}; + event_init.detail = serialized_detail.value(); + + m_document->dispatch_event(Web::DOM::CustomEvent::create(realm, WEB_UI_MESSAGE_EVENT, event_init)); +} + +void WebUIConnection::received_message_from_web_ui(String const& name, JS::Value data) +{ + if (!m_document->browsing_context()) + return; + + auto deserialized_data = Web::WebDriver::json_clone(*m_document->browsing_context(), data); + if (deserialized_data.is_error()) { + warnln("Unable to deserialize JS data from WebUI: {}", deserialized_data.error()); + return; + } + + async_received_message(name, deserialized_data.value()); +} + +} diff --git a/Services/WebContent/WebUIConnection.h b/Services/WebContent/WebUIConnection.h new file mode 100644 index 00000000000..fd6915a5d35 --- /dev/null +++ b/Services/WebContent/WebUIConnection.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebContent { + +class WebUIConnection final : public IPC::ConnectionFromClient { +public: + static ErrorOr> connect(IPC::File, Web::DOM::Document&); + virtual ~WebUIConnection() override; + + void visit_edges(JS::Cell::Visitor&); + + void received_message_from_web_ui(String const& name, JS::Value data); + +private: + WebUIConnection(IPC::Transport, Web::DOM::Document&); + + virtual void die() override { } + virtual void send_message(String name, JsonValue data) override; + + GC::Ref m_document; +}; + +} diff --git a/Services/WebContent/WebUIServer.ipc b/Services/WebContent/WebUIServer.ipc new file mode 100644 index 00000000000..2598c94e5f7 --- /dev/null +++ b/Services/WebContent/WebUIServer.ipc @@ -0,0 +1,3 @@ +endpoint WebUIServer { + send_message(String name, JsonValue data) =| +}