From a76f797bb5b808ca74f071ddc23fb2d14f17b768 Mon Sep 17 00:00:00 2001 From: Natalie Bunduwongse Date: Tue, 24 Feb 2026 11:43:29 +1300 Subject: [PATCH 1/2] test: improve windows ui test deeplinking --- sample/Tests/test/test_windows.py | 8 +- sample/Tests/test/test_windows_helpers.py | 298 ++++++++++++++-------- 2 files changed, 198 insertions(+), 108 deletions(-) diff --git a/sample/Tests/test/test_windows.py b/sample/Tests/test/test_windows.py index 840c02d7..c0e81dcc 100644 --- a/sample/Tests/test/test_windows.py +++ b/sample/Tests/test/test_windows.py @@ -7,6 +7,7 @@ These tests require proper authentication URL logging to work correctly. """ +import os import time import unittest @@ -108,7 +109,12 @@ def _perform_login(self): stop_browser() if attempt == 0: print(f"Login attempt 1 failed: {err}") - print("Retrying login...") + print("Retrying login... clearing browser data for fresh session") + import shutil + brave_data = r"C:\temp\brave_debug" + if os.path.exists(brave_data): + shutil.rmtree(brave_data, ignore_errors=True) + print("Cleared browser user data to avoid cached session issues") time.sleep(5) bring_sample_app_to_foreground() else: diff --git a/sample/Tests/test/test_windows_helpers.py b/sample/Tests/test/test_windows_helpers.py index 9d0b9a45..7ba0a199 100644 --- a/sample/Tests/test/test_windows_helpers.py +++ b/sample/Tests/test/test_windows_helpers.py @@ -215,7 +215,7 @@ def handle_cached_authentication(driver): print("Monitoring Unity logs for authentication completion...") product_name = os.getenv("UNITY_APP_NAME", get_product_name()) - log_path = os.path.join("C:\\Users\\WindowsBuildsdkServi\\AppData\\LocalLow\\Immutable", product_name, "Player.log") + log_path = os.path.join(os.path.expanduser("~"), "AppData", "LocalLow", "Immutable", product_name, "Player.log") auth_success = False for check_attempt in range(30): # Check for 30 seconds @@ -376,27 +376,22 @@ def login(): # If we ended up on chrome://newtab/ or similar, the redirect already happened if 'newtab' in driver.current_url or 'about:blank' in driver.current_url: - print("Browser was redirected to new tab - cached session completed redirect automatically!") - print("The immutablerunner:// callback was triggered but browser couldn't handle it") - print("This means authentication was successful, just need to wait for Unity to process it") + print("Browser redirected to new tab - cached session triggered immutablerunner:// callback") + print("Protocol handler should have delivered the callback to Unity") - # Wait and check Unity logs for authentication success instead of relying on scene changes product_name = os.getenv("UNITY_APP_NAME", get_product_name()) - log_path = os.path.join("C:\\Users\\WindowsBuildsdkServi\\AppData\\LocalLow\\Immutable", product_name, "Player.log") + log_path = os.path.join(os.path.expanduser("~"), "AppData", "LocalLow", "Immutable", product_name, "Player.log") auth_success = False - for check_attempt in range(20): # Check for 20 seconds + for check_attempt in range(30): try: with open(log_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() - # Look for signs of successful authentication in logs if any(phrase in content for phrase in [ - "AuthenticatedScene", - "COMPLETE_LOGIN_PKCE", + "AuthenticatedScene", + "COMPLETE_LOGIN_PKCE", "LoginPKCESuccess", "HandleLoginPkceSuccess", - "authentication successful", - "logged in successfully" ]): print("Authentication success detected in Unity logs!") auth_success = True @@ -408,7 +403,7 @@ def login(): if auth_success: print("Cached authentication confirmed successful via Unity logs") else: - print("Could not confirm authentication success in Unity logs") + print("Could not confirm authentication via Unity logs after 30s") return else: @@ -529,58 +524,54 @@ def login(): except: print("No consent screen found (expected behavior)") - # Handle deep link permission dialog - print("Waiting for deep link permission dialog...") + # Wait for deep link redirect to fire and be handled by the OS protocol handler. + # The checking page redirects to immutablerunner://callback via JavaScript. + # With the protocol handler and browser policy set up, this should happen automatically. + print("Waiting for deep link redirect from checking page...") print(f"Current URL: {driver.current_url}") - print(f"Page title: {driver.title}") - - # Give a moment for any page transitions to complete - time.sleep(3) - - try: - buttons = driver.find_elements(By.TAG_NAME, "button") - print(f"Found {len(buttons)} buttons on page:") - for i, btn in enumerate(buttons[:10]): - try: - text = btn.text.strip() - if text: - print(f" Button {i}: '{text}'") - except: - pass - product_name = os.getenv("UNITY_APP_NAME", get_product_name()) - deep_link_selectors = [ - f"//button[contains(text(),'Open {product_name}')]", - "//button[contains(text(),'Open')]", - "//button[contains(text(),'Allow')]", - "//a[contains(text(),'Open')]", - ] - - clicked = False - for selector in deep_link_selectors: - try: - deep_link_button = WebDriverWait(driver, 5).until( - EC.element_to_be_clickable((By.XPATH, selector))) - btn_text = deep_link_button.text.strip() - deep_link_button.click() - print(f"Clicked deep link button '{btn_text}' with selector: {selector}") - clicked = True + deep_link_handled = False + for wait_i in range(15): + time.sleep(2) + try: + current_url = driver.current_url + page_title = driver.title + + if 'checking' not in current_url and 'auth.immutable.com' not in current_url: + print(f"Page navigated away from checking: {current_url}") + deep_link_handled = True break - except: - continue - if not clicked: - print("No deep link button found with any selector - checking if redirect happened automatically") - current_url = driver.current_url - print(f"Current URL: {current_url}") - if 'checking' in current_url or 'callback' in current_url: - print("Still on checking page - deep link may not have fired") - except Exception as e: - print(f"Deep link dialog handling error: {e}") - print("This may cause the test to timeout waiting for scene change") + # Look for an "Open" button (browser permission dialog rendered in-page) + buttons = driver.find_elements(By.TAG_NAME, "button") + for btn in buttons: + try: + text = btn.text.strip() + if text and any(kw in text for kw in ['Open', 'Allow']): + btn.click() + print(f"Clicked deep link dialog button: '{text}'") + deep_link_handled = True + break + except: + pass + if deep_link_handled: + break + except Exception as e: + print(f"Deep link wait iteration {wait_i} error: {e}") - # Keep browser alive for Unity deep link redirect - # driver.quit() + if deep_link_handled: + print("Deep link redirect detected - Unity should receive the callback") + else: + print("Deep link redirect did not fire within timeout") + print("Protocol handler may not be working - attempting direct invocation...") + try: + auth_url = get_auth_url_from_unity_logs() + if auth_url: + print(f"Invoking protocol handler directly via OS for auth URL callback") + subprocess.run(["cmd", "/c", "start", "", "immutablerunner://callback"], + timeout=5, capture_output=True) + except Exception as e: + print(f"Direct protocol invocation failed: {e}") def clear_unity_data(): """Clear Unity's persistent data to force fresh start""" @@ -699,15 +690,14 @@ def bring_sample_app_to_foreground(): time.sleep(10) def setup_browser_permissions(): - """Set up browser permissions to allow auth.immutable.com to open external applications""" + """Set up browser preferences to allow auth.immutable.com to open external applications. + Writes to the user-data-dir that launch_browser() uses (C:\\temp\\brave_debug).""" print("Setting up browser permissions for auth.immutable.com...") - - # Create a browser preferences file to pre-allow the domain - user_data_dir = "C:\\temp\\brave_debug" - if not os.path.exists(user_data_dir): - os.makedirs(user_data_dir, exist_ok=True) - - # Create preferences file that allows auth.immutable.com to open external apps + + user_data_dir = r"C:\temp\brave_debug" + os.makedirs(user_data_dir, exist_ok=True) + + import json preferences = { "profile": { "content_settings": { @@ -719,72 +709,166 @@ def setup_browser_permissions(): } } } + }, + "exit_type": "Normal", + "exited_cleanly": True + }, + "protocol_handler": { + "excluded_schemes": { + "immutablerunner": False } + }, + "custom_handlers": { + "enabled": True } } - - import json - prefs_file = os.path.join(user_data_dir, "Default", "Preferences") - os.makedirs(os.path.dirname(prefs_file), exist_ok=True) - + + prefs_dir = os.path.join(user_data_dir, "Default") + os.makedirs(prefs_dir, exist_ok=True) + prefs_file = os.path.join(prefs_dir, "Preferences") + try: + existing = {} + if os.path.exists(prefs_file): + with open(prefs_file, 'r') as f: + try: + existing = json.load(f) + except json.JSONDecodeError: + pass + + def deep_merge(base, override): + for k, v in override.items(): + if k in base and isinstance(base[k], dict) and isinstance(v, dict): + deep_merge(base[k], v) + else: + base[k] = v + + deep_merge(existing, preferences) + with open(prefs_file, 'w') as f: - json.dump(preferences, f, indent=2) + json.dump(existing, f, indent=2) print("Browser permissions configured to allow auth.immutable.com") except Exception as e: print(f"Browser permission setup error: {e}") +def find_unity_executable(): + """Find the Unity executable path using the same logic as open_sample_app().""" + product_name = os.getenv("UNITY_APP_NAME", get_product_name()) + app_path_env = os.getenv("UNITY_APP_PATH") + + candidates = [] + if app_path_env: + candidates.append(app_path_env) + candidates.extend([ + f"{product_name}.exe", + f"../build/{product_name}.exe", + f"./{product_name}.exe", + ]) + + for candidate in candidates: + if os.path.exists(candidate): + return os.path.abspath(candidate) + return None + def setup_protocol_association(): - """Set up immutablerunner:// protocol association to avoid permission dialogs""" + """Set up immutablerunner:// protocol association so the OS can route deep links to Unity.""" print("Setting up protocol association for immutablerunner://...") - - product_name = os.getenv("UNITY_APP_NAME", get_product_name()) - - # PowerShell script to register the protocol + + exe_path = find_unity_executable() + if not exe_path: + print("WARNING: Could not find Unity executable for protocol association") + print("Deep link callback will not work - login will likely fail") + return + + print(f"Registering protocol handler: immutablerunner:// -> {exe_path}") + ps_script = f''' - # Register immutablerunner protocol + $exePath = "{exe_path}" $protocolKey = "HKCU:\\Software\\Classes\\immutablerunner" $commandKey = "$protocolKey\\shell\\open\\command" - - # Create the registry keys + if (!(Test-Path $protocolKey)) {{ New-Item -Path $protocolKey -Force | Out-Null }} if (!(Test-Path $commandKey)) {{ New-Item -Path $commandKey -Force | Out-Null }} - - # Set the protocol values + Set-ItemProperty -Path $protocolKey -Name "(Default)" -Value "URL:immutablerunner Protocol" Set-ItemProperty -Path $protocolKey -Name "URL Protocol" -Value "" - - # Find the Unity sample app executable - $sampleAppPath = "C:\\Immutable\\unity-immutable-sdk\\sample\\build\\{product_name}.exe" - if (Test-Path $sampleAppPath) {{ - Set-ItemProperty -Path $commandKey -Name "(Default)" -Value "`"$sampleAppPath`" `"%1`"" - Write-Host "Protocol association set up successfully" + Set-ItemProperty -Path $commandKey -Name "(Default)" -Value "`"$exePath`" `"%1`"" + + $registered = Get-ItemProperty -Path $commandKey -Name "(Default)" -ErrorAction SilentlyContinue + if ($registered) {{ + Write-Host "Protocol association set up successfully: $($registered.'(Default)')" }} else {{ - Write-Host "Sample app not found at expected path" + Write-Host "ERROR: Failed to write protocol handler to registry" }} ''' - + try: - result = subprocess.run(["powershell", "-Command", ps_script], + result = subprocess.run(["powershell", "-Command", ps_script], capture_output=True, text=True, timeout=10) - if "successfully" in result.stdout: - print("Protocol association configured - dialog should not appear!") + output = result.stdout.strip() + print(output) + if "successfully" in output: + print("Protocol handler registered") else: - print("Protocol setup may have failed, dialog might still appear") + print("Protocol setup may have failed") + if result.stderr: + print(f"stderr: {result.stderr.strip()}") except Exception as e: print(f"Protocol setup error: {e}") +def setup_browser_policy(): + """Set Brave browser enterprise policy to auto-allow immutablerunner:// from auth.immutable.com. + This prevents the native 'Open app?' dialog that Selenium cannot interact with.""" + print("Setting up browser policy for protocol auto-launch...") + + ps_script = ''' + $policyValue = '[{"protocol":"immutablerunner","allowed_origins":["https://auth.immutable.com"]}]' + $success = $false + + foreach ($root in @("HKLM", "HKCU")) { + $policyPath = "${root}:\\Software\\Policies\\BraveSoftware\\Brave" + try { + if (!(Test-Path $policyPath)) { + New-Item -Path $policyPath -Force | Out-Null + } + Set-ItemProperty -Path $policyPath -Name "AutoLaunchProtocolsFromOrigins" -Value $policyValue -Type String + Write-Host "Browser policy set at: $policyPath" + $success = $true + } catch { + Write-Host "Could not set policy at ${policyPath}: $_" + } + } + + if (!$success) { + Write-Host "WARNING: Could not set browser policy at any location" + } + ''' + + try: + result = subprocess.run(["powershell", "-Command", ps_script], + capture_output=True, text=True, timeout=10) + print(result.stdout.strip()) + except Exception as e: + print(f"Browser policy setup error: {e}") + def launch_browser(): print("Starting Brave...") - - # Set up browser permissions and protocol association first - setup_browser_permissions() + + # Kill any existing Brave processes for a clean start + subprocess.run(["powershell.exe", "-Command", + 'Get-Process -Name "brave" -ErrorAction SilentlyContinue | Stop-Process -Force'], + capture_output=True, timeout=10) + time.sleep(2) + + # Set up protocol handler, browser permissions, and enterprise policy setup_protocol_association() - + setup_browser_policy() + setup_browser_permissions() + browser_paths = [ r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe" ] @@ -799,10 +883,12 @@ def launch_browser(): print("Brave executable not found.") exit(1) - # Launch Brave with CI-friendly flags to handle protocol dialogs automatically + user_data_dir = r"C:\temp\brave_debug" + browser_args = [ '--remote-debugging-port=9222', - '--disable-web-security', + f'--user-data-dir={user_data_dir}', + '--disable-web-security', '--allow-running-insecure-content', '--disable-features=VizDisplayCompositor', '--disable-popup-blocking', @@ -817,8 +903,7 @@ def launch_browser(): '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding' ] - - # Check if we're in CI environment + is_ci = os.getenv('CI') or os.getenv('GITHUB_ACTIONS') or os.getenv('BUILD_ID') if is_ci: print("CI environment detected - adding additional protocol handling flags") @@ -828,15 +913,14 @@ def launch_browser(): '--disable-ipc-flooding-protection', '--force-permission-policy-unload-default-enabled' ]) - + args_string = "', '".join(browser_args) result = subprocess.run([ "powershell.exe", "-Command", f"$process = Start-Process -FilePath '{browser_path}' -ArgumentList '{args_string}' -PassThru; Write-Output $process.Id" ], capture_output=True, text=True, check=True) - - # Store the debug browser process ID globally for later use + global debug_browser_pid debug_browser_pid = result.stdout.strip() print(f"Debug browser launched with PID: {debug_browser_pid}") From 648c9258e2660e7687ed5addce761c675b5a97f0 Mon Sep 17 00:00:00 2001 From: Natalie Bunduwongse Date: Tue, 24 Feb 2026 12:38:28 +1300 Subject: [PATCH 2/2] ci: clean up windows browsers --- .github/workflows/ui-tests.yml | 19 ++- sample/Tests/test/test_windows.py | 8 +- sample/Tests/test/test_windows_helpers.py | 193 ++++++++++++++++------ 3 files changed, 157 insertions(+), 63 deletions(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index e2c27b47..232a0b88 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -315,6 +315,10 @@ jobs: } else { pip install -r sample/Tests/requirements-desktop.txt } + - name: Kill leftover Brave processes + if: always() + run: | + Get-Process | Where-Object { $_.Name -like "*brave*" -or $_.Name -eq "chromedriver" } | Stop-Process -Force -ErrorAction SilentlyContinue - name: Run UI tests env: UNITY_APP_PATH: Sample Unity 6 Windows.exe @@ -324,6 +328,10 @@ jobs: BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} working-directory: sample-unity6/Tests run: python -m pytest -xs test/test_windows.py::WindowsTest + - name: Close Brave Browser + if: always() + run: | + Get-Process | Where-Object { $_.Name -like "*brave*" -or $_.Name -eq "chromedriver" } | Stop-Process -Force -ErrorAction SilentlyContinue - name: Upload build log if: always() uses: actions/upload-artifact@v4 @@ -334,6 +342,7 @@ jobs: test: name: Run ${{ matrix.targetPlatform }} UI tests 🧪 needs: build + if: ${{ !cancelled() }} strategy: fail-fast: false matrix: @@ -396,6 +405,10 @@ jobs: - name: Install dependencies (Mac) if: contains(matrix.targetPlatform, 'StandaloneOSX') && (github.event_name != 'workflow_dispatch' || github.event.inputs.targetPlatform == 'All' || github.event.inputs.targetPlatform == matrix.targetPlatform) run: pip install -r "${{ matrix.projectPath }}/Tests/requirements-desktop.txt" + - name: Kill leftover Brave processes (Windows) + if: matrix.targetPlatform == 'StandaloneWindows64' && always() + run: | + Get-Process | Where-Object { $_.Name -like "*brave*" -or $_.Name -eq "chromedriver" } | Stop-Process -Force -ErrorAction SilentlyContinue - name: Run UI tests if: github.event_name != 'workflow_dispatch' || github.event.inputs.targetPlatform == 'All' || github.event.inputs.targetPlatform == matrix.targetPlatform env: @@ -406,12 +419,16 @@ jobs: BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} working-directory: ${{ matrix.projectPath }}/Tests run: ${{ matrix.test_script }} - - name: Close Brave Browser + - name: Close Brave Browser (macOS) if: contains(matrix.targetPlatform, 'StandaloneOSX') && always() run: | osascript -e 'tell application "Brave Browser" to quit' 2>/dev/null || true sleep 2 pkill -f "Brave Browser" 2>/dev/null || true + - name: Close Brave Browser (Windows) + if: matrix.targetPlatform == 'StandaloneWindows64' && always() + run: | + Get-Process | Where-Object { $_.Name -like "*brave*" -or $_.Name -eq "chromedriver" } | Stop-Process -Force -ErrorAction SilentlyContinue - name: Remove temporary keychain if: contains(matrix.targetPlatform, 'StandaloneOSX') && (github.event_name != 'workflow_dispatch' || github.event.inputs.targetPlatform == 'All' || github.event.inputs.targetPlatform == matrix.targetPlatform) run: | diff --git a/sample/Tests/test/test_windows.py b/sample/Tests/test/test_windows.py index c0e81dcc..840c02d7 100644 --- a/sample/Tests/test/test_windows.py +++ b/sample/Tests/test/test_windows.py @@ -7,7 +7,6 @@ These tests require proper authentication URL logging to work correctly. """ -import os import time import unittest @@ -109,12 +108,7 @@ def _perform_login(self): stop_browser() if attempt == 0: print(f"Login attempt 1 failed: {err}") - print("Retrying login... clearing browser data for fresh session") - import shutil - brave_data = r"C:\temp\brave_debug" - if os.path.exists(brave_data): - shutil.rmtree(brave_data, ignore_errors=True) - print("Cleared browser user data to avoid cached session issues") + print("Retrying login...") time.sleep(5) bring_sample_app_to_foreground() else: diff --git a/sample/Tests/test/test_windows_helpers.py b/sample/Tests/test/test_windows_helpers.py index 7ba0a199..db5d09f1 100644 --- a/sample/Tests/test/test_windows_helpers.py +++ b/sample/Tests/test/test_windows_helpers.py @@ -310,6 +310,47 @@ def login(): # Connect to the existing Brave browser instance driver = webdriver.Chrome(service=service, options=chrome_options) + # Inject a script on all future page loads to intercept immutablerunner:// redirects. + # The auth0 "checking" page redirects via JS to immutablerunner://callback?code=...&state=... + # but Brave blocks it with a native dialog Selenium cannot click. By intercepting the URL + # before the navigation fires, we can invoke the deep link directly from the OS. + try: + driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { + "source": """ + window.__capturedProtocolUrl = null; + if (window.navigation) { + window.navigation.addEventListener('navigate', function(e) { + if (e.destination && e.destination.url && + e.destination.url.startsWith('immutablerunner://')) { + window.__capturedProtocolUrl = e.destination.url; + e.preventDefault(); + } + }); + } + try { + var origAssign = Location.prototype.assign; + Location.prototype.assign = function(url) { + if (typeof url === 'string' && url.startsWith('immutablerunner://')) { + window.__capturedProtocolUrl = url; + return; + } + return origAssign.call(this, url); + }; + var origReplace = Location.prototype.replace; + Location.prototype.replace = function(url) { + if (typeof url === 'string' && url.startsWith('immutablerunner://')) { + window.__capturedProtocolUrl = url; + return; + } + return origReplace.call(this, url); + }; + } catch(e) {} + """ + }) + print("Injected protocol redirect interceptor via CDP") + except Exception as e: + print(f"Could not inject interceptor (non-fatal): {e}") + # HYBRID APPROACH: Try multi-window detection first (proven to work in CI), # then fall back to Unity log monitoring if needed @@ -524,54 +565,55 @@ def login(): except: print("No consent screen found (expected behavior)") - # Wait for deep link redirect to fire and be handled by the OS protocol handler. - # The checking page redirects to immutablerunner://callback via JavaScript. - # With the protocol handler and browser policy set up, this should happen automatically. - print("Waiting for deep link redirect from checking page...") + # The checking page will try to redirect to immutablerunner://callback?code=...&state=... + # Our injected CDP script intercepts this URL. We poll for it, then invoke it directly + # from the OS -- bypassing Brave's protocol permission dialog entirely. + print("Waiting for callback URL to be intercepted...") print(f"Current URL: {driver.current_url}") - deep_link_handled = False - for wait_i in range(15): - time.sleep(2) + callback_url = None + for wait_i in range(30): + time.sleep(1) try: - current_url = driver.current_url - page_title = driver.title + # Check if our interceptor captured the URL + url = driver.execute_script("return window.__capturedProtocolUrl") + if url: + callback_url = url + print(f"Intercepted callback URL: {url[:100]}...") + break + # Also check if the browser navigated away (deep link fired natively) + current_url = driver.current_url if 'checking' not in current_url and 'auth.immutable.com' not in current_url: - print(f"Page navigated away from checking: {current_url}") - deep_link_handled = True + print(f"Browser navigated away from checking page: {current_url}") break - # Look for an "Open" button (browser permission dialog rendered in-page) - buttons = driver.find_elements(By.TAG_NAME, "button") - for btn in buttons: + # Try clicking any in-page "Open" / "Allow" button + for btn in driver.find_elements(By.TAG_NAME, "button"): try: text = btn.text.strip() if text and any(kw in text for kw in ['Open', 'Allow']): btn.click() print(f"Clicked deep link dialog button: '{text}'") - deep_link_handled = True break except: pass - if deep_link_handled: - break except Exception as e: - print(f"Deep link wait iteration {wait_i} error: {e}") + if wait_i % 10 == 9: + print(f"Deep link poll iteration {wait_i}: {e}") - if deep_link_handled: - print("Deep link redirect detected - Unity should receive the callback") - else: - print("Deep link redirect did not fire within timeout") - print("Protocol handler may not be working - attempting direct invocation...") + if callback_url: + print("Invoking deep link directly via OS (bypassing browser dialog)...") try: - auth_url = get_auth_url_from_unity_logs() - if auth_url: - print(f"Invoking protocol handler directly via OS for auth URL callback") - subprocess.run(["cmd", "/c", "start", "", "immutablerunner://callback"], - timeout=5, capture_output=True) + subprocess.run( + ["powershell", "-Command", f"Start-Process '{callback_url}'"], + capture_output=True, timeout=10 + ) + print("Deep link invoked via OS") except Exception as e: - print(f"Direct protocol invocation failed: {e}") + print(f"OS invocation failed: {e}") + else: + print("Could not intercept callback URL - deep link may have fired natively via protocol handler") def clear_unity_data(): """Clear Unity's persistent data to force fresh start""" @@ -689,16 +731,56 @@ def bring_sample_app_to_foreground(): subprocess.run(command, check=True) time.sleep(10) +def get_brave_default_profile_dir(): + """Return the path to Brave's default user profile on Windows.""" + local_app_data = os.environ.get('LOCALAPPDATA', os.path.join(os.path.expanduser("~"), "AppData", "Local")) + return os.path.join(local_app_data, "BraveSoftware", "Brave-Browser", "User Data", "Default") + +def ensure_browser_clean(): + """Kill any running Brave/chromedriver processes and clean session data so Brave + starts cleanly without restoring previous tabs (mirrors the Mac helper approach).""" + # Kill all Brave processes + subprocess.run(["powershell.exe", "-Command", + 'Get-Process | Where-Object { $_.Name -like "*brave*" } | Stop-Process -Force -ErrorAction SilentlyContinue'], + capture_output=True, timeout=10) + subprocess.run(["powershell.exe", "-Command", + 'Get-Process -Name "chromedriver" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue'], + capture_output=True, timeout=10) + time.sleep(2) + + brave_profile = get_brave_default_profile_dir() + + import shutil + for session_dir_name in ("Sessions", "Session Storage"): + session_dir = os.path.join(brave_profile, session_dir_name) + if os.path.isdir(session_dir): + shutil.rmtree(session_dir, ignore_errors=True) + print(f"Removed {session_dir_name} directory") + + import json + prefs_path = os.path.join(brave_profile, "Preferences") + try: + if os.path.exists(prefs_path): + with open(prefs_path, "r") as f: + prefs = json.load(f) + prefs.setdefault("profile", {})["exit_type"] = "Normal" + prefs["profile"]["exited_cleanly"] = True + prefs.setdefault("session", {})["restore_on_startup"] = 5 + with open(prefs_path, "w") as f: + json.dump(prefs, f) + print("Patched Preferences: exit_type=Normal, restore_on_startup=NewTab") + except Exception as e: + print(f"Could not patch Preferences: {e}") + def setup_browser_permissions(): - """Set up browser preferences to allow auth.immutable.com to open external applications. - Writes to the user-data-dir that launch_browser() uses (C:\\temp\\brave_debug).""" + """Merge protocol-handler permissions into Brave's default profile Preferences.""" print("Setting up browser permissions for auth.immutable.com...") - user_data_dir = r"C:\temp\brave_debug" - os.makedirs(user_data_dir, exist_ok=True) + brave_profile = get_brave_default_profile_dir() + os.makedirs(brave_profile, exist_ok=True) import json - preferences = { + permissions_overlay = { "profile": { "content_settings": { "exceptions": { @@ -709,9 +791,7 @@ def setup_browser_permissions(): } } } - }, - "exit_type": "Normal", - "exited_cleanly": True + } }, "protocol_handler": { "excluded_schemes": { @@ -723,9 +803,7 @@ def setup_browser_permissions(): } } - prefs_dir = os.path.join(user_data_dir, "Default") - os.makedirs(prefs_dir, exist_ok=True) - prefs_file = os.path.join(prefs_dir, "Preferences") + prefs_file = os.path.join(brave_profile, "Preferences") try: existing = {} @@ -743,7 +821,7 @@ def deep_merge(base, override): else: base[k] = v - deep_merge(existing, preferences) + deep_merge(existing, permissions_overlay) with open(prefs_file, 'w') as f: json.dump(existing, f, indent=2) @@ -858,11 +936,11 @@ def setup_browser_policy(): def launch_browser(): print("Starting Brave...") - # Kill any existing Brave processes for a clean start - subprocess.run(["powershell.exe", "-Command", - 'Get-Process -Name "brave" -ErrorAction SilentlyContinue | Stop-Process -Force'], - capture_output=True, timeout=10) - time.sleep(2) + # Clean up any existing Brave/chromedriver processes and stale session data. + # This ensures our instance is the ONLY Brave running so that Unity's + # Application.OpenURL() opens the auth page in the same browser we control + # via remote debugging (instead of spawning a second uncontrolled instance). + ensure_browser_clean() # Set up protocol handler, browser permissions, and enterprise policy setup_protocol_association() @@ -883,16 +961,17 @@ def launch_browser(): print("Brave executable not found.") exit(1) - user_data_dir = r"C:\temp\brave_debug" - + # NO --user-data-dir: we intentionally use the default profile so that when + # Unity calls Application.OpenURL() the OS opens it in THIS Brave instance + # (instead of launching a separate default-profile instance). browser_args = [ '--remote-debugging-port=9222', - f'--user-data-dir={user_data_dir}', '--disable-web-security', '--allow-running-insecure-content', '--disable-features=VizDisplayCompositor', '--disable-popup-blocking', '--no-first-run', + '--no-default-browser-check', '--disable-default-apps', '--disable-extensions', '--disable-component-extensions-with-background-pages', @@ -901,7 +980,8 @@ def launch_browser(): '--enable-automation', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', - '--disable-renderer-backgrounding' + '--disable-renderer-backgrounding', + '--disable-session-crashed-bubble', ] is_ci = os.getenv('CI') or os.getenv('GITHUB_ACTIONS') or os.getenv('BUILD_ID') @@ -930,16 +1010,19 @@ def launch_browser(): def stop_browser(): print("Stopping Brave...") powershell_command = """ - $process = Get-Process -Name "brave" -ErrorAction SilentlyContinue - if ($process) { - $process | ForEach-Object { + # Kill all Brave processes (browser, renderer, GPU, crashpad, etc.) + $braveProcs = Get-Process | Where-Object { $_.Name -like "*brave*" } -ErrorAction SilentlyContinue + if ($braveProcs) { + $braveProcs | ForEach-Object { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } - Write-Output "All Brave processes have been closed." + Write-Output "Stopped $($braveProcs.Count) Brave process(es)." } else { Write-Output "Brave is not running." } + # Also kill any orphaned chromedriver processes + Get-Process -Name "chromedriver" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue """ subprocess.run(["powershell.exe", "-Command", powershell_command], check=True) - time.sleep(5) + time.sleep(3) print("Stopped Brave") \ No newline at end of file