diff --git a/CMakeLists.txt b/CMakeLists.txt index 35a4d10..1c3fcfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,14 @@ if(BUILD_TESTING) COMMAND renderer-fps-test $) set_tests_properties(blur-transition-fps-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + add_executable(shared-profile-test tests/shared-profile-test.c) + target_compile_features(shared-profile-test PRIVATE c_std_11) + target_compile_definitions(shared-profile-test PRIVATE _GNU_SOURCE) + target_include_directories(shared-profile-test PRIVATE src) + add_test(NAME shared-profile-test + COMMAND shared-profile-test $) + set_tests_properties(shared-profile-test PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77) + add_executable(module-load-test tests/module-load-test.c) target_compile_features(module-load-test PRIVATE c_std_11) target_link_libraries(module-load-test PRIVATE PkgConfig::OBS) diff --git a/README.md b/README.md index f902a28..c6981e6 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,16 @@ DMA-BUF. Кадры передаются в OBS напрямую как BGRA, б - перезагрузка страницы и остановка renderer-процесса у скрытого source. - пользовательские web-панели в меню «Док-панели» OBS с сохранением URL в коллекции сцен, восстановлением после запуска и интерактивным вводом. +- единый постоянный WebKit-профиль для источников на сцене и web-панелей: общие + cookies, HTTP cache, localStorage, IndexedDB, service workers и credentials; + страницы одного origin взаимодействуют штатными `storage`-событиями и + `BroadcastChannel`, как вкладки одного браузера. Звук страницы намеренно выключен: добавьте в OBS отдельный Application Audio Capture. +Все WebView одной сессии OBS работают в общем renderer-процессе и одном +`WebKitWebContext`; маленькие процессы-посредники сохраняют прежнюю независимую +жизнь каждого source/dock и изоляцию от OBS. Профиль хранится в стандартных каталогах +данных и cache пользователя (`obs-webkit-browser/profile`) и переживает перезапуск OBS. Renderer наследует X11/XWayland-дисплей OBS; его нативное окно имеет нулевую opacity, пустую input shape и не появляется на рабочем столе. Дополнительный display server не запускается. WebKit принудительно включает аппаратный compositor и выбирает доступный diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index f452f12..cbd2be9 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -17,3 +17,4 @@ Transparent="Transparent background" CustomCSS="Custom CSS" ShutdownWhenHidden="Shut down renderer when source is hidden" Reload="Reload page" +Refresh="Refresh" diff --git a/data/locale/ru-RU.ini b/data/locale/ru-RU.ini index 6b73ed8..e8ff4f1 100644 --- a/data/locale/ru-RU.ini +++ b/data/locale/ru-RU.ini @@ -17,3 +17,4 @@ Transparent="Прозрачный фон" CustomCSS="Пользовательский CSS" ShutdownWhenHidden="Останавливать рендерер, когда источник скрыт" Reload="Перезагрузить страницу" +Refresh="Обновить" diff --git a/src/docks.cpp b/src/docks.cpp index 322514b..63cd849 100644 --- a/src/docks.cpp +++ b/src/docks.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -600,6 +601,8 @@ public: /* OBS clears frontend callbacks and owns the dock wrappers before * unloading modules during shutdown. Calling frontend removal APIs * from obs_module_unload is therefore too late. */ + refreshButtonTimer_.stop(); + delete refreshButton_; panels_.clear(); if (menuAction_) delete menuAction_; @@ -607,22 +610,96 @@ public: void setupFrontend() { - if (menuAction_ || !QApplication::instance()) + if (!QApplication::instance()) return; auto *main = static_cast(obs_frontend_get_main_window()); if (!main) return; - menuAction_ = new QAction(text("CustomBrowserDocks"), main); - QObject::connect(menuAction_, &QAction::triggered, main, [this] { showEditor(); }); - if (QMenu *docksMenu = main->findChild("menuDocks")) - docksMenu->addAction(menuAction_); - else - main->menuBar()->addAction(menuAction_); + if (!menuAction_) { + menuAction_ = new QAction(text("CustomBrowserDocks"), main); + QObject::connect(menuAction_, &QAction::triggered, main, [this] { showEditor(); }); + if (QMenu *docksMenu = main->findChild("menuDocks")) + docksMenu->addAction(menuAction_); + else + main->menuBar()->addAction(menuAction_); + } + setupRefreshButton(main); reconcile(); } private: static QString dockId(const QString &id) { return QString::fromUtf8(kDockPrefix) + id; } + static constexpr const char *kSourceId = "webkit_browser_source"; + + obs_source_t *currentContextSource() const + { + if (!contextSourceLabel_) + return nullptr; + QByteArray name = contextSourceLabel_->text().toUtf8(); + return obs_get_source_by_name(name.constData()); + } + + void updateRefreshButton() + { + if (!refreshButton_) + return; + + bool visible = false; + obs_source_t *source = currentContextSource(); + if (source) { + const char *id = obs_source_get_unversioned_id(source); + visible = id && strcmp(id, kSourceId) == 0; + obs_source_release(source); + } + + refreshButton_->setVisible(visible); + if (sourceInteractButton_) + refreshButton_->setText(sourceInteractButton_->text().isEmpty() ? QString() : text("Refresh")); + } + + void setupRefreshButton(QMainWindow *main) + { + if (refreshButton_) + return; + + sourceInteractButton_ = main->findChild("sourceInteractButton"); + contextSourceLabel_ = main->findChild("contextSourceLabel"); + if (!sourceInteractButton_ || !contextSourceLabel_) + return; + + auto *layout = qobject_cast(sourceInteractButton_->parentWidget()->layout()); + if (!layout) + return; + + refreshButton_ = new QPushButton(text("Refresh"), sourceInteractButton_->parentWidget()); + refreshButton_->setObjectName("webkitBrowserRefreshButton"); + refreshButton_->setMinimumHeight(22); + refreshButton_->setToolTip(text("Refresh")); + refreshButton_->setIcon(QIcon(":/res/images/refresh.svg")); + refreshButton_->setProperty("class", "icon-refresh"); + layout->insertWidget(layout->indexOf(sourceInteractButton_) + 1, refreshButton_); + + QObject::connect(refreshButton_, &QPushButton::clicked, main, [this] { + obs_source_t *source = currentContextSource(); + if (!source) + return; + const char *id = obs_source_get_unversioned_id(source); + if (id && strcmp(id, kSourceId) == 0) { + obs_properties_t *properties = obs_source_properties(source); + obs_property_t *reload = properties ? obs_properties_get(properties, "reload") : nullptr; + if (reload) + obs_property_button_clicked(reload, source); + if (properties) + obs_properties_destroy(properties); + } + obs_source_release(source); + }); + + refreshButtonTimer_.setInterval(100); + QObject::connect(&refreshButtonTimer_, &QTimer::timeout, main, [this] { updateRefreshButton(); }); + refreshButtonTimer_.start(); + updateRefreshButton(); + } static void eventCallback(obs_frontend_event event, void *opaque) { @@ -803,6 +880,10 @@ private: QVector definitions_; QHash> panels_; QPointer menuAction_; + QPointer sourceInteractButton_; + QPointer contextSourceLabel_; + QPointer refreshButton_; + QTimer refreshButtonTimer_; }; DockManager *manager = nullptr; diff --git a/src/renderer.c b/src/renderer.c index 7374883..274ebad 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -14,15 +14,19 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include +#include #include #include +#include #include #include "protocol.h" @@ -40,6 +44,13 @@ struct renderer { struct config config; GtkWidget *window; WebKitWebView *web_view; + int control_fd; + int frame_fd; + int log_fd; + int shared_fd; + int client_fd; + guint control_source; + guint client_source; struct owb_shared_frames *shared; size_t shared_size; uint64_t next_sequence; @@ -59,6 +70,22 @@ struct renderer { bool capture_attached; }; +static GList *renderers; +static WebKitWebContext *shared_context; +static guint idle_source; + +static void destroy_renderer(struct renderer *renderer); + +static void renderer_log(struct renderer *renderer, const char *format, ...) +{ + if (!renderer || renderer->log_fd < 0) + return; + va_list arguments; + va_start(arguments, format); + vdprintf(renderer->log_fd, format, arguments); + va_end(arguments); +} + static bool read_exact(int fd, void *data, size_t size) { uint8_t *cursor = data; @@ -74,10 +101,10 @@ static bool read_exact(int fd, void *data, size_t size) return true; } -static bool read_config(struct config *config) +static bool read_config(int fd, struct config *config) { struct owb_config_header header; - if (!read_exact(STDIN_FILENO, &header, sizeof(header)) || header.magic != OWB_CONFIG_MAGIC || + if (!read_exact(fd, &header, sizeof(header)) || header.magic != OWB_CONFIG_MAGIC || header.version != OWB_PROTOCOL_VERSION || header.width < 2 || header.height < 2 || header.width > 8192 || header.height > 8192 || header.fps < 1 || header.fps > 60 || header.url_length > OWB_MAX_STRING || header.css_length > OWB_MAX_STRING) @@ -93,8 +120,8 @@ static bool read_config(struct config *config) config->css = calloc((size_t)header.css_length + 1, 1); if (!config->url || !config->css) return false; - return read_exact(STDIN_FILENO, config->url, header.url_length) && - read_exact(STDIN_FILENO, config->css, header.css_length); + return read_exact(fd, config->url, header.url_length) && + read_exact(fd, config->css, header.css_length); } static char *normalize_uri(const char *value) @@ -162,10 +189,10 @@ static bool map_shared_frames(struct renderer *renderer) { struct stat info; renderer->shared_size = owb_shared_size(renderer->config.width, renderer->config.height); - if (fstat(OWB_SHARED_FRAME_FD, &info) != 0 || (size_t)info.st_size < renderer->shared_size) + if (fstat(renderer->shared_fd, &info) != 0 || (size_t)info.st_size < renderer->shared_size) return false; renderer->shared = mmap(NULL, renderer->shared_size, PROT_READ | PROT_WRITE, MAP_SHARED, - OWB_SHARED_FRAME_FD, 0); + renderer->shared_fd, 0); if (renderer->shared == MAP_FAILED) return false; return renderer->shared->magic == OWB_SHARED_MAGIC && @@ -299,7 +326,7 @@ static void publish_frame(struct renderer *renderer, const uint8_t *pixels, int } __atomic_store_n(&renderer->shared->sequence[slot], complete_sequence, __ATOMIC_RELEASE); uint8_t notification = (uint8_t)slot; - ssize_t ignored = write(STDOUT_FILENO, ¬ification, sizeof(notification)); + ssize_t ignored = write(renderer->frame_fd, ¬ification, sizeof(notification)); (void)ignored; } @@ -352,7 +379,7 @@ static void *capture_loop(void *opaque) None, None); bool published = publish_x11_frame(renderer); if (published && !transport_reported && process_uses_drm_render_node()) { - fprintf(stderr, "DRM render node: active\n"); + renderer_log(renderer, "DRM render node: active\n"); transport_reported = true; } first_frame = false; @@ -498,7 +525,8 @@ static gboolean control_ready(gint fd, GIOCondition condition, gpointer opaque) { struct renderer *renderer = opaque; if (condition & (G_IO_HUP | G_IO_ERR)) { - gtk_main_quit(); + renderer->control_source = 0; + destroy_renderer(renderer); return G_SOURCE_REMOVE; } for (;;) { @@ -509,7 +537,8 @@ static gboolean control_ready(gint fd, GIOCondition condition, gpointer opaque) if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return G_SOURCE_CONTINUE; if (count <= 0) { - gtk_main_quit(); + renderer->control_source = 0; + destroy_renderer(renderer); return G_SOURCE_REMOVE; } renderer->command_used += (size_t)count; @@ -530,22 +559,14 @@ static gboolean clear_window(GtkWidget *widget, cairo_t *context, gpointer unuse return FALSE; } -static gboolean terminate_renderer(gpointer opaque) -{ - struct renderer *renderer = opaque; - atomic_store_explicit(&renderer->stopping, true, memory_order_release); - gtk_main_quit(); - return G_SOURCE_REMOVE; -} - static void script_message_received(WebKitUserContentManager *manager, - WebKitJavascriptResult *message, gpointer unused) + WebKitJavascriptResult *message, gpointer opaque) { (void)manager; - (void)unused; + struct renderer *renderer = opaque; JSCValue *value = webkit_javascript_result_get_js_value(message); char *text = jsc_value_to_string(value); - fprintf(stderr, "Page: %s\n", text ? text : "(empty message)"); + renderer_log(renderer, "Page: %s\n", text ? text : "(empty message)"); g_free(text); } @@ -554,34 +575,102 @@ static gboolean load_failed(WebKitWebView *view, WebKitLoadEvent event, const ch { (void)view; (void)event; - (void)unused; - fprintf(stderr, "Page load failed for %s: %s\n", uri, error->message); + struct renderer *renderer = unused; + renderer_log(renderer, "Page load failed for %s: %s\n", uri, error->message); return FALSE; } -int main(void) +static gboolean exit_when_idle(gpointer unused) { - struct renderer renderer = {0}; - atomic_init(&renderer.stopping, false); - if (!read_config(&renderer.config)) { - fprintf(stderr, "Invalid renderer configuration\n"); - return 2; + (void)unused; + idle_source = 0; + if (!renderers) + gtk_main_quit(); + return G_SOURCE_REMOVE; +} + +static void destroy_renderer(struct renderer *renderer) +{ + if (!renderer) + return; + renderers = g_list_remove(renderers, renderer); + if (renderer->control_source) + g_source_remove(renderer->control_source); + if (renderer->client_source) + g_source_remove(renderer->client_source); + atomic_store_explicit(&renderer->stopping, true, memory_order_release); + if (renderer->capture_thread_started) + pthread_join(renderer->capture_thread, NULL); + destroy_x11_capture(renderer); + if (renderer->window) + gtk_widget_destroy(renderer->window); + if (renderer->shared && renderer->shared != MAP_FAILED) + munmap(renderer->shared, renderer->shared_size); + if (renderer->control_fd >= 0) + close(renderer->control_fd); + if (renderer->frame_fd >= 0) + close(renderer->frame_fd); + if (renderer->log_fd >= 0) + close(renderer->log_fd); + if (renderer->shared_fd >= 0) + close(renderer->shared_fd); + if (renderer->client_fd >= 0) + close(renderer->client_fd); + free(renderer->config.url); + free(renderer->config.css); + free(renderer); + if (!renderers && !idle_source) + idle_source = g_timeout_add_seconds(2, exit_when_idle, NULL); +} + +static gboolean client_ready(gint fd, GIOCondition condition, gpointer opaque) +{ + struct renderer *renderer = opaque; + if (condition & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) { + renderer->client_source = 0; + destroy_renderer(renderer); + return G_SOURCE_REMOVE; } - prepare_webkit(); - if (!map_shared_frames(&renderer)) { - fprintf(stderr, "Cannot map shared frame transport\n"); - return 4; + char ignored[16]; + ssize_t count = recv(fd, ignored, sizeof(ignored), MSG_DONTWAIT); + if (count == 0) { + renderer->client_source = 0; + destroy_renderer(renderer); + return G_SOURCE_REMOVE; } - if (!gtk_init_check(NULL, NULL)) { - fprintf(stderr, "GTK could not connect to the OBS display\n"); - return 5; + return G_SOURCE_CONTINUE; +} + +static struct renderer *create_renderer(int client_fd, const int descriptors[4]) +{ + struct renderer *renderer = calloc(1, sizeof(*renderer)); + if (!renderer) { + for (size_t index = 0; index < 4; index++) + close(descriptors[index]); + close(client_fd); + return NULL; + } + renderer->control_fd = descriptors[0]; + renderer->frame_fd = descriptors[1]; + renderer->log_fd = descriptors[2]; + renderer->shared_fd = descriptors[3]; + renderer->client_fd = client_fd; + renderer->capture_shm.shmid = -1; + atomic_init(&renderer->stopping, false); + if (!read_config(renderer->control_fd, &renderer->config)) { + renderer_log(renderer, "Invalid renderer configuration\n"); + goto fail; + } + if (!map_shared_frames(renderer)) { + renderer_log(renderer, "Cannot map shared frame transport\n"); + goto fail; } WebKitUserContentManager *manager = webkit_user_content_manager_new(); g_signal_connect(manager, "script-message-received::owbLog", - G_CALLBACK(script_message_received), NULL); + G_CALLBACK(script_message_received), renderer); if (!webkit_user_content_manager_register_script_message_handler(manager, "owbLog")) - fprintf(stderr, "Cannot register page diagnostic message handler\n"); + renderer_log(renderer, "Cannot register page diagnostic message handler\n"); static const char error_reporter[] = "(()=>{const p=(k,v)=>{try{window.webkit.messageHandlers.owbLog.postMessage(k+': '+v)}catch{}};" "addEventListener('error',e=>p('error',e.message+' @ '+e.filename+':'+e.lineno));" @@ -592,7 +681,7 @@ int main(void) NULL, NULL); webkit_user_content_manager_add_script(manager, reporter); webkit_user_script_unref(reporter); - if (renderer.config.transparent) { + if (renderer->config.transparent) { static const char transparent_page_css[] = "html,body{background:transparent!important}"; WebKitUserStyleSheet *transparent_sheet = webkit_user_style_sheet_new( @@ -601,96 +690,303 @@ int main(void) webkit_user_content_manager_add_style_sheet(manager, transparent_sheet); webkit_user_style_sheet_unref(transparent_sheet); } - if (*renderer.config.css) { - WebKitUserStyleSheet *sheet = webkit_user_style_sheet_new(renderer.config.css, + if (*renderer->config.css) { + WebKitUserStyleSheet *sheet = webkit_user_style_sheet_new(renderer->config.css, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_STYLE_LEVEL_USER, NULL, NULL); webkit_user_content_manager_add_style_sheet(manager, sheet); webkit_user_style_sheet_unref(sheet); } - renderer.web_view = WEBKIT_WEB_VIEW(webkit_web_view_new_with_user_content_manager(manager)); - g_signal_connect(renderer.web_view, "load-failed", G_CALLBACK(load_failed), NULL); + renderer->web_view = WEBKIT_WEB_VIEW(g_object_new( + WEBKIT_TYPE_WEB_VIEW, "web-context", shared_context, + "user-content-manager", manager, NULL)); + g_signal_connect(renderer->web_view, "load-failed", G_CALLBACK(load_failed), renderer); g_object_unref(manager); - WebKitSettings *settings = webkit_web_view_get_settings(renderer.web_view); + WebKitSettings *settings = webkit_web_view_get_settings(renderer->web_view); + webkit_settings_set_enable_html5_local_storage(settings, TRUE); webkit_settings_set_enable_webgl(settings, TRUE); webkit_settings_set_enable_page_cache(settings, TRUE); webkit_settings_set_enable_smooth_scrolling(settings, TRUE); webkit_settings_set_media_playback_requires_user_gesture(settings, FALSE); webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS); - webkit_web_view_set_is_muted(renderer.web_view, TRUE); - GdkRGBA background = renderer.config.transparent ? (GdkRGBA){0, 0, 0, 0} : (GdkRGBA){1, 1, 1, 1}; - webkit_web_view_set_background_color(renderer.web_view, &background); + webkit_web_view_set_is_muted(renderer->web_view, TRUE); + GdkRGBA background = renderer->config.transparent ? (GdkRGBA){0, 0, 0, 0} : (GdkRGBA){1, 1, 1, 1}; + webkit_web_view_set_background_color(renderer->web_view, &background); - renderer.window = gtk_window_new(GTK_WINDOW_POPUP); + renderer->window = gtk_window_new(GTK_WINDOW_POPUP); GdkScreen *screen = gdk_screen_get_default(); - GdkVisual *visual = renderer.config.transparent ? gdk_screen_get_rgba_visual(screen) + GdkVisual *visual = renderer->config.transparent ? gdk_screen_get_rgba_visual(screen) : gdk_screen_get_system_visual(screen); if (visual) - gtk_widget_set_visual(renderer.window, visual); - gtk_widget_set_app_paintable(renderer.window, renderer.config.transparent); - if (renderer.config.transparent) - g_signal_connect(renderer.window, "draw", G_CALLBACK(clear_window), NULL); - gtk_window_set_decorated(GTK_WINDOW(renderer.window), FALSE); - gtk_window_set_resizable(GTK_WINDOW(renderer.window), FALSE); - gtk_window_set_skip_taskbar_hint(GTK_WINDOW(renderer.window), TRUE); - gtk_window_set_skip_pager_hint(GTK_WINDOW(renderer.window), TRUE); - gtk_window_set_accept_focus(GTK_WINDOW(renderer.window), FALSE); - gtk_window_set_focus_on_map(GTK_WINDOW(renderer.window), FALSE); - gtk_window_move(GTK_WINDOW(renderer.window), 0, 0); - gtk_window_set_default_size(GTK_WINDOW(renderer.window), (int)renderer.config.width, - (int)renderer.config.height); - gtk_widget_set_size_request(GTK_WIDGET(renderer.web_view), (int)renderer.config.width, - (int)renderer.config.height); - gtk_container_add(GTK_CONTAINER(renderer.window), GTK_WIDGET(renderer.web_view)); - gtk_widget_realize(renderer.window); - GdkWindow *renderer_window = gtk_widget_get_window(renderer.window); + gtk_widget_set_visual(renderer->window, visual); + gtk_widget_set_app_paintable(renderer->window, renderer->config.transparent); + if (renderer->config.transparent) + g_signal_connect(renderer->window, "draw", G_CALLBACK(clear_window), NULL); + gtk_window_set_decorated(GTK_WINDOW(renderer->window), FALSE); + gtk_window_set_resizable(GTK_WINDOW(renderer->window), FALSE); + gtk_window_set_skip_taskbar_hint(GTK_WINDOW(renderer->window), TRUE); + gtk_window_set_skip_pager_hint(GTK_WINDOW(renderer->window), TRUE); + gtk_window_set_accept_focus(GTK_WINDOW(renderer->window), FALSE); + gtk_window_set_focus_on_map(GTK_WINDOW(renderer->window), FALSE); + gtk_window_move(GTK_WINDOW(renderer->window), 0, 0); + gtk_window_set_default_size(GTK_WINDOW(renderer->window), (int)renderer->config.width, + (int)renderer->config.height); + gtk_widget_set_size_request(GTK_WIDGET(renderer->web_view), (int)renderer->config.width, + (int)renderer->config.height); + gtk_container_add(GTK_CONTAINER(renderer->window), GTK_WIDGET(renderer->web_view)); + gtk_widget_realize(renderer->window); + GdkWindow *renderer_window = gtk_widget_get_window(renderer->window); if (!renderer_window || gdk_window_get_window_type(renderer_window) == GDK_WINDOW_OFFSCREEN) { - fprintf(stderr, "GTK did not create the native window required for GPU rendering\n"); - return 6; + renderer_log(renderer, "GTK did not create the native window required for GPU rendering\n"); + goto fail; } cairo_region_t *empty_input = cairo_region_create(); gdk_window_input_shape_combine_region(renderer_window, empty_input, 0, 0); cairo_region_destroy(empty_input); gdk_window_set_opacity(renderer_window, 0.0); - gtk_widget_show_all(renderer.window); + gtk_widget_show_all(renderer->window); while (gtk_events_pending()) gtk_main_iteration(); - if (!initialize_x11_capture(&renderer)) { - fprintf(stderr, "Cannot initialize XComposite/XDamage/XShm GPU capture\n"); - return 7; + if (!initialize_x11_capture(renderer)) { + renderer_log(renderer, "Cannot initialize XComposite/XDamage/XShm GPU capture\n"); + goto fail; } - int output_flags = fcntl(STDOUT_FILENO, F_GETFL, 0); + int output_flags = fcntl(renderer->frame_fd, F_GETFL, 0); if (output_flags >= 0) - fcntl(STDOUT_FILENO, F_SETFL, output_flags | O_NONBLOCK); + fcntl(renderer->frame_fd, F_SETFL, output_flags | O_NONBLOCK); - int flags = fcntl(STDIN_FILENO, F_GETFL, 0); + int flags = fcntl(renderer->control_fd, F_GETFL, 0); if (flags >= 0) - fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); - g_unix_fd_add(STDIN_FILENO, G_IO_IN | G_IO_HUP | G_IO_ERR, control_ready, &renderer); - char *uri = normalize_uri(renderer.config.url); - webkit_web_view_load_uri(renderer.web_view, uri); + fcntl(renderer->control_fd, F_SETFL, flags | O_NONBLOCK); + renderer->control_source = g_unix_fd_add(renderer->control_fd, + G_IO_IN | G_IO_HUP | G_IO_ERR, control_ready, renderer); + renderer->client_source = g_unix_fd_add(renderer->client_fd, + G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, client_ready, renderer); + char *uri = normalize_uri(renderer->config.url); + webkit_web_view_load_uri(renderer->web_view, uri); g_free(uri); - if (pthread_create(&renderer.capture_thread, NULL, capture_loop, &renderer) != 0) { - fprintf(stderr, "Cannot start GPU capture thread\n"); - return 8; + if (pthread_create(&renderer->capture_thread, NULL, capture_loop, renderer) != 0) { + renderer_log(renderer, "Cannot start GPU capture thread\n"); + goto fail; } - renderer.capture_thread_started = true; - fprintf(stderr, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, GPU DMA-BUF, XComposite/XShm capture\n", + renderer->capture_thread_started = true; + renderers = g_list_prepend(renderers, renderer); + if (idle_source) { + g_source_remove(idle_source); + idle_source = 0; + } + renderer_log(renderer, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, shared profile, GPU DMA-BUF, XComposite/XShm capture\n", webkit_get_major_version(), - webkit_get_minor_version(), webkit_get_micro_version(), renderer.config.width, - renderer.config.height, renderer.config.fps); - fflush(stderr); - g_unix_signal_add(SIGTERM, terminate_renderer, &renderer); - g_unix_signal_add(SIGINT, terminate_renderer, &renderer); + webkit_get_minor_version(), webkit_get_micro_version(), renderer->config.width, + renderer->config.height, renderer->config.fps); + return renderer; + +fail: + destroy_renderer(renderer); + return NULL; +} + +static gboolean accept_client(gint server_fd, GIOCondition condition, gpointer unused) +{ + (void)unused; + if (condition & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) { + gtk_main_quit(); + return G_SOURCE_REMOVE; + } + for (;;) { + int client_fd = accept4(server_fd, NULL, NULL, SOCK_CLOEXEC); + if (client_fd < 0 && errno == EINTR) + continue; + if (client_fd < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) + return G_SOURCE_CONTINUE; + if (client_fd < 0) + return G_SOURCE_CONTINUE; + + int descriptors[4] = {-1, -1, -1, -1}; + char byte; + struct iovec payload = {.iov_base = &byte, .iov_len = sizeof(byte)}; + char control[CMSG_SPACE(sizeof(descriptors))]; + struct msghdr message = {0}; + message.msg_iov = &payload; + message.msg_iovlen = 1; + message.msg_control = control; + message.msg_controllen = sizeof(control); + ssize_t received = recvmsg(client_fd, &message, 0); + struct cmsghdr *header = received == 1 ? CMSG_FIRSTHDR(&message) : NULL; + if (!header || header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS || + header->cmsg_len != CMSG_LEN(sizeof(descriptors))) { + close(client_fd); + continue; + } + memcpy(descriptors, CMSG_DATA(header), sizeof(descriptors)); + for (size_t index = 0; index < 4; index++) { + int flags = fcntl(descriptors[index], F_GETFD); + if (flags >= 0) + fcntl(descriptors[index], F_SETFD, flags | FD_CLOEXEC); + } + create_renderer(client_fd, descriptors); + } +} + +static gboolean terminate_daemon(gpointer unused) +{ + (void)unused; + gtk_main_quit(); + return G_SOURCE_REMOVE; +} + +static socklen_t daemon_address(struct sockaddr_un *address, pid_t owner) +{ + memset(address, 0, sizeof(*address)); + address->sun_family = AF_UNIX; + g_snprintf(address->sun_path + 1, sizeof(address->sun_path) - 1, + "obs-webkit-browser-%lu-%ld", (unsigned long)getuid(), (long)owner); + return (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + + strlen(address->sun_path + 1)); +} + +static int open_daemon_socket(pid_t owner) +{ + int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0); + if (fd < 0) + return -1; + struct sockaddr_un address; + socklen_t length = daemon_address(&address, owner); + if (connect(fd, (struct sockaddr *)&address, length) == 0) + return fd; + close(fd); + return -1; +} + +static int daemon_main(pid_t owner) +{ + if (setsid() < 0) + return 10; + prepare_webkit(); + if (!gtk_init_check(NULL, NULL)) + return 5; + + int server_fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (server_fd < 0) + return 10; + struct sockaddr_un address; + socklen_t address_length = daemon_address(&address, owner); + if (bind(server_fd, (struct sockaddr *)&address, address_length) != 0 || + listen(server_fd, 32) != 0) { + close(server_fd); + return 10; + } + + char *data_directory = g_build_filename(g_get_user_data_dir(), + "obs-webkit-browser", "profile", NULL); + char *cache_directory = g_build_filename(g_get_user_cache_dir(), + "obs-webkit-browser", "profile", NULL); + g_mkdir_with_parents(data_directory, 0700); + g_mkdir_with_parents(cache_directory, 0700); + WebKitWebsiteDataManager *data_manager = webkit_website_data_manager_new( + "base-data-directory", data_directory, + "base-cache-directory", cache_directory, NULL); + webkit_website_data_manager_set_persistent_credential_storage_enabled(data_manager, TRUE); + WebKitCookieManager *cookie_manager = + webkit_website_data_manager_get_cookie_manager(data_manager); + char *cookie_file = g_build_filename(data_directory, "cookies.sqlite", NULL); + webkit_cookie_manager_set_persistent_storage(cookie_manager, cookie_file, + WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE); + webkit_cookie_manager_set_accept_policy(cookie_manager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS); + shared_context = webkit_web_context_new_with_website_data_manager(data_manager); + webkit_web_context_set_cache_model(shared_context, WEBKIT_CACHE_MODEL_WEB_BROWSER); + g_object_unref(data_manager); + g_free(cookie_file); + g_free(data_directory); + g_free(cache_directory); + + /* The first proxy inherited these descriptors when it forked this daemon. + * Only the SCM_RIGHTS copies accepted above may own an individual view. */ + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + close(OWB_SHARED_FRAME_FD); + + g_unix_fd_add(server_fd, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, + accept_client, NULL); + g_unix_signal_add(SIGTERM, terminate_daemon, NULL); + g_unix_signal_add(SIGINT, terminate_daemon, NULL); gtk_main(); - atomic_store_explicit(&renderer.stopping, true, memory_order_release); - if (renderer.capture_thread_started) - pthread_join(renderer.capture_thread, NULL); - destroy_x11_capture(&renderer); - munmap(renderer.shared, renderer.shared_size); - free(renderer.config.url); - free(renderer.config.css); + while (renderers) + destroy_renderer(renderers->data); + if (idle_source) { + g_source_remove(idle_source); + idle_source = 0; + } + g_object_unref(shared_context); + shared_context = NULL; + close(server_fd); + return 0; +} + +static bool pass_view_descriptors(int client_fd) +{ + int descriptors[4] = {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO, + OWB_SHARED_FRAME_FD}; + char byte = 1; + struct iovec payload = {.iov_base = &byte, .iov_len = sizeof(byte)}; + char control[CMSG_SPACE(sizeof(descriptors))]; + memset(control, 0, sizeof(control)); + struct msghdr message = {0}; + message.msg_iov = &payload; + message.msg_iovlen = 1; + message.msg_control = control; + message.msg_controllen = sizeof(control); + struct cmsghdr *header = CMSG_FIRSTHDR(&message); + header->cmsg_level = SOL_SOCKET; + header->cmsg_type = SCM_RIGHTS; + header->cmsg_len = CMSG_LEN(sizeof(descriptors)); + memcpy(CMSG_DATA(header), descriptors, sizeof(descriptors)); + return sendmsg(client_fd, &message, MSG_NOSIGNAL) == 1; +} + +int main(void) +{ + pid_t owner = getppid(); + pid_t launched_daemon = 0; + int client_fd = open_daemon_socket(owner); + if (client_fd < 0) { + launched_daemon = fork(); + if (launched_daemon < 0) + return 10; + if (launched_daemon == 0) + _exit(daemon_main(owner)); + for (unsigned attempt = 0; attempt < 200 && client_fd < 0; attempt++) { + struct timespec pause = {.tv_nsec = 10000000L}; + nanosleep(&pause, NULL); + client_fd = open_daemon_socket(owner); + } + } + if (client_fd < 0) { + int status = 0; + if (launched_daemon > 0 && waitpid(launched_daemon, &status, WNOHANG) == launched_daemon && + WIFEXITED(status)) + return WEXITSTATUS(status); + fprintf(stderr, "Cannot start the shared WebKit renderer process\n"); + return 10; + } + if (launched_daemon > 0) + waitpid(launched_daemon, NULL, WNOHANG); + if (!pass_view_descriptors(client_fd)) { + fprintf(stderr, "Cannot attach view to the shared WebKit renderer process\n"); + close(client_fd); + return 11; + } + + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + close(OWB_SHARED_FRAME_FD); + char ignored; + while (recv(client_fd, &ignored, sizeof(ignored), 0) < 0 && errno == EINTR) { + } + close(client_fd); return 0; } diff --git a/tests/shared-profile-test.c b/tests/shared-profile-test.c new file mode 100644 index 0000000..5ee42da --- /dev/null +++ b/tests/shared-profile-test.c @@ -0,0 +1,202 @@ +#include "protocol.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; + +struct view { + pid_t pid; + int control; + int frames; + int shared_fd; + struct owb_shared_frames *shared; + size_t shared_size; +}; + +static bool write_all(int fd, const void *data, size_t size) +{ + const uint8_t *cursor = data; + while (size) { + ssize_t count = write(fd, cursor, size); + if (count < 0 && errno == EINTR) + continue; + if (count <= 0) + return false; + cursor += (size_t)count; + size -= (size_t)count; + } + return true; +} + +static void serve_pages(int server) +{ + static const char page[] = + "" + ""; + char response[2048]; + int length = snprintf(response, sizeof(response), + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nCache-Control: no-store\r\n" + "Connection: close\r\nContent-Length: %zu\r\n\r\n%s", + strlen(page), page); + for (;;) { + int client = accept(server, NULL, NULL); + if (client < 0 && errno == EINTR) + continue; + if (client < 0) + _exit(0); + char request[1024]; + ssize_t ignored = read(client, request, sizeof(request)); + (void)ignored; + write_all(client, response, (size_t)length); + close(client); + } +} + +static bool start_view(struct view *view, const char *renderer, const char *url) +{ + memset(view, 0, sizeof(*view)); + view->pid = 0; + view->control = view->frames = view->shared_fd = -1; + int control[2] = {-1, -1}; + int frames[2] = {-1, -1}; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, control) != 0 || pipe(frames) != 0) + return false; + const uint32_t width = 64, height = 64; + view->shared_size = owb_shared_size(width, height); + view->shared_fd = memfd_create("shared-profile-test", MFD_CLOEXEC); + if (view->shared_fd < 0 || ftruncate(view->shared_fd, (off_t)view->shared_size) != 0) + return false; + view->shared = mmap(NULL, view->shared_size, PROT_READ | PROT_WRITE, MAP_SHARED, + view->shared_fd, 0); + if (view->shared == MAP_FAILED) + return false; + memset(view->shared, 0, sizeof(*view->shared)); + view->shared->magic = OWB_SHARED_MAGIC; + view->shared->version = OWB_PROTOCOL_VERSION; + view->shared->width = width; + view->shared->height = height; + view->shared->stride = width * 4; + view->shared->slot_count = OWB_SHARED_FRAME_SLOTS; + + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + posix_spawn_file_actions_adddup2(&actions, control[1], STDIN_FILENO); + posix_spawn_file_actions_adddup2(&actions, frames[1], STDOUT_FILENO); + posix_spawn_file_actions_adddup2(&actions, view->shared_fd, OWB_SHARED_FRAME_FD); + char *arguments[] = {(char *)renderer, NULL}; + int error = posix_spawn(&view->pid, renderer, &actions, NULL, arguments, environ); + posix_spawn_file_actions_destroy(&actions); + close(control[1]); + close(frames[1]); + if (error) + return false; + view->control = control[0]; + view->frames = frames[0]; + struct owb_config_header config = { + .magic = OWB_CONFIG_MAGIC, .version = OWB_PROTOCOL_VERSION, + .width = width, .height = height, .fps = 20, .transparent = 0, + .hardware_acceleration = 1, .url_length = (uint32_t)strlen(url), .css_length = 0, + }; + return write_all(view->control, &config, sizeof(config)) && + write_all(view->control, url, strlen(url)); +} + +static int stop_view(struct view *view) +{ + int status = 0; + if (view->control >= 0) { + shutdown(view->control, SHUT_RDWR); + close(view->control); + } + if (view->frames >= 0) + close(view->frames); + if (view->shared && view->shared != MAP_FAILED) + munmap(view->shared, view->shared_size); + if (view->shared_fd >= 0) + close(view->shared_fd); + if (view->pid > 0) { + kill(view->pid, SIGTERM); + while (waitpid(view->pid, &status, 0) < 0 && errno == EINTR) { + } + } + return status; +} + +int main(int argc, char **argv) +{ + if (argc != 2) + return 2; + int server = socket(AF_INET, SOCK_STREAM, 0); + int enabled = 1; + setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)); + struct sockaddr_in address = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)}; + if (server < 0 || bind(server, (struct sockaddr *)&address, sizeof(address)) != 0 || + listen(server, 16) != 0) + return 77; + socklen_t address_size = sizeof(address); + if (getsockname(server, (struct sockaddr *)&address, &address_size) != 0) + return 2; + pid_t http_server = fork(); + if (http_server == 0) + serve_pages(server); + close(server); + + char writer_url[256], reader_url[256]; + unsigned port = ntohs(address.sin_port); + snprintf(writer_url, sizeof(writer_url), "http://127.0.0.1:%u/writer?token=%ld", port, + (long)getpid()); + snprintf(reader_url, sizeof(reader_url), "http://127.0.0.1:%u/reader?token=%ld", port, + (long)getpid()); + struct view writer, reader; + bool started_writer = start_view(&writer, argv[1], writer_url); + bool started_reader = started_writer && start_view(&reader, argv[1], reader_url); + bool shared = false; + for (unsigned attempt = 0; started_reader && attempt < 200; attempt++) { + struct pollfd item = {.fd = reader.frames, .events = POLLIN}; + if (poll(&item, 1, 100) <= 0) + continue; + uint8_t slot; + if (read(reader.frames, &slot, sizeof(slot)) != 1 || slot >= OWB_SHARED_FRAME_SLOTS) + continue; + uint8_t *frame = owb_shared_slot(reader.shared, slot); + size_t center = ((size_t)32 * 64 + 32) * 4; + uint8_t *bgra = frame + center; + if (bgra[0] < 55 && bgra[1] > 210 && bgra[2] < 35 && bgra[3] == 255) { + shared = true; + break; + } + } + int reader_status = started_reader ? stop_view(&reader) : 0; + int writer_status = started_writer ? stop_view(&writer) : 0; + kill(http_server, SIGTERM); + waitpid(http_server, NULL, 0); + if (!started_writer || !started_reader) + return 2; + if ((WIFEXITED(reader_status) && WEXITSTATUS(reader_status) == 5) || + (WIFEXITED(writer_status) && WEXITSTATUS(writer_status) == 5)) + return 77; + if (!shared) { + fprintf(stderr, "two renderer views did not share localStorage and BroadcastChannel\n"); + return 1; + } + return 0; +}