Initial commit
This commit is contained in:
parent
30102c9871
commit
739e24b38b
9 changed files with 705 additions and 158 deletions
19
README.md
19
README.md
|
|
@ -66,11 +66,14 @@ script assets. Wo prefetches and verifies those files before navigation, caches
|
|||
verified assets by URL and hash, then asks the user before exposing the
|
||||
requested API groups. Granting access applies only to that page/window; a
|
||||
top-level navigation revokes it. Scripts for a manifest-enabled page are served
|
||||
from Wo's verified cache, and unlisted script loads are blocked.
|
||||
from Wo's verified cache, and unlisted script loads are blocked. A saved grant
|
||||
is reused only for the same origin, document hash, declared script hashes, and
|
||||
capability request.
|
||||
|
||||
The available permission names are `window-management`, `overlay-rendering`,
|
||||
`desktop-applications`, `xdg`, `dbus`, `portals`, `filesystem`,
|
||||
`process-launch`, `system-control`, `notifications`, and `audio-capture`. A
|
||||
`process-launch`, `system-control`, `notifications`, `audio-capture`, and
|
||||
`settings`. A
|
||||
page can inspect `window.wo.permissions.requested`, `.granted`, and `.has(name)`.
|
||||
The compositor bridge groups (`window.compositor` and `window.woClient`) are
|
||||
present only after at least one API group is approved; unapproved groups are
|
||||
|
|
@ -95,6 +98,18 @@ component theme tokens and keyboard focus styling that Wo preloads and applies
|
|||
to the page. Websites can use the same manifest format to request a window
|
||||
surface or use the overlay-rendering bridge for HTML/CSS/JS surfaces.
|
||||
|
||||
API consent is remembered per origin and exact site build. Wo records the
|
||||
verified HTML hash, the declared JavaScript asset hashes, and the requested
|
||||
capability set. The grant survives restarts; changing the HTML, a declared
|
||||
script, or requested capabilities produces a new consent request. Sites with
|
||||
`settings` can use `window.wo.settings` (`get`, `set`, `remove`, `keys`) for
|
||||
persistent JSON values. Wo keeps each web origin in its own user settings file
|
||||
with owner-only permissions, so apps can share their settings across pages on
|
||||
the same origin without sharing them with other hosts. Store passwords and
|
||||
tokens with `getSecret`, `setSecret`, and `removeSecret`; Wo encrypts these using
|
||||
GNOME Keyring/Secret Service or KDE Wallet and declines secret writes when a
|
||||
secure desktop keyring is unavailable.
|
||||
|
||||
Sites with `window-management` can subscribe to the compositor's monitor
|
||||
layout with `window.wo.windows?.monitors(callback)`. Sites with
|
||||
`overlay-rendering` can pin an element to a monitor's global desktop rectangle:
|
||||
|
|
|
|||
328
electron/dist/main.js
vendored
328
electron/dist/main.js
vendored
|
|
@ -1,5 +1,5 @@
|
|||
// src/main.ts
|
||||
import { app, BrowserWindow, dialog, ipcMain, Menu, protocol } from "electron";
|
||||
import { app, BrowserWindow, dialog, ipcMain, Menu, protocol, safeStorage } from "electron";
|
||||
import { createConnection, createServer } from "net";
|
||||
import { fileURLToPath as fileURLToPath2 } from "url";
|
||||
import { dirname, resolve as resolvePath, join as joinPath } from "path";
|
||||
|
|
@ -281,7 +281,8 @@ var WO_CAPABILITIES = [
|
|||
"process-launch",
|
||||
"system-control",
|
||||
"notifications",
|
||||
"audio-capture"
|
||||
"audio-capture",
|
||||
"settings"
|
||||
];
|
||||
var MAX_INDEX_BYTES = 4 * 1024 * 1024;
|
||||
var MAX_ASSET_BYTES = 16 * 1024 * 1024;
|
||||
|
|
@ -791,7 +792,7 @@ function setupWoCacheProtocol() {
|
|||
debugLog(`[Wo] Could not rewrite cached module imports for ${hash.slice(0, 12)}: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
return new Response(responseBytes, {
|
||||
return new Response(new Uint8Array(responseBytes), {
|
||||
headers: {
|
||||
"Content-Type": isStylesheet ? "text/css; charset=utf-8" : "text/javascript; charset=utf-8",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
|
|
@ -968,6 +969,164 @@ function resourceApprovalKey(pageUrl, resourceUrl) {
|
|||
return createHash2("sha256").update(`${pageUrl}
|
||||
${resourceUrl}`).digest("hex");
|
||||
}
|
||||
function apiConsentKey(site) {
|
||||
let origin = "";
|
||||
try {
|
||||
origin = new URL(site.pageUrl).origin;
|
||||
} catch {}
|
||||
const identity = {
|
||||
origin,
|
||||
appId: site.appId,
|
||||
indexSha256: site.indexSha256,
|
||||
scriptSha256: site.scriptBodies.map((script) => script.hash).sort(),
|
||||
capabilities: [...site.capabilities].sort()
|
||||
};
|
||||
return createHash2("sha256").update(JSON.stringify(identity)).digest("hex");
|
||||
}
|
||||
function apiConsentPath(key) {
|
||||
return joinPath(app.getPath("userData"), "wo-api-consents", `${key}.json`);
|
||||
}
|
||||
async function loadApiConsent(site) {
|
||||
if (!site.trusted || !site.indexSha256)
|
||||
return null;
|
||||
try {
|
||||
const key = apiConsentKey(site);
|
||||
const record = JSON.parse(await fs.promises.readFile(apiConsentPath(key), "utf8"));
|
||||
const origin = new URL(site.pageUrl).origin;
|
||||
const scriptSha256 = site.scriptBodies.map((script) => script.hash).sort();
|
||||
if (record.key !== key || record.origin !== origin || record.indexSha256 !== site.indexSha256 || JSON.stringify(record.scriptSha256) !== JSON.stringify(scriptSha256) || !Array.isArray(record.capabilities) || JSON.stringify([...record.capabilities].sort()) !== JSON.stringify([...site.capabilities].sort()) || !Array.isArray(record.granted) || !record.granted.every((capability) => site.capabilities.includes(capability)))
|
||||
return null;
|
||||
return record.granted;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function saveApiConsent(site, granted) {
|
||||
const key = apiConsentKey(site);
|
||||
const origin = new URL(site.pageUrl).origin;
|
||||
const path = apiConsentPath(key);
|
||||
const directory = dirname(path);
|
||||
const temporary = `${path}.${randomUUID()}.tmp`;
|
||||
const record = {
|
||||
key,
|
||||
origin,
|
||||
appId: site.appId,
|
||||
indexSha256: site.indexSha256,
|
||||
scriptSha256: site.scriptBodies.map((script) => script.hash).sort(),
|
||||
capabilities: [...site.capabilities].sort(),
|
||||
granted: [...granted].sort(),
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
await fs.promises.mkdir(directory, { recursive: true, mode: 448 });
|
||||
await fs.promises.writeFile(temporary, JSON.stringify(record, null, 2), { mode: 384 });
|
||||
await fs.promises.rename(temporary, path);
|
||||
}
|
||||
var MAX_SITE_SETTINGS_BYTES = 64 * 1024;
|
||||
var SETTINGS_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
||||
var siteSettingsTransactions = new Map;
|
||||
var siteSecretTransactions = new Map;
|
||||
function siteSettingsPath(origin) {
|
||||
const originHash = createHash2("sha256").update(origin).digest("hex");
|
||||
return joinPath(app.getPath("userData"), "site-settings", `${originHash}.json`);
|
||||
}
|
||||
function siteSecretsPath(origin) {
|
||||
const originHash = createHash2("sha256").update(origin).digest("hex");
|
||||
return joinPath(app.getPath("userData"), "site-secrets", `${originHash}.json`);
|
||||
}
|
||||
function secureSecretStorageAvailable() {
|
||||
if (!safeStorage.isEncryptionAvailable())
|
||||
return false;
|
||||
const backend = safeStorage.getSelectedStorageBackend();
|
||||
return ["gnome_libsecret", "kwallet", "kwallet5", "kwallet6"].includes(backend);
|
||||
}
|
||||
async function readSiteSecrets(origin) {
|
||||
if (!secureSecretStorageAvailable())
|
||||
throw new Error("Secure secret storage requires GNOME Keyring/Secret Service or KDE Wallet");
|
||||
let stored;
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.promises.readFile(siteSecretsPath(origin), "utf8"));
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
||||
return Object.create(null);
|
||||
stored = parsed;
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT")
|
||||
return Object.create(null);
|
||||
throw error;
|
||||
}
|
||||
const decrypted = Object.create(null);
|
||||
for (const [key, encoded] of Object.entries(stored)) {
|
||||
if (typeof encoded !== "string")
|
||||
continue;
|
||||
decrypted[key] = safeStorage.decryptString(Buffer.from(encoded, "base64"));
|
||||
}
|
||||
return decrypted;
|
||||
}
|
||||
async function updateSiteSecrets(origin, update) {
|
||||
if (!secureSecretStorageAvailable())
|
||||
throw new Error("Secure secret storage requires GNOME Keyring/Secret Service or KDE Wallet");
|
||||
const previous = siteSecretTransactions.get(origin) ?? Promise.resolve();
|
||||
const transaction = previous.catch(() => {
|
||||
return;
|
||||
}).then(async () => {
|
||||
const secrets = await readSiteSecrets(origin);
|
||||
update(secrets);
|
||||
const encrypted = Object.create(null);
|
||||
for (const [key, value] of Object.entries(secrets)) {
|
||||
encrypted[key] = safeStorage.encryptString(value).toString("base64");
|
||||
}
|
||||
const path = siteSecretsPath(origin);
|
||||
const directory = dirname(path);
|
||||
const temporary = `${path}.${randomUUID()}.tmp`;
|
||||
await fs.promises.mkdir(directory, { recursive: true, mode: 448 });
|
||||
await fs.promises.writeFile(temporary, JSON.stringify(encrypted), { mode: 384 });
|
||||
await fs.promises.rename(temporary, path);
|
||||
});
|
||||
siteSecretTransactions.set(origin, transaction);
|
||||
try {
|
||||
await transaction;
|
||||
} finally {
|
||||
if (siteSecretTransactions.get(origin) === transaction)
|
||||
siteSecretTransactions.delete(origin);
|
||||
}
|
||||
}
|
||||
async function readSiteSettings(origin) {
|
||||
try {
|
||||
const value = JSON.parse(await fs.promises.readFile(siteSettingsPath(origin), "utf8"));
|
||||
if (!value || typeof value !== "object" || Array.isArray(value))
|
||||
return {};
|
||||
return Object.assign(Object.create(null), value);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
async function writeSiteSettings(origin, settings) {
|
||||
const path = siteSettingsPath(origin);
|
||||
const directory = dirname(path);
|
||||
const temporary = `${path}.${randomUUID()}.tmp`;
|
||||
const bytes = JSON.stringify(settings);
|
||||
if (Buffer.byteLength(bytes) > 1024 * 1024)
|
||||
throw new Error("site settings exceed 1 MiB");
|
||||
await fs.promises.mkdir(directory, { recursive: true, mode: 448 });
|
||||
await fs.promises.writeFile(temporary, bytes, { mode: 384 });
|
||||
await fs.promises.rename(temporary, path);
|
||||
}
|
||||
async function updateSiteSettings(origin, update) {
|
||||
const previous = siteSettingsTransactions.get(origin) ?? Promise.resolve();
|
||||
const transaction = previous.catch(() => {
|
||||
return;
|
||||
}).then(async () => {
|
||||
const settings = await readSiteSettings(origin);
|
||||
update(settings);
|
||||
await writeSiteSettings(origin, settings);
|
||||
});
|
||||
siteSettingsTransactions.set(origin, transaction);
|
||||
try {
|
||||
await transaction;
|
||||
} finally {
|
||||
if (siteSettingsTransactions.get(origin) === transaction)
|
||||
siteSettingsTransactions.delete(origin);
|
||||
}
|
||||
}
|
||||
function resourceCacheDirectory() {
|
||||
return joinPath(app.getPath("userData"), "wo-approved-resources");
|
||||
}
|
||||
|
|
@ -1206,9 +1365,16 @@ function debugLog(...args) {
|
|||
}
|
||||
debugLog("[Wo] Electron starting, NODE_VERSION=", process.version);
|
||||
var requestedOzonePlatform = (process.env.WO_ELECTRON_OZONE_PLATFORM || "auto").toLowerCase();
|
||||
var defaultGpuBackend = "angle";
|
||||
var defaultGpuBackend = "auto";
|
||||
var requestedGpuBackend = (process.env.WO_ELECTRON_GPU_BACKEND || defaultGpuBackend).toLowerCase();
|
||||
var gpuBackend = ["vulkan", "gl", "angle", "desktop", "auto"].includes(requestedGpuBackend) ? requestedGpuBackend : defaultGpuBackend;
|
||||
var validatedGpuBackend = ["vulkan", "gl", "angle", "desktop", "auto"].includes(requestedGpuBackend) ? requestedGpuBackend : defaultGpuBackend;
|
||||
var waylandOzone = requestedOzonePlatform === "wayland";
|
||||
var gpuBackend = waylandOzone && ["auto", "vulkan"].includes(validatedGpuBackend) ? "angle" : validatedGpuBackend;
|
||||
if (waylandOzone && validatedGpuBackend === "vulkan") {
|
||||
debugLog("[Wo] Vulkan was requested, but Chromium Vulkan is incompatible with native Wayland; using ANGLE OpenGL.");
|
||||
} else if (waylandOzone && validatedGpuBackend === "auto") {
|
||||
debugLog("[Wo] Native Wayland uses ANGLE OpenGL to avoid Chromium selecting its incompatible Vulkan surface path.");
|
||||
}
|
||||
if (gpuBackend === "desktop") {
|
||||
app.commandLine.appendSwitch("use-gl", "desktop");
|
||||
} else if (gpuBackend === "angle") {
|
||||
|
|
@ -1229,9 +1395,9 @@ var chromiumFeatures = [
|
|||
];
|
||||
if (gpuBackend === "vulkan") {
|
||||
chromiumFeatures.push("Vulkan", "DefaultANGLEVulkan", "VulkanFromANGLE");
|
||||
} else if (requestedOzonePlatform === "wayland" || gpuBackend !== "auto") {
|
||||
} else if (waylandOzone || gpuBackend !== "auto") {
|
||||
app.commandLine.appendSwitch("disable-vulkan");
|
||||
app.commandLine.appendSwitch("disable-features", "Vulkan");
|
||||
app.commandLine.appendSwitch("disable-features", "Vulkan,DefaultANGLEVulkan,VulkanFromANGLE");
|
||||
}
|
||||
app.commandLine.appendSwitch("enable-features", chromiumFeatures.join(","));
|
||||
app.commandLine.appendSwitch("enable-accelerated-video-decode");
|
||||
|
|
@ -2587,6 +2753,63 @@ function sendSiteHashesToCompositor() {
|
|||
}
|
||||
function setupIpcHandlers() {
|
||||
ipcMain.handle("wo:performance-status", () => getPerformanceStatus());
|
||||
ipcMain.handle("wo:settings", async (event, operation, key, value) => {
|
||||
if (!senderHasCapability(event.sender, "settings"))
|
||||
return permissionDenied("settings");
|
||||
if (typeof operation !== "string" || !["get", "set", "remove", "keys", "getSecret", "setSecret", "removeSecret"].includes(operation)) {
|
||||
return { ok: false, error: "Unknown settings operation" };
|
||||
}
|
||||
if (operation !== "keys" && (typeof key !== "string" || !SETTINGS_KEY_PATTERN.test(key) || ["__proto__", "prototype", "constructor"].includes(key))) {
|
||||
return { ok: false, error: "Setting keys must be 1-128 letters, digits, dots, underscores, colons, or hyphens" };
|
||||
}
|
||||
let origin;
|
||||
try {
|
||||
origin = new URL(event.sender.getURL()).origin;
|
||||
} catch {
|
||||
return { ok: false, error: "Settings require a valid web origin" };
|
||||
}
|
||||
try {
|
||||
if (operation === "get") {
|
||||
const settings = await readSiteSettings(origin);
|
||||
return settings[key];
|
||||
}
|
||||
if (operation === "keys")
|
||||
return Object.keys(await readSiteSettings(origin)).sort();
|
||||
if (operation === "getSecret")
|
||||
return (await readSiteSecrets(origin))[key];
|
||||
if (operation === "removeSecret") {
|
||||
await updateSiteSecrets(origin, (secrets) => {
|
||||
delete secrets[key];
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
if (operation === "setSecret") {
|
||||
if (typeof value !== "string" || Buffer.byteLength(value) > MAX_SITE_SETTINGS_BYTES) {
|
||||
return { ok: false, error: "Secret values must be text and at most 64 KiB" };
|
||||
}
|
||||
await updateSiteSecrets(origin, (secrets) => {
|
||||
secrets[key] = value;
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
if (operation === "remove") {
|
||||
await updateSiteSettings(origin, (settings) => {
|
||||
delete settings[key];
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
const encoded = JSON.stringify(value);
|
||||
if (encoded === undefined || Buffer.byteLength(encoded) > MAX_SITE_SETTINGS_BYTES) {
|
||||
return { ok: false, error: "Settings values must be JSON and at most 64 KiB" };
|
||||
}
|
||||
await updateSiteSettings(origin, (settings) => {
|
||||
settings[key] = JSON.parse(encoded);
|
||||
});
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
ipcMain.on("wo:portal-ui-ready", (event) => {
|
||||
if (CLIENT_MODE || !mainWindow || event.sender !== mainWindow.webContents)
|
||||
return;
|
||||
|
|
@ -2796,8 +3019,7 @@ function setupIpcHandlers() {
|
|||
theme: typeof params.theme === "string" ? params.theme : undefined
|
||||
}) ?? null;
|
||||
}
|
||||
case "launch":
|
||||
case "exec": {
|
||||
case "launch": {
|
||||
const { spawn } = await import("child_process");
|
||||
const command = params.command || "";
|
||||
if (!command) {
|
||||
|
|
@ -2815,6 +3037,26 @@ function setupIpcHandlers() {
|
|||
return { ok: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
case "exec": {
|
||||
const { execFile } = await import("child_process");
|
||||
const command = typeof params.command === "string" ? params.command : "";
|
||||
const args = Array.isArray(params.args) ? params.args.filter((arg) => typeof arg === "string").slice(0, 64) : [];
|
||||
if (!command || args.length !== (Array.isArray(params.args) ? params.args.length : 0)) {
|
||||
return { ok: false, error: "Command and string arguments are required" };
|
||||
}
|
||||
return await new Promise((resolve) => {
|
||||
execFile(command, args, { encoding: "utf8", timeout: 15000, maxBuffer: 1048576 }, (error, stdout, stderr) => {
|
||||
const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
|
||||
resolve({
|
||||
ok: !error,
|
||||
stdout: String(stdout || ""),
|
||||
stderr: String(stderr || ""),
|
||||
exitCode,
|
||||
...error ? { error: String(error) } : {}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
case "listdir": {
|
||||
const fs2 = await import("fs");
|
||||
const path = await import("path");
|
||||
|
|
@ -3069,24 +3311,39 @@ async function createWindow() {
|
|||
apiGrantPageUrl = siteAssets.pageUrl;
|
||||
debugLog("[Wo] Granted verified comraw system-shell capabilities");
|
||||
} else if (siteAssets.trusted && siteAssets.capabilities.length > 0) {
|
||||
const permissionResult = await dialog.showMessageBox({
|
||||
type: "question",
|
||||
title: "Wo API permission request",
|
||||
message: `${siteAssets.appId} requests access to Wo APIs`,
|
||||
detail: `Page: ${pageUrl}
|
||||
Verified index.html SHA-256: ${siteAssets.indexSha256}
|
||||
const cachedGrant = await loadApiConsent(siteAssets);
|
||||
if (cachedGrant) {
|
||||
apiGrants = new Set(cachedGrant);
|
||||
apiGrantPageUrl = siteAssets.pageUrl;
|
||||
debugLog(`[Wo] Restored remembered API consent for ${siteAssets.appId} (${apiConsentKey(siteAssets).slice(0, 12)})`);
|
||||
} else {
|
||||
const scriptHashes = siteAssets.scriptBodies.map((script) => script.hash.slice(0, 12)).join(", ") || "(no external scripts)";
|
||||
const permissionResult = await dialog.showMessageBox({
|
||||
type: "question",
|
||||
title: "Wo API permission request",
|
||||
message: `${siteAssets.appId} requests access to Wo APIs`,
|
||||
detail: `Origin: ${new URL(pageUrl).origin}
|
||||
Verified HTML SHA-256: ${siteAssets.indexSha256}
|
||||
Verified JavaScript SHA-256: ${scriptHashes}
|
||||
|
||||
Requested permissions: ${siteAssets.capabilities.join(", ")}
|
||||
|
||||
The site APIs are exposed only for this window while it is open.`,
|
||||
buttons: ["Allow requested APIs", "Open without Wo APIs"],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
noLink: true
|
||||
});
|
||||
if (permissionResult.response === 0) {
|
||||
apiGrants = new Set(siteAssets.capabilities);
|
||||
apiGrantPageUrl = siteAssets.pageUrl;
|
||||
Allowing remembers this decision for this origin and these exact HTML/JavaScript assets. Wo will ask again if any of them change.`,
|
||||
buttons: ["Allow and remember", "Open without Wo APIs"],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
noLink: true
|
||||
});
|
||||
if (permissionResult.response === 0) {
|
||||
apiGrants = new Set(siteAssets.capabilities);
|
||||
apiGrantPageUrl = siteAssets.pageUrl;
|
||||
try {
|
||||
await saveApiConsent(siteAssets, [...apiGrants]);
|
||||
debugLog(`[Wo] Remembered API consent for ${siteAssets.appId} (${apiConsentKey(siteAssets).slice(0, 12)})`);
|
||||
} catch (error) {
|
||||
debugLog("[Wo] Could not persist API consent:", String(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const requested = siteAssets.trusted ? siteAssets.capabilities : [];
|
||||
|
|
@ -3312,14 +3569,21 @@ The site APIs are exposed only for this window while it is open.`,
|
|||
debugLog("[Wo] Loading content URL: " + pageUrl);
|
||||
try {
|
||||
const loadPromise = mainWindow.loadURL(pageUrl);
|
||||
await Promise.race([
|
||||
loadPromise,
|
||||
new Promise((r) => setTimeout(() => {
|
||||
debugLog("[Wo] Load timeout, continuing");
|
||||
r(undefined);
|
||||
}, 3000))
|
||||
]);
|
||||
debugLog("[Wo] Content loaded from: " + pageUrl);
|
||||
let loadTimeout;
|
||||
const navigationOutcome = await Promise.race([
|
||||
loadPromise.then(() => "loaded"),
|
||||
new Promise((resolve) => {
|
||||
loadTimeout = setTimeout(() => {
|
||||
debugLog("[Wo] Page navigation is still pending after 3s; continuing compositor startup");
|
||||
resolve("pending");
|
||||
}, 3000);
|
||||
})
|
||||
]).finally(() => {
|
||||
if (loadTimeout)
|
||||
clearTimeout(loadTimeout);
|
||||
});
|
||||
if (navigationOutcome === "loaded")
|
||||
debugLog("[Wo] Content loaded from: " + pageUrl);
|
||||
} catch (e) {
|
||||
debugLog("[Wo] Load failed:", String(e));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { app, BrowserWindow, dialog, ipcMain, Menu, protocol } from 'electron';
|
||||
import { app, BrowserWindow, dialog, ipcMain, Menu, protocol, safeStorage } from 'electron';
|
||||
import { createConnection, createServer, Socket, Server } from 'net';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, resolve as resolvePath, join as joinPath } from 'path';
|
||||
|
|
@ -57,7 +57,7 @@ function setupWoCacheProtocol() {
|
|||
debugLog(`[Wo] Could not rewrite cached module imports for ${hash.slice(0, 12)}: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
return new Response(responseBytes, {
|
||||
return new Response(new Uint8Array(responseBytes), {
|
||||
headers: {
|
||||
'Content-Type': isStylesheet ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
|
|
@ -218,6 +218,168 @@ function resourceApprovalKey(pageUrl: string, resourceUrl: string): string {
|
|||
return createHash('sha256').update(`${pageUrl}\n${resourceUrl}`).digest('hex');
|
||||
}
|
||||
|
||||
function apiConsentKey(site: WoSiteAssets): string {
|
||||
let origin = '';
|
||||
try { origin = new URL(site.pageUrl).origin; } catch { /* untrusted URLs are not cached */ }
|
||||
const identity = {
|
||||
origin,
|
||||
appId: site.appId,
|
||||
indexSha256: site.indexSha256,
|
||||
scriptSha256: site.scriptBodies.map((script) => script.hash).sort(),
|
||||
capabilities: [...site.capabilities].sort(),
|
||||
};
|
||||
return createHash('sha256').update(JSON.stringify(identity)).digest('hex');
|
||||
}
|
||||
|
||||
function apiConsentPath(key: string): string {
|
||||
return joinPath(app.getPath('userData'), 'wo-api-consents', `${key}.json`);
|
||||
}
|
||||
|
||||
async function loadApiConsent(site: WoSiteAssets): Promise<WoCapability[] | null> {
|
||||
if (!site.trusted || !site.indexSha256) return null;
|
||||
try {
|
||||
const key = apiConsentKey(site);
|
||||
const record = JSON.parse(await fs.promises.readFile(apiConsentPath(key), 'utf8')) as {
|
||||
key?: string; origin?: string; indexSha256?: string; scriptSha256?: string[];
|
||||
capabilities?: WoCapability[]; granted?: WoCapability[];
|
||||
};
|
||||
const origin = new URL(site.pageUrl).origin;
|
||||
const scriptSha256 = site.scriptBodies.map((script) => script.hash).sort();
|
||||
if (record.key !== key || record.origin !== origin || record.indexSha256 !== site.indexSha256
|
||||
|| JSON.stringify(record.scriptSha256) !== JSON.stringify(scriptSha256)
|
||||
|| !Array.isArray(record.capabilities)
|
||||
|| JSON.stringify([...record.capabilities].sort()) !== JSON.stringify([...site.capabilities].sort())
|
||||
|| !Array.isArray(record.granted)
|
||||
|| !record.granted.every((capability) => site.capabilities.includes(capability))) return null;
|
||||
return record.granted;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveApiConsent(site: WoSiteAssets, granted: WoCapability[]): Promise<void> {
|
||||
const key = apiConsentKey(site);
|
||||
const origin = new URL(site.pageUrl).origin;
|
||||
const path = apiConsentPath(key);
|
||||
const directory = dirname(path);
|
||||
const temporary = `${path}.${randomUUID()}.tmp`;
|
||||
const record = {
|
||||
key,
|
||||
origin,
|
||||
appId: site.appId,
|
||||
indexSha256: site.indexSha256,
|
||||
scriptSha256: site.scriptBodies.map((script) => script.hash).sort(),
|
||||
capabilities: [...site.capabilities].sort(),
|
||||
granted: [...granted].sort(),
|
||||
savedAt: new Date().toISOString(),
|
||||
};
|
||||
await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
|
||||
await fs.promises.writeFile(temporary, JSON.stringify(record, null, 2), { mode: 0o600 });
|
||||
await fs.promises.rename(temporary, path);
|
||||
}
|
||||
|
||||
const MAX_SITE_SETTINGS_BYTES = 64 * 1024;
|
||||
const SETTINGS_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
||||
const siteSettingsTransactions = new Map<string, Promise<void>>();
|
||||
const siteSecretTransactions = new Map<string, Promise<void>>();
|
||||
|
||||
function siteSettingsPath(origin: string): string {
|
||||
const originHash = createHash('sha256').update(origin).digest('hex');
|
||||
return joinPath(app.getPath('userData'), 'site-settings', `${originHash}.json`);
|
||||
}
|
||||
|
||||
function siteSecretsPath(origin: string): string {
|
||||
const originHash = createHash('sha256').update(origin).digest('hex');
|
||||
return joinPath(app.getPath('userData'), 'site-secrets', `${originHash}.json`);
|
||||
}
|
||||
|
||||
function secureSecretStorageAvailable(): boolean {
|
||||
if (!safeStorage.isEncryptionAvailable()) return false;
|
||||
const backend = safeStorage.getSelectedStorageBackend();
|
||||
return ['gnome_libsecret', 'kwallet', 'kwallet5', 'kwallet6'].includes(backend);
|
||||
}
|
||||
|
||||
async function readSiteSecrets(origin: string): Promise<Record<string, string>> {
|
||||
if (!secureSecretStorageAvailable()) throw new Error('Secure secret storage requires GNOME Keyring/Secret Service or KDE Wallet');
|
||||
let stored: Record<string, string>;
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.promises.readFile(siteSecretsPath(origin), 'utf8'));
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return Object.create(null);
|
||||
stored = parsed;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return Object.create(null);
|
||||
throw error;
|
||||
}
|
||||
const decrypted = Object.create(null) as Record<string, string>;
|
||||
for (const [key, encoded] of Object.entries(stored)) {
|
||||
if (typeof encoded !== 'string') continue;
|
||||
decrypted[key] = safeStorage.decryptString(Buffer.from(encoded, 'base64'));
|
||||
}
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
async function updateSiteSecrets(origin: string, update: (secrets: Record<string, string>) => void): Promise<void> {
|
||||
if (!secureSecretStorageAvailable()) throw new Error('Secure secret storage requires GNOME Keyring/Secret Service or KDE Wallet');
|
||||
const previous = siteSecretTransactions.get(origin) ?? Promise.resolve();
|
||||
const transaction = previous.catch(() => undefined).then(async () => {
|
||||
const secrets = await readSiteSecrets(origin);
|
||||
update(secrets);
|
||||
const encrypted = Object.create(null) as Record<string, string>;
|
||||
for (const [key, value] of Object.entries(secrets)) {
|
||||
encrypted[key] = safeStorage.encryptString(value).toString('base64');
|
||||
}
|
||||
const path = siteSecretsPath(origin);
|
||||
const directory = dirname(path);
|
||||
const temporary = `${path}.${randomUUID()}.tmp`;
|
||||
await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
|
||||
await fs.promises.writeFile(temporary, JSON.stringify(encrypted), { mode: 0o600 });
|
||||
await fs.promises.rename(temporary, path);
|
||||
});
|
||||
siteSecretTransactions.set(origin, transaction);
|
||||
try { await transaction; }
|
||||
finally {
|
||||
if (siteSecretTransactions.get(origin) === transaction) siteSecretTransactions.delete(origin);
|
||||
}
|
||||
}
|
||||
|
||||
async function readSiteSettings(origin: string): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const value = JSON.parse(await fs.promises.readFile(siteSettingsPath(origin), 'utf8'));
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
||||
return Object.assign(Object.create(null) as Record<string, unknown>, value);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSiteSettings(origin: string, settings: Record<string, unknown>): Promise<void> {
|
||||
const path = siteSettingsPath(origin);
|
||||
const directory = dirname(path);
|
||||
const temporary = `${path}.${randomUUID()}.tmp`;
|
||||
const bytes = JSON.stringify(settings);
|
||||
if (Buffer.byteLength(bytes) > 1024 * 1024) throw new Error('site settings exceed 1 MiB');
|
||||
await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
|
||||
await fs.promises.writeFile(temporary, bytes, { mode: 0o600 });
|
||||
await fs.promises.rename(temporary, path);
|
||||
}
|
||||
|
||||
async function updateSiteSettings(
|
||||
origin: string,
|
||||
update: (settings: Record<string, unknown>) => void,
|
||||
): Promise<void> {
|
||||
const previous = siteSettingsTransactions.get(origin) ?? Promise.resolve();
|
||||
const transaction = previous.catch(() => undefined).then(async () => {
|
||||
const settings = await readSiteSettings(origin);
|
||||
update(settings);
|
||||
await writeSiteSettings(origin, settings);
|
||||
});
|
||||
siteSettingsTransactions.set(origin, transaction);
|
||||
try { await transaction; }
|
||||
finally {
|
||||
if (siteSettingsTransactions.get(origin) === transaction) siteSettingsTransactions.delete(origin);
|
||||
}
|
||||
}
|
||||
|
||||
function resourceCacheDirectory(): string {
|
||||
return joinPath(app.getPath('userData'), 'wo-approved-resources');
|
||||
}
|
||||
|
|
@ -469,14 +631,25 @@ debugLog('[Wo] Electron starting, NODE_VERSION=', process.version);
|
|||
// Wayland without depending on a separate X server; explicit backend choices
|
||||
// remain available for systems that need desktop GL or Vulkan.
|
||||
const requestedOzonePlatform = (process.env.WO_ELECTRON_OZONE_PLATFORM || 'auto').toLowerCase();
|
||||
// The nested Wayland surface cannot present through Chromium's Vulkan path.
|
||||
// Pin ANGLE/OpenGL by default; set WO_ELECTRON_GPU_BACKEND=auto to ask Chromium
|
||||
// to choose its own implementation when diagnosing a driver-specific issue.
|
||||
const defaultGpuBackend = 'angle';
|
||||
// Let Chromium select the GL implementation from the active Wayland/driver
|
||||
// stack by default. Chromium's automatic backend currently selects Vulkan on
|
||||
// some Wayland/NVIDIA setups even when Vulkan is disabled below, so pin native
|
||||
// Wayland to ANGLE's OpenGL renderer. This keeps the hardware EGL/GLES path and
|
||||
// avoids Chromium's incompatible Wayland/Vulkan surface factory.
|
||||
const defaultGpuBackend = 'auto';
|
||||
const requestedGpuBackend = (process.env.WO_ELECTRON_GPU_BACKEND || defaultGpuBackend).toLowerCase();
|
||||
const gpuBackend = ['vulkan', 'gl', 'angle', 'desktop', 'auto'].includes(requestedGpuBackend)
|
||||
const validatedGpuBackend = ['vulkan', 'gl', 'angle', 'desktop', 'auto'].includes(requestedGpuBackend)
|
||||
? requestedGpuBackend
|
||||
: defaultGpuBackend;
|
||||
const waylandOzone = requestedOzonePlatform === 'wayland';
|
||||
const gpuBackend = waylandOzone && ['auto', 'vulkan'].includes(validatedGpuBackend)
|
||||
? 'angle'
|
||||
: validatedGpuBackend;
|
||||
if (waylandOzone && validatedGpuBackend === 'vulkan') {
|
||||
debugLog('[Wo] Vulkan was requested, but Chromium Vulkan is incompatible with native Wayland; using ANGLE OpenGL.');
|
||||
} else if (waylandOzone && validatedGpuBackend === 'auto') {
|
||||
debugLog('[Wo] Native Wayland uses ANGLE OpenGL to avoid Chromium selecting its incompatible Vulkan surface path.');
|
||||
}
|
||||
if (gpuBackend === 'desktop') {
|
||||
// On X11, ask Chromium for the system GLX/EGL implementation directly.
|
||||
// ANGLE's GL backend can fail to initialize on nested XWayland and then
|
||||
|
|
@ -500,11 +673,12 @@ const chromiumFeatures = [
|
|||
];
|
||||
if (gpuBackend === 'vulkan') {
|
||||
chromiumFeatures.push('Vulkan', 'DefaultANGLEVulkan', 'VulkanFromANGLE');
|
||||
} else if (requestedOzonePlatform === 'wayland' || gpuBackend !== 'auto') {
|
||||
// Chromium's Linux Wayland surface factory rejects Vulkan. Keep it disabled
|
||||
// while using the explicitly selected ANGLE/OpenGL implementation.
|
||||
} else if (waylandOzone || gpuBackend !== 'auto') {
|
||||
// Chromium may start its Vulkan surface path even when its reported feature
|
||||
// status says Vulkan is disabled. Disable the Vulkan feature family on
|
||||
// Wayland, while leaving GL/EGL backend selection automatic and accelerated.
|
||||
app.commandLine.appendSwitch('disable-vulkan');
|
||||
app.commandLine.appendSwitch('disable-features', 'Vulkan');
|
||||
app.commandLine.appendSwitch('disable-features', 'Vulkan,DefaultANGLEVulkan,VulkanFromANGLE');
|
||||
}
|
||||
app.commandLine.appendSwitch('enable-features', chromiumFeatures.join(','));
|
||||
app.commandLine.appendSwitch('enable-accelerated-video-decode');
|
||||
|
|
@ -1845,6 +2019,51 @@ function sendSiteHashesToCompositor(): void {
|
|||
function setupIpcHandlers() {
|
||||
ipcMain.handle('wo:performance-status', () => getPerformanceStatus());
|
||||
|
||||
ipcMain.handle('wo:settings', async (event, operation: unknown, key: unknown, value?: unknown) => {
|
||||
if (!senderHasCapability(event.sender, 'settings')) return permissionDenied('settings');
|
||||
if (typeof operation !== 'string' || !['get', 'set', 'remove', 'keys', 'getSecret', 'setSecret', 'removeSecret'].includes(operation)) {
|
||||
return { ok: false, error: 'Unknown settings operation' };
|
||||
}
|
||||
if (operation !== 'keys' && (typeof key !== 'string' || !SETTINGS_KEY_PATTERN.test(key)
|
||||
|| ['__proto__', 'prototype', 'constructor'].includes(key))) {
|
||||
return { ok: false, error: 'Setting keys must be 1-128 letters, digits, dots, underscores, colons, or hyphens' };
|
||||
}
|
||||
let origin: string;
|
||||
try { origin = new URL(event.sender.getURL()).origin; }
|
||||
catch { return { ok: false, error: 'Settings require a valid web origin' }; }
|
||||
try {
|
||||
if (operation === 'get') {
|
||||
const settings = await readSiteSettings(origin);
|
||||
return settings[key as string];
|
||||
}
|
||||
if (operation === 'keys') return Object.keys(await readSiteSettings(origin)).sort();
|
||||
if (operation === 'getSecret') return (await readSiteSecrets(origin))[key as string];
|
||||
if (operation === 'removeSecret') {
|
||||
await updateSiteSecrets(origin, (secrets) => { delete secrets[key as string]; });
|
||||
return { ok: true };
|
||||
}
|
||||
if (operation === 'setSecret') {
|
||||
if (typeof value !== 'string' || Buffer.byteLength(value) > MAX_SITE_SETTINGS_BYTES) {
|
||||
return { ok: false, error: 'Secret values must be text and at most 64 KiB' };
|
||||
}
|
||||
await updateSiteSecrets(origin, (secrets) => { secrets[key as string] = value; });
|
||||
return { ok: true };
|
||||
}
|
||||
if (operation === 'remove') {
|
||||
await updateSiteSettings(origin, (settings) => { delete settings[key as string]; });
|
||||
return { ok: true };
|
||||
}
|
||||
const encoded = JSON.stringify(value);
|
||||
if (encoded === undefined || Buffer.byteLength(encoded) > MAX_SITE_SETTINGS_BYTES) {
|
||||
return { ok: false, error: 'Settings values must be JSON and at most 64 KiB' };
|
||||
}
|
||||
await updateSiteSettings(origin, (settings) => { settings[key as string] = JSON.parse(encoded); });
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('wo:portal-ui-ready', (event) => {
|
||||
if (CLIENT_MODE || !mainWindow || event.sender !== mainWindow.webContents) return;
|
||||
portalUiRendererReady = true;
|
||||
|
|
@ -2067,7 +2286,7 @@ function setupIpcHandlers() {
|
|||
}
|
||||
|
||||
case 'launch':
|
||||
case 'exec': {
|
||||
{
|
||||
const { spawn } = await import('child_process');
|
||||
const command = (params.command as string) || '';
|
||||
|
||||
|
|
@ -2088,6 +2307,29 @@ function setupIpcHandlers() {
|
|||
}
|
||||
}
|
||||
|
||||
case 'exec': {
|
||||
const { execFile } = await import('child_process');
|
||||
const command = typeof params.command === 'string' ? params.command : '';
|
||||
const args = Array.isArray(params.args)
|
||||
? params.args.filter((arg): arg is string => typeof arg === 'string').slice(0, 64)
|
||||
: [];
|
||||
if (!command || args.length !== (Array.isArray(params.args) ? params.args.length : 0)) {
|
||||
return { ok: false, error: 'Command and string arguments are required' };
|
||||
}
|
||||
return await new Promise((resolve) => {
|
||||
execFile(command, args, { encoding: 'utf8', timeout: 15_000, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
||||
const exitCode = typeof error?.code === 'number' ? error.code : error ? 1 : 0;
|
||||
resolve({
|
||||
ok: !error,
|
||||
stdout: String(stdout || ''),
|
||||
stderr: String(stderr || ''),
|
||||
exitCode,
|
||||
...(error ? { error: String(error) } : {}),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
case 'listdir': {
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
|
|
@ -2369,19 +2611,33 @@ async function createWindow() {
|
|||
apiGrantPageUrl = siteAssets.pageUrl;
|
||||
debugLog('[Wo] Granted verified comraw system-shell capabilities');
|
||||
} else if (siteAssets.trusted && siteAssets.capabilities.length > 0) {
|
||||
const permissionResult = await dialog.showMessageBox({
|
||||
type: 'question',
|
||||
title: 'Wo API permission request',
|
||||
message: `${siteAssets.appId} requests access to Wo APIs`,
|
||||
detail: `Page: ${pageUrl}\nVerified index.html SHA-256: ${siteAssets.indexSha256}\n\nRequested permissions: ${siteAssets.capabilities.join(', ')}\n\nThe site APIs are exposed only for this window while it is open.`,
|
||||
buttons: ['Allow requested APIs', 'Open without Wo APIs'],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
noLink: true,
|
||||
});
|
||||
if (permissionResult.response === 0) {
|
||||
apiGrants = new Set(siteAssets.capabilities);
|
||||
const cachedGrant = await loadApiConsent(siteAssets);
|
||||
if (cachedGrant) {
|
||||
apiGrants = new Set(cachedGrant);
|
||||
apiGrantPageUrl = siteAssets.pageUrl;
|
||||
debugLog(`[Wo] Restored remembered API consent for ${siteAssets.appId} (${apiConsentKey(siteAssets).slice(0, 12)})`);
|
||||
} else {
|
||||
const scriptHashes = siteAssets.scriptBodies.map((script) => script.hash.slice(0, 12)).join(', ') || '(no external scripts)';
|
||||
const permissionResult = await dialog.showMessageBox({
|
||||
type: 'question',
|
||||
title: 'Wo API permission request',
|
||||
message: `${siteAssets.appId} requests access to Wo APIs`,
|
||||
detail: `Origin: ${new URL(pageUrl).origin}\nVerified HTML SHA-256: ${siteAssets.indexSha256}\nVerified JavaScript SHA-256: ${scriptHashes}\n\nRequested permissions: ${siteAssets.capabilities.join(', ')}\n\nAllowing remembers this decision for this origin and these exact HTML/JavaScript assets. Wo will ask again if any of them change.`,
|
||||
buttons: ['Allow and remember', 'Open without Wo APIs'],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
noLink: true,
|
||||
});
|
||||
if (permissionResult.response === 0) {
|
||||
apiGrants = new Set(siteAssets.capabilities);
|
||||
apiGrantPageUrl = siteAssets.pageUrl;
|
||||
try {
|
||||
await saveApiConsent(siteAssets, [...apiGrants]);
|
||||
debugLog(`[Wo] Remembered API consent for ${siteAssets.appId} (${apiConsentKey(siteAssets).slice(0, 12)})`);
|
||||
} catch (error) {
|
||||
debugLog('[Wo] Could not persist API consent:', String(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const requested = siteAssets.trusted ? siteAssets.capabilities : [];
|
||||
|
|
@ -2624,11 +2880,19 @@ async function createWindow() {
|
|||
debugLog('[Wo] Loading content URL: ' + pageUrl);
|
||||
try {
|
||||
const loadPromise = mainWindow!.loadURL(pageUrl);
|
||||
await Promise.race([
|
||||
loadPromise,
|
||||
new Promise(r => setTimeout(() => { debugLog('[Wo] Load timeout, continuing'); r(undefined); }, 3000))
|
||||
]);
|
||||
debugLog('[Wo] Content loaded from: ' + pageUrl);
|
||||
let loadTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
const navigationOutcome = await Promise.race([
|
||||
loadPromise.then(() => 'loaded' as const),
|
||||
new Promise<'pending'>((resolve) => {
|
||||
loadTimeout = setTimeout(() => {
|
||||
debugLog('[Wo] Page navigation is still pending after 3s; continuing compositor startup');
|
||||
resolve('pending');
|
||||
}, 3000);
|
||||
}),
|
||||
]).finally(() => {
|
||||
if (loadTimeout) clearTimeout(loadTimeout);
|
||||
});
|
||||
if (navigationOutcome === 'loaded') debugLog('[Wo] Content loaded from: ' + pageUrl);
|
||||
} catch (e) {
|
||||
debugLog('[Wo] Load failed:', String(e));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import type {
|
|||
WoPermissionState,
|
||||
WoPerformanceStatus,
|
||||
WoMonitor,
|
||||
WoSettings,
|
||||
} from 'wo-types';
|
||||
import { createRendererDamageHelper } from './renderer-damage-helper.js';
|
||||
|
||||
|
|
@ -89,6 +90,16 @@ const compositorAPI: CompositorAPI = {
|
|||
return await ipcRenderer.invoke('wo:syscall', type, params);
|
||||
},
|
||||
|
||||
settings: {
|
||||
get: <T = unknown>(key: string) => ipcRenderer.invoke('wo:settings', 'get', key) as Promise<T | undefined>,
|
||||
set: (key: string, value: unknown) => ipcRenderer.invoke('wo:settings', 'set', key, value) as Promise<{ ok: boolean; error?: string }>,
|
||||
remove: (key: string) => ipcRenderer.invoke('wo:settings', 'remove', key) as Promise<{ ok: boolean; error?: string }>,
|
||||
keys: () => ipcRenderer.invoke('wo:settings', 'keys') as Promise<string[]>,
|
||||
getSecret: (key: string) => ipcRenderer.invoke('wo:settings', 'getSecret', key) as Promise<string | undefined>,
|
||||
setSecret: (key: string, value: string) => ipcRenderer.invoke('wo:settings', 'setSecret', key, value) as Promise<{ ok: boolean; error?: string }>,
|
||||
removeSecret: (key: string) => ipcRenderer.invoke('wo:settings', 'removeSecret', key) as Promise<{ ok: boolean; error?: string }>,
|
||||
} satisfies WoSettings,
|
||||
|
||||
dbus: {
|
||||
call: (options: DBusCallOptions) => ipcRenderer.invoke('wo:syscall', 'dbus_call', options),
|
||||
getProperty: (options: DBusPropertyOptions) => ipcRenderer.invoke('wo:syscall', 'dbus_get_property', options),
|
||||
|
|
@ -279,6 +290,7 @@ const woClientAPI: WoClientAPI = {
|
|||
icons: compositorAPI.icons,
|
||||
xdg: compositorAPI.xdg,
|
||||
audio: compositorAPI.audio,
|
||||
settings: compositorAPI.settings,
|
||||
};
|
||||
|
||||
function readCapabilityArgument(name: string): WoCapability[] {
|
||||
|
|
@ -326,6 +338,7 @@ if (!grantedCapabilities.has('dbus')) hide('dbus');
|
|||
if (!grantedCapabilities.has('xdg')) hide('icons', 'xdg', 'onEnvUpdate');
|
||||
if (!grantedCapabilities.has('portals')) hide('portals', 'onPortalRequest', 'onScreencopyEvent');
|
||||
if (!grantedCapabilities.has('audio-capture')) hide('audio');
|
||||
if (!grantedCapabilities.has('settings')) hide('settings');
|
||||
if (!grantedCapabilities.has('notifications')) hide('onNotification');
|
||||
if (grantedCapabilities.size > 0) {
|
||||
contextBridge.exposeInMainWorld('compositor', compositorExposure);
|
||||
|
|
@ -341,6 +354,7 @@ if (grantedCapabilities.size > 0) {
|
|||
if (!grantedCapabilities.has('xdg')) delete clientExposure.icons;
|
||||
if (!grantedCapabilities.has('xdg')) delete clientExposure.xdg;
|
||||
if (!grantedCapabilities.has('audio-capture')) delete clientExposure.audio;
|
||||
if (!grantedCapabilities.has('settings')) delete clientExposure.settings;
|
||||
contextBridge.exposeInMainWorld('woClient', clientExposure);
|
||||
|
||||
const woExposure: Record<string, unknown> = {
|
||||
|
|
@ -410,6 +424,7 @@ if (grantedCapabilities.size > 0) {
|
|||
if (grantedCapabilities.has('dbus')) woExposure.dbus = compositorAPI.dbus;
|
||||
if (grantedCapabilities.has('portals')) woExposure.portals = compositorAPI.portals;
|
||||
if (grantedCapabilities.has('audio-capture')) woExposure.audio = compositorAPI.audio;
|
||||
if (grantedCapabilities.has('settings')) woExposure.settings = compositorAPI.settings;
|
||||
if (grantedCapabilities.has('notifications')) woExposure.onNotification = compositorAPI.onNotification;
|
||||
woExposure.syscall = compositorAPI.syscall;
|
||||
contextBridge.exposeInMainWorld('wo', woExposure);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|||
|
||||
export const WO_CAPABILITIES = [
|
||||
'window-management', 'overlay-rendering', 'desktop-applications', 'xdg', 'dbus', 'portals',
|
||||
'filesystem', 'process-launch', 'system-control', 'notifications', 'audio-capture',
|
||||
'filesystem', 'process-launch', 'system-control', 'notifications', 'audio-capture', 'settings',
|
||||
] as const;
|
||||
|
||||
export type WoCapability = typeof WO_CAPABILITIES[number];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020"],
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"moduleResolution": "node",
|
||||
|
||||
"baseUrl": ".",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export type {
|
|||
WoAudio,
|
||||
WoAudioSource,
|
||||
WoAudioChunk,
|
||||
WoSettings,
|
||||
XdgDirectories,
|
||||
DBusBus,
|
||||
DBusCallOptions,
|
||||
|
|
|
|||
16
electron/types/types.d.ts
vendored
16
electron/types/types.d.ts
vendored
|
|
@ -124,7 +124,7 @@ export interface PortalRequestEvent {
|
|||
|
||||
export type WoCapability =
|
||||
| 'window-management' | 'overlay-rendering' | 'desktop-applications' | 'xdg' | 'dbus'
|
||||
| 'portals' | 'filesystem' | 'process-launch' | 'system-control' | 'notifications' | 'audio-capture';
|
||||
| 'portals' | 'filesystem' | 'process-launch' | 'system-control' | 'notifications' | 'audio-capture' | 'settings';
|
||||
|
||||
export interface WoPermissionState {
|
||||
requested: readonly WoCapability[];
|
||||
|
|
@ -161,10 +161,22 @@ export interface WoGlobalAPI {
|
|||
dbus?: WoDBus;
|
||||
portals?: WoPortals;
|
||||
audio?: WoAudio;
|
||||
/** Persistent JSON settings isolated to the requesting web origin. */
|
||||
settings?: WoSettings;
|
||||
syscall?: Compositor['syscall'];
|
||||
onNotification?: Compositor['onNotification'];
|
||||
}
|
||||
|
||||
export interface WoSettings {
|
||||
get<T = unknown>(key: string): Promise<T | undefined>;
|
||||
set(key: string, value: unknown): Promise<{ ok: boolean; error?: string }>;
|
||||
remove(key: string): Promise<{ ok: boolean; error?: string }>;
|
||||
keys(): Promise<string[]>;
|
||||
getSecret(key: string): Promise<string | undefined>;
|
||||
setSecret(key: string, value: string): Promise<{ ok: boolean; error?: string }>;
|
||||
removeSecret(key: string): Promise<{ ok: boolean; error?: string }>;
|
||||
}
|
||||
|
||||
export interface ScreencopyEvent {
|
||||
active: boolean;
|
||||
clientCount: number;
|
||||
|
|
@ -339,6 +351,7 @@ export interface WoClient {
|
|||
icons: WoIcons;
|
||||
xdg: WoXdg;
|
||||
audio: WoAudio;
|
||||
settings: WoSettings;
|
||||
}
|
||||
|
||||
export interface WoPortals {
|
||||
|
|
@ -419,6 +432,7 @@ export interface Compositor {
|
|||
icons: WoIcons;
|
||||
xdg: WoXdg;
|
||||
audio: WoAudio;
|
||||
settings: WoSettings;
|
||||
|
||||
/**
|
||||
* Subscribe to focus changes
|
||||
|
|
|
|||
158
whatever.txt
158
whatever.txt
|
|
@ -1,34 +1,34 @@
|
|||
[2m2026-10-26T03:36:18.813061Z[0m [32m INFO[0m [2mwo[0m[2m:[0m wo compositor starting
|
||||
[2m2026-10-26T03:36:18.813121Z[0m [32m INFO[0m [2mwo::config[0m[2m:[0m Loading config from "/home/hellings/.config/wo/config.toml"
|
||||
[2m2026-10-26T03:36:18.813191Z[0m [32m INFO[0m [2mwo[0m[2m:[0m Attempting nested mode (gles renderer)
|
||||
[2m2026-10-26T03:36:18.813218Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Starting nested compositor mode
|
||||
[2m2026-10-26T03:36:18.813231Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Press ESC to exit
|
||||
[2m2026-10-26T03:36:18.813449Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Wayland socket created [3msocket[0m[2m=[0mwo-0
|
||||
[2m2026-10-26T03:36:18.813504Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Using host Wayland display for nested startup [3msocket[0m[2m=[0mwayland-0
|
||||
[2m2026-10-26T03:36:18.856187Z[0m [31mERROR[0m [2msmithay::backend::egl::ffi[0m[2m:[0m [EGL] 0x3004 (BAD_ATTRIBUTE) eglCreateSyncKHR: EGL_BAD_ATTRIBUTE error: In eglCreateSyncKHR: EGL_SYNC_NATIVE_FENCE_FD_ANDROID specified valid fd butEGL_SYNC_STATUS is also being set
|
||||
[2m2026-10-26T04:03:16.173817Z[0m [32m INFO[0m [2mwo[0m[2m:[0m wo compositor starting
|
||||
[2m2026-10-26T04:03:16.173881Z[0m [32m INFO[0m [2mwo::config[0m[2m:[0m Loading config from "/home/hellings/.config/wo/config.toml"
|
||||
[2m2026-10-26T04:03:16.173950Z[0m [32m INFO[0m [2mwo[0m[2m:[0m Attempting nested mode (gles renderer)
|
||||
[2m2026-10-26T04:03:16.173979Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Starting nested compositor mode
|
||||
[2m2026-10-26T04:03:16.173994Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Press ESC to exit
|
||||
[2m2026-10-26T04:03:16.174203Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Wayland socket created [3msocket[0m[2m=[0mwo-0
|
||||
[2m2026-10-26T04:03:16.174268Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Using host Wayland display for nested startup [3msocket[0m[2m=[0mwayland-0
|
||||
[2m2026-10-26T04:03:16.223721Z[0m [31mERROR[0m [2msmithay::backend::egl::ffi[0m[2m:[0m [EGL] 0x3004 (BAD_ATTRIBUTE) eglCreateSyncKHR: EGL_BAD_ATTRIBUTE error: In eglCreateSyncKHR: EGL_SYNC_NATIVE_FENCE_FD_ANDROID specified valid fd butEGL_SYNC_STATUS is also being set
|
||||
|
||||
[2m2026-10-26T03:36:18.911103Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Nested window size [3mwidth[0m[2m=[0m1280 [3mheight[0m[2m=[0m800
|
||||
[2m2026-10-26T03:36:18.911169Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Using render node: /dev/dri/renderD128
|
||||
[2m2026-10-26T03:36:18.911238Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Collected 672 dmabuf formats from renderer
|
||||
[2m2026-10-26T03:36:18.911502Z[0m [32m INFO[0m [2mwo[0m[2m:[0m Started configured service before root windows [3mname[0m[2m=[0mvite [3mpid[0m[2m=[0m1378486
|
||||
[2m2026-10-26T03:36:18.913053Z[0m [32m INFO[0m [2mwo::handlers::screencopy[0m[2m:[0m Registered zwlr_screencopy_manager_v1 global (v3)
|
||||
[2m2026-10-26T03:36:18.913289Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'Adwaita' (size 24)
|
||||
[2m2026-10-26T03:36:18.913344Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'default' (size 24)
|
||||
[2m2026-10-26T03:36:18.913378Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'DMZ-White' (size 24)
|
||||
[2m2026-10-26T03:36:18.913400Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'breeze' (size 24)
|
||||
[2m2026-10-26T03:36:18.913478Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m IPC socket listening [3msocket[0m[2m=[0m"/run/user/1000/wo-ipc.sock"
|
||||
[2m2026-10-26T03:36:18.916655Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m XWayland server starting
|
||||
[2m2026-10-26T03:36:18.916688Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Set WAYLAND_DISPLAY=wo-0
|
||||
[2m2026-10-26T03:36:18.919374Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["WAYLAND_DISPLAY"] to DBus activation environment
|
||||
[2m2026-10-26T03:36:18.922988Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["WAYLAND_DISPLAY"] to systemd user environment
|
||||
[2m2026-10-26T03:36:18.923014Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Nested compositor running
|
||||
[2m2026-10-26T03:36:18.923039Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Window resized to 1280x800
|
||||
[2m2026-10-26T03:36:18.923058Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Window resized to 1280x800
|
||||
[2m2026-10-26T03:36:18.962626Z[0m [32m INFO[0m [2mwo::handlers::compositor[0m[2m:[0m Client missing ClientData (likely XWayland), using fallback compositor state
|
||||
[2m2026-10-26T03:36:19.023297Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Spawned portal service: "/home/hellings/.local/share/wo/bin/wo-portal"
|
||||
[2m2026-10-26T03:36:19.025666Z[0m [32m INFO[0m [2mwo_portal[0m[2m:[0m xdg-desktop-portal-wo starting
|
||||
[2m2026-10-26T03:36:19.026881Z[0m [32m INFO[0m [2mwo_portal[0m[2m:[0m Portal service registered as org.freedesktop.impl.portal.desktop.wo
|
||||
[2m2026-10-26T03:36:19.026938Z[0m [32m INFO[0m [2mwo_portal[0m[2m:[0m Listening for screen capture requests...
|
||||
[2m2026-10-26T04:03:16.283939Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Nested window size [3mwidth[0m[2m=[0m1280 [3mheight[0m[2m=[0m800
|
||||
[2m2026-10-26T04:03:16.283997Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Using render node: /dev/dri/renderD128
|
||||
[2m2026-10-26T04:03:16.284065Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Collected 672 dmabuf formats from renderer
|
||||
[2m2026-10-26T04:03:16.284257Z[0m [32m INFO[0m [2mwo[0m[2m:[0m Started configured service before root windows [3mname[0m[2m=[0mvite [3mpid[0m[2m=[0m1405610
|
||||
[2m2026-10-26T04:03:16.285775Z[0m [32m INFO[0m [2mwo::handlers::screencopy[0m[2m:[0m Registered zwlr_screencopy_manager_v1 global (v3)
|
||||
[2m2026-10-26T04:03:16.286007Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'Adwaita' (size 24)
|
||||
[2m2026-10-26T04:03:16.286058Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'default' (size 24)
|
||||
[2m2026-10-26T04:03:16.286091Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'DMZ-White' (size 24)
|
||||
[2m2026-10-26T04:03:16.286114Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loading cursor theme 'breeze' (size 24)
|
||||
[2m2026-10-26T04:03:16.286187Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m IPC socket listening [3msocket[0m[2m=[0m"/run/user/1000/wo-ipc.sock"
|
||||
[2m2026-10-26T04:03:16.288689Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m XWayland server starting
|
||||
[2m2026-10-26T04:03:16.288724Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Set WAYLAND_DISPLAY=wo-0
|
||||
[2m2026-10-26T04:03:16.291220Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["WAYLAND_DISPLAY"] to DBus activation environment
|
||||
[2m2026-10-26T04:03:16.294950Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["WAYLAND_DISPLAY"] to systemd user environment
|
||||
[2m2026-10-26T04:03:16.294980Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Nested compositor running
|
||||
[2m2026-10-26T04:03:16.295013Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Window resized to 1280x800
|
||||
[2m2026-10-26T04:03:16.295036Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Window resized to 1280x800
|
||||
[2m2026-10-26T04:03:16.328406Z[0m [32m INFO[0m [2mwo::handlers::compositor[0m[2m:[0m Client missing ClientData (likely XWayland), using fallback compositor state
|
||||
[2m2026-10-26T04:03:16.395419Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m Spawned portal service: "/home/hellings/.local/share/wo/bin/wo-portal"
|
||||
[2m2026-10-26T04:03:16.397631Z[0m [32m INFO[0m [2mwo_portal[0m[2m:[0m xdg-desktop-portal-wo starting
|
||||
[2m2026-10-26T04:03:16.398478Z[0m [32m INFO[0m [2mwo_portal[0m[2m:[0m Portal service registered as org.freedesktop.impl.portal.desktop.wo
|
||||
[2m2026-10-26T04:03:16.398508Z[0m [32m INFO[0m [2mwo_portal[0m[2m:[0m Listening for screen capture requests...
|
||||
(II) GLX: Initialized glamor GL provider for screen 0
|
||||
The XKEYBOARD keymap compiler (xkbcomp) reports:
|
||||
> Warning: Could not resolve keysym XF86CameraAccessEnable
|
||||
|
|
@ -49,106 +49,80 @@ The XKEYBOARD keymap compiler (xkbcomp) reports:
|
|||
> Warning: Could not resolve keysym XF86SidevuSonar
|
||||
> Warning: Could not resolve keysym XF86NavInfo
|
||||
Errors from xkbcomp are not fatal to the X server
|
||||
[2m2026-10-26T03:36:19.106508Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m XWayland ready on DISPLAY :1
|
||||
[2m2026-10-26T03:36:19.108904Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["DISPLAY", "WAYLAND_DISPLAY"] to DBus activation environment
|
||||
[2m2026-10-26T03:36:19.112684Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["DISPLAY", "WAYLAND_DISPLAY"] to systemd user environment
|
||||
[2m2026-10-26T04:03:16.476543Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m XWayland ready on DISPLAY :1
|
||||
[2m2026-10-26T04:03:16.478811Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["DISPLAY", "WAYLAND_DISPLAY"] to DBus activation environment
|
||||
[2m2026-10-26T04:03:16.482425Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m Propagated ["DISPLAY", "WAYLAND_DISPLAY"] to systemd user environment
|
||||
The XKEYBOARD keymap compiler (xkbcomp) reports:
|
||||
> Warning: Unsupported maximum keycode 708, clipping.
|
||||
> X11 cannot support keycodes above 255.
|
||||
> Warning: Virtual modifier ScrollLock multiply defined
|
||||
> Using 0, ignoring 0
|
||||
Errors from xkbcomp are not fatal to the X server
|
||||
[2m2026-10-26T03:36:19.116806Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m X11 window manager attached
|
||||
[2m2026-10-26T03:36:19.116829Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m Spawning Electron for window 'main' with config: WindowConfig { name: "main", system_shell: true, url: Some("http://127.0.0.1:5173"), html: None, css: None, js: None, width: 1280, height: 800, x: 0, y: 0, z_order: -1073741824, fps: 0, format: "ARGB8888", focusable: false, interactive: false, floating: false }
|
||||
[2m2026-10-26T03:36:19.116868Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m Serialized config: {"name":"main","url":"http://127.0.0.1:5173","html":null,"css":null,"js":null,"width":1280,"height":800,"x":0,"y":0,"z_order":-1073741824,"fps":0,"format":"ARGB8888","focusable":false,"interactive":false,"floating":false}
|
||||
[2m2026-10-26T03:36:19.117076Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m spawned Electron process [3mwindow[0m[2m=[0mmain [3mpid[0m[2m=[0m1378527 [3mrender_node[0m[2m=[0m"/dev/dri/renderD128" [3mozone[0m[2m=[0mwayland
|
||||
[2m2026-10-26T04:03:16.486053Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m X11 window manager attached
|
||||
[2m2026-10-26T04:03:16.486078Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m Spawning Electron for window 'main' with config: WindowConfig { name: "main", system_shell: true, url: Some("http://127.0.0.1:5173"), html: None, css: None, js: None, width: 1280, height: 800, x: 0, y: 0, z_order: -1073741824, fps: 0, format: "ARGB8888", focusable: false, interactive: false, floating: false }
|
||||
[2m2026-10-26T04:03:16.486111Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m Serialized config: {"name":"main","url":"http://127.0.0.1:5173","html":null,"css":null,"js":null,"width":1280,"height":800,"x":0,"y":0,"z_order":-1073741824,"fps":0,"format":"ARGB8888","focusable":false,"interactive":false,"floating":false}
|
||||
[2m2026-10-26T04:03:16.486335Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m spawned Electron process [3mwindow[0m[2m=[0mmain [3mpid[0m[2m=[0m1405653 [3mrender_node[0m[2m=[0m"/dev/dri/renderD128" [3mozone[0m[2m=[0mwayland
|
||||
[Wo] Electron starting, NODE_VERSION= v24.22.0
|
||||
[Wo] GPU backend preference: auto; Ozone platform: wayland
|
||||
[Wo] Graphics process context: {"uid":1000,"groups":[4,24,27,30,44,46,100,117,125,995,1000],"env":{"WAYLAND_DISPLAY":"wo-0","DISPLAY":":1","XDG_SESSION_TYPE":"tty","XDG_RUNTIME_DIR":"/run/user/1000","EGL_PLATFORM":"wayland"},"renderNodeAccess":{"path":"/dev/dri/renderD128","mode":"660","readable":true,"writable":true},"switches":[{"name":"ozone-platform","present":true,"value":"wayland"},{"name":"use-gl","present":false,"value":""},{"name":"use-angle","present":false,"value":""},{"name":"disable-gpu","present":false,"value":""},{"name":"disable-vulkan","present":true,"value":""},{"name":"disable-features","present":true,"value":"Vulkan"}]}
|
||||
[Wo] Graphics process context: {"uid":1000,"groups":[4,24,27,30,44,46,100,117,125,995,1000],"env":{"WAYLAND_DISPLAY":"wo-0","DISPLAY":":1","XDG_SESSION_TYPE":"tty","XDG_RUNTIME_DIR":"/run/user/1000","EGL_PLATFORM":"wayland"},"renderNodeAccess":{"path":"/dev/dri/renderD128","mode":"660","readable":true,"writable":true},"switches":[{"name":"ozone-platform","present":true,"value":"wayland"},{"name":"use-gl","present":false,"value":""},{"name":"use-angle","present":false,"value":""},{"name":"disable-gpu","present":false,"value":""},{"name":"disable-vulkan","present":true,"value":""},{"name":"disable-features","present":true,"value":"Vulkan,DefaultANGLEVulkan,VulkanFromANGLE"}]}
|
||||
[Wo] WO_WINDOW_CONFIG env = {"name":"main","url":"http://127.0.0.1:5173","html":null,"css":null,"js":null,"width":1280,"height":800,"x":0,"y":0,"z_order":-1073741824,"fps":0,"format":"ARGB8888","focusable":false,"interactive":false,"floating":false}
|
||||
[Wo] WINDOW_CONFIG parsed = {"name":"main","url":"http://127.0.0.1:5173","html":null,"css":null,"js":null,"width":1280,"height":800,"x":0,"y":0,"z_order":-1073741824,"fps":0,"format":"ARGB8888","focusable":false,"interactive":false,"floating":false}
|
||||
[Wo] App ready, CLIENT_MODE= false WO_STANDALONE= undefined
|
||||
[Wo] GPU feature status: {"2d_canvas":"disabled_software","direct_rendering_display_compositor":"disabled_off_ok","gpu_compositing":"disabled_software","multiple_raster_threads":"enabled_force_on","opengl":"disabled_off","rasterization":"disabled_software","raw_draw":"enabled_on","skia_graphite":"disabled_off","trees_in_viz":"disabled_off","video_decode":"disabled_software","video_encode":"disabled_software","vulkan":"disabled_off","webgl":"disabled_off","webgpu":"disabled_off","webgpu_on_vk_via_gl_interop":"disabled_off","webnn":"disabled_off"}
|
||||
[Wo] GPU feature status: {"2d_canvas":"disabled_software","direct_rendering_display_compositor":"disabled_off_ok","gpu_compositing":"disabled_software","multiple_raster_threads":"enabled_force_on","opengl":"disabled_off","rasterization":"disabled_software","raw_draw":"disabled_off_ok","skia_graphite":"disabled_off","trees_in_viz":"disabled_off","video_decode":"disabled_software","video_encode":"disabled_software","vulkan":"disabled_off","webgl":"disabled_off","webgpu":"disabled_off","webgpu_on_vk_via_gl_interop":"disabled_off","webnn":"disabled_off"}
|
||||
[Wo] GPU info: {"auxAttributes":{"amdSwitchable":false,"canSupportThreadedTextureMailbox":false,"glImplementationParts":"(gl=none,angle=none)","glResetNotificationStrategy":0,"hardwareSupportsVulkan":false,"inProcessGpu":true,"initializationTime":0,"isAsan":false,"isClangCoverage":false,"jpegDecodeAcceleratorSupported":false,"optimus":false,"passthroughCmdDecoder":false,"sandboxed":false,"skiaBackendType":"None","subpixelFontRendering":true,"targetCpuBits":64,"visibilityCallbackCallCount":0},"gpuDevice":[{"active":true,"deviceId":9352,"gpuPreference":0,"vendorId":4318}]}
|
||||
[Wo] Offscreen shared texture: disabled (GPU compositor=disabled_software, threaded texture mailbox=false, WebGL=disabled_off)
|
||||
[Wo] Will connect to compositor
|
||||
[Wo] Connecting to compositor IPC: /run/user/1000/wo-ipc.sock
|
||||
[Wo] Portal UI bridge listening at /run/user/1000/wo-portal-ui.sock
|
||||
[2m2026-10-26T03:36:19.251134Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m Electron window connected [3mwindow[0m[2m=[0mmain
|
||||
[2m2026-10-26T04:03:16.636555Z[0m [32m INFO[0m [2mwo::electron[0m[2m:[0m Electron window connected [3mwindow[0m[2m=[0mmain
|
||||
[Wo] IPC connected, sent HELLO message
|
||||
[Wo] Initializing GBM on render node: /dev/dri/renderD128
|
||||
[Wo] DMABUF sender initialized
|
||||
[Wo] Creating window
|
||||
[Wo] createWindow starting
|
||||
127.0.0.1 - - [25/Oct/2026 23:36:19] "GET / HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [25/Oct/2026 23:36:19] "GET /_wo.capabilities.json HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [25/Oct/2026 23:36:19] "GET /_wo.components.css HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [25/Oct/2026 23:36:19] "GET /assets/index-HBQlw-Wm.js HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [25/Oct/2026 23:36:19] "GET /assets/index-D7GQWtqC.css HTTP/1.1" 200 -
|
||||
[Wo] Verified site rip.crit.comraw; requested capabilities: window-management, overlay-rendering, desktop-applications, xdg, dbus, portals, process-launch, system-control, notifications
|
||||
127.0.0.1 - - [26/Oct/2026 00:03:16] "GET / HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [26/Oct/2026 00:03:16] "GET /_wo.capabilities.json HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [26/Oct/2026 00:03:16] "GET /_wo.components.css HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [26/Oct/2026 00:03:16] "GET /assets/index-CW7FlnjF.js HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [26/Oct/2026 00:03:16] "GET /assets/index-C8Au-Yf1.css HTTP/1.1" 200 -
|
||||
[Wo] Verified site rip.crit.comraw; requested capabilities: window-management, overlay-rendering, desktop-applications, xdg, dbus, portals, process-launch, system-control, notifications, settings
|
||||
[Wo] Granted verified comraw system-shell capabilities
|
||||
[Wo] preloadPath=/home/hellings/.local/share/wo/electron/dist/preload.js
|
||||
[2m2026-10-26T03:36:19.290019Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m new_fractional_scale: setting preferred_scale(1.0)
|
||||
[2m2026-10-26T03:36:19.290063Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m new_fractional_scale: done
|
||||
[2m2026-10-26T03:36:19.290115Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m computed and registered document and stylesheet hashes [3mwindow[0m[2m=[0mmain [3mpage[0m[2m=[0mhttp://127.0.0.1:5173/
|
||||
[2m2026-10-26T04:03:16.676016Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m new_fractional_scale: setting preferred_scale(1.0)
|
||||
[2m2026-10-26T04:03:16.676055Z[0m [32m INFO[0m [2mwo::state[0m[2m:[0m new_fractional_scale: done
|
||||
[2m2026-10-26T04:03:16.676116Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m computed and registered document and stylesheet hashes [3mwindow[0m[2m=[0mmain [3mpage[0m[2m=[0mhttp://127.0.0.1:5173/
|
||||
[Wo] BrowserWindow created, CLIENT_MODE=false offscreen=true painting=true
|
||||
[Wo] setupIpcHandlers completed
|
||||
[Wo] Loading content URL: http://127.0.0.1:5173
|
||||
127.0.0.1 - - [25/Oct/2026 23:36:19] "GET / HTTP/1.1" 304 -
|
||||
127.0.0.1 - - [26/Oct/2026 00:03:16] "GET / HTTP/1.1" 304 -
|
||||
[Wo] Locking verified script http://127.0.0.1:5173/_wo.components.css -> 8337fca251ea
|
||||
[Wo] Locking verified script http://127.0.0.1:5173/assets/index-HBQlw-Wm.js -> d1647de85605
|
||||
[Wo] Locking verified script http://127.0.0.1:5173/assets/index-D7GQWtqC.css -> c0e2f97ed4d1
|
||||
[1378559:1025/233619.349346:ERROR:ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc:249] '--ozone-platform=wayland' is not compatible with Vulkan. Consider switching to '--ozone-platform=x11' or disabling Vulkan
|
||||
[Wo] Locking verified script http://127.0.0.1:5173/assets/index-CW7FlnjF.js -> ca1bcebf0e74
|
||||
[Wo] Locking verified script http://127.0.0.1:5173/assets/index-C8Au-Yf1.css -> 6f4280063490
|
||||
[Wo] Resource request failed: net::ERR_ABORTED stylesheet http://127.0.0.1:5173/_wo.components.css
|
||||
[Wo] Resource request failed: net::ERR_ABORTED stylesheet http://127.0.0.1:5173/assets/index-D7GQWtqC.css
|
||||
[Wo] Resource request failed: net::ERR_ABORTED stylesheet http://127.0.0.1:5173/assets/index-C8Au-Yf1.css
|
||||
[Wo] Serving verified script 8337fca251ea (2190 bytes)
|
||||
[Wo] Resource request failed: net::ERR_ABORTED script http://127.0.0.1:5173/assets/index-HBQlw-Wm.js
|
||||
[Wo] Serving verified script c0e2f97ed4d1 (36664 bytes)
|
||||
[Wo] Serving verified script d1647de85605 (268639 bytes)
|
||||
[Wo] Resource request failed: net::ERR_ABORTED script http://127.0.0.1:5173/assets/index-CW7FlnjF.js
|
||||
[Wo] Serving verified script 6f4280063490 (37886 bytes)
|
||||
[Wo] Serving verified script ca1bcebf0e74 (274364 bytes)
|
||||
[1405690:1026/000316.820865:ERROR:ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc:249] '--ozone-platform=wayland' is not compatible with Vulkan. Consider switching to '--ozone-platform=x11' or disabling Vulkan
|
||||
[Wo] Content loaded from: http://127.0.0.1:5173
|
||||
127.0.0.1 - - [25/Oct/2026 23:36:19] "GET /elementary.jpg HTTP/1.1" 304 -
|
||||
[Wo] Page render state: {"readyState":"complete","title":"comraw","bodyText":"1\n2\n3\n4\nSun, Oct 25\n11:36 PM","bodyBackground":"rgba(0, 0, 0, 0)","bodyBackgroundImage":"none","bodyDisplay":"block","portalCardBackground":"","portalCardBounds":null,"buttons":12,"resourceConsentBridge":false,"rootChildren":1,"shellPresent":true,"wallpaperImage":"url(\"http://127.0.0.1:5173/elementary.jpg\")","compositorApi":true,"woApi":true}
|
||||
127.0.0.1 - - [26/Oct/2026 00:03:16] "GET /elementary.jpg HTTP/1.1" 304 -
|
||||
[Wo] Page render state: {"readyState":"complete","title":"comraw","bodyText":"1\n2\n3\n4\nMon, Oct 26\n12:03 AM","bodyBackground":"rgba(0, 0, 0, 0)","bodyBackgroundImage":"none","bodyDisplay":"block","portalCardBackground":"","portalCardBounds":null,"buttons":13,"resourceConsentBridge":false,"rootChildren":1,"shellPresent":true,"wallpaperImage":"url(\"http://127.0.0.1:5173/elementary.jpg\")","compositorApi":true,"woApi":true}
|
||||
[Wo] Page initialized, setting up rendering
|
||||
[Wo] Offscreen frame pacing: 60 Hz (window cap auto)
|
||||
[Wo] createWindow completed successfully
|
||||
[Wo] Window created successfully
|
||||
[Wo] Ignoring empty offscreen paint 0x0; content=1280x800 dirty=0,0,1280,800
|
||||
[1378559:1025/233619.594500:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:438] Unable to initialize SkSurface
|
||||
[1378559:1025/233619.594551:ERROR:gpu/command_buffer/service/raster_decoder.cc:2939] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glBeginRasterCHROMIUM: failed to create surface
|
||||
[1378559:1025/233619.594579:ERROR:gpu/command_buffer/service/raster_decoder.cc:3001] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glRasterCHROMIUM: RasterCHROMIUM without BeginRasterCHROMIUM
|
||||
[1378559:1025/233619.594599:ERROR:gpu/command_buffer/service/raster_decoder.cc:3090] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glEndRasterCHROMIUM: EndRasterCHROMIUM without BeginRasterCHROMIUM
|
||||
[1378559:1025/233619.594683:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:438] Unable to initialize SkSurface
|
||||
[1378559:1025/233619.594712:ERROR:gpu/command_buffer/service/raster_decoder.cc:2939] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glBeginRasterCHROMIUM: failed to create surface
|
||||
[1378559:1025/233619.594738:ERROR:gpu/command_buffer/service/raster_decoder.cc:3001] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glRasterCHROMIUM: RasterCHROMIUM without BeginRasterCHROMIUM
|
||||
[1378559:1025/233619.594758:ERROR:gpu/command_buffer/service/raster_decoder.cc:3090] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glEndRasterCHROMIUM: EndRasterCHROMIUM without BeginRasterCHROMIUM
|
||||
[1378559:1025/233619.594801:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:438] Unable to initialize SkSurface
|
||||
[1378559:1025/233619.594816:ERROR:gpu/command_buffer/service/raster_decoder.cc:2939] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glBeginRasterCHROMIUM: failed to create surface
|
||||
[1378559:1025/233619.594828:ERROR:gpu/command_buffer/service/raster_decoder.cc:3001] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glRasterCHROMIUM: RasterCHROMIUM without BeginRasterCHROMIUM
|
||||
[1378559:1025/233619.594848:ERROR:gpu/command_buffer/service/raster_decoder.cc:3090] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glEndRasterCHROMIUM: EndRasterCHROMIUM without BeginRasterCHROMIUM
|
||||
[1378559:1025/233619.604006:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:438] Unable to initialize SkSurface
|
||||
[1378559:1025/233619.604035:ERROR:gpu/command_buffer/service/raster_decoder.cc:2939] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glBeginRasterCHROMIUM: failed to create surface
|
||||
[1378559:1025/233619.604047:ERROR:gpu/command_buffer/service/raster_decoder.cc:3001] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glRasterCHROMIUM: RasterCHROMIUM without BeginRasterCHROMIUM
|
||||
[1378559:1025/233619.604061:ERROR:gpu/command_buffer/service/raster_decoder.cc:3090] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glEndRasterCHROMIUM: EndRasterCHROMIUM without BeginRasterCHROMIUM
|
||||
[1378559:1025/233619.604410:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:593] Attempt to read from an uninitialized SharedImage. Initialized region: (0, 0, 0, 0) Size: (1280, 832)
|
||||
[1378559:1025/233619.607032:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:593] Attempt to read from an uninitialized SharedImage. Initialized region: (0, 0, 0, 0) Size: (1280, 64)
|
||||
[1378559:1025/233619.609323:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:593] Attempt to read from an uninitialized SharedImage. Initialized region: (0, 0, 0, 0) Size: (192, 256)
|
||||
[1378559:1025/233619.609467:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:593] Attempt to read from an uninitialized SharedImage. Initialized region: (0, 0, 0, 0) Size: (64, 64)
|
||||
[Wo] First bitmap pixels (BGRA top-left/mid-left/center/bottom-right): [[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255]]
|
||||
[Wo] First bitmap pixels (BGRA top-left/mid-left/center/bottom-right): [[22,17,17,255],[62,51,111,255],[77,67,127,255],[50,35,49,255]]
|
||||
[Wo] First bitmap frame sent as DMA-BUF: seq=1 size=1280x800 stride=5120
|
||||
[Wo] Second bitmap pixels (BGRA top-left/mid-left/center/bottom-right): [[255,255,255,255],[255,255,255,255],[255,255,255,255],[255,255,255,255]]
|
||||
[Wo] Second bitmap pixels (BGRA top-left/mid-left/center/bottom-right): [[22,17,17,255],[62,51,111,255],[77,67,127,255],[50,35,49,255]]
|
||||
[2m2026-10-26T04:03:17.187064Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m first Electron frame imported as a DMA-BUF texture [3mwindow[0m[2m=[0mmain [3mwidth[0m[2m=[0m1280 [3mheight[0m[2m=[0m800 [3mseq[0m[2m=[0m1
|
||||
[2m2026-10-26T04:03:17.187588Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loaded cursor 'default' (24x24, hotspot 3,1) from /usr/share/icons/Adwaita/cursors/default
|
||||
[2m2026-10-26T04:03:17.188289Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m first Electron texture rendered into the output frame [3mwindow[0m[2m=[0mmain [3mx[0m[2m=[0m0 [3my[0m[2m=[0m0 [3moutput_width[0m[2m=[0m1280 [3moutput_height[0m[2m=[0m800
|
||||
[Wo] SKIPPED paint due to frame backpressure inFlight=2
|
||||
[2m2026-10-26T03:36:19.634388Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m first Electron frame imported as a DMA-BUF texture [3mwindow[0m[2m=[0mmain [3mwidth[0m[2m=[0m1280 [3mheight[0m[2m=[0m800 [3mseq[0m[2m=[0m1
|
||||
[2m2026-10-26T03:36:19.634880Z[0m [32m INFO[0m [2mwo::cursor[0m[2m:[0m Loaded cursor 'default' (24x24, hotspot 3,1) from /usr/share/icons/Adwaita/cursors/default
|
||||
[2m2026-10-26T03:36:19.635561Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m first Electron texture rendered into the output frame [3mwindow[0m[2m=[0mmain [3mx[0m[2m=[0m0 [3my[0m[2m=[0m0 [3moutput_width[0m[2m=[0m1280 [3moutput_height[0m[2m=[0m800
|
||||
[2m2026-10-26T03:36:19.663496Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m first nested output frame submitted successfully
|
||||
[1378559:1025/233619.899568:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:438] Unable to initialize SkSurface
|
||||
[1378559:1025/233619.899611:ERROR:gpu/command_buffer/service/raster_decoder.cc:2939] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glBeginRasterCHROMIUM: failed to create surface
|
||||
[1378559:1025/233619.899629:ERROR:gpu/command_buffer/service/raster_decoder.cc:3001] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glRasterCHROMIUM: RasterCHROMIUM without BeginRasterCHROMIUM
|
||||
[1378559:1025/233619.899638:ERROR:gpu/command_buffer/service/raster_decoder.cc:3090] [.RendererRasterWorker-0x91400051b00]GL ERROR :GL_INVALID_OPERATION : glEndRasterCHROMIUM: EndRasterCHROMIUM without BeginRasterCHROMIUM
|
||||
[1378559:1025/233619.900098:ERROR:gpu/command_buffer/service/shared_image/shared_image_representation.cc:593] Attempt to read from an uninitialized SharedImage. Initialized region: (0, 0, 0, 0) Size: (1280, 832)
|
||||
[Wo] Load timeout, continuing
|
||||
[2m2026-10-26T04:03:17.218902Z[0m [32m INFO[0m [2mwo::nested[0m[2m:[0m first nested output frame submitted successfully
|
||||
Serving HTTP on 127.0.0.1 port 5173 (http://127.0.0.1:5173/) ...
|
||||
|
||||
Keyboard interrupt received, exiting.
|
||||
[Wo] IPC connection closed
|
||||
Gdk-Message: 23:36:25.499: Error reading events from display: Broken pipe
|
||||
Gdk-Message: 00:03:23.498: Error reading events from display: Broken pipe
|
||||
|
|
|
|||
Loading…
Reference in a new issue