From eefdf6c037835fc7052f81c4775d00bb11ac6703 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Thu, 16 Apr 2026 15:30:50 -0700 Subject: [PATCH 1/2] Vendor CEFWebView Swift package for Chromium browser engine Phase 1 of an effort to replace WKWebView in BrowserPanel with a Chromium-backed engine via brennanMKE/CEFWebView. This commit only adds the vendored package and build infrastructure: no Xcode target wiring, no embedded framework, no helper apps yet. The cmux app is unchanged and the existing WKWebView path stays the default. What lands here: - vendor/CEFWebView/: package source (no .git, no demo WebView/, no Tests/) - Package.swift bumped to .macOS(.v14) to match cmux's deployment target - testTarget removed (Tests/ not vendored) - build_cpp.sh patched to use find -L so symlinked CEF distros work - scripts/setup-cefwebview.sh: idempotent script that downloads or symlinks a pinned CEF binary distribution into vendor/CEFWebView/CEF/, builds libcef_dll_wrapper.a, and produces vendor/CEFWebView/Frameworks/ - .gitignore: excludes the CEF binary distribution and built Frameworks/ - vendor/CEFWebView/INTEGRATION_NOTES.md: tracks remaining Xcode integration steps and the parity items still to port from BrowserPanel After scripts/setup-cefwebview.sh runs, the package compiles cleanly under swift build. Hooking it into GhosttyTabs.xcodeproj (framework embed phases, helper-app build phases, code signing) is Phase 2. --- .gitignore | 5 + scripts/setup-cefwebview.sh | 57 ++ vendor/CEFWebView/.gitattributes | 8 + vendor/CEFWebView/.gitignore | 11 + vendor/CEFWebView/IMPLEMENTATION_GUIDE.md | 792 +++++++++++++++++ vendor/CEFWebView/INTEGRATION_NOTES.md | 42 + vendor/CEFWebView/JIT.md | 66 ++ vendor/CEFWebView/LICENSE | 21 + vendor/CEFWebView/Package.swift | 100 +++ vendor/CEFWebView/README.md | 7 + vendor/CEFWebView/Sources/CEFHelper/main.mm | 16 + .../Sources/CEFHelperRenderer/main.mm | 24 + .../Sources/CEFWebView/CEFBridge.swift | 380 ++++++++ .../Sources/CEFWebView/CEFWebView.swift | 229 +++++ .../Sources/CEFWebView/CEFWebViewState.swift | 75 ++ .../Sources/CEFWrapper/CEFWrapper.mm | 808 ++++++++++++++++++ .../Sources/CEFWrapper/include/CEFWrapper.h | 91 ++ vendor/CEFWebView/build.sh | 208 +++++ vendor/CEFWebView/build_cpp.sh | 154 ++++ vendor/CEFWebView/capture_logs.sh | 99 +++ vendor/CEFWebView/diagnose_renderer.sh | 44 + vendor/CEFWebView/fix_cef_framework.sh | 34 + 22 files changed, 3271 insertions(+) create mode 100755 scripts/setup-cefwebview.sh create mode 100644 vendor/CEFWebView/.gitattributes create mode 100644 vendor/CEFWebView/.gitignore create mode 100644 vendor/CEFWebView/IMPLEMENTATION_GUIDE.md create mode 100644 vendor/CEFWebView/INTEGRATION_NOTES.md create mode 100644 vendor/CEFWebView/JIT.md create mode 100644 vendor/CEFWebView/LICENSE create mode 100644 vendor/CEFWebView/Package.swift create mode 100644 vendor/CEFWebView/README.md create mode 100644 vendor/CEFWebView/Sources/CEFHelper/main.mm create mode 100644 vendor/CEFWebView/Sources/CEFHelperRenderer/main.mm create mode 100644 vendor/CEFWebView/Sources/CEFWebView/CEFBridge.swift create mode 100644 vendor/CEFWebView/Sources/CEFWebView/CEFWebView.swift create mode 100644 vendor/CEFWebView/Sources/CEFWebView/CEFWebViewState.swift create mode 100644 vendor/CEFWebView/Sources/CEFWrapper/CEFWrapper.mm create mode 100644 vendor/CEFWebView/Sources/CEFWrapper/include/CEFWrapper.h create mode 100755 vendor/CEFWebView/build.sh create mode 100755 vendor/CEFWebView/build_cpp.sh create mode 100755 vendor/CEFWebView/capture_logs.sh create mode 100755 vendor/CEFWebView/diagnose_renderer.sh create mode 100755 vendor/CEFWebView/fix_cef_framework.sh diff --git a/.gitignore b/.gitignore index 071e93da9487..1588f2d6659c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,11 @@ __pycache__/ .zig-cache/ zig-out/ +# CEFWebView vendored package: CEF binary distribution + built frameworks +vendor/CEFWebView/CEF/ +vendor/CEFWebView/Frameworks/ +vendor/CEFWebView/.build/ + # Node node_modules/ .next/ diff --git a/scripts/setup-cefwebview.sh b/scripts/setup-cefwebview.sh new file mode 100755 index 000000000000..b8efe1d0a726 --- /dev/null +++ b/scripts/setup-cefwebview.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Set up the vendored CEFWebView Swift package: download CEF binaries, +# build the C++ wrapper, populate vendor/CEFWebView/Frameworks. +# +# Idempotent. Skips work that has already completed. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PKG_ROOT="$REPO_ROOT/vendor/CEFWebView" +CEF_ROOT="$PKG_ROOT/CEF" +FRAMEWORKS="$PKG_ROOT/Frameworks" + +# Pinned CEF version. Match Chromium 146 stable for Apple Silicon. +# To update: pick a build from https://cef-builds.spotifycdn.com/index.html#macosarm64 +CEF_VERSION="${CEF_VERSION:-146.0.5+g4db0d88+chromium-146.0.7680.65}" +CEF_PLATFORM="${CEF_PLATFORM:-macosarm64}" +CEF_DIST="cef_binary_${CEF_VERSION}_${CEF_PLATFORM}_beta" +CEF_TARBALL="${CEF_DIST}.tar.bz2" +CEF_URL="https://cef-builds.spotifycdn.com/${CEF_TARBALL//+/%2B}" + +# If a sibling cef-swift-mvp checkout already has the same distribution +# extracted, reuse it instead of re-downloading hundreds of MB. +SIBLING_CEF="$REPO_ROOT/../../cef-swift-mvp/third_party/cef/${CEF_DIST}" + +mkdir -p "$CEF_ROOT" + +if [ -d "$CEF_ROOT/$CEF_DIST" ]; then + echo "✓ CEF $CEF_VERSION already extracted in vendor/CEFWebView/CEF/" +elif [ -d "$SIBLING_CEF" ]; then + echo "↪︎ Linking CEF distribution from $SIBLING_CEF" + ln -sfn "$SIBLING_CEF" "$CEF_ROOT/$CEF_DIST" +else + echo "⬇︎ Downloading CEF $CEF_VERSION ($CEF_PLATFORM)..." + echo " $CEF_URL" + TMP_TARBALL="$CEF_ROOT/$CEF_TARBALL" + curl -fL --retry 3 -o "$TMP_TARBALL" "$CEF_URL" + tar -xjf "$TMP_TARBALL" -C "$CEF_ROOT" + rm -f "$TMP_TARBALL" +fi + +# Build libcef_dll_wrapper.a if missing. +WRAPPER_LIB="$CEF_ROOT/$CEF_DIST/libcef_dll_wrapper/Release/libcef_dll_wrapper.a" +if [ ! -f "$WRAPPER_LIB" ]; then + echo "🔨 Building libcef_dll_wrapper..." + ( + cd "$CEF_ROOT/$CEF_DIST" + cmake -G Xcode -DPROJECT_ARCH=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 . >/dev/null + xcodebuild -configuration Release -target libcef_dll_wrapper -quiet + ) +fi + +# Restructure into Frameworks/ for Xcode embedding. +SRCROOT="$PKG_ROOT" "$PKG_ROOT/build_cpp.sh" + +echo "" +echo "✅ CEFWebView is ready at $PKG_ROOT/Frameworks" +echo " Next: run scripts/reload.sh --tag once Xcode integration is wired." diff --git a/vendor/CEFWebView/.gitattributes b/vendor/CEFWebView/.gitattributes new file mode 100644 index 000000000000..7a527e8a122a --- /dev/null +++ b/vendor/CEFWebView/.gitattributes @@ -0,0 +1,8 @@ +*.sh text +*.swift text +*.py text +*.md text +*.pdf binary +*.jpg binary +*.jpeg binary +*.png binary diff --git a/vendor/CEFWebView/.gitignore b/vendor/CEFWebView/.gitignore new file mode 100644 index 000000000000..c8d5a8b2da25 --- /dev/null +++ b/vendor/CEFWebView/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +default.profraw +build/ +.build/ +xcuserdata/ +logs/ + +# CEF +CEF/ +Frameworks +build_cef_dll/ diff --git a/vendor/CEFWebView/IMPLEMENTATION_GUIDE.md b/vendor/CEFWebView/IMPLEMENTATION_GUIDE.md new file mode 100644 index 000000000000..c07f38deb1c3 --- /dev/null +++ b/vendor/CEFWebView/IMPLEMENTATION_GUIDE.md @@ -0,0 +1,792 @@ +# CEFWebView: Implementation Guide + +**Version:** 1.0 (Package Migration Phase) +**Last Updated:** 2026-04-14 +**Status:** In Progress - Core Migration Complete + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture](#architecture) +3. [Getting Started](#getting-started) +4. [Building the Package](#building-the-package) +5. [Integration with Apps](#integration-with-apps) +6. [Helper App Bundles](#helper-app-bundles) +7. [Multi-Process vs Single-Process](#multi-process-vs-single-process) +8. [Public API Reference](#public-api-reference) +9. [Troubleshooting](#troubleshooting) +10. [Known Issues](#known-issues) + +--- + +## Overview + +CEFWebView is a **Swift package** that brings the Chromium Embedded Framework (CEF) to macOS SwiftUI applications. It provides a drop-in replacement for WKWebView with support for: + +- **Chromium rendering engine** - Full WebRTC, V8 JavaScript, modern web standards +- **Multi-process architecture** - Separate GPU, renderer, and utility processes for stability +- **SwiftUI integration** - `NSViewRepresentable`-based `CEFWebView` view +- **Observable state** - `CEFWebViewState` with `@Observable` for reactive updates +- **C++ API** - Uses CEF C++ bindings (not C API) for automatic memory management + +### Why CEFWebView Over WKWebView? + +| Feature | WKWebView | CEFWebView | +|---------|-----------|-----------| +| **Chromium Extensions** | ❌ Not supported | ✅ Full support (future) | +| **WebRTC** | ⚠️ Limited | ✅ Full | +| **V8 JavaScript** | ❌ JavaScriptCore | ✅ V8 engine | +| **DevTools** | ❌ Limited | ✅ Remote DevTools protocol | +| **Custom PDF Rendering** | ❌ System PDF | ✅ PDFium | +| **Customizable UI** | ⚠️ Limited | ✅ Full control | +| **Process Model** | System-managed | ✅ Custom multi-process | + +--- + +## Architecture + +### Package Structure + +``` +CEFWebView/ +├── Package.swift # Swift Package definition +├── build_cpp.sh # Builds CEF C++ wrapper +├── fix_cef_framework.sh # Fixes symlinks after embedding +├── Frameworks/ # CEF binaries (git-ignored) +│ ├── Chromium Embedded Framework.framework/ +│ ├── libcef_dll_wrapper.a +│ └── include/ # CEF C++ headers +│ +└── Sources/ + ├── CEFWrapper/ # Objective-C++ target (private) + │ ├── include/ + │ │ └── CEFWrapper.h # Public ObjC interface + │ └── CEFWrapper.mm # C++ implementation + │ + ├── CEFWebView/ # Swift library (public) + │ ├── CEFWebView.swift # NSViewRepresentable view + │ ├── CEFWebViewState.swift # @Observable state + │ └── CEFBridge.swift # CEF lifecycle manager + │ + ├── CEFHelper/ # Executable (GPU, utility) + │ └── main.c # Subprocess entry point + │ + └── CEFHelperRenderer/ # Executable (renderer) + └── main.c # Renderer subprocess entry +``` + +### Process Architecture + +``` +ChromiumWebView.app (Main Process) +├── Runs CEFApplication (message pump, lifecycle) +├── Creates CEFBrowserHost (wraps CEF browser instance) +└── Launches helper subprocesses: + ├── CEFHelper (--type=gpu-process) → GPU rendering + ├── CEFHelper (--type=utility --utility-sub-type=network.mojom.NetworkService) → Network + ├── CEFHelper (--type=utility --utility-sub-type=storage.mojom.StorageService) → Storage + └── CEFHelperRenderer (--type=renderer) → Page rendering +``` + +### Data Flow + +``` +SwiftUI View (ContentView) + ↓ +@State CEFWebViewState + ↓ +CEFWebView (NSViewRepresentable) + ↓ +CEFApplication (singleton, manages CEF lifecycle) + ↓ +CEFBrowserHost (wraps CEF browser) + ↓ +ChromiumClient (C++ class, receives callbacks) + ↓ +Chromium Embedded Framework.framework + ↓ +Subprocess (renderer, GPU, network) +``` + +### Why C++ API Instead of C API? + +CEFWebView uses the **CEF C++ API** instead of the C API for critical memory safety reasons: + +- **C API**: Manual ref-counting with offset arithmetic. One mistake = `free()` crash on wrong pointer. +- **C++ API**: `CefRefPtr` smart pointers handle ref-counting automatically via `IMPLEMENT_REFCOUNTING` macro. + +**Result**: Certain categories of memory management bugs are structurally impossible. See `CEF_API_MIGRATION.md` for details. + +--- + +## Getting Started + +### Prerequisites + +1. **macOS 15+** (Swift 6 required) +2. **Xcode 16+** with Swift 6 language mode +3. **CEF Binaries** - Downloaded and built via `build_cpp.sh` +4. **Git** - For cloning the repo + +### Quick Start (Using CEFWebView Package) + +1. **Add as a Swift package dependency:** + ```swift + // In your app's Package.swift or Xcode project + .package(path: "../CEFWebView") + ``` + +2. **Import in your Swift code:** + ```swift + import CEFWebView + + struct ContentView: View { + @State private var url: URL? = URL(string: "https://google.com") + @State private var webState = CEFWebViewState() + + var body: some View { + CEFWebView(url: $url, state: $webState) + } + } + ``` + +3. **Build the package:** + ```bash + cd CEFWebView + ./build_cpp.sh # Populates Frameworks/ with CEF binaries + swift build # Builds CEFWrapper, CEFWebView, and helpers + ``` + +4. **In your Xcode app project:** + - Add `import CEFWebView` to your Swift files + - See "Integration with Apps" section for helper app bundle setup + +--- + +## Building the Package + +### Step 1: Populate CEF Binaries + +The `build_cpp.sh` script: +1. Finds the CEF binary distribution in `./CEF/cef_binary_*/` +2. Builds the C++ wrapper library (`libcef_dll_wrapper.a`) +3. Copies the framework and headers to `./Frameworks/` +4. Restructures the framework to macOS versioned layout + +**Run:** +```bash +cd CEFWebView +./build_cpp.sh +``` + +**Expected output:** +``` +🚀 Building CEFWebView CEF dependencies... +📍 Found CEF at: /path/to/CEFWebView/CEF/cef_binary_123.0.0_macos_arm64 +🔨 Building CEF C++ wrapper... +✓ CEF C++ wrapper built +📦 Copying static library and headers... +✓ Copied libcef_dll_wrapper.a and headers +📚 Copying dynamic Chromium Embedded Framework... +✓ Copied Chromium Embedded Framework to Frameworks/ +✅ Build complete! Frameworks/ is ready for Xcode. +``` + +### Step 2: Build Swift Package + +```bash +swift build -c release # or 'debug' for development +``` + +**What gets built:** +- `libcef_dll_wrapper.a` is linked into the CEFWrapper target +- `CEFWrapper.mm` compiles to Objective-C++ object files +- Swift stdlib wraps it as the `CEFWebView` library product +- `CEFHelper` and `CEFHelperRenderer` executables compiled + +### Step 3: Verify Build + +```bash +# Check framework was created +ls -la Frameworks/Chromium\ Embedded\ Framework.framework + +# Check static library +ls -la Frameworks/libcef_dll_wrapper.a + +# Check headers +ls -la Frameworks/include/ | head -20 +``` + +--- + +## Integration with Apps + +### For Xcode App Projects + +#### 1. Add Package Dependency + +In Xcode: +- File → Add Packages +- Enter path: `../CEFWebView` (or absolute path) +- Select "Add to Target: YourApp" + +Or in `Package.swift`: +```swift +dependencies: [ + .package(path: "../CEFWebView") +] +``` + +#### 2. Update Your App Code + +**AppDelegate (lifecycle):** +```swift +import SwiftUI +import CEFWebView + +@main +struct YourApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} + +final class AppDelegate: NSObject, NSApplicationDelegate { + func applicationWillTerminate(_ notification: Notification) { + CEFApplication.shared.shutdown() + } +} +``` + +**ContentView (UI):** +```swift +import SwiftUI +import CEFWebView + +struct ContentView: View { + @State private var url: URL? = URL(string: "https://google.com") + @State private var webState = CEFWebViewState() + + var body: some View { + VStack(spacing: 0) { + // Navigation toolbar + HStack { + Button(action: { webState.goBack() }) { + Image(systemName: "chevron.left") + } + .disabled(!webState.canGoBack) + + Button(action: { webState.goForward() }) { + Image(systemName: "chevron.right") + } + .disabled(!webState.canGoForward) + + TextField("URL", text: .constant(url?.absoluteString ?? "")) + .onSubmit { + if let newURL = URL(string: urlInput) { + url = newURL + } + } + } + .padding() + + // Web view + CEFWebView(url: $url, state: $webState) + .background(.white) + } + } +} +``` + +#### 3. Configure Build Settings + +**Build.xcconfig** (in your app project): +```bash +// Point to CEFWebView package Frameworks directory +FRAMEWORK_SEARCH_PATHS = $(SRCROOT)/../CEFWebView/Frameworks + +// Swift settings +SWIFT_VERSION = 6.0 +SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor + +// Allow build phase scripts to read $SRCROOT +ENABLE_USER_SCRIPT_SANDBOXING = NO + +// Entitlements for JIT and Mach IPC +CODE_SIGN_ENTITLEMENTS = YourApp/YourApp.entitlements +``` + +**Entitlements (YourApp.entitlements):** +```xml + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.get-task-allow + + + +``` + +#### 4. Create Helper App Bundles (Build Phase) + +Add a "Run Script" build phase in Xcode to create the helper app structures. + +**Script:** +```bash +# Get the built helper executables from SPM build directory +HELPER_EXEC="${BUILD_DIR}/Release/CEFHelper" +RENDERER_EXEC="${BUILD_DIR}/Release/CEFHelperRenderer" + +# Determine app framework path +FRAMEWORKS="${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/../Frameworks" + +# Create CEFHelper.app bundle +HELPER_APP="${FRAMEWORKS}/CEFHelper.app" +mkdir -p "${HELPER_APP}/Contents/MacOS" +cp "${HELPER_EXEC}" "${HELPER_APP}/Contents/MacOS/CEFHelper" +chmod +x "${HELPER_APP}/Contents/MacOS/CEFHelper" + +# Create Info.plist for CEFHelper +cat > "${HELPER_APP}/Contents/Info.plist" < + + + + CFBundleExecutable + CEFHelper + CFBundleIdentifier + com.yourcompany.yourapp.helper + CFBundleVersion + 1.0 + + +EOF + +# Create CEFHelper (Renderer).app bundle +RENDERER_APP="${FRAMEWORKS}/CEFHelper (Renderer).app" +mkdir -p "${RENDERER_APP}/Contents/MacOS" +cp "${RENDERER_EXEC}" "${RENDERER_APP}/Contents/MacOS/CEFHelper" +chmod +x "${RENDERER_APP}/Contents/MacOS/CEFHelper" + +# Create Info.plist for renderer +cat > "${RENDERER_APP}/Contents/Info.plist" < + + + + CFBundleExecutable + CEFHelper + CFBundleIdentifier + com.yourcompany.yourapp.helper.renderer + CFBundleVersion + 1.0 + + +EOF + +echo "✓ Helper app bundles created" +``` + +#### 5. Add Framework Symlink Fix Phase + +Add a second "Run Script" build phase **after** "Embed Frameworks": + +```bash +# Call the package's framework fix script +"${SRCROOT}/../CEFWebView/fix_cef_framework.sh" +``` + +--- + +## Helper App Bundles + +### Why Separate Helper Apps? + +On macOS, CEF requires subprocess helper executables to follow the standard app bundle naming convention: + +``` +MyApp.app/Contents/Frameworks/ +├── MyApp Helper.app/Contents/MacOS/MyApp Helper (GPU, utility, network) +└── MyApp Helper (Renderer).app/Contents/MacOS/MyApp Helper (renderer) +``` + +**Why this matters:** +- CEF detects process roles via `--type=` command-line arguments +- Each process type may have different requirements (renderer needs specialized resource handling) +- macOS app signing and code injection protections expect this structure +- Old pattern (reusing main executable) fails silently on renderer process + +### Structure + +**CEFHelper.app:** +``` +Contents/ +├── MacOS/ +│ └── CEFHelper (executable, 20-30MB) +└── Info.plist (minimal, BundleIdentifier only) +``` + +**CEFHelper (Renderer).app:** +``` +Contents/ +├── MacOS/ +│ └── CEFHelper (same or different executable) +└── Info.plist (minimal, BundleIdentifier only) +``` + +**Note:** Both can point to the same executable or different ones. CEF detects the role from command-line `--type=` argument, not the executable name. + +--- + +## Multi-Process vs Single-Process + +### Multi-Process Mode (Default) + +**Enabled by:** `CEF_MULTI_PROCESS` compilation condition in `Package.swift` + +**Configuration in CEFWrapper.mm:** +```objc +#ifdef CEF_MULTI_PROCESS + settings.single_process = 0; + + // Point to helper app bundle for subprocess launches + NSString* helperPath = [[[NSBundle mainBundle] bundlePath] + stringByAppendingPathComponent:@"Contents/Frameworks/CEFHelper.app/Contents/MacOS/CEFHelper"]; + CefString(&settings.browser_subprocess_path).FromString([helperPath UTF8String]); +#else + settings.single_process = 1; +#endif +``` + +**Advantages:** +- ✅ Stability - crash in one process doesn't crash the app +- ✅ Performance - GPU and network in separate processes +- ✅ Security - renderer process has limited access +- ✅ Production-ready + +**Disadvantages:** +- ⚠️ More complex setup (helper app bundles required) +- ⚠️ IPC overhead +- ⚠️ Higher memory usage + +### Single-Process Mode + +**To use:** Remove `CEF_MULTI_PROCESS` from `Package.swift` build settings + +**Advantages:** +- ✅ Simpler setup - no helper app bundles +- ✅ Lower memory +- ✅ Faster development/debugging + +**Disadvantages:** +- ❌ Stability risk - one crash kills the whole app +- ❌ Performance worse (no separate GPU process) +- ❌ Security risk (no process isolation) +- ❌ Not recommended for production + +--- + +## Public API Reference + +### CEFWebView (View) + +The main SwiftUI view for embedding Chromium. + +```swift +public struct CEFWebView: NSViewRepresentable { + @Binding var url: URL? + @Binding var state: CEFWebViewState + + public init(url: Binding, state: Binding) + + public func makeNSView(context: Context) -> NSView + public func updateNSView(_ nsView: NSView, context: Context) + public func makeCoordinator() -> Coordinator + public static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) +} +``` + +**Usage:** +```swift +@State private var url: URL? = URL(string: "https://google.com") +@State private var webState = CEFWebViewState() + +var body: some View { + CEFWebView(url: $url, state: $webState) +} +``` + +### CEFWebViewState (Observable) + +Reactive state for the browser instance. + +```swift +@Observable +public final class CEFWebViewState { + public var isLoading: Bool + public var estimatedProgress: Double + public var title: String? + public var currentURL: URL? + public var canGoBack: Bool + public var canGoForward: Bool + + public func reload() + public func goBack() + public func goForward() +} +``` + +**Usage:** +```swift +// Read state +if webState.isLoading { + ProgressView() +} + +Text(webState.title ?? "Loading...") + +// Perform actions +Button("Back") { webState.goBack() } + .disabled(!webState.canGoBack) +``` + +### CEFWrapper (Objective-C Interface) + +Low-level wrapper for CEF C++ API. Typically not used directly by Swift code. + +```objc +@interface CEFWrapper : NSObject + ++ (BOOL)initializeCEFWithError:(NSError **)error; ++ (nullable NSView *)createBrowserInView:(NSView *)parentView + url:(NSString *)urlString; ++ (void)loadURL:(NSString *)urlString; ++ (void)goBack; ++ (void)goForward; ++ (void)reloadBrowser; ++ (void)closeBrowser; ++ (void)doMessageLoopWork; ++ (void)shutdown; + ++ (BOOL)isLoading; ++ (BOOL)canGoBack; ++ (BOOL)canGoForward; ++ (nullable NSString *)currentTitle; ++ (nullable NSString *)currentURL; + +@end +``` + +### CEFBridge (Swift Private) + +Internal wrapper for CEF browser lifecycle and state management. + +```swift +@MainActor +final class CEFApplication { + static let shared = CEFApplication() + func initialize() throws + func shutdown() +} + +@MainActor +public final class CEFBrowserHost { + public init(parentView: NSView, url: URL, state: CEFWebViewState?) throws + func loadURL(_ url: URL) + func reload() + func goBack() + func goForward() + func close() +} +``` + +--- + +## Troubleshooting + +### "Framework not found: Chromium Embedded Framework" + +**Cause:** `build_cpp.sh` hasn't been run, so `Frameworks/` doesn't exist. + +**Fix:** +```bash +cd CEFWebView +./build_cpp.sh +``` + +### "CEF not initialized — cannot create browser" + +**Cause:** `CEFApplication.shared.initialize()` hasn't been called, or failed silently. + +**Fix:** Check logs for CEF initialization errors: +```bash +cat ~/Library/Caches/com.chromium.webview/debug.log +``` + +### Browser window is blank (white page) + +**Possible causes:** +1. Renderer process not spawning +2. Helper app bundles not created correctly +3. Framework symlinks broken + +**Debug:** +```bash +# Check if processes are spawning +ps aux | grep CEFHelper + +# Check logs +tail -f ~/Library/Caches/com.chromium.webview/debug.log + +# Run the framework fix script +./fix_cef_framework.sh +``` + +### "Mach-O, but wrong architecture" + +**Cause:** CEF binaries compiled for wrong architecture (Intel vs ARM). + +**Fix:** Rebuild CEF binaries matching your Mac: +```bash +# Check your architecture +uname -m # arm64 or x86_64 + +# In build_cpp.sh, adjust cmake line: +cmake -G "Xcode" -DPROJECT_ARCH="arm64" . # for Apple Silicon +# or +cmake -G "Xcode" -DPROJECT_ARCH="x86_64" . # for Intel +``` + +### Xcode: "Module not found: CEFWebView" + +**Cause:** Package dependency not properly configured. + +**Fix:** +1. In Xcode, delete derived data: `Cmd+Shift+K` +2. Verify `Package.swift` has correct dependency path +3. Run `swift package resolve` in CEFWebView directory +4. Rebuild: `Cmd+Shift+K`, then `Cmd+B` + +### Runtime crash on `CefInitialize` + +**Cause:** Settings not properly configured, or CEF version mismatch. + +**Fix:** +1. Check CEF version matches headers in `Frameworks/include/` +2. Verify `libcef_dll_wrapper.a` was built successfully +3. Check CEF debug log for DCHECK failures +4. Ensure entitlements are set (JIT requirements) + +--- + +## Known Issues + +### Issue 1: Renderer Process Not Spawning + +**Status:** Under Investigation +**Symptom:** Google.com renders blank, but other sites load partially +**Root Cause:** Likely missing or incorrect renderer helper app bundle + +**Workaround:** Use single-process mode (slower, but works): +```swift +// In Package.swift, remove CEF_MULTI_PROCESS define +``` + +**Tracking:** See `CEF_Usage.md` and `Status.md` for details. + +### Issue 2: Framework Symlinks Break After Xcode Embedding + +**Status:** FIXED (via `fix_cef_framework.sh`) +**Symptom:** Build succeeds but app crashes on launch with framework not found +**Cause:** Xcode's "Embed Frameworks" phase expands symlinks to copies + +**Fix:** Add build phase script: +```bash +"${SRCROOT}/../CEFWebView/fix_cef_framework.sh" +``` + +### Issue 3: High Memory Usage + +**Status:** Expected +**Explanation:** Chromium + separate processes = ~100-200MB minimum + +**Mitigation:** +- Use single-process mode if memory-constrained +- Profile with Instruments to find leaks +- Consider WebKit (WKWebView) if Chromium features not needed + +### Issue 4: Code Signing Issues + +**Status:** Needs Testing +**Possible Issue:** Helper app bundles must be signed with same identity + +**Mitigation:** Ensure build settings use consistent signing identity across all targets. + +--- + +## References + +### Documentation Files + +- **PackageMigration.md** - High-level package migration plan +- **MIGRATION_STATUS.md** - Current migration phase status +- **CEF_API_MIGRATION.md** - Why we use C++ API instead of C API +- **CEF_Usage.md** - CEF configuration details and renderer process setup +- **Status.md** - Current app status and known issues (from app project) + +### External Resources + +- [CEF General Usage](https://chromiumembedded.github.io/cef/general_usage.html) +- [CEF macOS Wiki](https://github.com/dliw/fpCEF3/wiki/macOS) +- [CefSettings API Reference](https://cef-builds.spotifycdn.com/docs/114.2/structcef__settings__t.html) + +--- + +## Next Steps + +### For Development + +1. ✅ Package structure created +2. ✅ CEFWrapper migrated to package +3. ✅ Helper executables defined +4. ⏳ **TODO:** Test helper app bundle creation +5. ⏳ **TODO:** Verify renderer process spawning +6. ⏳ **TODO:** Performance and memory profiling +7. ⏳ **TODO:** Add JavaScript bridge (evaluateJavaScript) +8. ⏳ **TODO:** Add custom certificate handling +9. ⏳ **TODO:** Add DevTools remote debugging support + +### For Production + +1. ⏳ Code signing and notarization +2. ⏳ App Store submission testing +3. ⏳ Performance optimization +4. ⏳ Documentation and examples +5. ⏳ Automated CI/CD builds + +--- + +## Contributing + +When working on CEFWebView: + +1. **Always run `build_cpp.sh`** after pulling changes to ensure Frameworks/ is in sync +2. **Test both single and multi-process modes** - toggle `CEF_MULTI_PROCESS` in Package.swift +3. **Check CEF logs** - `~/Library/Caches/com.chromium.webview/debug.log` +4. **Profile memory** - Use Instruments to check for leaks +5. **Document changes** - Update relevant `.md` files + +--- + +**For questions or issues, refer to the issue tracker or check the CEF documentation linked above.** diff --git a/vendor/CEFWebView/INTEGRATION_NOTES.md b/vendor/CEFWebView/INTEGRATION_NOTES.md new file mode 100644 index 000000000000..a5904fbe2bee --- /dev/null +++ b/vendor/CEFWebView/INTEGRATION_NOTES.md @@ -0,0 +1,42 @@ +# CEFWebView vendoring notes + +cmux vendors [brennanMKE/CEFWebView](https://github.com/brennanMKE/CEFWebView) as a starting point for a Chromium-backed browser engine that can replace WKWebView in `Sources/Panels/BrowserPanel.swift` over time. + +This file tracks the local divergence from upstream and what remains before CEF actually loads inside cmux. + +## What ships in this PR (Phase 1) + +- `vendor/CEFWebView/` — vendored copy of the package (no `.git`, no demo `WebView/` app, no `Tests/`). +- `scripts/setup-cefwebview.sh` — idempotent script that downloads or links a CEF binary distribution into `vendor/CEFWebView/CEF/`, builds `libcef_dll_wrapper.a`, and produces `vendor/CEFWebView/Frameworks/`. Reuses a sibling `cef-swift-mvp/third_party/cef/` checkout when present. +- `.gitignore` excludes `vendor/CEFWebView/CEF/`, `Frameworks/`, and `.build/` (hundreds of MB of binaries). + +The package builds cleanly via `cd vendor/CEFWebView && swift build` after `scripts/setup-cefwebview.sh` runs. + +## Local patches vs upstream + +1. `vendor/CEFWebView/Package.swift` + - `platforms: [.macOS(.v14)]` (was `.v15`) so cmux's `MACOSX_DEPLOYMENT_TARGET = 14.0` matches. + - Removed the `CEFWebViewTests` test target (the upstream `Tests/` directory isn't vendored). +2. `vendor/CEFWebView/build_cpp.sh` + - `find -L` so the `CEF/` symlink to a sibling checkout is followed. + +## What's still missing before cmux can load chromium + +The `cmux` Xcode target (`GhosttyTabs.xcodeproj`) is not yet wired to CEFWebView. To finish the integration: + +1. Add the local Swift package to `GhosttyTabs.xcodeproj`: + - `XCLocalSwiftPackageReference` with `relativePath = vendor/CEFWebView` + - `XCSwiftPackageProductDependency` with `productName = CEFWebView` + - Reference both from the `cmux` target's `packageProductDependencies` and `Frameworks` build phase +2. Build settings on the cmux target: + - `FRAMEWORK_SEARCH_PATHS = $(SRCROOT)/vendor/CEFWebView/Frameworks` + - `LIBRARY_SEARCH_PATHS = $(SRCROOT)/vendor/CEFWebView/Frameworks` + - `LD_RUNPATH_SEARCH_PATHS` includes `@executable_path/../Frameworks` +3. Embed phase: copy `vendor/CEFWebView/Frameworks/Chromium Embedded Framework.framework` into `cmux.app/Contents/Frameworks/` with code-sign-on-copy. +4. Run-script phases (after Embed): + - Create `cmux Helper.app` and `cmux Helper (Renderer).app` in `Contents/Frameworks/` from `CEFHelper`/`CEFHelperRenderer` SPM products. See CEFWebView's `IMPLEMENTATION_GUIDE.md` for the exact bash. + - Run `vendor/CEFWebView/fix_cef_framework.sh` to repair symlinks Xcode flattens during embedding. +5. Entitlements: cmux already has `com.apple.security.cs.allow-jit`, `allow-unsigned-executable-memory`, and `disable-library-validation`; helper apps need a matching minimal entitlements file (see `cmux-helper.entitlements`). +6. Swift integration: introduce a `Sources/BrowserEngine/` module that exposes a CEF-backed alternative to `BrowserPanel`'s `WKWebView`, gated behind a Debug menu toggle until parity is reached. The high-risk parity items (profile isolation, WebAuthn bridge, OAuth `window.opener`, SSO/MDM auth challenges) are inventoried in this PR description. + +Earlier branches `task-cef-alloy`, `task-owl-chromium`, and `task-raw-chromium` attempted similar swaps with a custom `cef_bridge.cpp` and crashed on right-click teardown / view close. Switching to CEFWebView's package layout with proper multi-process helper apps is intended to address those crash classes. diff --git a/vendor/CEFWebView/JIT.md b/vendor/CEFWebView/JIT.md new file mode 100644 index 000000000000..eeab558dd49c --- /dev/null +++ b/vendor/CEFWebView/JIT.md @@ -0,0 +1,66 @@ +# macOS JIT Entitlements for Chromium V8 + +## Problem + +Chromium's V8 JavaScript engine requires **JIT (Just-In-Time) compilation**, which allocates memory with write+execute permissions. On macOS with Hardened Runtime, this is blocked by default, causing the renderer process to crash with `EXC_BAD_ACCESS (SIGSEGV)` the instant V8 initializes. + +**Symptoms:** +- Browser loads but pages show blank/empty content +- No errors or logs from the renderer process +- Crash report shows `CrBrowserMain` thread crashing in V8 code (`_v8_internal_Node_Print`) +- Page "loads" but takes <1 second with empty title + +## Solution + +Add a **Code Signing Entitlements** file to the Xcode target with three required entitlements: + +### File: `ChromiumWebView/ChromiumWebView.entitlements` + +```xml + + + + + + com.apple.security.cs.allow-jit + + + com.apple.security.cs.allow-unsigned-executable-memory + + + com.apple.security.get-task-allow + + + +``` + +### Xcode Configuration + +1. Select the **ChromiumWebView** target +2. Go to **Build Settings** +3. Search for **"Code Signing Entitlements"** +4. Set the value to: `ChromiumWebView/ChromiumWebView.entitlements` +5. Apply to **both Debug and Release** configurations + +### Why Each Entitlement + +| Entitlement | Purpose | +|---|---| +| `com.apple.security.cs.allow-jit` | Allows V8 to allocate memory with write+execute permissions for JIT code generation. **Critical for JavaScript execution.** | +| `com.apple.security.cs.allow-unsigned-executable-memory` | Covers edge cases in V8's interpreter and JIT boundary conditions. V8 sometimes writes executable code outside the main JIT region. | +| `com.apple.security.get-task-allow` | Enables Mach task port access between processes. Fixes "Unable to obtain a task name port right" errors from CEF helper processes. Also required for debugger attachment in development builds. | + +## Testing + +After adding entitlements and rebuilding: + +1. Browser should render pages correctly +2. Console logs should show page title changes +3. Crash reports should no longer show V8 crashes +4. No "Unable to obtain a task name port right" errors + +## References + +- [Apple Security and Hardened Runtime](https://developer.apple.com/documentation/security) +- [V8 JIT on macOS](https://v8.dev/docs/build-gn) +- [Chromium on macOS](https://chromium.googlesource.com/chromium/src/+/main/docs/mac_build_instructions.md) diff --git a/vendor/CEFWebView/LICENSE b/vendor/CEFWebView/LICENSE new file mode 100644 index 000000000000..72818c7fa6bc --- /dev/null +++ b/vendor/CEFWebView/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Brennan Stehling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/CEFWebView/Package.swift b/vendor/CEFWebView/Package.swift new file mode 100644 index 000000000000..b0e978c8d063 --- /dev/null +++ b/vendor/CEFWebView/Package.swift @@ -0,0 +1,100 @@ +// swift-tools-version: 6.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import Foundation +import PackageDescription + +/// Absolute path so `swift build` can link `-lcef_dll_wrapper` for helper executables (relative `-L` breaks). +private let cefFrameworksDirectory: String = { + let packageDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent() + return packageDir.appendingPathComponent("Frameworks", isDirectory: true).path +}() + +let package = Package( + name: "CEFWebView", + platforms: [.macOS(.v14)], + products: [ + .library( + name: "CEFWebView", + targets: ["CEFWebView"] + ), + // Built by SPM for embedding in the host app bundle (see IMPLEMENTATION_GUIDE.md). + .executable(name: "CEFHelper", targets: ["CEFHelper"]), + .executable(name: "CEFHelperRenderer", targets: ["CEFHelperRenderer"]), + ], + targets: [ + // MARK: - CEFWrapper: ObjC++ bridge to CEF C++ API + .target( + name: "CEFWrapper", + publicHeadersPath: "include", + cxxSettings: [ + .headerSearchPath("../../Frameworks/include"), + .headerSearchPath("../../Frameworks"), + .unsafeFlags(["-std=c++20"]) + ], + linkerSettings: [ + .linkedLibrary("cef_dll_wrapper"), + .linkedFramework("Chromium Embedded Framework") + // -L for libcef_dll_wrapper.a: SwiftPM resolves when building the package; Xcode apps should + // set LIBRARY_SEARCH_PATHS to the package Frameworks dir (see WebView/Configuration/Build.xcconfig). + // Do not add -L../../Frameworks or -rpath here — Xcode forwards unsafeFlags to the app linker + // with the wrong cwd and duplicates LD_RUNPATH on the app target. + ] + ), + + // MARK: - CEFWebView: Swift library target (public API) + .target( + name: "CEFWebView", + dependencies: ["CEFWrapper"], + swiftSettings: [ + .defaultIsolation(MainActor.self) + ] + ), + + // MARK: - CEF subprocess helpers (link CEFWrapper; copy into *.app under Contents/Frameworks/) + // Helpers embed as …/Frameworks/WebView Helper*.app/Contents/MacOS/WebView Helper; CEF is + // …/Frameworks/Chromium Embedded Framework.framework (sibling of the helper .app). @rpath + // from build_cpp.sh points inside that framework — add @loader_path so dyld finds it at runtime. + // Second rpath keeps `swift build` + run from .build/.../debug/CEFHelper working (Frameworks at repo root). + .executableTarget( + name: "CEFHelper", + dependencies: ["CEFWrapper"], + path: "Sources/CEFHelper", + sources: ["main.mm"], + cxxSettings: [ + .unsafeFlags(["-std=c++20"]), + ], + linkerSettings: [ + .linkedFramework("Foundation"), + .linkedFramework("AppKit"), + .unsafeFlags([ + "-L", cefFrameworksDirectory, + "-F", cefFrameworksDirectory, + "-Xlinker", "-rpath", "-Xlinker", "@loader_path/../../..", + "-Xlinker", "-rpath", "-Xlinker", "@loader_path/../../../Frameworks", + ]), + ] + ), + .executableTarget( + name: "CEFHelperRenderer", + dependencies: ["CEFWrapper"], + path: "Sources/CEFHelperRenderer", + sources: ["main.mm"], + cxxSettings: [ + .unsafeFlags(["-std=c++20"]), + ], + linkerSettings: [ + .linkedFramework("Foundation"), + .linkedFramework("AppKit"), + .unsafeFlags([ + "-L", cefFrameworksDirectory, + "-F", cefFrameworksDirectory, + "-Xlinker", "-rpath", "-Xlinker", "@loader_path/../../..", + "-Xlinker", "-rpath", "-Xlinker", "@loader_path/../../../Frameworks", + ]), + ] + ), + + ], + swiftLanguageModes: [.v6] +) diff --git a/vendor/CEFWebView/README.md b/vendor/CEFWebView/README.md new file mode 100644 index 000000000000..4eadf322349b --- /dev/null +++ b/vendor/CEFWebView/README.md @@ -0,0 +1,7 @@ +# CEFWebView + +A work in progress. + +## License + +MIT License - See LICENSE file for details. diff --git a/vendor/CEFWebView/Sources/CEFHelper/main.mm b/vendor/CEFWebView/Sources/CEFHelper/main.mm new file mode 100644 index 000000000000..7948025cd84b --- /dev/null +++ b/vendor/CEFWebView/Sources/CEFHelper/main.mm @@ -0,0 +1,16 @@ +// CEFHelper — subprocess entry (GPU, utility, network, etc.) +// Links CEFWrapper so library load + CefExecuteProcess match the main app. + +#import +#import "CEFWrapper.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + int code = [CEFWrapper executeSubprocessWithArgc:argc argv:argv]; + if (code >= 0) { + return code; + } + // Not a CEF subprocess role (should not happen for this binary alone). + return 1; + } +} diff --git a/vendor/CEFWebView/Sources/CEFHelperRenderer/main.mm b/vendor/CEFWebView/Sources/CEFHelperRenderer/main.mm new file mode 100644 index 000000000000..9c026df37e2c --- /dev/null +++ b/vendor/CEFWebView/Sources/CEFHelperRenderer/main.mm @@ -0,0 +1,24 @@ +// CEFHelperRenderer — renderer subprocess entry (same logic as CEFHelper; CEF selects role via argv). + +#import +#import "CEFWrapper.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + NSLog(@"🚀 CEFHelperRenderer main() called with argc=%d", argc); + for (int i = 0; i < argc; i++) { + NSLog(@" argv[%d]: %s", i, argv[i] ? argv[i] : "(null)"); + } + NSLog(@" Calling CEFWrapper.executeSubprocessWithArgc:argv:"); + + int code = [CEFWrapper executeSubprocessWithArgc:argc argv:argv]; + NSLog(@" CEFWrapper.executeSubprocessWithArgc returned: %d", code); + + if (code >= 0) { + NSLog(@"✅ CEFHelperRenderer exiting with code: %d", code); + return code; + } + NSLog(@"❌ CEFHelperRenderer: not a subprocess, returning 1"); + return 1; + } +} diff --git a/vendor/CEFWebView/Sources/CEFWebView/CEFBridge.swift b/vendor/CEFWebView/Sources/CEFWebView/CEFBridge.swift new file mode 100644 index 000000000000..1706da8384fb --- /dev/null +++ b/vendor/CEFWebView/Sources/CEFWebView/CEFBridge.swift @@ -0,0 +1,380 @@ +import Cocoa +import CEFWrapper +import os.log + +nonisolated(unsafe) let cefLogger = Logger(subsystem: "co.sstools.CEFWebView", category: "CEFApplication") + +// MARK: - CEF Application Lifecycle Manager + +/// Manages CEF initialization and shutdown on the main thread. +@MainActor +public final class CEFApplication { + public static let shared = CEFApplication() + + private var isInitialized = false + weak var activeBrowserHost: CEFBrowserHost? + + /// Tracks helper processes that spawned before a `CEFBrowserHost` exists (GPU/network/storage + /// typically spawn during `CefInitialize`, before SwiftUI creates the browser). + private var helperSpawnedGPU = false + private var helperSpawnedNetwork = false + private var helperSpawnedStorage = false + private var helperSpawnedRenderer = false + + /// Tracks helper failures (renderer can fail even if it spawned) + private var helperFailedGPU = false + private var helperFailedNetwork = false + private var helperFailedStorage = false + private var helperFailedRenderer = false + + /// Main-frame load error details (from `OnLoadError`), buffered until `CEFBrowserHost` exists. + private var lastMainFrameLoadErrorCode: Int? + private var lastMainFrameLoadErrorText: String? + + nonisolated(unsafe) private var spawnedObserver: NSObjectProtocol? + nonisolated(unsafe) private var failedObserver: NSObjectProtocol? + + private init() { + cefLogger.info("🔔 CEFApplication.init() - registering notification observers") + + // Observe helper spawning notifications from CEFWrapper (C/Objective-C side) + spawnedObserver = NotificationCenter.default.addObserver( + forName: NSNotification.Name("CEFHelperSpawned"), + object: nil, + queue: .main + ) { notification in + cefLogger.info("🟢 CEFHelperSpawned notification RECEIVED on main queue") + cefLogger.debug("Notification object: \(String(describing: notification.object))") + if let userInfo = notification.userInfo as? [String: String] { + cefLogger.debug("✅ userInfo parsed successfully: \(userInfo)") + if let helperType = userInfo["type"] { + cefLogger.info("🟢 Found helperType in userInfo: \(helperType)") + Task { @MainActor in + cefLogger.info("📋 Calling recordHelperSpawned(\(helperType))") + CEFApplication.shared.recordHelperSpawned(helperType) + } + } else { + cefLogger.warning("❌ userInfo has keys but missing 'type' key: \(userInfo.keys)") + } + } else { + cefLogger.warning("❌ CEFHelperSpawned userInfo not [String: String]. Raw userInfo: \(String(describing: notification.userInfo))") + } + } + + // Observe helper failure notifications from CEFWrapper (C/Objective-C side) + failedObserver = NotificationCenter.default.addObserver( + forName: NSNotification.Name("CEFHelperFailed"), + object: nil, + queue: .main + ) { notification in + cefLogger.info("CEFHelperFailed notification received") + guard let userInfo = notification.userInfo, + let helperType = userInfo["type"] as? String + else { + cefLogger.warning("CEFHelperFailed missing userInfo or type: \(String(describing: notification.userInfo))") + return + } + let errorCode = (userInfo["errorCode"] as? NSNumber).map { $0.intValue } + let errorText = userInfo["errorText"] as? String + Task { @MainActor in + CEFApplication.shared.recordHelperFailed( + helperType, + mainFrameLoadErrorCode: errorCode, + mainFrameLoadErrorText: errorText + ) + } + } + + cefLogger.info("✅ Notification observers registered successfully") + } + + deinit { + if let observer = spawnedObserver { + NotificationCenter.default.removeObserver(observer) + } + if let observer = failedObserver { + NotificationCenter.default.removeObserver(observer) + } + } + + /// Handle CEF subprocess roles at the very start of the app, before any other initialization. + /// If this process is a CEF subprocess (renderer, GPU, utility, network), this method will + /// call exit() — it does not return. If this is the main browser process, returns normally. + /// Must be called from the main thread before SwiftUI initializes. + public nonisolated static func handleSubprocessIfNeeded() { + cefLogger.debug("handleSubprocessIfNeeded called") + + let exitCode = CEFWrapper.executeSubprocess(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv) + cefLogger.debug("CefExecuteProcess returned exitCode=\(exitCode)") + + if exitCode >= 0 { + // This is a subprocess; exit with the exit code returned by CefExecuteProcess + cefLogger.debug("This is a subprocess, exiting with code \(exitCode)") + exit(exitCode) + } + // exitCode < 0 means main browser process — continue with normal app startup + cefLogger.debug("This is the main browser process, continuing startup") + } + + /// Initialize CEF framework. Call once at app startup. + public func initialize() throws { + guard !isInitialized else { + cefLogger.debug("CEF already initialized, skipping") + return + } + + // Swift bridges initializeCEFWithError: as a throwing method + do { + try CEFWrapper.initializeCEF() + } catch let error as NSError { + let errorDesc = error.localizedDescription + cefLogger.error("CEF INITIALIZATION FAILED: \(errorDesc, privacy: .public) (code: \(error.code))") + throw CEFError.initializationFailed(reason: errorDesc) + } catch { + cefLogger.error("CEF initialization failed with unknown error: \(String(describing: error), privacy: .public)") + throw CEFError.initializationFailed(reason: error.localizedDescription) + } + + cefLogger.info("CEF Initialized Successfully") + isInitialized = true + } + + /// Records a helper spawn from `CEFHelperSpawned` notifications (may arrive before `CEFBrowserHost` exists). + func recordHelperSpawned(_ type: String) { + cefLogger.info("🟢 recordHelperSpawned called with type: \(type)") + let lowerType = type.lowercased() + cefLogger.debug("Type lowercased: \(lowerType)") + + switch lowerType { + case "gpu": + cefLogger.debug("Setting helperSpawnedGPU = true") + helperSpawnedGPU = true + case "network": + cefLogger.debug("Setting helperSpawnedNetwork = true") + helperSpawnedNetwork = true + case "storage": + cefLogger.debug("Setting helperSpawnedStorage = true") + helperSpawnedStorage = true + case "renderer": + cefLogger.debug("Setting helperSpawnedRenderer = true") + helperSpawnedRenderer = true + default: + cefLogger.warning("⚠️ Unknown helper type from CEF: \(type)") + return + } + + cefLogger.info("✅ Recorded helper spawn (accumulated): \(type)") + + if let state = activeBrowserHost?.state { + cefLogger.info("🟢 Broadcasting state change to CEFWebViewState") + applyAccumulatedHelperFlags(to: state) + } else { + cefLogger.info("ℹ️ No activeBrowserHost.state available yet - flags will be applied when browser attaches") + } + } + + /// Records a helper failure from `CEFHelperFailed` notifications. + /// `mainFrameLoadErrorCode` / `mainFrameLoadErrorText` are set when the failure comes from CEF `OnLoadError` (main frame). + func recordHelperFailed( + _ type: String, + mainFrameLoadErrorCode: Int? = nil, + mainFrameLoadErrorText: String? = nil + ) { + cefLogger.info("recordHelperFailed type=\(type) loadErrorCode=\(String(describing: mainFrameLoadErrorCode))") + let lowerType = type.lowercased() + + switch lowerType { + case "gpu": + helperFailedGPU = true + helperSpawnedGPU = false + case "network": + helperFailedNetwork = true + helperSpawnedNetwork = false + case "storage": + helperFailedStorage = true + helperSpawnedStorage = false + case "renderer": + helperFailedRenderer = true + helperSpawnedRenderer = false + if let code = mainFrameLoadErrorCode { + lastMainFrameLoadErrorCode = code + lastMainFrameLoadErrorText = mainFrameLoadErrorText + } else { + lastMainFrameLoadErrorCode = nil + lastMainFrameLoadErrorText = nil + } + default: + cefLogger.warning("Unknown helper type from CEF: \(type)") + return + } + + if let state = activeBrowserHost?.state { + applyAccumulatedHelperFlags(to: state) + if lowerType == "renderer" { + state.isLoading = false + } + } else { + cefLogger.debug("No activeBrowserHost.state yet; failure flags buffered for attach") + } + } + + /// Clears renderer failure state when starting a new navigation (user or programmatic load). + func clearRendererFailureStateForNewNavigation() { + helperFailedRenderer = false + lastMainFrameLoadErrorCode = nil + lastMainFrameLoadErrorText = nil + } + + /// Copies accumulated flags into `CEFWebViewState` (call when the browser host attaches state). + fileprivate func applyAccumulatedHelperFlags(to state: CEFWebViewState) { + cefLogger.info("🔄 applyAccumulatedHelperFlags: applying spawn flags") + + if helperSpawnedGPU { + cefLogger.debug(" → Setting state.gpuHelperSpawned = true") + state.gpuHelperSpawned = true + } + if helperSpawnedNetwork { + cefLogger.debug(" → Setting state.networkHelperSpawned = true") + state.networkHelperSpawned = true + } + if helperSpawnedStorage { + cefLogger.debug(" → Setting state.storageHelperSpawned = true") + state.storageHelperSpawned = true + } + if helperSpawnedRenderer { + cefLogger.debug(" → Setting state.rendererHelperSpawned = true") + state.rendererHelperSpawned = true + } + + cefLogger.info("🔄 applyAccumulatedHelperFlags: applying failure flags") + + if helperFailedGPU { + cefLogger.error(" → 🔴 Setting state.gpuHelperFailed = true") + state.gpuHelperFailed = true + } + if helperFailedNetwork { + cefLogger.error(" → 🔴 Setting state.networkHelperFailed = true") + state.networkHelperFailed = true + } + if helperFailedStorage { + cefLogger.error(" → 🔴 Setting state.storageHelperFailed = true") + state.storageHelperFailed = true + } + if helperFailedRenderer { + state.rendererHelperFailed = true + state.lastMainFrameLoadErrorCode = lastMainFrameLoadErrorCode + state.lastMainFrameLoadErrorText = lastMainFrameLoadErrorText + } + + cefLogger.debug("applyAccumulatedHelperFlags complete") + } + + /// Shutdown CEF gracefully. Should be called on app termination. + public func shutdown() { + CEFWrapper.shutdown() + isInitialized = false + } +} + +// MARK: - CEF Browser Host Wrapper + +/// Wraps a CEF browser instance on the main thread. +@MainActor +public final class CEFBrowserHost { + private var isAlive = true + /// Observable SwiftUI state; internal so `CEFApplication` can merge accumulated helper-spawn flags. + internal weak var state: CEFWebViewState? + + /// The native macOS view for CEF rendering. + public let nativeView: NSView? + + /// Create a browser instance with an initial URL. + public init(parentView: NSView, url: URL, state: CEFWebViewState? = nil) throws { + cefLogger.debug("CEFBrowserHost.init() - parentView: \(NSStringFromRect(parentView.frame)), url: \(url.absoluteString)") + + guard let browserView = CEFWrapper.createBrowser(in: parentView, url: url.absoluteString) else { + let errorMsg = "CEFWrapper.createBrowser returned nil. This indicates the browser subprocess failed to initialize. " + + "Likely causes: helper executable crash, dyld library loading failure, JIT entitlements missing, " + + "or invalid code signatures. Check system logs and CEF debug log at ~/Library/Caches/com.chromium.webview/debug.log" + cefLogger.error("BROWSER CREATION FAILED: \(errorMsg, privacy: .public)") + throw CEFError.browserCreationFailed(reason: errorMsg) + } + + cefLogger.info("CEF Browser View Created Successfully: \(String(describing: browserView))") + self.nativeView = browserView + self.state = state + if let state { + CEFApplication.shared.applyAccumulatedHelperFlags(to: state) + } + } + + /// Load a URL in the browser. + func loadURL(_ url: URL) { + guard isAlive else { + cefLogger.warning("loadURL called but browserHost is dead") + return + } + CEFApplication.shared.clearRendererFailureStateForNewNavigation() + state?.rendererHelperFailed = false + state?.lastMainFrameLoadErrorCode = nil + state?.lastMainFrameLoadErrorText = nil + + cefLogger.info("loadURL: \(url.absoluteString)") + CEFWrapper.loadURL(url.absoluteString) + updateState() + } + + func reload() { + guard isAlive else { return } + CEFWrapper.reloadBrowser() + } + + func goBack() { + guard isAlive else { return } + CEFWrapper.goBack() + updateState() + } + + func goForward() { + guard isAlive else { return } + CEFWrapper.goForward() + updateState() + } + + /// Update state from CEF's current values. + func updateState() { + state?.isLoading = CEFWrapper.isLoading() + state?.canGoBack = CEFWrapper.canGoBack() + state?.canGoForward = CEFWrapper.canGoForward() + state?.title = CEFWrapper.currentTitle() + if let urlString = CEFWrapper.currentURL(), + let url = URL(string: urlString) { + state?.currentURL = url + } + } + + func close() { + guard isAlive else { return } + isAlive = false + CEFWrapper.closeBrowser() + } +} + +// MARK: - Error Types + +enum CEFError: Error { + case resourcesNotFound + case initializationFailed(reason: String) + case browserCreationFailed(reason: String) + + var localizedDescription: String { + switch self { + case .resourcesNotFound: + return "CEF resources not found" + case .initializationFailed(let reason): + return "CEF initialization failed: \(reason)" + case .browserCreationFailed(let reason): + return "Browser creation failed: \(reason)" + } + } +} diff --git a/vendor/CEFWebView/Sources/CEFWebView/CEFWebView.swift b/vendor/CEFWebView/Sources/CEFWebView/CEFWebView.swift new file mode 100644 index 000000000000..90a449bc7187 --- /dev/null +++ b/vendor/CEFWebView/Sources/CEFWebView/CEFWebView.swift @@ -0,0 +1,229 @@ +import AppKit +import CEFWrapper +import SwiftUI +import os.log + +private let logger = Logger(subsystem: "co.sstools.CEFWebView", category: "CEFWebView") + +/// Hosting view that tells CEF when SwiftUI/AppKit changes its size (required for SetAsChild / windowed embedding). +private final class CEFBrowserContainerView: NSView { + /// Match top-left origin with Chromium’s SetAsChild expectations (default NSView is bottom-left). + override var isFlipped: Bool { true } + + override var intrinsicContentSize: NSSize { + // No intrinsic size - let SwiftUI frame dictate the size + return NSSize(width: NSView.noIntrinsicMetric, height: NSView.noIntrinsicMetric) + } + + override func layout() { + super.layout() + logger.debug("📐 CEFBrowserContainerView.layout() called, bounds: \(NSStringFromRect(self.bounds))") + CEFWrapper.notifyBrowserViewGeometryChanged() + } + + override func setFrameSize(_ newSize: NSSize) { + logger.debug("📏 CEFBrowserContainerView.setFrameSize(\(NSStringFromSize(newSize))) called") + super.setFrameSize(newSize) + logger.debug("📏 After setFrameSize, bounds: \(NSStringFromRect(self.bounds))") + CEFWrapper.notifyBrowserViewGeometryChanged() + } +} + +// MARK: - NSViewRepresentable Wrapper + +public struct CEFWebView: NSViewRepresentable { + @Binding var url: URL? + @Binding var state: CEFWebViewState + + public init(url: Binding, state: Binding) { + self._url = url + self._state = state + } + + public func makeNSView(context: Context) -> NSView { + logger.info("🔍 [1/3 CRITICAL PATH] CEFWebView.makeNSView ENTRY") + + // Create container view — will be sized by SwiftUI layout system + let container = CEFBrowserContainerView(frame: .zero) + logger.debug("📦 Created CEFBrowserContainerView with frame: \(NSStringFromRect(container.frame))") + container.wantsLayer = true + container.layer?.backgroundColor = NSColor.white.cgColor + container.autoresizesSubviews = true + + // Store container for later use + context.coordinator.container = container + logger.debug("✅ Stored container in coordinator") + + // Initialize CEF on first use (only once, safe to call repeatedly) + do { + logger.debug("🔧 Attempting CEF initialization...") + try CEFApplication.shared.initialize() + logger.debug("✅ CEFApplication.shared.initialize() succeeded") + } catch let error as NSError { + let errorDesc = "CEF Initialization Failed: \(error.localizedDescription)" + logger.error("❌ CEF initialization failed: \(errorDesc, privacy: .public)") + state.initializationError = errorDesc + return container + } catch { + let errorDesc = "CEF Initialization Failed: \(error)" + logger.error("❌ CEF initialization failed: \(errorDesc, privacy: .public)") + state.initializationError = errorDesc + return container + } + + // DON'T create browser here — we have zero bounds. Wait for updateNSView when view is properly sized. + context.coordinator.lastNavigationKeyFromBinding = nil + logger.debug("📝 Set lastNavigationKeyFromBinding = nil, waiting for updateNSView...") + + return container + } + + public func updateNSView(_ nsView: NSView, context: Context) { + guard let container = context.coordinator.container else { + logger.error("❌ updateNSView: container is nil!") + return + } + + logger.info("📍 updateNSView called (browserHost: \(context.coordinator.browserHost != nil ? "EXISTS" : "nil"))") + logger.info(" - nsView.bounds: \(NSStringFromRect(nsView.bounds))") + logger.info(" - nsView.frame: \(NSStringFromRect(nsView.frame))") + logger.info(" - container.bounds: \(NSStringFromRect(container.bounds))") + logger.info(" - container.frame: \(NSStringFromRect(container.frame))") + logger.info(" - browserHost present: \(context.coordinator.browserHost != nil)") + + // Update container frame to match parent bounds + if !nsView.bounds.isEmpty { + if container.frame != nsView.bounds { + logger.debug("📐 updateNSView: Updating container from \(NSStringFromRect(container.frame)) to \(NSStringFromRect(nsView.bounds))") + container.frame = nsView.bounds + CEFWrapper.notifyBrowserViewGeometryChanged() + } + } else { + // SwiftUI not sizing the view - use a sensible default so CEF can render + let defaultSize = NSSize(width: 800, height: 600) + if container.frame.size != defaultSize { + logger.warning("⚠️ nsView.bounds is EMPTY, setting default container size: \(NSStringFromSize(defaultSize))") + container.frame = NSRect(origin: .zero, size: defaultSize) + CEFWrapper.notifyBrowserViewGeometryChanged() + } + } + + // Create browser on first updateNSView call (bounds may still be zero, but we need to try) + logger.debug("🔍 Checking browser creation: browserHost=\(context.coordinator.browserHost != nil)") + if context.coordinator.browserHost == nil { + logger.info("🔍 [2/3 CRITICAL PATH] First updateNSView - creating browser") + let initialURL = url ?? URL(string: "about:blank")! + logger.debug("📋 Initial URL: \(initialURL.absoluteString)") + logger.debug("📐 Container bounds: \(NSStringFromRect(container.bounds))") + logger.debug("📐 NSView bounds: \(NSStringFromRect(nsView.bounds))") + + do { + logger.debug("🔧 Calling CEFBrowserHost.init()...") + let host = try CEFBrowserHost(parentView: container, url: initialURL, state: state) + logger.debug("✅ CEFBrowserHost created successfully") + + context.coordinator.browserHost = host + logger.debug("✅ Stored browserHost in coordinator") + + CEFApplication.shared.activeBrowserHost = host + logger.debug("✅ Set activeBrowserHost on CEFApplication") + + state.setBrowserHost(host) + logger.debug("✅ Set browserHost on state") + + state.currentURL = url + context.coordinator.lastNavigationKeyFromBinding = url.map(Self.stableNavigationKey) + + // Load the initial URL immediately after browser creation + // (updateNSView may not be called again after this, so do it now) + if let urlToLoad = url { + logger.info("🔍 Loading initial URL immediately: \(urlToLoad.absoluteString)") + host.loadURL(urlToLoad) + } + + logger.info("✅ [2/3 CRITICAL PATH] CEFBrowserHost initialized successfully") + } catch let error as CEFError { + logger.error("❌ [2/3 CRITICAL PATH] Browser creation failed: \(error.localizedDescription, privacy: .public)") + state.initializationError = error.localizedDescription + return + } catch { + logger.error("❌ [2/3 CRITICAL PATH] Browser creation failed: \(String(describing: error), privacy: .public)") + state.initializationError = "Browser Creation Failed: \(error)" + return + } + } + + // Now handle URL binding changes + guard let host = context.coordinator.browserHost else { + logger.debug("⚠️ No browserHost yet") + return + } + + let key = url.map(Self.stableNavigationKey) + logger.debug("URL binding check: key=\(key?.prefix(50) ?? "nil"), lastKey=\(context.coordinator.lastNavigationKeyFromBinding?.prefix(50) ?? "nil")") + + if key != context.coordinator.lastNavigationKeyFromBinding { + context.coordinator.lastNavigationKeyFromBinding = key + if let url { + logger.info("🔍 [3/3 CRITICAL PATH] Binding URL changed, calling loadURL: \(url.absoluteString)") + host.loadURL(url) + state.currentURL = url + } else { + logger.info("URL is nil, not loading") + } + } else { + logger.debug("URL key unchanged, skipping loadURL") + } + } + + /// Normalized key for “same navigation” so trailing slashes / percent-encoding don’t double-load. + private static func stableNavigationKey(_ url: URL) -> String { + url.absoluteString.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + } + + public func makeCoordinator() -> Coordinator { + Coordinator() + } + + public static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) { + coordinator.browserHost?.close() + CEFApplication.shared.activeBrowserHost = nil + } + + public class Coordinator { + var browserHost: CEFBrowserHost? + var container: NSView? + /// Stable string form of last applied `$url` (see `stableNavigationKey`). + var lastNavigationKeyFromBinding: String? + } + +} + +#Preview { + @Previewable @State var url: URL? = URL(string: "https://google.com") + @Previewable @State var state = CEFWebViewState() + + VStack { + HStack { + Button(action: { state.goBack() }) { + Image(systemName: "chevron.left") + } + .disabled(!state.canGoBack) + + Button(action: { state.goForward() }) { + Image(systemName: "chevron.right") + } + .disabled(!state.canGoForward) + + Button(action: { state.reload() }) { + Image(systemName: "arrow.clockwise") + } + + TextField("URL", value: $url, format: .url) + .textFieldStyle(.roundedBorder) + } + .padding() + + CEFWebView(url: $url, state: $state) + } +} diff --git a/vendor/CEFWebView/Sources/CEFWebView/CEFWebViewState.swift b/vendor/CEFWebView/Sources/CEFWebView/CEFWebViewState.swift new file mode 100644 index 000000000000..d3732a736109 --- /dev/null +++ b/vendor/CEFWebView/Sources/CEFWebView/CEFWebViewState.swift @@ -0,0 +1,75 @@ +import SwiftUI + +// MARK: - Helper Process Status + +public struct HelperStatus { + public let name: String + public var isSpawned: Bool = false + public var timestamp: Date? +} + +// MARK: - Observable State + +@MainActor +@Observable +public final class CEFWebViewState { + public var isLoading: Bool = false + public var estimatedProgress: Double = 0.0 + public var title: String? + public var currentURL: URL? + public var canGoBack: Bool = false + public var canGoForward: Bool = false + /// Error message if CEF initialization or browser creation failed. Non-nil indicates a critical failure. + public var initializationError: String? + + /// Helper process status tracking + public var gpuHelperSpawned: Bool = false + public var networkHelperSpawned: Bool = false + public var storageHelperSpawned: Bool = false + public var rendererHelperSpawned: Bool = false + + /// Helper failure tracking + public var gpuHelperFailed: Bool = false + public var networkHelperFailed: Bool = false + public var storageHelperFailed: Bool = false + public var rendererHelperFailed: Bool = false + + /// Main-frame load error from CEF `OnLoadError` (when renderer failure is due to navigation error). + public var lastMainFrameLoadErrorCode: Int? + public var lastMainFrameLoadErrorText: String? + + /// Single line for the status bar when `rendererHelperFailed` is true. + public var rendererFailureStatusLine: String { + if let code = lastMainFrameLoadErrorCode { + let detail = lastMainFrameLoadErrorText?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if detail.isEmpty { + return "Load failed (error \(code))" + } + return "Load failed: \(detail) (error \(code))" + } + if let t = lastMainFrameLoadErrorText, !t.isEmpty { + return "Load failed: \(t)" + } + return "Renderer process failed" + } + + private weak var browserHost: CEFBrowserHost? + + public init() {} + + internal func setBrowserHost(_ host: CEFBrowserHost) { + self.browserHost = host + } + + public func reload() { + browserHost?.reload() + } + + public func goBack() { + browserHost?.goBack() + } + + public func goForward() { + browserHost?.goForward() + } +} diff --git a/vendor/CEFWebView/Sources/CEFWrapper/CEFWrapper.mm b/vendor/CEFWebView/Sources/CEFWrapper/CEFWrapper.mm new file mode 100644 index 000000000000..c0ecf03533d8 --- /dev/null +++ b/vendor/CEFWebView/Sources/CEFWrapper/CEFWrapper.mm @@ -0,0 +1,808 @@ +// +// CEFWrapper.mm +// CEFWebView +// +// Objective-C++ wrapper for the CEF C++ API. +// + +#import "include/CEFWrapper.h" + +#import +#import +#import + +#include +#include + +#include "include/cef_app.h" +#include "include/cef_browser.h" +#include "include/cef_client.h" +#include "include/cef_command_line.h" +#include "include/cef_load_handler.h" +#include "include/cef_display_handler.h" +#include "include/cef_version.h" +#include "include/wrapper/cef_library_loader.h" + +// ─── C Function Bridge Forward Declaration ────────────────────────────────── + +extern "C" { + void NotifyHelperSpawned(const char* helperType); + void NotifyHelperFailed(const char* helperType); + void NotifyHelperFailedWithLoadError(const char* helperType, + int errorCode, + const char* errorText, + const char* failedUrl); +} + +// ─── CEF App: command-line switches applied in every process ─────────────────── +// +// macOS often fails to spawn the GPU process (error 1003) under the default app +// configuration (Hardened Runtime / Mach limits), then Chromium aborts with +// "GPU process isn't usable". Disabling GPU forces software rendering and avoids +// that subprocess. Pass the same CefApp to CefExecuteProcess and CefInitialize. + +namespace { + +class CEFWebViewApp final : public CefApp, public CefBrowserProcessHandler { +public: + CEFWebViewApp() = default; + + void OnBeforeCommandLineProcessing(const CefString& process_type, + CefRefPtr command_line) override { + // Disable GPU to avoid process launch failures under Hardened Runtime / Mach limits + command_line->AppendSwitch("disable-gpu"); + // Do NOT disable gpu-compositing — it can break renderer rendering + command_line->AppendSwitch("disable-gpu-sandbox"); + // QUIC / HTTP3 can complicate loads on some networks + command_line->AppendSwitch("disable-quic"); + } + + /// Called before each child process is launched (GPU, utility, renderer, etc.). + /// Maps Chromium subprocess types to our Swift UI helper indicators. + void OnBeforeChildProcessLaunch(CefRefPtr command_line) override { + if (!command_line.get()) { + return; + } + CefString typeStr = command_line->GetSwitchValue("type"); + std::string type = typeStr.ToString(); + if (type.empty()) { + return; + } + if (type == "gpu-process") { + NotifyHelperSpawned("gpu"); + } else if (type == "utility") { + CefString sub = command_line->GetSwitchValue("utility-sub-type"); + std::string subStr = sub.ToString(); + if (subStr.find("NetworkService") != std::string::npos) { + NotifyHelperSpawned("network"); + } else if (subStr.find("Storage") != std::string::npos) { + NotifyHelperSpawned("storage"); + } + } else if (type == "renderer") { + NotifyHelperSpawned("renderer"); + } + NSLog(@"🔧 OnBeforeChildProcessLaunch: type=%s utility-sub-type=%s", + type.c_str(), + command_line->GetSwitchValue("utility-sub-type").ToString().c_str()); + } + + /// Required when using external_message_pump + CefDoMessageLoopWork; without this, renderer IPC + /// is not serviced promptly and navigations fail with ERR_ABORTED / blank views. + void OnScheduleMessagePumpWork(int64_t delay_ms) override { + // Disabled this log message because it spams the logs + // NSLog(@"⏰ OnScheduleMessagePumpWork: delay_ms=%lld", delay_ms); + if (delay_ms <= 0) { + dispatch_async(dispatch_get_main_queue(), ^{ + CefDoMessageLoopWork(); + }); + } else { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay_ms * NSEC_PER_MSEC), + dispatch_get_main_queue(), ^{ + CefDoMessageLoopWork(); + }); + } + } + + CefRefPtr GetBrowserProcessHandler() override { return this; } + +private: + IMPLEMENT_REFCOUNTING(CEFWebViewApp); +}; + +} // namespace + +// ─── C++ Client Class ─────────────────────────────────────────────────────────── +// +// Inherits from CefClient, CefLoadHandler, and CefDisplayHandler. +// Implements the virtual methods for load state and title tracking. +// IMPLEMENT_REFCOUNTING macro handles all ref-counting automatically. + +class ChromiumClient : public CefClient, + public CefLoadHandler, + public CefDisplayHandler, + public CefRequestHandler { +public: + // CefClient overrides + CefRefPtr GetLoadHandler() override { + return this; + } + + CefRefPtr GetDisplayHandler() override { + return this; + } + + CefRefPtr GetRequestHandler() override { + return this; + } + + // CefRequestHandler overrides + bool OnBeforeBrowse(CefRefPtr browser, + CefRefPtr frame, + CefRefPtr request, + bool user_gesture, + bool is_redirect) override { + NSLog(@"🧭 OnBeforeBrowse: isMain=%s user_gesture=%d is_redirect=%d", + frame->IsMain() ? "YES" : "NO", + user_gesture, + is_redirect); + return false; // Don't cancel the navigation + } + + // CefLoadHandler overrides + void OnLoadingStateChange(CefRefPtr browser, + bool isLoading, bool canGoBack, bool canGoForward) override { + _isLoading = isLoading; + _canGoBack = canGoBack; + _canGoForward = canGoForward; + NSLog(@"📊 OnLoadingStateChange: isLoading=%d canGoBack=%d canGoForward=%d", + isLoading, canGoBack, canGoForward); + } + + void OnLoadStart(CefRefPtr browser, + CefRefPtr frame, + TransitionType transition_type) override { + NSLog(@"⬇️ OnLoadStart: url=%s isMain=%s", + frame->GetURL().ToString().c_str(), + frame->IsMain() ? "YES" : "NO"); + + if (frame->IsMain()) { + NSLog(@"📢 OnLoadStart: main frame load started (renderer already tracked via OnBeforeChildProcessLaunch)"); + } + } + + void OnLoadEnd(CefRefPtr browser, + CefRefPtr frame, + int httpStatusCode) override { + NSLog(@"✅ OnLoadEnd: url=%s status=%d isMain=%s", + frame->GetURL().ToString().c_str(), + httpStatusCode, + frame->IsMain() ? "YES" : "NO"); + + // Ensure loading state is cleared when main frame finishes + if (frame->IsMain()) { + _isLoading = false; + NSLog(@"📊 OnLoadEnd (MAIN FRAME): set isLoading=false"); + } + } + + void OnLoadError(CefRefPtr browser, + CefRefPtr frame, + ErrorCode errorCode, + const CefString& errorText, + const CefString& failedUrl) override { + // Subframe failures (ads, trackers, blocked embeds) often report ERR_ABORTED for URLs that + // look like the top document; only treat main-frame errors as document load failures. + if (!frame->IsMain()) { + NSLog(@"⏭️ OnLoadError (SUBFRAME - ignoring): url=%s error=%d text=%s", + failedUrl.ToString().c_str(), + errorCode, + errorText.ToString().c_str()); + return; + } + + // ERR_ABORTED (-3) is normal for redirects and navigation interruptions. + // The page often loads successfully despite this error, so don't report it. + if (errorCode == -3) { // net::ERR_ABORTED + NSLog(@"⏭️ OnLoadError (MAIN FRAME - ERR_ABORTED, ignoring): url=%s", + failedUrl.ToString().c_str()); + return; + } + + NSLog(@"❌ OnLoadError (MAIN FRAME - CRITICAL): url=%s error=%d text=%s", + failedUrl.ToString().c_str(), + errorCode, + errorText.ToString().c_str()); + NSLog(@"💥 Main frame failed to load - renderer will be marked as failed"); + + // Renderer process failed to load main frame - mark renderer as failed + NSLog(@"🔔 Calling NotifyHelperFailedWithLoadError from OnLoadError"); + NotifyHelperFailedWithLoadError("renderer", + static_cast(errorCode), + errorText.ToString().c_str(), + failedUrl.ToString().c_str()); + } + + void OnRenderProcessTerminated(CefRefPtr browser, + TerminationStatus status) { + NSLog(@"💥 OnRenderProcessTerminated: status=%d (0=normal, 1=abnormal, 2=crashed, 3=oom, 4=launch_failed)", (int)status); + NSLog(@"🔔 Calling NotifyHelperFailed(\"renderer\") from OnRenderProcessTerminated"); + NotifyHelperFailed("renderer"); + } + + // CefDisplayHandler overrides + void OnTitleChange(CefRefPtr browser, + const CefString& title) override { + NSString* titleCopy = [NSString stringWithUTF8String:title.ToString().c_str()]; + NSLog(@"📄 OnTitleChange: %@", titleCopy); + dispatch_async(dispatch_get_main_queue(), ^{ + _currentTitle = titleCopy; + }); + } + + void OnAddressChange(CefRefPtr browser, + CefRefPtr frame, + const CefString& url) override { + NSString* urlCopy = [NSString stringWithUTF8String:url.ToString().c_str()]; + NSLog(@"🔗 OnAddressChange: %@", urlCopy); + dispatch_async(dispatch_get_main_queue(), ^{ + _currentURL = urlCopy; + }); + } + + // State accessors (called from Objective-C methods below) + bool IsLoading() const { return _isLoading; } + bool CanGoBack() const { return _canGoBack; } + bool CanGoForward() const { return _canGoForward; } + NSString* GetTitle() const { return _currentTitle; } + NSString* GetURL() const { return _currentURL; } + +private: + std::atomic _isLoading{false}; + std::atomic _canGoBack{false}; + std::atomic _canGoForward{false}; + NSString* __strong _currentTitle = nil; + NSString* __strong _currentURL = nil; + + IMPLEMENT_REFCOUNTING(ChromiumClient); +}; + + +// ─── Global State ─────────────────────────────────────────────────────────────── + +static BOOL g_cefInitialized = NO; +static CefRefPtr g_browser; +static CefRefPtr g_client; + +/// Single loader per process. On macOS, main browser must call LoadInMain(); CEF helper +/// subprocesses (same binary, `--type=...`) must call LoadInHelper() — see cef_library_loader.h. +static CefScopedLibraryLoader g_cefLibraryLoader; + +/// CefScopedLibraryLoader does not allow calling LoadInMain/LoadInHelper twice; the second +/// call fails. We load once in executeSubprocessWithArgc, then initializeCEFWithError must skip. +static BOOL g_cefFrameworkDylibLoaded = NO; + +static bool CEFIsHelperProcess(int argc, char **argv) { + for (int i = 1; i < argc; i++) { + const char *a = argv[i]; + if (a && std::strncmp(a, "--type=", 7) == 0) { + return true; + } + } + return false; +} + +/// |isHelperProcess|: YES → LoadInHelper(), NO → LoadInMain(). Ignored if already loaded. +static BOOL EnsureCEFFrameworkLoaded(BOOL isHelperProcess, NSError **error) { + if (g_cefFrameworkDylibLoaded) { + return YES; + } + const bool ok = isHelperProcess ? g_cefLibraryLoader.LoadInHelper() : g_cefLibraryLoader.LoadInMain(); + if (!ok) { + NSLog(@"❌ Failed to load Chromium Embedded Framework (%s process)", + isHelperProcess ? "helper" : "main"); + if (error) { + *error = [NSError errorWithDomain:@"CEFWrapper" + code:2 + userInfo:@{NSLocalizedDescriptionKey: @"Failed to load Chromium Embedded Framework"}]; + } + return NO; + } + g_cefFrameworkDylibLoaded = YES; + return YES; +} + +// ─── CEFWrapper Implementation ─────────────────────────────────────────────────── + +@implementation CEFWrapper + ++ (void)notifyHelperSpawned:(NSString *)helperType { + NSLog(@"📢 CEFWrapper.notifyHelperSpawned called with type: %@", helperType); + + if (!helperType) { + return; + } + + // Post a notification that the Swift side can observe + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:@"CEFHelperSpawned" + object:nil + userInfo:@{@"type": helperType}]; + }); +} + ++ (void)notifyHelperFailed:(NSString *)helperType { + NSLog(@"📢 CEFWrapper.notifyHelperFailed called with type: %@", helperType); + + if (!helperType) { + NSLog(@"⚠️ CEFWrapper.notifyHelperFailed: helperType is nil, returning early"); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + NSDictionary *userInfo = @{@"type": helperType}; + NSLog(@"📮 [MAIN QUEUE] Posting CEFHelperFailed userInfo: %@", userInfo); + [[NSNotificationCenter defaultCenter] postNotificationName:@"CEFHelperFailed" + object:nil + userInfo:userInfo]; + }); +} + ++ (void)notifyHelperFailed:(NSString *)helperType + mainFrameLoadErrorCode:(NSInteger)errorCode + errorText:(NSString *)errorText + failedUrl:(NSString *)failedUrl { + if (!helperType) { + return; + } + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + userInfo[@"type"] = helperType; + userInfo[@"errorCode"] = @(errorCode); + if (errorText) { + userInfo[@"errorText"] = errorText; + } + if (failedUrl) { + userInfo[@"failedUrl"] = failedUrl; + } + dispatch_async(dispatch_get_main_queue(), ^{ + NSLog(@"📮 [MAIN QUEUE] Posting CEFHelperFailed (main frame load error) userInfo: %@", userInfo); + [[NSNotificationCenter defaultCenter] postNotificationName:@"CEFHelperFailed" + object:nil + userInfo:[userInfo copy]]; + }); +} + ++ (int)executeSubprocessWithArgc:(int)argc argv:(char **)argv { + NSMutableString* argDump = [NSMutableString stringWithFormat:@"executeSubprocessWithArgc: argc=%d", argc]; + if (argc > 0 && argv) { + for (int i = 0; i < argc; i++) { + const char* a = argv[i]; + NSString* s = a ? [NSString stringWithUTF8String:a] : @"(null)"; + [argDump appendFormat:@"\n argv[%d] = %@", i, s]; + } + } else { + [argDump appendString:@" (argv nil or argc <= 0)"]; + } + NSLog(@"%@", argDump); + + const BOOL isCEFHelperArgv = CEFIsHelperProcess(argc, argv); + if (isCEFHelperArgv) { + NSLog(@"executeSubprocessWithArgc: this process has --type=… — CEF helper role; " + @"CefExecuteProcess will handle it and this process should exit."); + } else { + NSLog(@"executeSubprocessWithArgc: no --type= in argv — this is the main browser binary " + @"(Xcode often adds -NSDocumentRevisionsDebugMode etc.). " + @"GPU/utility/renderer helpers are separate OS processes (WebView Helper.app); " + @"their executeSubprocessWithArgc logs appear under the WebView Helper process, " + @"not WebView — include helper processes in Console or log show."); + } + + // Without loading the framework first, CefExecuteProcess jumps through a null stub (crash at 0x0). + // Helper subprocesses must use LoadInHelper(), not LoadInMain() — see CEF cef_library_loader.h. + if (!EnsureCEFFrameworkLoaded(CEFIsHelperProcess(argc, argv) ? YES : NO, nil)) { + return 1; + } + CefMainArgs args(argc, argv); + CefRefPtr app = new CEFWebViewApp(); + return CefExecuteProcess(args, app, nullptr); +} + ++ (BOOL)initializeCEFWithError:(NSError **)error { + if (g_cefInitialized) { + NSLog(@"✓ CEF already initialized"); + return YES; + } + + // Browser process only (helpers never reach initializeCEF). Framework was already loaded + // in executeSubprocessWithArgc — EnsureCEFFrameworkLoaded skips the second load. + if (!EnsureCEFFrameworkLoaded(NO, error)) { + return NO; + } + NSLog(@"✓ Chromium Embedded Framework loaded"); + + CefMainArgs args; + CefSettings settings; + + settings.no_sandbox = 1; + settings.external_message_pump = 1; + + // Enable verbose logging + settings.log_severity = LOGSEVERITY_VERBOSE; + CefString(&settings.log_file).FromString("/tmp/cef_debug.log"); + + // Also append command-line switches for extra verbosity + CefRefPtr command_line = CefCommandLine::GetGlobalCommandLine(); + if (command_line.get()) { + command_line->AppendSwitch("enable-logging"); + command_line->AppendSwitchWithValue("v", "3"); // v=3 for very verbose + + // TEST: Single-process mode to isolate subprocess spawning issues + // If single-process mode works, the problem is subprocess spawning. + // If it still fails, the problem is in CEF core or configuration. + // IMPORTANT: Remove this after testing — single-process is not production-ready. + // Uncomment the line below to enable: + // command_line->AppendSwitch("single-process"); + } + +#if TARGET_OS_OSX + // macOS: set absolute path to the generic helper (see WebView/Scripts/embed_cef_helpers.sh). + NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; + NSLog(@"🔍 Main bundle path: %@", bundlePath); + NSString *frameworksDir = [bundlePath stringByAppendingPathComponent:@"Contents/Frameworks"]; + NSLog(@"🔍 Frameworks dir: %@", frameworksDir); + + // HARD FAIL: CEF framework must exist + NSString *cefFrameworkPath = + [frameworksDir stringByAppendingPathComponent:@"Chromium Embedded Framework.framework"]; + if (![[NSFileManager defaultManager] fileExistsAtPath:cefFrameworkPath]) { + NSString *errorMsg = [NSString stringWithFormat: + @"CEF framework not found at %@. This is a critical initialization failure. " + @"Verify the Chromium Embedded Framework is properly embedded in the app bundle.", + cefFrameworkPath]; + NSLog(@"💥 INITIALIZATION FAILED: %@", errorMsg); + if (error) { + *error = [NSError errorWithDomain:@"CEFWrapper" + code:3 + userInfo:@{NSLocalizedDescriptionKey: errorMsg}]; + } + return NO; + } + NSLog(@"✅ CEF framework verified at: %@", cefFrameworkPath); + + // HARD FAIL: CEF resources (icudtl.dat, *.pak files) must exist + NSString *cefResourcesPath = [cefFrameworkPath stringByAppendingPathComponent:@"Resources"]; + if (![[NSFileManager defaultManager] fileExistsAtPath:cefResourcesPath]) { + NSString *errorMsg = [NSString stringWithFormat: + @"CEF resources not found at %@. This is a critical initialization failure. " + @"Without these resources, page navigation will abort with ERR_ABORTED.", + cefResourcesPath]; + NSLog(@"💥 INITIALIZATION FAILED: %@", errorMsg); + if (error) { + *error = [NSError errorWithDomain:@"CEFWrapper" + code:4 + userInfo:@{NSLocalizedDescriptionKey: errorMsg}]; + } + return NO; + } + NSLog(@"✅ CEF resources verified at: %@", cefResourcesPath); + + // HARD FAIL: WebView Helper executable must exist and be executable + NSString *helperExe = [frameworksDir stringByAppendingPathComponent: + @"WebView Helper.app/Contents/MacOS/WebView Helper"]; + NSLog(@"🔍 Checking helper at: %@", helperExe); + + if (![[NSFileManager defaultManager] fileExistsAtPath:helperExe]) { + NSString *errorMsg = [NSString stringWithFormat: + @"WebView Helper executable not found at %@. This is a critical initialization failure. " + @"Run the 'embed_cef_helpers' build phase or execute 'swift build' to build helpers.", + helperExe]; + NSLog(@"💥 INITIALIZATION FAILED: %@", errorMsg); + if (error) { + *error = [NSError errorWithDomain:@"CEFWrapper" + code:5 + userInfo:@{NSLocalizedDescriptionKey: errorMsg}]; + } + return NO; + } + NSLog(@"✅ Helper executable exists at: %@", helperExe); + + if (![[NSFileManager defaultManager] isExecutableFileAtPath:helperExe]) { + NSString *errorMsg = [NSString stringWithFormat: + @"WebView Helper at %@ exists but is not executable. This is a critical initialization failure. " + @"The helper may lack proper code signing or JIT entitlements. " + @"Rebuild with 'swift build' and ensure the embed_cef_helpers build phase runs.", + helperExe]; + NSLog(@"💥 INITIALIZATION FAILED: %@", errorMsg); + if (error) { + *error = [NSError errorWithDomain:@"CEFWrapper" + code:6 + userInfo:@{NSLocalizedDescriptionKey: errorMsg}]; + } + return NO; + } + NSLog(@"✅ Helper is executable"); +#endif + + // Set CEF settings using direct method that keeps data alive + CefString(&settings.browser_subprocess_path).FromString([helperExe UTF8String]); + NSLog(@"✅ CEF browser_subprocess_path configured: %@", helperExe); + + CefString(&settings.resources_dir_path).FromString([cefResourcesPath UTF8String]); + CefString(&settings.framework_dir_path).FromString([cefFrameworkPath UTF8String]); + CefString(&settings.main_bundle_path).FromString([bundlePath UTF8String]); + + NSString *ua = [NSString stringWithFormat: + @"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) " + @"Chrome/%d.%d.%d.%d Safari/537.36", + CHROME_VERSION_MAJOR, CHROME_VERSION_MINOR, CHROME_VERSION_BUILD, CHROME_VERSION_PATCH]; + CefString(&settings.user_agent).FromString([ua UTF8String]); + + // Enable verbose CEF logging to debug page rendering issues + settings.log_severity = LOGSEVERITY_VERBOSE; + NSString* cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; + if (cacheDir) { + NSString* logPath = [cacheDir stringByAppendingPathComponent:@"com.chromium.webview/debug.log"]; + CefString(&settings.log_file).FromString([logPath UTF8String]); + NSLog(@"✅ CEF debug log configured at: %@", logPath); + } + + // Set cache path to avoid singleton behavior warnings + if (cacheDir) { + cacheDir = [cacheDir stringByAppendingPathComponent:@"com.chromium.webview"]; + CefString(&settings.root_cache_path).FromString([cacheDir UTF8String]); + NSLog(@"✅ CEF cache path configured at: %@", cacheDir); + } + + NSLog(@"🔧 ========================================"); + NSLog(@"🔧 CEF Initialization Settings:"); + NSLog(@"🔧 no_sandbox: %d", settings.no_sandbox); + NSLog(@"🔧 external_message_pump: %d", settings.external_message_pump); + NSLog(@"🔧 browser_subprocess_path: %@", + [NSString stringWithUTF8String:CefString(&settings.browser_subprocess_path).ToString().c_str()]); + NSLog(@"🔧 resources_dir_path: %@", + [NSString stringWithUTF8String:CefString(&settings.resources_dir_path).ToString().c_str()]); + NSLog(@"🔧 ========================================"); + + CefRefPtr app = new CEFWebViewApp(); + NSLog(@"🔧 Calling CefInitialize..."); + bool initResult = CefInitialize(args, settings, app, nullptr); + NSLog(@"🔧 CefInitialize returned: %s", initResult ? "true" : "false"); + + if (!initResult) { + NSString *errorMsg = [NSString stringWithFormat: + @"CefInitialize failed. This is a critical initialization failure. " + @"Check the CEF debug log at %@/com.chromium.webview/debug.log for details. " + @"Common causes: missing/invalid helper executable, resource file (icudtl.dat) not found, " + @"framework path configuration error, or renderer subprocess initialization failure.", + cacheDir ?: @"~/Library/Caches"]; + NSLog(@"💥 INITIALIZATION FAILED: %@", errorMsg); + if (error) { + *error = [NSError errorWithDomain:@"CEFWrapper" + code:1 + userInfo:@{NSLocalizedDescriptionKey: errorMsg}]; + } + return NO; + } + + NSLog(@"✅ CefInitialize succeeded - CEF is now initialized and ready"); + g_cefInitialized = YES; + return YES; +} + ++ (nullable NSView *)createBrowserInView:(NSView *)parentView + url:(NSString *)urlString { + if (!g_cefInitialized) { + NSLog(@"💥 CRITICAL: CEF not initialized — cannot create browser. " + @"Call initializeCEFWithError: first and check for errors."); + return nil; + } + + if (!parentView) { + NSLog(@"💥 CRITICAL: parentView is nil — cannot create browser"); + return nil; + } + + if (!urlString || [urlString length] == 0) { + NSLog(@"💥 CRITICAL: urlString is nil or empty — cannot create browser"); + return nil; + } + + NSLog(@"🔧 Creating browser for URL: %@", urlString); + NSLog(@"🖼️ parentView: %@ bounds: %@", parentView, NSStringFromRect(parentView.bounds)); + + // Window info — SetAsChild forces Alloy (windowed-child) mode + CefWindowInfo windowInfo; + NSRect parentBounds = parentView.bounds; + NSLog(@"🔧 Calling SetAsChild with parentView bounds=(%d,%d,%d,%d)", + (int)parentBounds.origin.x, (int)parentBounds.origin.y, + (int)parentBounds.size.width, (int)parentBounds.size.height); + windowInfo.SetAsChild((__bridge void*)parentView, + CefRect((int)parentBounds.origin.x, (int)parentBounds.origin.y, + (int)parentBounds.size.width, (int)parentBounds.size.height)); + + // Browser settings + CefBrowserSettings browserSettings; + + // Create the C++ client + g_client = new ChromiumClient(); + NSLog(@"✅ ChromiumClient created"); + + // Create the browser + CefString url; + url.FromString([urlString UTF8String]); + + NSLog(@"🔧 Calling CefBrowserHost::CreateBrowserSync..."); + CefRefPtr browser = + CefBrowserHost::CreateBrowserSync(windowInfo, g_client, url, browserSettings, nullptr, nullptr); + + if (!browser) { + NSLog(@"💥 CRITICAL: CreateBrowserSync returned null. This means the browser subprocess failed to initialize. " + @"Likely causes: helper process crash, dyld framework loading failure, JIT entitlements missing, " + @"or invalid code signatures. Check the CEF debug log and system logs for details."); + g_client = nullptr; + return nil; + } + + NSLog(@"✅ Browser object created: %p", browser.get()); + g_browser = browser; + + // Get the native view that CEF created and is rendering into + NSView* cefView = (__bridge NSView*)browser->GetHost()->GetWindowHandle(); + if (!cefView) { + NSLog(@"💥 CRITICAL: GetWindowHandle returned null despite successful browser creation"); + return nil; + } + + NSLog(@"✅ Browser view obtained: %@ frame=%@", cefView, NSStringFromRect(cefView.frame)); + NSLog(@"✅ Browser initialization complete. Initial URL: %@", urlString); + + // Return the CEF-created view (it's already added to parentView by SetAsChild) + return cefView; +} + ++ (void)reloadBrowser { + if (g_browser) { + g_browser->Reload(); + } else { + NSLog(@"⚠️ No browser"); + } +} + ++ (void)closeBrowser { + if (g_browser) { + NSLog(@"🛑 Closing browser"); + g_browser->GetHost()->CloseBrowser(true); + g_browser = nullptr; + g_client = nullptr; + } +} + ++ (void)goBack { + if (g_browser) { + g_browser->GoBack(); + } +} + ++ (void)goForward { + if (g_browser) { + g_browser->GoForward(); + } +} + ++ (void)loadURL:(NSString *)urlString { + NSLog(@"🌐 [loadURL] Entering: %@ g_browser=%p", urlString, g_browser.get()); + + if (!g_browser) { + NSLog(@"⚠️ [loadURL] No browser instance"); + return; + } + + CefRefPtr frame = g_browser->GetMainFrame(); + if (!frame) { + NSLog(@"⚠️ [loadURL] No main frame"); + return; + } + + CefString url; + url.FromString([urlString UTF8String]); + NSLog(@"🌐 [loadURL] Calling frame->LoadURL(%@)", urlString); + frame->LoadURL(url); + NSLog(@"🌐 [loadURL] frame->LoadURL returned for %@", urlString); +} + ++ (void)doMessageLoopWork { + if (g_cefInitialized) { + CefDoMessageLoopWork(); + } +} + ++ (void)notifyBrowserViewGeometryChanged { + if (!g_browser) { + return; + } + CefRefPtr host = g_browser->GetHost(); + if (host.get()) { + host->WasResized(); + } +} + ++ (void)shutdown { + if (!g_cefInitialized) return; + + NSLog(@"🛑 Shutting down CEF"); + + // Release the browser and client references + g_browser = nullptr; + g_client = nullptr; + + CefShutdown(); + g_cefInitialized = NO; +} + ++ (BOOL)isLoading { + return g_client && g_client->IsLoading(); +} + ++ (BOOL)canGoBack { + return g_client && g_client->CanGoBack(); +} + ++ (BOOL)canGoForward { + return g_client && g_client->CanGoForward(); +} + ++ (nullable NSString *)currentTitle { + return g_client ? g_client->GetTitle() : nil; +} + ++ (nullable NSString *)currentURL { + return g_client ? g_client->GetURL() : nil; +} + +@end + +// ─── C Function Bridge Implementation ──────────────────────────────────────── +// These must be defined after @implementation so they can call Objective-C methods + +extern "C" { + void NotifyHelperSpawned(const char* helperType) { + if (!helperType) return; + NSString* type = [NSString stringWithUTF8String:helperType]; + NSLog(@"🚀 [C BRIDGE] Helper spawned: %@", type); + dispatch_async(dispatch_get_main_queue(), ^{ + [CEFWrapper notifyHelperSpawned:type]; + }); + } + + void NotifyHelperFailed(const char* helperType) { + if (!helperType) { + NSLog(@"❌ [C BRIDGE] NotifyHelperFailed called with null helperType"); + return; + } + NSString* type = [NSString stringWithUTF8String:helperType]; + NSLog(@"❌ [C BRIDGE] Helper failed: %@", type); + dispatch_async(dispatch_get_main_queue(), ^{ + [CEFWrapper notifyHelperFailed:type]; + }); + } + + void NotifyHelperFailedWithLoadError(const char* helperType, + int errorCode, + const char* errorText, + const char* failedUrl) { + if (!helperType) { + return; + } + NSString* type = [NSString stringWithUTF8String:helperType]; + NSString* text = errorText ? [NSString stringWithUTF8String:errorText] : nil; + NSString* url = failedUrl ? [NSString stringWithUTF8String:failedUrl] : nil; + NSString* safeText = text ?: @""; + NSString* safeUrl = url ?: @""; + NSLog(@"❌ [C BRIDGE] Helper failed with load error: %@ code=%d text=%@ url=%@", type, errorCode, safeText, safeUrl); + dispatch_async(dispatch_get_main_queue(), ^{ + [CEFWrapper notifyHelperFailed:type + mainFrameLoadErrorCode:errorCode + errorText:text + failedUrl:url]; + }); + } +} diff --git a/vendor/CEFWebView/Sources/CEFWrapper/include/CEFWrapper.h b/vendor/CEFWebView/Sources/CEFWrapper/include/CEFWrapper.h new file mode 100644 index 000000000000..3ad8b33a4c24 --- /dev/null +++ b/vendor/CEFWebView/Sources/CEFWrapper/include/CEFWrapper.h @@ -0,0 +1,91 @@ +// +// CEFWrapper.h +// CEFWebView +// +// Objective-C wrapper for CEF C++ API - provides simple methods for Swift to interact with CEF +// + +#ifndef CEFWrapper_h +#define CEFWrapper_h + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Objective-C wrapper exposing CEF functionality to Swift +@interface CEFWrapper : NSObject + +/// Handle subprocess role detection — call this at the very start of the app, before any CEF initialization +/// Returns the subprocess exit code if this process is a CEF subprocess (renderer, GPU, utility, network) +/// Returns -1 if this is the main browser process and should continue with normal app startup +/// If subprocess is detected, this method does not return — the process exits with the returned code ++ (int)executeSubprocessWithArgc:(int)argc argv:(char * _Nullable * _Nullable)argv; + +/// Initialize CEF framework +/// Must be called on the main thread, once at app startup ++ (BOOL)initializeCEFWithError:(NSError **)error; + +/// Create a browser in windowed mode within a parent view +/// @param parentView The parent NSView to embed the browser in +/// @param urlString The initial URL to load (e.g., "https://google.com") +/// @return The NSView containing the CEF browser, or nil on error ++ (nullable NSView *)createBrowserInView:(NSView *)parentView + url:(NSString *)urlString; + +/// Reload the current page in the browser ++ (void)reloadBrowser; + +/// Close the current browser (call before view teardown) ++ (void)closeBrowser; + +/// Navigate back ++ (void)goBack; + +/// Navigate forward ++ (void)goForward; + +/// Load a new URL ++ (void)loadURL:(NSString *)urlString; + +/// Process pending CEF messages (call regularly from message pump) ++ (void)doMessageLoopWork; + +/// Call after the embedding NSView changes size or layout (SwiftUI updates frames asynchronously). +/// Maps to CefBrowserHost::WasResized(); required or the browser can stay at 0×0 and show a blank view. ++ (void)notifyBrowserViewGeometryChanged; + +/// Shutdown CEF gracefully ++ (void)shutdown; + +/// Check if a page is currently loading ++ (BOOL)isLoading; + +/// Check if the browser can navigate back ++ (BOOL)canGoBack; + +/// Check if the browser can navigate forward ++ (BOOL)canGoForward; + +/// Get the current page title ++ (nullable NSString *)currentTitle; + +/// Get the current page URL (updated on every navigation) ++ (nullable NSString *)currentURL; + +/// Internal: Notify that a helper process has spawned (called from C++ notification handler) ++ (void)notifyHelperSpawned:(NSString *)helperType; + +/// Internal: Notify that a helper process has failed (called from C++ notification handler) ++ (void)notifyHelperFailed:(NSString *)helperType; + +/// Internal: Helper failed with main-frame load error details (OnLoadError). ++ (void)notifyHelperFailed:(NSString *)helperType + mainFrameLoadErrorCode:(NSInteger)errorCode + errorText:(nullable NSString *)errorText + failedUrl:(nullable NSString *)failedUrl; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* CEFWrapper_h */ diff --git a/vendor/CEFWebView/build.sh b/vendor/CEFWebView/build.sh new file mode 100755 index 000000000000..0dd9ddbe7277 --- /dev/null +++ b/vendor/CEFWebView/build.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env zsh + +set -e + +# Colors for output +RED=$'\033[0;31m' +GREEN=$'\033[0;32m' +YELLOW=$'\033[1;33m' +BLUE=$'\033[0;34m' +NC=$'\033[0m' # No Color + +# Directories +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WEBVIEW_PROJECT="$PROJECT_DIR/WebView" +BUILD_DIR="$PROJECT_DIR/.build" +RELEASE_DIR="$PROJECT_DIR/Release" + +# Function to print colored output +log_info() { + echo "${BLUE}ℹ ${1}${NC}" +} + +log_success() { + echo "${GREEN}✓ ${1}${NC}" +} + +log_warning() { + echo "${YELLOW}⚠ ${1}${NC}" +} + +log_error() { + echo "${RED}✗ ${1}${NC}" +} + +# Build CEF dependencies +build_cef() { + log_info "Building CEF dependencies..." + + cd "$PROJECT_DIR" + ./build_cpp.sh > /dev/null 2>&1 || { + log_error "CEF build failed" + return 1 + } + + log_success "CEF dependencies built" +} + +# Clean action +clean() { + log_info "Cleaning build artifacts..." + + # Clean Xcode derived data + if [ -d "$BUILD_DIR" ]; then + rm -rf "$BUILD_DIR" + log_success "Cleaned .build directory" + fi + + log_success "Clean complete" +} + +# Build action +build() { + log_info "Building WebView..." + + # Build CEF first + build_cef + + # Build the Xcode project + log_info "Building Xcode project with Swift package..." + cd "$WEBVIEW_PROJECT" + + xcodebuild -project WebView.xcodeproj \ + -scheme WebView \ + -configuration Debug \ + -derivedDataPath "$BUILD_DIR/xcode" \ + build 2>&1 | tee "$PROJECT_DIR/build.log" || { + log_error "Xcode build failed. See build.log for details." + return 1 + } + + log_success "WebView built successfully" +} + +# Release action +release() { + log_info "Preparing release build..." + + # Clean first + clean + + # Build CEF dependencies + build_cef + + # Build release configuration + log_info "Building release configuration..." + cd "$WEBVIEW_PROJECT" + + xcodebuild -project WebView.xcodeproj \ + -scheme WebView \ + -configuration Release \ + -derivedDataPath "$BUILD_DIR/xcode" \ + build 2>&1 | tee "$PROJECT_DIR/release.log" || { + log_error "Release build failed. See release.log for details." + return 1 + } + + # Create release directory + mkdir -p "$RELEASE_DIR" + + # Copy release artifacts + log_info "Copying release artifacts..." + RELEASE_APP="$BUILD_DIR/xcode/Build/Products/Release/WebView.app" + if [ -d "$RELEASE_APP" ]; then + cp -R "$RELEASE_APP" "$RELEASE_DIR/" + log_success "App copied to $RELEASE_DIR" + fi + + # Generate release notes + RELEASE_NOTES="$RELEASE_DIR/RELEASE_NOTES.txt" + cat > "$RELEASE_NOTES" << EOF +# WebView Release +Generated: $(date) + +## Build Info +- Configuration: Release +- Xcode Version: $(xcodebuild -version) +- CEF Version: 146.0.10 + +## Contents +- WebView.app: macOS application +- Build artifacts in .build/release + +## Notes +- See release.log for detailed build output +- Swift package (CEFWebView) built as app dependency +EOF + + log_success "Release package created in $RELEASE_DIR" + log_info "Release notes: $RELEASE_NOTES" +} + +# Version check +version() { + log_info "Version Information:" + echo " Xcode: $(xcodebuild -version)" + echo " Project: $PROJECT_DIR" + echo " Swift: $(swift --version 2>/dev/null | head -1)" +} + +# Help message +show_help() { + echo -e "${BLUE}WebView Build Script${NC}\n" + echo -e "${GREEN}Usage:${NC}" + echo " ./build.sh [action]" + echo "" + echo -e "${GREEN}Actions:${NC}" + echo " clean - Remove all build artifacts" + echo " build - Build debug configuration (includes CEF and Swift package)" + echo " release - Build release configuration with artifacts" + echo " version - Show version information" + echo " help - Show this help message" + echo "" + echo -e "${GREEN}Examples:${NC}" + echo " ./build.sh clean" + echo " ./build.sh build" + echo " ./build.sh release" + echo "" + echo -e "${YELLOW}Note:${NC} Run from project root directory" + echo -e "${YELLOW}Note:${NC} Builds WebView.xcodeproj with the root CEFWebView Swift package" +} + +# Main script logic +main() { + # If no arguments provided, show help + if [[ $# -eq 0 ]]; then + show_help + return 0 + fi + + # Execute each action in sequence + for action in "$@"; do + case "$action" in + clean) + clean + ;; + build) + build + ;; + release) + release + ;; + version) + version + ;; + help) + show_help + ;; + *) + log_error "Unknown action: $action" + show_help + exit 1 + ;; + esac + done +} + +# Run main script +main "$@" diff --git a/vendor/CEFWebView/build_cpp.sh b/vendor/CEFWebView/build_cpp.sh new file mode 100755 index 000000000000..1be9d04195ed --- /dev/null +++ b/vendor/CEFWebView/build_cpp.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env zsh + +set -e # Exit on error + +# ─── Configuration ─────────────────────────────────────────────────────────── + +SRCROOT="${SRCROOT:-.}" +FRAMEWORKS_DIR="$SRCROOT/Frameworks" + +# ─── Helper Functions ──────────────────────────────────────────────────────── + +# Find the CEF binary directory +find_cef_dir() { + local cef_dir=$(find -L "$SRCROOT/CEF" -maxdepth 1 -type d -name "cef_binary_*" | head -1) + if [ -z "$cef_dir" ]; then + echo "❌ Error: Could not find CEF binary directory in $SRCROOT/CEF" + exit 1 + fi + echo "$cef_dir" +} + +# Build the C++ DLL wrapper from source +build_cef_wrapper() { + local cef_dir="$1" + echo "🔨 Building CEF C++ wrapper..." + + ( + cd "$cef_dir" + cmake -G "Xcode" -DPROJECT_ARCH="arm64" . + xcodebuild -configuration Release + ) + + echo "✓ CEF C++ wrapper built" +} + +# Restructure framework from flat to versioned macOS layout +restructure_framework() { + local fw="$1" + local fw_name="Chromium Embedded Framework" + + echo " ↳ Restructuring to macOS versioned layout..." + + # Create versioned directory structure + mkdir -p "$fw/Versions/A" + + # Move binary to Versions/A + mv "$fw/$fw_name" "$fw/Versions/A/" + + # Move Resources to Versions/A + mv "$fw/Resources" "$fw/Versions/A/" + + # Move Libraries to Versions/A if it exists + if [ -d "$fw/Libraries" ]; then + mv "$fw/Libraries" "$fw/Versions/A/" + fi + + # Create Current symlink (Versions/Current -> A) + ln -s "A" "$fw/Versions/Current" + + # Create root-level symlinks pointing to Versions/Current + ln -s "Versions/Current/$fw_name" "$fw/$fw_name" + ln -s "Versions/Current/Resources" "$fw/Resources" + if [ -d "$fw/Versions/A/Libraries" ]; then + ln -s "Versions/Current/Libraries" "$fw/Libraries" + fi + + # Update the binary's install name to the versioned path using @rpath. + # Xcode strips the root-level binary symlink during code signing, so dyld must + # resolve the binary directly via Versions/A/. @rpath is resolved using the + # app's LD_RUNPATH_SEARCH_PATHS (@executable_path/../Frameworks). + local versioned_id="@rpath/$fw_name.framework/Versions/A/$fw_name" + install_name_tool -id "$versioned_id" "$fw/Versions/A/$fw_name" +} + +# Copy static library and headers to Frameworks +copy_static_artifacts() { + local cef_dir="$1" + echo "📦 Copying static library and headers..." + + mkdir -p "$FRAMEWORKS_DIR/include" + cp "$cef_dir/libcef_dll_wrapper/Release/libcef_dll_wrapper.a" "$FRAMEWORKS_DIR/" + cp -R "$cef_dir/include/" "$FRAMEWORKS_DIR/include/" + + echo "✓ Copied libcef_dll_wrapper.a and headers" +} + +# Copy dynamic framework and helper apps +copy_dynamic_framework() { + local cef_dir="$1" + echo "📚 Copying dynamic Chromium Embedded Framework..." + + mkdir -p "$FRAMEWORKS_DIR" + + # Copy Chromium Embedded Framework from CEF Release directory + local cef_framework="$cef_dir/Release/Chromium Embedded Framework.framework" + if [ -d "$cef_framework" ]; then + rm -rf "$FRAMEWORKS_DIR/Chromium Embedded Framework.framework" + cp -R "$cef_framework" "$FRAMEWORKS_DIR/" + + # CEF ships the framework as a flat bundle, but macOS requires versioned layout. + # Restructure Versions/A/Resources/Info.plist for Xcode code signing. + local fw_dest="$FRAMEWORKS_DIR/Chromium Embedded Framework.framework" + restructure_framework "$fw_dest" + + # Remove pre-existing signature from CEF's team so Xcode can re-sign with our team cert + echo " 🔓 Removing CEF's pre-existing signature..." + codesign --remove-signature "$fw_dest/Versions/A/Chromium Embedded Framework" 2>/dev/null || true + + echo "✓ Copied Chromium Embedded Framework to Frameworks/" + else + echo "⚠️ Warning: Could not find Chromium Embedded Framework at $cef_framework" + return 1 + fi + + # Copy CEF helper apps if they exist (optional; normally in unpacked dirs) + setopt nullglob # Make glob expansion return empty list instead of error when no matches + local helper_count=0 + for helper in "$cef_dir/Release/"*.app; do + [ -d "$helper" ] || continue + local bundle_name=$(basename "$helper") + rm -rf "$FRAMEWORKS_DIR/$bundle_name" + cp -R "$helper" "$FRAMEWORKS_DIR/" + echo "✓ Copied $bundle_name to Frameworks/" + helper_count=$((helper_count + 1)) + done + unsetopt nullglob + + if [ $helper_count -eq 0 ]; then + echo "ℹ️ No helper apps found in Release directory (this is normal)" + fi +} + +# ─── Main Build Flow ──────────────────────────────────────────────────────── + +echo "🚀 Building CEFWebView CEF dependencies..." +echo "" + +# Find CEF directory +cef_dir=$(find_cef_dir) +echo "📍 Found CEF at: $cef_dir" +echo "" + +# Build the C++ wrapper +build_cef_wrapper "$cef_dir" +echo "" + +# Copy artifacts to Frameworks +copy_static_artifacts "$cef_dir" +echo "" + +copy_dynamic_framework "$cef_dir" +echo "" + +echo "✅ Build complete! Frameworks/ is ready for Xcode." diff --git a/vendor/CEFWebView/capture_logs.sh b/vendor/CEFWebView/capture_logs.sh new file mode 100755 index 000000000000..bb210b1ea7f8 --- /dev/null +++ b/vendor/CEFWebView/capture_logs.sh @@ -0,0 +1,99 @@ +#!/bin/zsh +# capture_logs.sh - Capture co.sstools.WebView app logs (Swift + C++ + CEF) + +BUNDLE_ID="co.sstools.WebView" +SUBSYSTEMS='subsystem == "co.sstools.WebView" OR subsystem == "co.sstools.CEFWebView"' + +# Create logs directory if it doesn't exist +mkdir -p logs + +# Generate timestamp for filename +TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") +LOG_FILE="logs/logs_${TIMESTAMP}.txt" + +echo "📋 Capturing logs from the past 2 minutes..." +echo " Bundle ID: $BUNDLE_ID" +echo "💾 Writing to: $LOG_FILE" + +# Capture all logs for the app +{ + echo "==========================================" + echo "WebView App Logs - Captured at: $(date)" + echo "Bundle ID: $BUNDLE_ID" + echo "==========================================" + echo "" + + # 1. Main app process and Logger subsystems + echo "--- Swift Logger + NSLog (App Subsystems) ---" + echo "" + /usr/bin/log show --predicate "$SUBSYSTEMS" --debug --info --last 2m 2>/dev/null || { + echo "⚠️ Could not read system logs." + } + + echo "" + echo "--- NSLog Output from WebView Process ---" + echo "" + /usr/bin/log show --predicate 'process == "WebView"' --debug --info --last 2m 2>/dev/null || { + echo "⚠️ Could not read WebView process logs." + } + + echo "" + echo "--- System Logs (All App-Related Processes) ---" + echo "" + /usr/bin/log show --predicate "processImagePath CONTAINS \"$BUNDLE_ID\" OR process == \"CEFHelper\" OR process == \"CEFHelperRenderer\" OR process == \"WebView\"" --debug --info --last 2m 2>/dev/null || { + echo "⚠️ Could not read system logs. You may need to run with elevated permissions." + echo "Try: sudo $0" + } + + echo "" + echo "--- CEF Helper Debug Log (/tmp) ---" + echo "" + + # Capture CEF helper debug log from /tmp + if [ -f "/tmp/cef_helper_debug.log" ]; then + echo "Found: /tmp/cef_helper_debug.log (last 100 lines):" + echo "" + tail -100 "/tmp/cef_helper_debug.log" + else + echo "⚠️ CEF helper debug log not found at /tmp/cef_helper_debug.log" + fi + + echo "" + echo "--- CEF Debug Log (if available) ---" + echo "" + + # Find CEF debug log in Chromium WebView cache directory + CEF_CACHE_DIR="$HOME/Library/Caches/com.chromium.webview" + if [ -d "$CEF_CACHE_DIR" ]; then + echo "Searching for CEF logs in: $CEF_CACHE_DIR" + if [ -f "$CEF_CACHE_DIR/debug.log" ]; then + echo "Found: $CEF_CACHE_DIR/debug.log (last 100 lines):" + echo "" + tail -100 "$CEF_CACHE_DIR/debug.log" + else + echo "⚠️ CEF debug.log not found in $CEF_CACHE_DIR" + echo " Available files in cache directory:" + ls -la "$CEF_CACHE_DIR" 2>/dev/null | head -20 + fi + else + echo "⚠️ Cache directory not found: $CEF_CACHE_DIR" + echo " The app may not have run yet." + fi + + echo "" + echo "--- Running Processes ---" + echo "" + echo "Main app process:" + ps aux | grep -E "WebView|$BUNDLE_ID" | grep -v grep || echo " (not found)" + + echo "" + echo "CEF helper processes:" + ps aux | grep -E "CEFHelper|CEFHelperRenderer" | grep -v grep || echo " (none found)" + + echo "" + echo "==========================================" + echo "End of Logs" + echo "==========================================" +} > "$LOG_FILE" + +echo "$LOG_FILE" diff --git a/vendor/CEFWebView/diagnose_renderer.sh b/vendor/CEFWebView/diagnose_renderer.sh new file mode 100755 index 000000000000..65a3f798ae1c --- /dev/null +++ b/vendor/CEFWebView/diagnose_renderer.sh @@ -0,0 +1,44 @@ +#!/bin/zsh +# Diagnose renderer process spawn failure + +echo "🔍 === CEF RENDERER SPAWN DIAGNOSTIC ===" +echo "" + +echo "📋 1. CEF Debug Log (last 100 lines, filter for 'renderer' and errors):" +echo "---" +if [ -f ~/Library/Caches/com.chromium.webview/debug.log ]; then + tail -100 ~/Library/Caches/com.chromium.webview/debug.log | grep -i "renderer\|error\|failed\|crash" +else + echo "❌ File not found: ~/Library/Caches/com.chromium.webview/debug.log" + echo " Has the app been run yet?" +fi +echo "" + +echo "📋 2. Helper Invocation Log (all entries):" +echo "---" +if [ -f /tmp/cef_helper_debug.log ]; then + cat /tmp/cef_helper_debug.log +else + echo "⚠️ File not found: /tmp/cef_helper_debug.log" + echo " Helper process may not be spawning at all" +fi +echo "" + +echo "📋 3. System Logs - WebView crashes (last 50 entries):" +echo "---" +log show --predicate 'process == "WebView Helper"' --last 50m 2>/dev/null | tail -50 || echo "⚠️ Could not fetch system logs" +echo "" + +echo "📋 4. Check if renderer --type= appears in logs:" +echo "---" +if [ -f ~/Library/Caches/com.chromium.webview/debug.log ]; then + grep -i "type=renderer\|--renderer" ~/Library/Caches/com.chromium.webview/debug.log | head -5 +else + echo "⚠️ Debug log not available" +fi +echo "" + +echo "✅ Diagnostic complete. Key things to check:" +echo " - Does /tmp/cef_helper_debug.log show any '--type=renderer' invocations?" +echo " - Are there 'error' or 'failed' entries in debug.log?" +echo " - Do system logs show WebView Helper crashes?" diff --git a/vendor/CEFWebView/fix_cef_framework.sh b/vendor/CEFWebView/fix_cef_framework.sh new file mode 100755 index 000000000000..0239448a8d53 --- /dev/null +++ b/vendor/CEFWebView/fix_cef_framework.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Fix CEF framework symlink structure after Xcode embedding +# Xcode's Embed Frameworks phase expands symlinks to directories — this restores the correct structure + +FRAMEWORK_PATH="${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/../Frameworks/Chromium Embedded Framework.framework" + +if [ ! -d "$FRAMEWORK_PATH" ]; then + echo "⚠️ Framework not found at $FRAMEWORK_PATH" + exit 0 +fi + +echo "🔧 Fixing CEF framework symlinks at: $FRAMEWORK_PATH" + +# Remove mis-embedded root files (should be symlinks, not copies) +rm -f "$FRAMEWORK_PATH/Chromium Embedded Framework" +rm -f "$FRAMEWORK_PATH/Resources" +rm -f "$FRAMEWORK_PATH/Libraries" + +# Create proper root-level symlinks to Versions/Current +cd "$FRAMEWORK_PATH" +ln -sf "Versions/Current/Chromium Embedded Framework" "Chromium Embedded Framework" +ln -sf "Versions/Current/Resources" "Resources" + +if [ -d "Versions/A/Libraries" ]; then + ln -sf "Versions/Current/Libraries" "Libraries" +fi + +# Fix Versions/Current — should be symlink to A, not a directory +if [ -d "Versions/Current" ] && [ ! -L "Versions/Current" ]; then + rm -rf "Versions/Current" + ln -s "A" "Versions/Current" +fi + +echo "✓ CEF framework structure fixed" From 29cb75a40c93410fa4ae9b4b126c29f5b9302922 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Thu, 16 Apr 2026 15:53:55 -0700 Subject: [PATCH 2/2] Phase 2: wire CEFWebView into cmux + Debug > Chromium (CEF) window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What lands: - GhosttyTabs.xcodeproj: register vendor/CEFWebView as XCLocalSwiftPackageReference, add CEFWebView product dep on cmux target, set FRAMEWORK_SEARCH_PATHS / LIBRARY_SEARCH_PATHS for Debug + Release. - Sources/CefDebugWindow.swift: NSWindow with CEFWebView, omnibar, back/forward/reload, status line. Behind Debug menu only. - Sources/cmuxApp.swift: Debug > Debug Windows > Chromium (CEF)… - scripts/embed-cefwebview.sh: runs after reload.sh build. Embeds Chromium Embedded Framework.framework, builds CEFHelper + CEFHelperRenderer SPM products into "WebView Helper.app" / "WebView Helper (Renderer).app" inside Contents/Frameworks/, and inside-out ad-hoc signs (no --deep — would clobber CLI helper signatures and trip amfi errno 163 on macOS 26 Tahoe). Status (cef-spike build): - CEF initialization, helper spawning, and network requests work (renderer subprocess alive, google.com/accounts traffic in ~/Library/Caches/com.chromium.webview/debug.log). - Visible NSView inside the SwiftUI window stays blank — the known CEFWebView "white page" issue (see vendor/CEFWebView/ IMPLEMENTATION_GUIDE.md "Known Issues"). Needs an upstream fix in CEFWebView or a switch to OSR/CALayerHost compositing before this is dogfoodable. Use: ./scripts/reload.sh --tag cef-spike then ./scripts/embed-cefwebview.sh "$APP_PATH" --- GhosttyTabs.xcodeproj/project.pbxproj | 31 ++++++ Sources/CefDebugWindow.swift | 128 +++++++++++++++++++++ Sources/cmuxApp.swift | 3 + scripts/embed-cefwebview.sh | 153 ++++++++++++++++++++++++++ 4 files changed, 315 insertions(+) create mode 100644 Sources/CefDebugWindow.swift create mode 100755 scripts/embed-cefwebview.sh diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 7e410de4afeb..fc956997e2f6 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ FE001101 /* FileExplorerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE001001 /* FileExplorerStore.swift */; }; + CEF00200 /* CefDebugWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF00201 /* CefDebugWindow.swift */; }; FE001102 /* FileExplorerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE001002 /* FileExplorerView.swift */; }; FE002101 /* FileExplorerRootResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE002001 /* FileExplorerRootResolverTests.swift */; }; FE002102 /* FileExplorerStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE002002 /* FileExplorerStoreTests.swift */; }; @@ -212,6 +213,7 @@ /* Begin PBXFileReference section */ FE001001 /* FileExplorerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorerStore.swift; sourceTree = ""; }; + CEF00201 /* CefDebugWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CefDebugWindow.swift; sourceTree = ""; }; FE001002 /* FileExplorerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorerView.swift; sourceTree = ""; }; FE002001 /* FileExplorerRootResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorerRootResolverTests.swift; sourceTree = ""; }; FE002002 /* FileExplorerStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExplorerStoreTests.swift; sourceTree = ""; }; @@ -533,6 +535,7 @@ A5001655 /* CmuxDirectoryTrust.swift */, FE001001 /* FileExplorerStore.swift */, FE001002 /* FileExplorerView.swift */, + CEF00201 /* CefDebugWindow.swift */, ); path = Sources; sourceTree = ""; @@ -661,6 +664,7 @@ A5001271 /* PostHog */, A5001261 /* Bonsplit */, A5001291 /* MarkdownUI */, + CEF00101 /* CEFWebView */, ); name = GhosttyTabs; productName = GhosttyTabs; @@ -781,6 +785,7 @@ A5001272 /* XCRemoteSwiftPackageReference "posthog-ios" */, A5001292 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, A5001260 /* XCLocalSwiftPackageReference "bonsplit" */, + CEF00100 /* XCLocalSwiftPackageReference "CEFWebView" */, ); productRefGroup = A5001042 /* Products */; projectDirPath = ""; @@ -864,6 +869,7 @@ A5001654 /* CmuxDirectoryTrust.swift in Sources */, FE001101 /* FileExplorerStore.swift in Sources */, FE001102 /* FileExplorerView.swift in Sources */, + CEF00200 /* CefDebugWindow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1047,11 +1053,19 @@ DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/vendor/CEFWebView/Frameworks", + ); INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/vendor/CEFWebView/Frameworks", + ); MARKETING_VERSION = 0.63.2; OTHER_LDFLAGS = ( "-lc++", @@ -1086,11 +1100,19 @@ DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/vendor/CEFWebView/Frameworks", + ); INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/vendor/CEFWebView/Frameworks", + ); MARKETING_VERSION = 0.63.2; OTHER_LDFLAGS = ( "-lc++", @@ -1312,6 +1334,10 @@ isa = XCLocalSwiftPackageReference; relativePath = vendor/bonsplit; }; + CEF00100 /* XCLocalSwiftPackageReference "CEFWebView" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = vendor/CEFWebView; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1340,6 +1366,11 @@ package = A5001292 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; productName = MarkdownUI; }; + CEF00101 /* CEFWebView */ = { + isa = XCSwiftPackageProductDependency; + package = CEF00100 /* XCLocalSwiftPackageReference "CEFWebView" */; + productName = CEFWebView; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCConfigurationList section */ diff --git a/Sources/CefDebugWindow.swift b/Sources/CefDebugWindow.swift new file mode 100644 index 000000000000..d30ec6840856 --- /dev/null +++ b/Sources/CefDebugWindow.swift @@ -0,0 +1,128 @@ +import AppKit +import CEFWebView +import SwiftUI + +/// Debug-only window hosting a CEFWebView (Chromium) so we can dogfood the +/// engine before plumbing it into BrowserPanel. Open via Debug > Debug Windows > +/// Chromium (CEF)… +@MainActor +final class CefDebugWindowController: NSWindowController, NSWindowDelegate { + static let shared = CefDebugWindowController() + + private init() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 1024, height: 720), + styleMask: [.titled, .closable, .miniaturizable, .resizable], + backing: .buffered, + defer: false + ) + window.title = "Chromium (CEF)" + window.identifier = NSUserInterfaceItemIdentifier("cmux.cefDebug") + window.isReleasedWhenClosed = false + window.center() + window.contentView = NSHostingView(rootView: CefDebugView()) + super.init(window: window) + window.delegate = self + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func show() { + window?.makeKeyAndOrderFront(nil) + } +} + +private struct CefDebugView: View { + @State private var url: URL? = URL(string: "https://www.google.com") + @State private var urlText: String = "https://www.google.com" + @State private var state = CEFWebViewState() + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 6) { + Button { + state.goBack() + } label: { + Image(systemName: "chevron.left") + } + .disabled(!state.canGoBack) + + Button { + state.goForward() + } label: { + Image(systemName: "chevron.right") + } + .disabled(!state.canGoForward) + + Button { + state.reload() + } label: { + Image(systemName: "arrow.clockwise") + } + + TextField("URL", text: $urlText) + .textFieldStyle(.roundedBorder) + .onSubmit { + if let parsed = parseURL(urlText) { + url = parsed + } + } + } + .padding(8) + + Divider() + + ZStack { + CEFWebView(url: $url, state: $state) + + if let err = state.initializationError { + VStack(spacing: 12) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.largeTitle) + .foregroundStyle(.yellow) + Text("CEF initialization failed") + .font(.headline) + ScrollView { + Text(err) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + .padding() + } + } + .padding() + .background(.regularMaterial) + } + } + + HStack { + if state.isLoading { + ProgressView().controlSize(.small) + } + Text(state.title ?? state.currentURL?.absoluteString ?? "") + .font(.caption) + .lineLimit(1) + .truncationMode(.middle) + Spacer() + if state.rendererHelperFailed { + Text(state.rendererFailureStatusLine) + .font(.caption) + .foregroundStyle(.red) + } + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + } + } + + private func parseURL(_ text: String) -> URL? { + let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + if trimmed.contains("://") { + return URL(string: trimmed) + } + return URL(string: "https://\(trimmed)") + } +} diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 5c700ba51951..0adbdf426dd5 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -488,6 +488,9 @@ struct cmuxApp: App { Button("Background Debug…") { BackgroundDebugWindowController.shared.show() } + Button("Chromium (CEF)…") { + CefDebugWindowController.shared.show() + } Button("Browser Import Hint Debug…") { BrowserImportHintDebugWindowController.shared.show() } diff --git a/scripts/embed-cefwebview.sh b/scripts/embed-cefwebview.sh new file mode 100755 index 000000000000..b526bba73081 --- /dev/null +++ b/scripts/embed-cefwebview.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# Embed CEFWebView's Chromium framework + helper apps into a built cmux.app. +# +# Usage: scripts/embed-cefwebview.sh +# +# Run after xcodebuild produces cmux.app but before launch. Idempotent — +# safe to re-run. +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +APP="$1" +[ -d "$APP" ] || { echo "❌ App bundle not found: $APP" >&2; exit 1; } + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PKG_ROOT="$REPO_ROOT/vendor/CEFWebView" +PKG_FRAMEWORKS="$PKG_ROOT/Frameworks" +APP_FRAMEWORKS="$APP/Contents/Frameworks" + +# 1. Make sure CEF binaries + libcef_dll_wrapper.a are built. +if [ ! -d "$PKG_FRAMEWORKS/Chromium Embedded Framework.framework" ]; then + echo "==> Bootstrapping CEFWebView Frameworks/ (one-time, slow)" + "$REPO_ROOT/scripts/setup-cefwebview.sh" +fi + +# 2. Build SPM helper executables. +echo "==> Building CEFHelper + CEFHelperRenderer (release)" +swift build --package-path "$PKG_ROOT" -c release --product CEFHelper >/dev/null +swift build --package-path "$PKG_ROOT" -c release --product CEFHelperRenderer >/dev/null +HELPER_BUILD_DIR="$PKG_ROOT/.build/arm64-apple-macosx/release" +HELPER_BIN="$HELPER_BUILD_DIR/CEFHelper" +RENDERER_BIN="$HELPER_BUILD_DIR/CEFHelperRenderer" +[ -x "$HELPER_BIN" ] || { echo "❌ Missing $HELPER_BIN" >&2; exit 1; } +[ -x "$RENDERER_BIN" ] || { echo "❌ Missing $RENDERER_BIN" >&2; exit 1; } + +mkdir -p "$APP_FRAMEWORKS" + +# 3. Copy Chromium Embedded Framework.framework, then repair symlinks. +echo "==> Embedding Chromium Embedded Framework.framework" +rm -rf "$APP_FRAMEWORKS/Chromium Embedded Framework.framework" +cp -R "$PKG_FRAMEWORKS/Chromium Embedded Framework.framework" "$APP_FRAMEWORKS/" + +FW_PATH="$APP_FRAMEWORKS/Chromium Embedded Framework.framework" +( + cd "$FW_PATH" + rm -f "Chromium Embedded Framework" Resources Libraries + ln -sf "Versions/Current/Chromium Embedded Framework" "Chromium Embedded Framework" + ln -sf "Versions/Current/Resources" "Resources" + if [ -d "Versions/A/Libraries" ]; then + ln -sf "Versions/Current/Libraries" "Libraries" + fi + if [ -d "Versions/Current" ] && [ ! -L "Versions/Current" ]; then + rm -rf "Versions/Current" + ln -s "A" "Versions/Current" + fi +) + +# 4. Build helper app bundles. CEFWrapper.mm hardcodes the name +# "WebView Helper.app" relative to Contents/Frameworks/. +make_helper_app() { + local app_path="$1" + local exec_name="$2" + local source_bin="$3" + local bundle_id="$4" + + rm -rf "$app_path" + mkdir -p "$app_path/Contents/MacOS" + cp "$source_bin" "$app_path/Contents/MacOS/$exec_name" + chmod +x "$app_path/Contents/MacOS/$exec_name" + + cat > "$app_path/Contents/Info.plist" < + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $exec_name + CFBundleIdentifier + $bundle_id + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $exec_name + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 14.0 + LSUIElement + + + +EOF +} + +echo "==> Building WebView Helper.app + WebView Helper (Renderer).app" +make_helper_app "$APP_FRAMEWORKS/WebView Helper.app" \ + "WebView Helper" \ + "$HELPER_BIN" \ + "com.cmuxterm.app.helper" +make_helper_app "$APP_FRAMEWORKS/WebView Helper (Renderer).app" \ + "WebView Helper" \ + "$RENDERER_BIN" \ + "com.cmuxterm.app.helper.renderer" + +# 5. Inside-out ad-hoc sign helpers + framework + host app. +# Local DEV builds: ad-hoc only (no Developer ID identity needed). Never use +# --deep — it would overwrite the CLI helper signatures inside the host app +# and trigger amfi rejection (errno 163) on macOS 26 Tahoe. +SIGN_ID="-" +HELPER_ENTITLEMENTS="$REPO_ROOT/cmux-helper.entitlements" + +echo "==> Signing CEF framework + helpers (ad-hoc)" +# Sign nested executables inside framework first (Versions/A/Helpers/* if any). +find "$APP_FRAMEWORKS/Chromium Embedded Framework.framework/Versions/A" \ + -type f -perm -u+x ! -name '*.dylib' 2>/dev/null \ + | while read -r f; do + codesign --force --sign "$SIGN_ID" "$f" >/dev/null 2>&1 || true + done +# Sign the framework itself. +codesign --force --sign "$SIGN_ID" \ + "$APP_FRAMEWORKS/Chromium Embedded Framework.framework" >/dev/null + +# Sign helper executables, then their bundles. Helper exec names are literal +# "WebView Helper" (CEFWrapper.mm hardcodes this). +codesign --force --sign "$SIGN_ID" --options runtime \ + --entitlements "$HELPER_ENTITLEMENTS" \ + "$APP_FRAMEWORKS/WebView Helper.app/Contents/MacOS/WebView Helper" >/dev/null +codesign --force --sign "$SIGN_ID" --options runtime \ + --entitlements "$HELPER_ENTITLEMENTS" \ + "$APP_FRAMEWORKS/WebView Helper.app" >/dev/null +codesign --force --sign "$SIGN_ID" --options runtime \ + --entitlements "$HELPER_ENTITLEMENTS" \ + "$APP_FRAMEWORKS/WebView Helper (Renderer).app/Contents/MacOS/WebView Helper" >/dev/null +codesign --force --sign "$SIGN_ID" --options runtime \ + --entitlements "$HELPER_ENTITLEMENTS" \ + "$APP_FRAMEWORKS/WebView Helper (Renderer).app" >/dev/null + +# Re-seal the host app so the new Contents/Frameworks/ entries are in +# CodeResources. No --deep, no host-level entitlements (DEV bundle was +# originally signed ad-hoc with no entitlements). +echo "==> Re-sealing host app (ad-hoc, no --deep)" +codesign --force --sign "$SIGN_ID" "$APP" >/dev/null + +echo "✅ CEF embed complete."