Support running Big Picture as separate process
Enable Big Picture mode to run concurrently with Desktop mode by: - Implementing cross-platform process launching (LaunchSiblingExecutable) for Windows, macOS, and Linux - Adding app profile system to isolate CEF user data between modes - Making window class/title and mutex names profile-aware - Changing mode switching to launch sibling executable instead of in-process switching - Updating settings.js to use native bridge for Big Picture launch - Adding build.bat helper for Windows builds
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
#include "include/wrapper/cef_helpers.h"
|
||||
#include "platform/browser_host.h"
|
||||
#include "platform/default_browser.h"
|
||||
#include "platform/process.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::app {
|
||||
@@ -243,6 +244,13 @@ void NebulaController::OnWindowCloseRequested() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (launch_options_.mode == AppMode::BigPicture && !closing_) {
|
||||
if (!SwitchToSiblingMode(AppMode::Desktop)) {
|
||||
BeginShutdown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
BeginShutdown();
|
||||
}
|
||||
|
||||
@@ -420,7 +428,8 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
|
||||
CloseMenuPopup();
|
||||
tabs_.LoadURL(nebula::ui::GetSettingsUrl());
|
||||
} else if (command == "big-picture") {
|
||||
EnterBigPictureMode();
|
||||
CloseMenuPopup();
|
||||
SwitchToSiblingMode(AppMode::BigPicture);
|
||||
} else if (command == "gpu-diagnostics") {
|
||||
CloseMenuPopup();
|
||||
tabs_.LoadURL(nebula::ui::GetGpuDiagnosticsUrl());
|
||||
@@ -488,7 +497,9 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
|
||||
BeginShutdown();
|
||||
} else if (command == "exit-bigpicture" && window_) {
|
||||
if (launch_options_.mode == AppMode::BigPicture) {
|
||||
BeginShutdown();
|
||||
if (!SwitchToSiblingMode(AppMode::Desktop)) {
|
||||
BeginShutdown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
ExitBigPictureMode();
|
||||
@@ -728,6 +739,23 @@ void NebulaController::ExitBigPictureMode() {
|
||||
ResizeBrowsers();
|
||||
}
|
||||
|
||||
bool NebulaController::SwitchToSiblingMode(AppMode mode) {
|
||||
#if defined(_WIN32)
|
||||
const char* executable =
|
||||
mode == AppMode::BigPicture ? "NebulaBigPicture.exe" : "NebulaBrowser.exe";
|
||||
#else
|
||||
const char* executable =
|
||||
mode == AppMode::BigPicture ? "NebulaBigPicture" : "NebulaBrowser";
|
||||
#endif
|
||||
|
||||
if (!nebula::platform::LaunchSiblingExecutable(executable)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BeginShutdown();
|
||||
return true;
|
||||
}
|
||||
|
||||
nebula::window::BrowserLayout NebulaController::CurrentBrowserLayout() const {
|
||||
if (!window_) {
|
||||
return {};
|
||||
|
||||
@@ -53,6 +53,7 @@ private:
|
||||
void CreateContentBrowser();
|
||||
void EnterBigPictureMode();
|
||||
void ExitBigPictureMode();
|
||||
bool SwitchToSiblingMode(AppMode mode);
|
||||
nebula::window::BrowserLayout CurrentBrowserLayout() const;
|
||||
void ToggleMenuPopup();
|
||||
void CloseMenuPopup();
|
||||
|
||||
@@ -122,6 +122,10 @@ int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options
|
||||
return subprocess_exit_code;
|
||||
}
|
||||
|
||||
// Isolate Big Picture from the desktop browser so both can run together.
|
||||
nebula::ui::SetAppProfile(
|
||||
options.mode == AppMode::BigPicture ? "bigpicture" : "desktop");
|
||||
|
||||
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
|
||||
nebula::platform::InitCommandLine(command_line, startup);
|
||||
std::string initial_url = GetLaunchTarget(command_line);
|
||||
|
||||
@@ -111,6 +111,7 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser
|
||||
const bool allowed_settings_command =
|
||||
IsSettingsFrame(frame) && (command == "navigate" ||
|
||||
command == "new-tab" ||
|
||||
command == "big-picture" ||
|
||||
command == "check-default-browser" ||
|
||||
command == "set-default-browser" ||
|
||||
command == "clear-site-history" ||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "platform/process.h"
|
||||
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
bool LaunchSiblingExecutable(const std::string& executable_name) {
|
||||
const auto directory = ExecutableDirectory();
|
||||
if (directory.empty() || executable_name.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path path = directory / executable_name;
|
||||
std::error_code error;
|
||||
if (!std::filesystem::exists(path, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
return false;
|
||||
}
|
||||
if (pid == 0) {
|
||||
execl(path.c_str(), path.c_str(), static_cast<char*>(nullptr));
|
||||
_exit(127);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "platform/process.h"
|
||||
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
namespace nebula::platform {
|
||||
namespace {
|
||||
|
||||
std::filesystem::path ResolveSiblingPath(const std::string& executable_name) {
|
||||
const auto directory = ExecutableDirectory();
|
||||
if (directory.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::filesystem::path same_dir = directory / executable_name;
|
||||
std::error_code error;
|
||||
if (std::filesystem::exists(same_dir, error)) {
|
||||
return same_dir;
|
||||
}
|
||||
|
||||
// NebulaBrowser.app/Contents/MacOS -> sibling NebulaBigPicture.app/Contents/MacOS
|
||||
const std::filesystem::path sibling_app =
|
||||
directory / ".." / ".." / ".." / (executable_name + ".app") / "Contents" / "MacOS" /
|
||||
executable_name;
|
||||
if (std::filesystem::exists(sibling_app, error)) {
|
||||
return sibling_app.lexically_normal();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool LaunchSiblingExecutable(const std::string& executable_name) {
|
||||
const std::filesystem::path path = ResolveSiblingPath(executable_name);
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
return false;
|
||||
}
|
||||
if (pid == 0) {
|
||||
execl(path.c_str(), path.c_str(), static_cast<char*>(nullptr));
|
||||
_exit(127);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace nebula::platform {
|
||||
|
||||
// Launches an executable that lives next to the current process.
|
||||
bool LaunchSiblingExecutable(const std::string& executable_name);
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -7,11 +7,22 @@
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::window {
|
||||
namespace {
|
||||
|
||||
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
|
||||
constexpr wchar_t kWindowTitle[] = L"Nebula Browser";
|
||||
const wchar_t* WindowClassName() {
|
||||
return nebula::ui::GetAppProfile() == "bigpicture"
|
||||
? L"NebulaBigPictureWindow"
|
||||
: L"NebulaBrowserWindow";
|
||||
}
|
||||
|
||||
const wchar_t* WindowTitle() {
|
||||
return nebula::ui::GetAppProfile() == "bigpicture"
|
||||
? L"Nebula Big Picture"
|
||||
: L"Nebula Browser";
|
||||
}
|
||||
constexpr wchar_t kChildFrameHitTestOldProcProp[] = L"NebulaChildFrameHitTestOldProc";
|
||||
constexpr wchar_t kChildFrameHitTestParentProp[] = L"NebulaChildFrameHitTestParent";
|
||||
constexpr ULONG_PTR kOpenTargetCopyDataId = 0x4E42554CUL;
|
||||
@@ -415,7 +426,7 @@ void nebula::window::NebulaWindowImpl::RegisterClass(HINSTANCE instance_handle)
|
||||
window_class.lpfnWndProc = StaticWndProc;
|
||||
window_class.hInstance = instance_handle;
|
||||
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
window_class.lpszClassName = kWindowClassName;
|
||||
window_class.lpszClassName = WindowClassName();
|
||||
|
||||
RegisterClassExW(&window_class);
|
||||
}
|
||||
@@ -522,8 +533,8 @@ bool NebulaWindow::Create(const platform::AppStartup& startup) {
|
||||
|
||||
impl_->hwnd = CreateWindowExW(
|
||||
0,
|
||||
kWindowClassName,
|
||||
kWindowTitle,
|
||||
WindowClassName(),
|
||||
WindowTitle(),
|
||||
WS_POPUP | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CLIPCHILDREN,
|
||||
x,
|
||||
y,
|
||||
@@ -654,7 +665,7 @@ void NebulaWindow::SetTitle(const std::string& title) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::wstring wide = title.empty() ? kWindowTitle : Utf8ToWide(title);
|
||||
const std::wstring wide = title.empty() ? WindowTitle() : Utf8ToWide(title);
|
||||
SetWindowTextW(impl_->hwnd, wide.c_str());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "platform/process.h"
|
||||
|
||||
#include "platform/paths_platform.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
namespace nebula::platform {
|
||||
namespace {
|
||||
|
||||
std::wstring Utf8ToWide(const std::string& utf8) {
|
||||
if (utf8.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int size = MultiByteToWideChar(
|
||||
CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), nullptr, 0);
|
||||
if (size <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring result(static_cast<size_t>(size), L'\0');
|
||||
MultiByteToWideChar(
|
||||
CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), result.data(), size);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool LaunchSiblingExecutable(const std::string& executable_name) {
|
||||
const auto directory = ExecutableDirectory();
|
||||
if (directory.empty() || executable_name.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring exe_name = Utf8ToWide(executable_name);
|
||||
if (exe_name.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path path = directory / exe_name;
|
||||
std::error_code error;
|
||||
if (!std::filesystem::exists(path, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring path_wide = path.wstring();
|
||||
const std::wstring dir_wide = directory.wstring();
|
||||
|
||||
STARTUPINFOW startup_info = {};
|
||||
startup_info.cb = sizeof(startup_info);
|
||||
PROCESS_INFORMATION process_info = {};
|
||||
|
||||
// CreateProcess requires a writable command line buffer.
|
||||
std::wstring command_line = L"\"" + path_wide + L"\"";
|
||||
const BOOL created = CreateProcessW(
|
||||
path_wide.c_str(),
|
||||
command_line.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
0,
|
||||
nullptr,
|
||||
dir_wide.c_str(),
|
||||
&startup_info,
|
||||
&process_info);
|
||||
if (!created) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseHandle(process_info.hThread);
|
||||
CloseHandle(process_info.hProcess);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace nebula::platform
|
||||
@@ -10,10 +10,20 @@
|
||||
namespace nebula::platform {
|
||||
namespace {
|
||||
|
||||
constexpr wchar_t kMainInstanceMutexName[] = L"Local\\NebulaBrowserMainInstance";
|
||||
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
|
||||
constexpr ULONG_PTR kOpenTargetCopyDataId = 0x4E42554CUL;
|
||||
|
||||
const wchar_t* MainInstanceMutexName() {
|
||||
return nebula::ui::GetAppProfile() == "bigpicture"
|
||||
? L"Local\\NebulaBigPictureMainInstance"
|
||||
: L"Local\\NebulaBrowserMainInstance";
|
||||
}
|
||||
|
||||
const wchar_t* WindowClassName() {
|
||||
return nebula::ui::GetAppProfile() == "bigpicture"
|
||||
? L"NebulaBigPictureWindow"
|
||||
: L"NebulaBrowserWindow";
|
||||
}
|
||||
|
||||
class ScopedHandle {
|
||||
public:
|
||||
explicit ScopedHandle(HANDLE handle) : handle_(handle) {}
|
||||
@@ -54,7 +64,7 @@ void ForwardLaunchTargetToExistingWindow(const std::string& launch_target) {
|
||||
return;
|
||||
}
|
||||
|
||||
HWND existing_window = FindWindowW(kWindowClassName, nullptr);
|
||||
HWND existing_window = FindWindowW(WindowClassName(), nullptr);
|
||||
if (!existing_window) {
|
||||
return;
|
||||
}
|
||||
@@ -81,7 +91,7 @@ void PrepareApp() {
|
||||
}
|
||||
|
||||
bool TryAcquireSingleInstance(const std::string& launch_target) {
|
||||
static ScopedHandle mutex(CreateMutexW(nullptr, TRUE, kMainInstanceMutexName));
|
||||
static ScopedHandle mutex(CreateMutexW(nullptr, TRUE, MainInstanceMutexName()));
|
||||
const bool already_running = mutex.valid() && GetLastError() == ERROR_ALREADY_EXISTS;
|
||||
if (already_running) {
|
||||
ForwardLaunchTargetToExistingWindow(launch_target);
|
||||
|
||||
@@ -109,8 +109,18 @@ const InternalPage* FindInternalPageByFileUrl(const std::string& url) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string g_app_profile;
|
||||
|
||||
} // namespace
|
||||
|
||||
void SetAppProfile(std::string_view profile) {
|
||||
g_app_profile = std::string(profile);
|
||||
}
|
||||
|
||||
const std::string& GetAppProfile() {
|
||||
return g_app_profile;
|
||||
}
|
||||
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
return nebula::platform::ExecutableDirectory();
|
||||
}
|
||||
@@ -125,6 +135,9 @@ std::filesystem::path GetUserDataDirectory() {
|
||||
}
|
||||
|
||||
std::filesystem::path user_data = root / "Nebula" / "User Data";
|
||||
if (!g_app_profile.empty() && g_app_profile != "desktop") {
|
||||
user_data = root / "Nebula" / ("User Data " + g_app_profile);
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(user_data, ec);
|
||||
return user_data;
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace nebula::ui {
|
||||
|
||||
// Process-wide profile used to isolate desktop vs Big Picture CEF data.
|
||||
// Call before GetUserDataDirectory() / CEF initialize. Empty/"desktop" = default.
|
||||
void SetAppProfile(std::string_view profile);
|
||||
const std::string& GetAppProfile();
|
||||
|
||||
std::filesystem::path GetExecutableDirectory();
|
||||
std::filesystem::path GetUserDataDirectory();
|
||||
std::filesystem::path GetCacheDirectory();
|
||||
|
||||
Reference in New Issue
Block a user