diff --git a/.github/workflows/shared-testing-linux.yml b/.github/workflows/shared-testing-linux.yml
index a85cc3dfb..0c710d025 100644
--- a/.github/workflows/shared-testing-linux.yml
+++ b/.github/workflows/shared-testing-linux.yml
@@ -118,6 +118,15 @@ jobs:
eval "$(dbus-launch --sh-syntax)"
+ export GDK_BACKEND=x11
+ export NO_AT_BRIDGE=1
+ export GALLIUM_DRIVER=llvmpipe
+ export LIBGL_ALWAYS_SOFTWARE=1
+ export LIBGL_DRI3_DISABLE=1
+ export MESA_LOADER_DRIVER_OVERRIDE=llvmpipe
+ export WEBKIT_DISABLE_COMPOSITING_MODE=1
+ export WEBKIT_DISABLE_DMABUF_RENDERER=1
+
echo "Launching Xvfb..."
Xvfb :99 \
-screen 0 1920x1080x24 \
@@ -176,7 +185,19 @@ jobs:
dotnet test --solution InfiniFrame.GitHubActions.Testing.slnf \
--configuration Release \
--no-build \
- --no-restore
+ --no-restore \
+ -m:1 \
+ /p:TestTfmsInParallel=false || {
+ test_exit=$?
+ echo "=== Test command failed with exit code ${test_exit} ==="
+ echo "=== Xvfb log ==="
+ cat xvfb.log || true
+ echo "=== Mutter log ==="
+ cat mutter.log || true
+ echo "=== Process snapshot ==="
+ ps aux | grep -E '([X]vfb|[m]utter|[d]otnet|[W]ebKit)' || true
+ exit "${test_exit}"
+ }
- name: Pack Tool E2E
if: always()
diff --git a/.github/workflows/shared-testing-windows-playwright.yml b/.github/workflows/shared-testing-windows-playwright.yml
index cbcb96fd7..52d2f404a 100644
--- a/.github/workflows/shared-testing-windows-playwright.yml
+++ b/.github/workflows/shared-testing-windows-playwright.yml
@@ -126,16 +126,6 @@ jobs:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- - name: Prepare WebView2 Cache Directory
- shell: pwsh
- run: New-Item -ItemType Directory -Path ".cache\webview2" -Force | Out-Null
-
- - name: Cache WebView2
- uses: actions/cache@v5
- with:
- path: .cache\webview2
- key: webview2-installer-${{ runner.os }}-v1
-
- name: Install WebView2 Runtime
shell: pwsh
run: |
diff --git a/InfiniFrame.slnx b/InfiniFrame.slnx
index 464c22b2b..b4ac416fe 100644
--- a/InfiniFrame.slnx
+++ b/InfiniFrame.slnx
@@ -82,6 +82,7 @@
+
diff --git a/docker/infiniframe-linux/common.sh b/docker/infiniframe-linux/common.sh
index e3c95de38..05fcbdba5 100644
--- a/docker/infiniframe-linux/common.sh
+++ b/docker/infiniframe-linux/common.sh
@@ -93,7 +93,14 @@ start_virtual_display() {
setup_display_mode() {
local xvfb_log="${1:-/tmp/xvfb.log}"
local mutter_log="${2:-/tmp/mutter.log}"
+ export GDK_BACKEND="${GDK_BACKEND:-x11}"
export NO_AT_BRIDGE="${NO_AT_BRIDGE:-1}"
+ export GALLIUM_DRIVER="${GALLIUM_DRIVER:-llvmpipe}"
+ export LIBGL_ALWAYS_SOFTWARE="${LIBGL_ALWAYS_SOFTWARE:-1}"
+ export LIBGL_DRI3_DISABLE="${LIBGL_DRI3_DISABLE:-1}"
+ export MESA_LOADER_DRIVER_OVERRIDE="${MESA_LOADER_DRIVER_OVERRIDE:-llvmpipe}"
+ export WEBKIT_DISABLE_COMPOSITING_MODE="${WEBKIT_DISABLE_COMPOSITING_MODE:-1}"
+ export WEBKIT_DISABLE_DMABUF_RENDERER="${WEBKIT_DISABLE_DMABUF_RENDERER:-1}"
start_dbus_session
if [[ "${USE_HOST_DISPLAY}" == "1" ]]; then
diff --git a/docker/infiniframe-linux/tests.sh b/docker/infiniframe-linux/tests.sh
index 4773e7708..0835d9e51 100644
--- a/docker/infiniframe-linux/tests.sh
+++ b/docker/infiniframe-linux/tests.sh
@@ -18,4 +18,15 @@ dotnet test --solution "${SOLUTION_FILTER}" \
--no-build \
--no-restore \
/p:UseAppHost=false \
- "${COMMON_DOTNET_PROPS[@]}"
+ /p:TestTfmsInParallel=false \
+ "${COMMON_DOTNET_PROPS[@]}" || {
+ test_exit=$?
+ echo "=== Test command failed with exit code ${test_exit} ==="
+ echo "=== Xvfb log ==="
+ cat /tmp/xvfb.log || true
+ echo "=== Mutter log ==="
+ cat /tmp/mutter.log || true
+ echo "=== Process snapshot ==="
+ ps aux | grep -E '([X]vfb|[m]utter|[d]otnet|[W]ebKit)' || true
+ exit "${test_exit}"
+ }
diff --git a/docs/docs/migration/photino-backlog.md b/docs/docs/migration/photino-backlog.md
index d91021239..fbf7bd2f1 100644
--- a/docs/docs/migration/photino-backlog.md
+++ b/docs/docs/migration/photino-backlog.md
@@ -9,6 +9,7 @@ This backlog will be used to track any remaining issues or features that need to
- 🚧 : Under construction
- ❓ : Under discussion / unclear if should be handled natively
- ❌ : Rejected for native implementation
+- 🚫 : Out of scope. Usually means this is normal behavior or not an issue with the original Photino codebase
---
@@ -29,7 +30,7 @@ This backlog will be used to track any remaining issues or features that need to
| ❓ | Set icon in the taskbar on windows 11 | [photino.Blazor#161](https://github.com/tryphotino/photino.Blazor/issues/161) | |
| ✅ | .NET 10 support | [photino.Blazor#164](https://github.com/tryphotino/photino.Blazor/issues/164), [photino.Blazor#165](https://github.com/tryphotino/photino.Blazor/pull/165) | |
| ❓ | Fix handling of app:// URLs containing a #fragment | [photino.Blazor#166](https://github.com/tryphotino/photino.Blazor/pull/166) | |
-| ❓ | Top Empty window caption appear in SetChromeless(true) | [photino.Blazor#167](https://github.com/tryphotino/photino.Blazor/issues/167) | |
+| 🚫 | Top Empty window caption appear in SetChromeless(true) | [photino.Blazor#167](https://github.com/tryphotino/photino.Blazor/issues/167) | |
---
@@ -41,7 +42,7 @@ This backlog will be used to track any remaining issues or features that need to
| ❓ | Native Menu Support | [Photino.Native#44](https://github.com/tryphotino/photino.Native/issues/44) | |
| ❓ | Dedicated scheme for logging from UI to host app | [Photino.Native#45](https://github.com/tryphotino/photino.Native/issues/45) | |
| ❌ | Windows x86 | [Photino.Native#53](https://github.com/tryphotino/photino.Native/issues/53) | |
-| ❓ | RegisterWindowClosingHandler not working on linux (Debian) | [Photino.Native#75](https://github.com/tryphotino/photino.Native/issues/75) | |
+| ✅ | RegisterWindowClosingHandler not working on linux (Debian) | [Photino.Native#75](https://github.com/tryphotino/photino.Native/issues/75) | |
| ❓ | Add SingleInstanceMode to Photino initialization | [Photino.Native#111](https://github.com/tryphotino/photino.Native/issues/111) | |
| ❓ | Android Support | [Photino.Native#115](https://github.com/tryphotino/photino.Native/issues/115) | |
| ❓ | Stack overflow. at Photino.NET.PhotinoWindow.Photino_WaitForExit(IntPtr) | [Photino.Native#141](https://github.com/tryphotino/photino.Native/issues/141) | |
@@ -62,44 +63,44 @@ This backlog will be used to track any remaining issues or features that need to
## Photino.NET
-| Status | Feature | Links | InfiniFrame PR |
-|:-------|:----------------------------------------------------------------------------------------------|:------------------------------------------------------------------------|:----------------------------------------------------------------------|
-| ✅ | Problem with "insecure origins" | [Photino.NET#25](https://github.com/tryphotino/photino.NET/issues/25) | |
-| ❓ | JS injection into WebView | [Photino.NET#58](https://github.com/tryphotino/photino.NET/issues/58) | |
-| ❓ | Creating a 2nd PhotinoWindow after closing all others fails | [Photino.NET#59](https://github.com/tryphotino/photino.NET/issues/59) | |
-| ❓ | Is there a way to bypass WebKits SSL check? | [Photino.NET#65](https://github.com/tryphotino/photino.NET/issues/65) | |
-| ❓ | Make window transparent | [Photino.NET#73](https://github.com/tryphotino/photino.NET/issues/73) | |
-| ❓ | Javascript debugging | [Photino.NET#75](https://github.com/tryphotino/photino.NET/issues/75) | |
-| ❓ | Chromeless Window | [Photino.NET#80](https://github.com/tryphotino/photino.NET/issues/80) | |
-| ❓ | SendWebMessage from WindowCreated handler raises a System.AccessViolationException | [Photino.NET#87](https://github.com/tryphotino/photino.NET/issues/87) | |
-| ❓ | Icon failed to load | [Photino.NET#95](https://github.com/tryphotino/photino.NET/issues/95) | |
-| ❓ | The application lacks a taskbar Icon | [Photino.NET#106](https://github.com/tryphotino/photino.NET/issues/106) | |
-| ❓ | Possibility to hide the window from the taskbar/dock | [Photino.NET#107](https://github.com/tryphotino/photino.NET/issues/107) | |
-| ❓ | Make a window only miniable | [Photino.NET#112](https://github.com/tryphotino/photino.NET/issues/112) | |
-| ❓ | Interoperability between C # and JavaScript | [Photino.NET#120](https://github.com/tryphotino/photino.NET/issues/120) | |
-| ✅ | Window.external is deprecated by browsers | [Photino.NET#124](https://github.com/tryphotino/photino.NET/issues/124) | |
-| ❓ | Intercept navigation events | [Photino.NET#139](https://github.com/tryphotino/photino.NET/issues/139) | |
-| ❓ | ShowSaveFile How to set the default file name | [Photino.NET#140](https://github.com/tryphotino/photino.NET/issues/140) | |
-| ❓ | Running as administrator disables other instances | [Photino.NET#162](https://github.com/tryphotino/photino.NET/issues/162) | |
-| ❌ | Mica/Fluent design window for windows | [Photino.NET#167](https://github.com/tryphotino/photino.NET/issues/167) | |
-| ❓ | Retrieve current Url of PhotinoWindow | [Photino.NET#197](https://github.com/tryphotino/photino.NET/issues/197) | |
-| ✅ | Ctrl+ mouse wheel to zoom the page | [Photino.NET#217](https://github.com/tryphotino/photino.NET/issues/217) | |
-| ❓ | RegisterCustomSchemeHandler won't let do fetch and XMLHttpRequest requests | [Photino.NET#232](https://github.com/tryphotino/photino.NET/issues/232) | |
-| ❓ | Set the background color of the native window body | [Photino.NET#239](https://github.com/tryphotino/photino.NET/issues/239) | |
-| ✅ | Default csproj file uses WinExe but should use Exe so it will properly build on all platforms | [Photino.NET#240](https://github.com/tryphotino/photino.NET/issues/240) | |
-| ❓ | Disable browser shortcuts | [Photino.NET#251](https://github.com/tryphotino/photino.NET/issues/251) | |
-| ❓ | Fixed WebView2 runtime | [Photino.NET#254](https://github.com/tryphotino/photino.NET/issues/254) | |
-| ✅ | `ILogger` implementation | [Photino.NET#257](https://github.com/tryphotino/photino.NET/issues/257) | |
-| ❓ | Add complex notifications | [Photino.NET#261](https://github.com/tryphotino/photino.NET/pull/261) | |
-| ❓ | Import StartDragging, StartResizing | [Photino.NET#262](https://github.com/tryphotino/photino.NET/pull/262) | |
-| ❓ | Bindings for controlling taskbar progress/flash | [Photino.NET#263](https://github.com/tryphotino/photino.NET/pull/263) | |
-| ❓ | Allow parent window to be set before initialized | [Photino.NET#264](https://github.com/tryphotino/photino.NET/pull/264) | |
-| ❓ | Window header size | [Photino.NET#266](https://github.com/tryphotino/photino.NET/issues/266) | |
-| ❓ | Optional window title 31-char-length-limitation on Linux/GTK | [Photino.NET#267](https://github.com/tryphotino/photino.NET/pull/267) | |
-| ❓ | Inject Arbitrary Javascript into Webview | [Photino.NET#268](https://github.com/tryphotino/photino.NET/issues/268) | |
-| ❓ | Parent vs child window behavior | [Photino.NET#269](https://github.com/tryphotino/photino.NET/issues/269) | |
-| ❓ | WindowClosed event | [Photino.NET#271](https://github.com/tryphotino/photino.NET/issues/271) | |
-| ✅ | SetIconFile Linux crash | [Photino.NET#272](https://github.com/tryphotino/photino.NET/issues/272) | [InfiniFrame#165](https://github.com/InfiniLore/InfiniFrame/pull/165) |
+| Status | Feature | Links | InfiniFrame PR |
+|:-------|:----------------------------------------------------------------------------------------------|:------------------------------------------------------------------------|:------------------------------------------------------------------------|
+| ✅ | Problem with "insecure origins" | [Photino.NET#25](https://github.com/tryphotino/photino.NET/issues/25) | |
+| ❓ | JS injection into WebView | [Photino.NET#58](https://github.com/tryphotino/photino.NET/issues/58) | |
+| ✅ | Creating a 2nd PhotinoWindow after closing all others fails | [Photino.NET#59](https://github.com/tryphotino/photino.NET/issues/59) | |
+| ❓ | Is there a way to bypass WebKits SSL check? | [Photino.NET#65](https://github.com/tryphotino/photino.NET/issues/65) | |
+| ❓ | Make window transparent | [Photino.NET#73](https://github.com/tryphotino/photino.NET/issues/73) | |
+| ❓ | Javascript debugging | [Photino.NET#75](https://github.com/tryphotino/photino.NET/issues/75) | |
+| ❓ | Chromeless Window | [Photino.NET#80](https://github.com/tryphotino/photino.NET/issues/80) | |
+| ❓ | SendWebMessage from WindowCreated handler raises a System.AccessViolationException | [Photino.NET#87](https://github.com/tryphotino/photino.NET/issues/87) | |
+| ❓ | Icon failed to load | [Photino.NET#95](https://github.com/tryphotino/photino.NET/issues/95) | |
+| ❓ | The application lacks a taskbar Icon | [Photino.NET#106](https://github.com/tryphotino/photino.NET/issues/106) | |
+| ❓ | Possibility to hide the window from the taskbar/dock | [Photino.NET#107](https://github.com/tryphotino/photino.NET/issues/107) | |
+| ❓ | Make a window only miniable | [Photino.NET#112](https://github.com/tryphotino/photino.NET/issues/112) | |
+| ❓ | Interoperability between C # and JavaScript | [Photino.NET#120](https://github.com/tryphotino/photino.NET/issues/120) | |
+| ✅ | Window.external is deprecated by browsers | [Photino.NET#124](https://github.com/tryphotino/photino.NET/issues/124) | |
+| ❓ | Intercept navigation events | [Photino.NET#139](https://github.com/tryphotino/photino.NET/issues/139) | |
+| ❓ | ShowSaveFile How to set the default file name | [Photino.NET#140](https://github.com/tryphotino/photino.NET/issues/140) | |
+| ❓ | Running as administrator disables other instances | [Photino.NET#162](https://github.com/tryphotino/photino.NET/issues/162) | |
+| ❌ | Mica/Fluent design window for windows | [Photino.NET#167](https://github.com/tryphotino/photino.NET/issues/167) | |
+| ❓ | Retrieve current Url of PhotinoWindow | [Photino.NET#197](https://github.com/tryphotino/photino.NET/issues/197) | |
+| ✅ | Ctrl+ mouse wheel to zoom the page | [Photino.NET#217](https://github.com/tryphotino/photino.NET/issues/217) | |
+| ❓ | RegisterCustomSchemeHandler won't let do fetch and XMLHttpRequest requests | [Photino.NET#232](https://github.com/tryphotino/photino.NET/issues/232) | |
+| ❓ | Set the background color of the native window body | [Photino.NET#239](https://github.com/tryphotino/photino.NET/issues/239) | |
+| ✅ | Default csproj file uses WinExe but should use Exe so it will properly build on all platforms | [Photino.NET#240](https://github.com/tryphotino/photino.NET/issues/240) | |
+| ❓ | Disable browser shortcuts | [Photino.NET#251](https://github.com/tryphotino/photino.NET/issues/251) | |
+| 🚧 | Fixed WebView2 runtime | [Photino.NET#254](https://github.com/tryphotino/photino.NET/issues/254) | [InfiniFrame#275](https://github.com/InfiniLore/InfiniFrame/issues/275) |
+| ✅ | `ILogger` implementation | [Photino.NET#257](https://github.com/tryphotino/photino.NET/issues/257) | |
+| ❓ | Add complex notifications | [Photino.NET#261](https://github.com/tryphotino/photino.NET/pull/261) | |
+| ❓ | Import StartDragging, StartResizing | [Photino.NET#262](https://github.com/tryphotino/photino.NET/pull/262) | |
+| ❓ | Bindings for controlling taskbar progress/flash | [Photino.NET#263](https://github.com/tryphotino/photino.NET/pull/263) | |
+| ❓ | Allow parent window to be set before initialized | [Photino.NET#264](https://github.com/tryphotino/photino.NET/pull/264) | |
+| ❓ | Window header size | [Photino.NET#266](https://github.com/tryphotino/photino.NET/issues/266) | |
+| ❓ | Optional window title 31-char-length-limitation on Linux/GTK | [Photino.NET#267](https://github.com/tryphotino/photino.NET/pull/267) | |
+| ❓ | Inject Arbitrary Javascript into Webview | [Photino.NET#268](https://github.com/tryphotino/photino.NET/issues/268) | |
+| ❓ | Parent vs child window behavior | [Photino.NET#269](https://github.com/tryphotino/photino.NET/issues/269) | |
+| 🚧 | WindowClosed event | [Photino.NET#271](https://github.com/tryphotino/photino.NET/issues/271) | [InfiniFrame#277](https://github.com/InfiniLore/InfiniFrame/issues/277) |
+| ✅ | SetIconFile Linux crash | [Photino.NET#272](https://github.com/tryphotino/photino.NET/issues/272) | [InfiniFrame#165](https://github.com/InfiniLore/InfiniFrame/pull/165) |
---
@@ -107,4 +108,4 @@ This backlog will be used to track any remaining issues or features that need to
| Status | Feature | Links | InfiniFrame PR |
|:-------|:----------------------------|:----------------------------------------------------------------------------------|:---------------|
-| ✅ | Photino Development Server | [Photino.NET.Server#4](https://github.com/tryphotino/photino.NET.Server/issues/4) | |
\ No newline at end of file
+| ✅ | Photino Development Server | [Photino.NET.Server#4](https://github.com/tryphotino/photino.NET.Server/issues/4) | |
diff --git a/scripts/BuildFrontend.mjs b/scripts/BuildFrontend.mjs
new file mode 100644
index 000000000..3c67660e1
--- /dev/null
+++ b/scripts/BuildFrontend.mjs
@@ -0,0 +1,150 @@
+import {spawnSync} from 'node:child_process';
+import {
+ existsSync,
+ mkdirSync,
+ readdirSync,
+ readFileSync,
+ rmSync,
+ statSync,
+ writeFileSync,
+} from 'node:fs';
+import path from 'node:path';
+
+const [, , appDirectoryArg, stampFileArg, outputFileArg] = process.argv;
+
+if (!appDirectoryArg || !stampFileArg || !outputFileArg) {
+ console.error('Usage: node BuildFrontend.mjs ');
+ process.exit(1);
+}
+
+const appDirectory = path.resolve(appDirectoryArg);
+const stampFile = path.resolve(stampFileArg);
+const outputFile = path.resolve(outputFileArg);
+const lockDirectory = `${stampFile}.lock`;
+const sourceExclusions = new Set(['node_modules', '.git']);
+
+function sleep(milliseconds) {
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
+}
+
+function isProcessRunning(processId) {
+ if (!Number.isInteger(processId) || processId <= 0) {
+ return false;
+ }
+
+ try {
+ process.kill(processId, 0);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+function shouldRemoveExistingLock() {
+ const ownerFile = path.join(lockDirectory, 'owner.txt');
+
+ if (existsSync(ownerFile)) {
+ const ownerProcessId = Number.parseInt(readFileSync(ownerFile, 'utf8'), 10);
+ if (!isProcessRunning(ownerProcessId)) {
+ return true;
+ }
+ }
+
+ const lockAgeMilliseconds = Date.now() - statSync(lockDirectory).mtimeMs;
+ return lockAgeMilliseconds > 60 * 1000;
+}
+
+function acquireLock() {
+ mkdirSync(path.dirname(stampFile), {recursive: true});
+
+ const startedAt = Date.now();
+ while (true) {
+ try {
+ mkdirSync(lockDirectory);
+ writeFileSync(path.join(lockDirectory, 'owner.txt'), `${process.pid}\n`, 'utf8');
+ return;
+ } catch (error) {
+ if (error?.code !== 'EEXIST') {
+ throw error;
+ }
+
+ if (shouldRemoveExistingLock()) {
+ rmSync(lockDirectory, {recursive: true, force: true});
+ continue;
+ }
+
+ if (Date.now() - startedAt > 2 * 60 * 1000) {
+ throw new Error(`Timed out waiting for frontend build lock: ${lockDirectory}`);
+ }
+
+ sleep(250);
+ }
+ }
+}
+
+function getLatestSourceWriteTime(directory) {
+ let latest = statSync(directory).mtimeMs;
+
+ for (const entry of readdirSync(directory, {withFileTypes: true})) {
+ if (sourceExclusions.has(entry.name)) {
+ continue;
+ }
+
+ const entryPath = path.join(directory, entry.name);
+ const entryStats = statSync(entryPath);
+ latest = Math.max(latest, entryStats.mtimeMs);
+
+ if (entry.isDirectory()) {
+ latest = Math.max(latest, getLatestSourceWriteTime(entryPath));
+ }
+ }
+
+ return latest;
+}
+
+function isBuildCurrent() {
+ if (!existsSync(stampFile) || !existsSync(outputFile)) {
+ return false;
+ }
+
+ return statSync(stampFile).mtimeMs >= getLatestSourceWriteTime(appDirectory);
+}
+
+function runNpm(args) {
+ const npmCommand = process.platform === 'win32' ? 'cmd.exe' : 'npm';
+ const npmArgs = process.platform === 'win32'
+ ? ['/d', '/s', '/c', 'npm', ...args]
+ : args;
+
+ const result = spawnSync(npmCommand, npmArgs, {
+ cwd: appDirectory,
+ stdio: 'inherit',
+ });
+
+ if (result.error) {
+ console.error(result.error.message);
+ process.exit(1);
+ }
+
+ if (result.status !== 0) {
+ process.exit(result.status ?? 1);
+ }
+}
+
+acquireLock();
+
+try {
+ if (isBuildCurrent()) {
+ console.log('Frontend build output is current.');
+ process.exit(0);
+ }
+
+ if (process.env.CI === 'true') {
+ runNpm(['ci']);
+ }
+
+ runNpm(['run', 'build']);
+ writeFileSync(stampFile, `${new Date().toISOString()}\n`, 'utf8');
+} finally {
+ rmSync(lockDirectory, {recursive: true, force: true});
+}
diff --git a/scripts/clion-linux-environment.sh b/scripts/clion-linux-environment.sh
index e18653c1d..20f3bba93 100644
--- a/scripts/clion-linux-environment.sh
+++ b/scripts/clion-linux-environment.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/bash
set -e
echo "Updating package lists..."
diff --git a/scripts/docker-linux-compose.sh b/scripts/docker-linux-compose.sh
index c2c914178..661ae737b 100644
--- a/scripts/docker-linux-compose.sh
+++ b/scripts/docker-linux-compose.sh
@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
diff --git a/src/InfiniFrame.Native/Core/InfiniFrameWindow.h b/src/InfiniFrame.Native/Core/InfiniFrameWindow.h
index ddfe70914..8b364da73 100644
--- a/src/InfiniFrame.Native/Core/InfiniFrameWindow.h
+++ b/src/InfiniFrame.Native/Core/InfiniFrameWindow.h
@@ -78,6 +78,11 @@ class InfiniFrameWindow {
/** @brief Close the window and terminate the message loop */
void Close();
+#ifdef __linux__
+ /** @brief Request close from a GTK delete-event handler */
+ [[nodiscard]] bool RequestClose();
+#endif
+
// -----------------------------------------------------------------------------------------------------------------
// Get Properties
// -----------------------------------------------------------------------------------------------------------------
diff --git a/src/InfiniFrame.Native/Platform/Linux/Window.cpp b/src/InfiniFrame.Native/Platform/Linux/Window.cpp
index 7de503c1b..b06d02011 100644
--- a/src/InfiniFrame.Native/Platform/Linux/Window.cpp
+++ b/src/InfiniFrame.Native/Platform/Linux/Window.cpp
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -19,6 +20,7 @@
#include "Embedded/Embedded.h"
std::mutex invokeLockMutex;
+std::once_flag platformInitOnce;
struct InvokeWaitInfo {
ACTION callback;
@@ -51,6 +53,7 @@ struct InfiniFrameWindow::Impl : InfiniFrameWindowImpl {
std::string _temporaryFilesPath;
+ bool _closed = false;
bool _isFullScreen = false;
double _zoom = 100.0;
int _minWidth = 0;
@@ -186,6 +189,46 @@ static std::string escapeJsonString(std::string_view input) {
return result;
}
+static GtkWidget* create_web_view(
+ WebKitUserContentManager* contentManager,
+ const std::string& temporaryFilesPath
+ ) {
+ if (temporaryFilesPath.empty())
+ return webkit_web_view_new_with_user_content_manager(contentManager);
+
+ std::string dataDirectory = temporaryFilesPath + "/data";
+ std::string cacheDirectory = temporaryFilesPath + "/cache";
+
+ g_mkdir_with_parents(dataDirectory.c_str(), 0700);
+ g_mkdir_with_parents(cacheDirectory.c_str(), 0700);
+
+ WebKitWebsiteDataManager* dataManager = webkit_website_data_manager_new(
+ "base-data-directory", dataDirectory.c_str(),
+ "base-cache-directory", cacheDirectory.c_str(),
+ NULL
+ );
+ WebKitWebContext* context = webkit_web_context_new_with_website_data_manager(dataManager);
+ GtkWidget* webView = GTK_WIDGET(g_object_new(
+ WEBKIT_TYPE_WEB_VIEW,
+ "web-context", context,
+ "user-content-manager", contentManager,
+ NULL
+ ));
+
+ g_object_unref(context);
+ g_object_unref(dataManager);
+
+ return webView;
+}
+
+static void ensure_platform_initialized() {
+ std::call_once(platformInitOnce, [] {
+ XInitThreads();
+ gtk_init(nullptr, nullptr);
+ notify_init("InfiniFrame");
+ });
+}
+
// ---------------------------------------------------------------------------------------------------------------------
// Impl method definitions
// ---------------------------------------------------------------------------------------------------------------------
@@ -299,7 +342,9 @@ void InfiniFrameWindow::Impl::AddCustomSchemeHandlers() {
if (_customSchemeCallback == nullptr)
return;
- WebKitWebContext* context = webkit_web_context_get_default();
+ WebKitWebContext* context = _webview != nullptr
+ ? webkit_web_view_get_context(WEBKIT_WEB_VIEW(_webview))
+ : webkit_web_context_get_default();
WebKitSecurityManager* securityManager = webkit_web_context_get_security_manager(context);
for (const auto& value : _customSchemeNames) {
if (securityManager != nullptr && g_ascii_strcasecmp(value.c_str(), "app") == 0) {
@@ -323,9 +368,7 @@ void InfiniFrameWindow::Impl::AddCustomSchemeHandlers() {
InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) :
m_impl(std::make_unique()) {
- XInitThreads();
- gtk_init(nullptr, nullptr);
- notify_init(initParams->Title);
+ ensure_platform_initialized();
if (initParams->Size != sizeof(InfiniFrameInitParams)) {
GtkWidget* dialog = gtk_message_dialog_new(
@@ -494,8 +537,10 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) :
}
InfiniFrameWindow::~InfiniFrameWindow() {
- notify_uninit();
- gtk_widget_destroy(m_impl->_window);
+ if (!m_impl->_closed && m_impl->_window != nullptr) {
+ m_impl->_closed = true;
+ gtk_widget_destroy(m_impl->_window);
+ }
}
// ---------------------------------------------------------------------------------------------------------------------
@@ -547,7 +592,26 @@ void InfiniFrameWindow::ClearBrowserAutoFill() {
}
void InfiniFrameWindow::Close() {
- gtk_window_close(GTK_WINDOW(m_impl->_window));
+ RequestClose();
+}
+
+bool InfiniFrameWindow::RequestClose() {
+ if (m_impl->_closed)
+ return true;
+
+ if (InvokeClose())
+ return false;
+
+ m_impl->_closed = true;
+
+ if (m_impl->_window != nullptr) {
+ gtk_widget_destroy(m_impl->_window);
+ }
+
+ if (gtk_main_level() > 0)
+ gtk_main_quit();
+
+ return true;
}
// ---------------------------------------------------------------------------------------------------------------------
@@ -861,11 +925,15 @@ void InfiniFrameWindow::ShowNotification(const AutoString title, const AutoStrin
}
void InfiniFrameWindow::WaitForExit() {
+ if (m_impl->_closed)
+ return;
+
g_signal_connect(
G_OBJECT(m_impl->_window), "destroy",
G_CALLBACK(
+[](GtkWidget*, gpointer) {
- gtk_main_quit();
+ if (gtk_main_level() > 0)
+ gtk_main_quit();
}
),
nullptr
@@ -1002,7 +1070,8 @@ void InfiniFrameWindow::Show(bool isAlreadyShown) {
struct sigaction old_action;
sigaction(SIGCHLD, nullptr, &old_action);
WebKitUserContentManager* contentManager = webkit_user_content_manager_new();
- m_impl->_webview = webkit_web_view_new_with_user_content_manager(contentManager);
+ m_impl->_webview = create_web_view(contentManager, m_impl->_temporaryFilesPath);
+ g_object_unref(contentManager);
m_impl->set_webkit_settings();
@@ -1101,7 +1170,8 @@ gboolean on_window_state_event(GtkWidget* widget, GdkEventWindowState* event, co
gboolean on_widget_deleted(GtkWidget* widget, GdkEvent* event, const gpointer self) {
auto* instance = reinterpret_cast(self);
- return instance->InvokeClose();
+ instance->RequestClose();
+ return TRUE;
}
gboolean on_focus_in_event(GtkWidget* widget, GdkEvent* event, const gpointer self) {
diff --git a/src/InfiniFrame.Native/Platform/Mac/Window.mm b/src/InfiniFrame.Native/Platform/Mac/Window.mm
index 018b6b4ae..ef7c31e87 100644
--- a/src/InfiniFrame.Native/Platform/Mac/Window.mm
+++ b/src/InfiniFrame.Native/Platform/Mac/Window.mm
@@ -290,6 +290,13 @@
m_impl->_webviewConfiguration = [[WKWebViewConfiguration alloc] init];
+ if (!m_impl->_temporaryFilesPath.empty())
+ {
+ // WKWebView does not expose a public API for choosing an arbitrary website data directory.
+ // Use an isolated temporary store instead of silently falling back to the shared persistent store.
+ m_impl->_webviewConfiguration.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
+ }
+
for (const auto & scheme : m_impl->_customSchemeNames)
{
// Note:
diff --git a/src/InfiniFrame/InfiniFrameWindowNativeParameterBuilder.cs b/src/InfiniFrame/InfiniFrameWindowNativeParameterBuilder.cs
index abfe97217..03a338b1b 100644
--- a/src/InfiniFrame/InfiniFrameWindowNativeParameterBuilder.cs
+++ b/src/InfiniFrame/InfiniFrameWindowNativeParameterBuilder.cs
@@ -38,7 +38,7 @@ public class InfiniFrameWindowNativeParameterBuilder : IInfiniFrameWindowNativeP
public bool SmoothScrollingEnabled { get; set; } = true;
public string? StartString { get; set; }
public string? StartUrl { get; set; }
- public string? TemporaryFilesPath { get; set; } = Path.Join(Path.GetTempPath(), "infiniframe");
+ public string? TemporaryFilesPath { get; set; } = Path.Join(Path.GetTempPath(), "infiniframe", Environment.ProcessId.ToString());
public string Title { get; set; } = "InfiniFrame";
public int Top { get; set; }
public bool TopMost { get; set; }
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
index d94004f21..1ed37d8c0 100644
--- a/tests/Directory.Build.props
+++ b/tests/Directory.Build.props
@@ -2,6 +2,7 @@
net8.0;net9.0;net10.0
+ false
14.0
enable
diff --git a/tests/InfiniFrameTests.Playwright.WebApp.React/InfiniFrameTests.Playwright.WebApp.React.csproj b/tests/InfiniFrameTests.Playwright.WebApp.React/InfiniFrameTests.Playwright.WebApp.React.csproj
index 0bb9001db..152bf2ab1 100644
--- a/tests/InfiniFrameTests.Playwright.WebApp.React/InfiniFrameTests.Playwright.WebApp.React.csproj
+++ b/tests/InfiniFrameTests.Playwright.WebApp.React/InfiniFrameTests.Playwright.WebApp.React.csproj
@@ -26,19 +26,15 @@
-
+
-
-
-
+
@@ -69,4 +65,4 @@
SkipUnchangedFiles="true" />
-
\ No newline at end of file
+
diff --git a/tests/InfiniFrameTests.Playwright.WebApp.Vue/InfiniFrameTests.Playwright.WebApp.Vue.csproj b/tests/InfiniFrameTests.Playwright.WebApp.Vue/InfiniFrameTests.Playwright.WebApp.Vue.csproj
index 3719630a0..de01ab4d5 100644
--- a/tests/InfiniFrameTests.Playwright.WebApp.Vue/InfiniFrameTests.Playwright.WebApp.Vue.csproj
+++ b/tests/InfiniFrameTests.Playwright.WebApp.Vue/InfiniFrameTests.Playwright.WebApp.Vue.csproj
@@ -26,19 +26,15 @@
-
+
-
-
-
+
@@ -69,4 +65,4 @@
SkipUnchangedFiles="true" />
-
\ No newline at end of file
+
diff --git a/tests/InfiniFrameTests.Shared/InfiniFrameWindowTestUtility.cs b/tests/InfiniFrameTests.Shared/InfiniFrameWindowTestUtility.cs
index 973f20a00..b33760c83 100644
--- a/tests/InfiniFrameTests.Shared/InfiniFrameWindowTestUtility.cs
+++ b/tests/InfiniFrameTests.Shared/InfiniFrameWindowTestUtility.cs
@@ -45,38 +45,73 @@ public static InfiniFrameWindowTestUtility Create(
var windowBuilder = InfiniFrameWindowBuilder.Create();
windowBuilder.SetStartString(StartString);
+ if (OperatingSystem.IsLinux()) {
+ windowBuilder.SetTemporaryFilesPath(Path.Join(
+ Path.GetTempPath(),
+ "infiniframe-tests",
+ Environment.ProcessId.ToString(),
+ Guid.NewGuid().ToString("N")));
+ }
+
builder?.Invoke(windowBuilder);
- // Windows: WebView2 requires STA thread for COM initialization
- // Linux: GTK implicitly treats the calling thread as the main UI thread
- // macOS: Similar to Linux, but with additional main-thread restrictions for menu operations
- if (OperatingSystem.IsWindows()) {
- return CreateOnStaThread(windowBuilder);
- }
- else {
- // On Linux/macOS, create the window in the current thread to ensure proper GTK initialization
- IInfiniFrameWindow window = windowBuilder.Build();
-
- var utility = new InfiniFrameWindowTestUtility {
- Window = window
- };
-
- var thread = new Thread(() => {
- try {
- window.WaitForClose();
- }
- catch (ApplicationException) {
- // Ignore shutdown exceptions during test cleanup
- }
- }) {
- IsBackground = true
- };
-
- utility._windowThread = thread;
- thread.Start();
-
- return utility;
- }
+ // Windows: WebView2 requires STA thread for COM initialization.
+ // Linux: GTK's message loop must run on the same thread that created the window.
+ // macOS: Create the window on the current thread because Cocoa has main-thread restrictions.
+ if (OperatingSystem.IsWindows()) return CreateOnStaThread(windowBuilder);
+ if (OperatingSystem.IsLinux()) return CreateOnDedicatedThread(windowBuilder);
+
+ IInfiniFrameWindow window = windowBuilder.Build();
+
+ var utility = new InfiniFrameWindowTestUtility {
+ Window = window
+ };
+
+ var thread = new Thread(() => {
+ try {
+ window.WaitForClose();
+ }
+ catch (ApplicationException) {
+ // Ignore shutdown exceptions during test cleanup
+ }
+ }) {
+ IsBackground = true
+ };
+
+ utility._windowThread = thread;
+ thread.Start();
+
+ return utility;
+ }
+
+ [MustDisposeResource]
+ private static InfiniFrameWindowTestUtility CreateOnDedicatedThread(
+ InfiniFrameWindowBuilder windowBuilder
+ ) {
+ var windowSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var thread = new Thread(() => {
+ try {
+ IInfiniFrameWindow window = windowBuilder.Build();
+ windowSource.SetResult(window);
+ window.WaitForClose();
+ }
+ catch (Exception ex) when (IsNonFatalException(ex)) {
+ windowSource.TrySetException(ex);
+ }
+ }) {
+ IsBackground = true,
+ Name = "InfiniFrame Test Window Thread"
+ };
+
+ thread.Start();
+
+ var utility = new InfiniFrameWindowTestUtility {
+ Window = windowSource.Task.GetAwaiter().GetResult(),
+ _windowThread = thread
+ };
+
+ return utility;
}
[SupportedOSPlatform("windows")]
diff --git a/tests/InfiniFrameTests/MultiWindowTests.cs b/tests/InfiniFrameTests/MultiWindowTests.cs
new file mode 100644
index 000000000..37b798224
--- /dev/null
+++ b/tests/InfiniFrameTests/MultiWindowTests.cs
@@ -0,0 +1,37 @@
+// ---------------------------------------------------------------------------------------------------------------------
+// Imports
+// ---------------------------------------------------------------------------------------------------------------------
+using InfiniFrame;
+using InfiniFrameTests.Shared;
+
+namespace InfiniFrameTests;
+// ---------------------------------------------------------------------------------------------------------------------
+// Code
+// ---------------------------------------------------------------------------------------------------------------------
+public class MultiWindowTests {
+ [Test]
+ [SkipUtility.SkipOnMacOs]
+ [NotInParallel(ParallelControl.InfiniFrame)]
+ [Timeout(TimeoutUtility.DefaultTimeout)]
+ public async Task OpenWindowAfterOneCloses(CancellationToken ct) {
+ // Arrange
+ int closingRequestedCounter = 0;
+ var window1Utility = InfiniFrameWindowTestUtility.Create(
+ builder => builder.RegisterWindowClosingRequestedHandler(_ => {
+ Interlocked.Increment(ref closingRequestedCounter);
+ }), ct);
+
+ // Act
+ window1Utility.Dispose();
+
+ var window2Utility = InfiniFrameWindowTestUtility.Create(
+ builder => builder.RegisterWindowClosingRequestedHandler(_ => {
+ Interlocked.Increment(ref closingRequestedCounter);
+ }), ct);
+
+ window2Utility.Dispose();
+
+ // Assert
+ await Assert.That(closingRequestedCounter).IsEqualTo(2);
+ }
+}