mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
feat: various fixes for electron, and database migration and version on loading page
This commit is contained in:
parent
5ac3e4e279
commit
eebef7898a
14 changed files with 616 additions and 22 deletions
3
electron/app-version.json
Normal file
3
electron/app-version.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"version": "4.8.3"
|
||||
}
|
||||
29
electron/appVersion.js
Normal file
29
electron/appVersion.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Packaged Electron builds only ship electron/ in asar, so app.getVersion() can
|
||||
* read 0.0.0. Prefer app-version.json (synced from package.json) then dev package.json.
|
||||
*/
|
||||
function readPackagedAppVersion(fallback = "0.0.0") {
|
||||
const candidates = [
|
||||
path.join(__dirname, "app-version.json"),
|
||||
path.join(__dirname, "..", "package.json"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
||||
const version = raw && raw.version;
|
||||
if (version) {
|
||||
return String(version);
|
||||
}
|
||||
} catch {
|
||||
// try next candidate
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
module.exports = { readPackagedAppVersion };
|
||||
|
|
@ -104,9 +104,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-4 text-center text-[11px] text-slate-400 dark:text-zinc-600" id="app-version">
|
||||
v0.0.0
|
||||
</p>
|
||||
<p class="mt-4 text-center text-[11px] text-slate-400 dark:text-zinc-600" id="app-version"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -137,6 +135,15 @@
|
|||
const MAX_FAILURE_HISTORY = 8;
|
||||
|
||||
let protocolOrder = ["https", "http"];
|
||||
let detectedProtocol = "http";
|
||||
let attemptCount = 0;
|
||||
let runtimeProbeAttempt = 0;
|
||||
let cachedRuntimeState = null;
|
||||
let cachedDiagnostics = null;
|
||||
let cachedCrash = null;
|
||||
let startupFailed = false;
|
||||
let pollTimer = null;
|
||||
const recentFailures = [];
|
||||
|
||||
applyTheme(detectPreferredTheme());
|
||||
showAppVersion();
|
||||
|
|
@ -279,7 +286,9 @@
|
|||
async function showAppVersion() {
|
||||
try {
|
||||
const appVersion = await window.electron.appVersion();
|
||||
document.getElementById("app-version").innerText = "v" + appVersion;
|
||||
if (appVersion) {
|
||||
document.getElementById("app-version").innerText = "v" + appVersion;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
|
@ -312,16 +321,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
let detectedProtocol = "http";
|
||||
let attemptCount = 0;
|
||||
let runtimeProbeAttempt = 0;
|
||||
let cachedRuntimeState = null;
|
||||
let cachedDiagnostics = null;
|
||||
let cachedCrash = null;
|
||||
let startupFailed = false;
|
||||
let pollTimer = null;
|
||||
const recentFailures = [];
|
||||
|
||||
function parseStatusJson(text) {
|
||||
const helper = window.MeshchatLoadingStatusProbe;
|
||||
if (helper && typeof helper.parseStatusJson === "function") {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const fs = require("fs");
|
|||
const path = require("node:path");
|
||||
|
||||
const { createBackendProcessManager } = require("./backendProcess");
|
||||
const { readPackagedAppVersion } = require("./appVersion");
|
||||
const { getCrashRecoveryInfo } = require("./offlineRecovery");
|
||||
const {
|
||||
getUserProvidedArguments,
|
||||
|
|
@ -48,6 +49,14 @@ const {
|
|||
const { getLogsDir } = require("./backendCrashReport");
|
||||
const { installBrokenPipeGuards, createMainProcessLogger } = require("./safeConsole");
|
||||
|
||||
function resolvePreloadScriptPath() {
|
||||
const bundled = path.join(__dirname, "preload.bundle.js");
|
||||
if (fs.existsSync(bundled)) {
|
||||
return bundled;
|
||||
}
|
||||
return path.join(__dirname, "preload.js");
|
||||
}
|
||||
|
||||
installBrokenPipeGuards(process);
|
||||
|
||||
const mainProcessLogger = createMainProcessLogger({
|
||||
|
|
@ -168,7 +177,7 @@ function trustedIpcHandle(channel, listener) {
|
|||
|
||||
// allow fetching app version via ipc
|
||||
trustedIpcHandle("app-version", () => {
|
||||
return app.getVersion();
|
||||
return readPackagedAppVersion(app.getVersion());
|
||||
});
|
||||
|
||||
// allow fetching hardware acceleration status via ipc
|
||||
|
|
@ -525,7 +534,7 @@ function getChildBrowserWindowOptions() {
|
|||
return {
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
preload: resolvePreloadScriptPath(),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
|
|
@ -1048,7 +1057,7 @@ app.whenReady().then(async () => {
|
|||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
// used to inject logging over ipc
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
preload: resolvePreloadScriptPath(),
|
||||
// Security: disable node integration in renderer
|
||||
nodeIntegration: false,
|
||||
// Security: enable context isolation (default in Electron 12+)
|
||||
|
|
|
|||
378
electron/preload.bundle.js
Normal file
378
electron/preload.bundle.js
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
// SPDX-License-Identifier: 0BSD
|
||||
// Generated by scripts/bundle-electron-preload.cjs. Do not edit by hand.
|
||||
|
||||
"use strict";
|
||||
|
||||
const LOCAL_BACKEND_HOSTS = new Set(["127.0.0.1", "localhost"]);
|
||||
const LOCAL_BACKEND_PORT = "9337";
|
||||
|
||||
/**
|
||||
* Parse a URL string. Returns null when the input is not a valid absolute URL.
|
||||
* @param {unknown} url
|
||||
* @returns {URL | null}
|
||||
*/
|
||||
function parseAbsoluteUrl(url) {
|
||||
if (!url || typeof url !== "string") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner http(s) URL of a blob URL, or null.
|
||||
* blob:https://host/uuid has an origin of https://host, not the blob scheme itself.
|
||||
* @param {unknown} url
|
||||
* @returns {string | null}
|
||||
*/
|
||||
function blobInnerHttpUrl(url) {
|
||||
if (!url || typeof url !== "string" || !url.startsWith("blob:")) {
|
||||
return null;
|
||||
}
|
||||
const inner = url.slice("blob:".length);
|
||||
const parsed = parseAbsoluteUrl(inner);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return null;
|
||||
}
|
||||
return inner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the URL is the MeshChatX local backend origin (loading / API checks).
|
||||
* Parses so userinfo like http://127.0.0.1:9337@example.com is not local.
|
||||
* @param {unknown} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isLocalBackendUrl(url) {
|
||||
const parsed = parseAbsoluteUrl(url);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return false;
|
||||
}
|
||||
if (parsed.username !== "" || parsed.password !== "") {
|
||||
return false;
|
||||
}
|
||||
const host = String(parsed.hostname || "").toLowerCase();
|
||||
if (!LOCAL_BACKEND_HOSTS.has(host)) {
|
||||
return false;
|
||||
}
|
||||
return parsed.port === LOCAL_BACKEND_PORT;
|
||||
}
|
||||
|
||||
/**
|
||||
* blob: URLs whose inner origin is the local backend (print preview).
|
||||
* @param {unknown} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isTrustedBlobUrl(url) {
|
||||
const inner = blobInnerHttpUrl(url);
|
||||
return inner != null && isLocalBackendUrl(inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* file: loading.html and crash.html in the Electron shell. Not arbitrary files.
|
||||
* @param {unknown} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isTrustedShellFileUrl(url) {
|
||||
const parsed = parseAbsoluteUrl(url);
|
||||
if (!parsed || parsed.protocol !== "file:") {
|
||||
return false;
|
||||
}
|
||||
let pathname = parsed.pathname || "";
|
||||
try {
|
||||
pathname = decodeURIComponent(pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const normalized = pathname.replace(/\\/g, "/").toLowerCase();
|
||||
return normalized.endsWith("/loading.html") || normalized.endsWith("/crash.html");
|
||||
}
|
||||
|
||||
/**
|
||||
* Origins allowed to call preload window.electron IPC.
|
||||
* file: loading/crash pages, the local backend, and trusted print blobs.
|
||||
* @param {unknown} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isTrustedShellOrigin(url) {
|
||||
if (isTrustedShellFileUrl(url)) {
|
||||
return true;
|
||||
}
|
||||
if (isTrustedBlobUrl(url)) {
|
||||
return true;
|
||||
}
|
||||
return isLocalBackendUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether window.open should create a child Electron window instead of the OS browser.
|
||||
* Local backend popouts and call.html must stay in Electron so they keep the app session.
|
||||
* @param {unknown} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldOpenInElectronWindow(url) {
|
||||
if (!url || typeof url !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (url.startsWith("blob:")) {
|
||||
return isTrustedBlobUrl(url);
|
||||
}
|
||||
if (!isLocalBackendUrl(url)) {
|
||||
return false;
|
||||
}
|
||||
const parsed = parseAbsoluteUrl(url);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
const pathname = parsed.pathname || "";
|
||||
if (pathname === "/call.html" || pathname.endsWith("/call.html")) {
|
||||
return true;
|
||||
}
|
||||
return parsed.hash.startsWith("#/popout/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the main frame may navigate to this URL inside Electron (local app shell).
|
||||
* External http(s) links must open in the system browser instead.
|
||||
* data: and file: are denied. blob: is allowed only when the inner origin is local.
|
||||
* @param {unknown} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldAllowInWindowNavigation(url) {
|
||||
if (!url || typeof url !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (url.startsWith("blob:")) {
|
||||
return isTrustedBlobUrl(url);
|
||||
}
|
||||
return isLocalBackendUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL of the renderer frame that invoked an ipcMain handler.
|
||||
* Prefers senderFrame.url, then sender.getURL().
|
||||
* @param {unknown} event
|
||||
* @returns {string}
|
||||
*/
|
||||
function senderUrlFromIpcEvent(event) {
|
||||
if (!event || typeof event !== "object") {
|
||||
return "";
|
||||
}
|
||||
const frame = event.senderFrame;
|
||||
if (frame && typeof frame.url === "string" && frame.url) {
|
||||
return frame.url;
|
||||
}
|
||||
const sender = event.sender;
|
||||
if (sender && typeof sender.getURL === "function") {
|
||||
try {
|
||||
const url = sender.getURL();
|
||||
return typeof url === "string" ? url : "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether ipcMain may run for this invoke. Same allowlist as preload.
|
||||
* @param {unknown} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isTrustedIpcEvent(event) {
|
||||
return isTrustedShellOrigin(senderUrlFromIpcEvent(event));
|
||||
}
|
||||
|
||||
const { ipcRenderer, contextBridge } = require("electron");
|
||||
function originAllowed() {
|
||||
if (typeof location === "undefined") {
|
||||
return false;
|
||||
}
|
||||
return isTrustedShellOrigin(location.href);
|
||||
}
|
||||
|
||||
function invokeTrusted(channel, ...args) {
|
||||
if (!originAllowed()) {
|
||||
return Promise.reject(new Error("MeshChatX IPC blocked for this origin"));
|
||||
}
|
||||
return ipcRenderer.invoke(channel, ...args);
|
||||
}
|
||||
|
||||
function onTrusted(channel, listener) {
|
||||
ipcRenderer.on(channel, (event, ...payload) => {
|
||||
if (!originAllowed()) {
|
||||
return;
|
||||
}
|
||||
listener(event, ...payload);
|
||||
});
|
||||
}
|
||||
|
||||
onTrusted("log", (event, message) => console.log(message));
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
appVersion: async function () {
|
||||
return await invokeTrusted("app-version");
|
||||
},
|
||||
|
||||
electronVersion: function () {
|
||||
if (!originAllowed()) {
|
||||
return "";
|
||||
}
|
||||
return process.versions.electron;
|
||||
},
|
||||
|
||||
chromeVersion: function () {
|
||||
if (!originAllowed()) {
|
||||
return "";
|
||||
}
|
||||
return process.versions.chrome;
|
||||
},
|
||||
|
||||
nodeVersion: function () {
|
||||
if (!originAllowed()) {
|
||||
return "";
|
||||
}
|
||||
return process.versions.node;
|
||||
},
|
||||
|
||||
alert: async function (message) {
|
||||
return await invokeTrusted("alert", message);
|
||||
},
|
||||
|
||||
confirm: async function (message) {
|
||||
return await invokeTrusted("confirm", message);
|
||||
},
|
||||
|
||||
prompt: async function (message, defaultValue = "") {
|
||||
return await invokeTrusted("prompt", message, defaultValue);
|
||||
},
|
||||
|
||||
relaunch: async function () {
|
||||
return await invokeTrusted("relaunch");
|
||||
},
|
||||
|
||||
relaunchEmergency: async function () {
|
||||
return await invokeTrusted("relaunch-emergency");
|
||||
},
|
||||
|
||||
relaunchAutoRecover: async function () {
|
||||
return await invokeTrusted("relaunch-auto-recover");
|
||||
},
|
||||
|
||||
getCrashRecoveryInfo: async function () {
|
||||
return await invokeTrusted("crash-recovery-info");
|
||||
},
|
||||
|
||||
restoreDatabaseBackup: async function (backupPath) {
|
||||
return await invokeTrusted("restore-database-backup", backupPath);
|
||||
},
|
||||
|
||||
pickDatabaseBackup: async function () {
|
||||
return await invokeTrusted("pick-database-backup");
|
||||
},
|
||||
|
||||
shutdown: async function () {
|
||||
return await invokeTrusted("shutdown");
|
||||
},
|
||||
|
||||
getCloseSettings: async function () {
|
||||
return await invokeTrusted("get-close-settings");
|
||||
},
|
||||
|
||||
setCloseSettings: async function (partial) {
|
||||
return await invokeTrusted("set-close-settings", partial);
|
||||
},
|
||||
|
||||
getPlatform: function () {
|
||||
if (!originAllowed()) {
|
||||
return "";
|
||||
}
|
||||
return process.platform;
|
||||
},
|
||||
|
||||
getScreenSecuritySettings: async function () {
|
||||
return await invokeTrusted("get-screen-security-settings");
|
||||
},
|
||||
|
||||
setScreenSecurityEnabled: async function (enabled) {
|
||||
return await invokeTrusted("set-screen-security-enabled", enabled === true);
|
||||
},
|
||||
|
||||
getMemoryUsage: async function () {
|
||||
return await invokeTrusted("get-memory-usage");
|
||||
},
|
||||
|
||||
getBatteryStatus: async function () {
|
||||
return await invokeTrusted("get-battery-status");
|
||||
},
|
||||
|
||||
showPathInFolder: async function (path) {
|
||||
return await invokeTrusted("showPathInFolder", path);
|
||||
},
|
||||
openPath: async function (path) {
|
||||
return await invokeTrusted("open-path", path);
|
||||
},
|
||||
pickFile: async function () {
|
||||
return await invokeTrusted("pick-file");
|
||||
},
|
||||
pickDirectory: async function () {
|
||||
return await invokeTrusted("pick-directory");
|
||||
},
|
||||
isHardwareAccelerationEnabled: async function () {
|
||||
return await invokeTrusted("is-hardware-acceleration-enabled");
|
||||
},
|
||||
getIntegrityStatus: async function () {
|
||||
return await invokeTrusted("get-integrity-status");
|
||||
},
|
||||
showNotification: function (title, body, silent = false, destinationHash = null) {
|
||||
invokeTrusted("show-notification", { title, body, silent, destinationHash });
|
||||
},
|
||||
closeMessageNotifications: function (destinationHash = null) {
|
||||
return invokeTrusted("close-message-notifications", destinationHash);
|
||||
},
|
||||
setPowerSaveBlocker: async function (enabled) {
|
||||
return await invokeTrusted("set-power-save-blocker", enabled);
|
||||
},
|
||||
onProtocolLink: function (callback) {
|
||||
onTrusted("open-protocol-link", (event, url) => callback(url));
|
||||
},
|
||||
backendHttpOnly: async function () {
|
||||
return await invokeTrusted("backend-http-only");
|
||||
},
|
||||
backendRuntimeState: async function () {
|
||||
return await invokeTrusted("backend-runtime-state");
|
||||
},
|
||||
backendStartupDiagnostics: async function () {
|
||||
return await invokeTrusted("backend-startup-diagnostics");
|
||||
},
|
||||
markBackendHealthy: async function () {
|
||||
return await invokeTrusted("mark-backend-healthy");
|
||||
},
|
||||
restartBackend: async function () {
|
||||
return await invokeTrusted("restart-backend");
|
||||
},
|
||||
openBackendCrashReport: async function () {
|
||||
return await invokeTrusted("open-backend-crash-report");
|
||||
},
|
||||
onBackendProcessExited: function (callback) {
|
||||
if (typeof callback !== "function") {
|
||||
return;
|
||||
}
|
||||
onTrusted("backend-process-exited", (_event, payload) => callback(payload));
|
||||
},
|
||||
onBackendStartupFailed: function (callback) {
|
||||
if (typeof callback !== "function") {
|
||||
return;
|
||||
}
|
||||
onTrusted("backend-startup-failed", (_event, payload) => callback(payload));
|
||||
},
|
||||
});
|
||||
BIN
meshchatx.rsm
BIN
meshchatx.rsm
Binary file not shown.
|
|
@ -47,8 +47,30 @@ MIN_SIZE_RATIO = 0.2
|
|||
MIN_WIPE_MESSAGE_COUNT = 5
|
||||
MESSAGE_DROP_RATIO = 0.5
|
||||
MIN_SIZE_COMPARE_BYTES = 100_000
|
||||
_MIN_ZIP_DATE_TIME = (1980, 1, 1, 0, 0, 0)
|
||||
_EPOCH_1980 = 315532800.0
|
||||
_CONTENT_ANOMALY_PREFIX = "Database content anomaly:"
|
||||
|
||||
|
||||
def _zip_file_date_time(file_path: str) -> tuple:
|
||||
try:
|
||||
mtime = os.path.getmtime(file_path)
|
||||
except OSError:
|
||||
return _MIN_ZIP_DATE_TIME
|
||||
if mtime < _EPOCH_1980:
|
||||
return _MIN_ZIP_DATE_TIME
|
||||
return time.localtime(mtime)[:6]
|
||||
|
||||
|
||||
def _zip_write_file(zf: zipfile.ZipFile, file_path: str, arcname: str | None = None) -> None:
|
||||
name = arcname if arcname is not None else os.path.basename(file_path)
|
||||
with open(file_path, "rb") as handle:
|
||||
data = handle.read()
|
||||
zinfo = zipfile.ZipInfo(filename=name)
|
||||
zinfo.date_time = _zip_file_date_time(file_path)
|
||||
zinfo.compress_type = zipfile.ZIP_DEFLATED
|
||||
zf.writestr(zinfo, data)
|
||||
|
||||
_log = logging.getLogger("meshchatx.database")
|
||||
|
||||
|
||||
|
|
@ -696,7 +718,7 @@ class Database:
|
|||
rel_path = os.path.relpath(full_path, identity_dir).replace("\\", "/")
|
||||
if rel_path.startswith(".."):
|
||||
continue
|
||||
zf.write(full_path, arcname=rel_path)
|
||||
_zip_write_file(zf, full_path, arcname=rel_path)
|
||||
included.append(rel_path)
|
||||
return included
|
||||
|
||||
|
|
@ -733,11 +755,11 @@ class Database:
|
|||
db_basenames = {os.path.basename(p) for p in paths.values()}
|
||||
backup_abs = os.path.abspath(backup_path)
|
||||
with zipfile.ZipFile(backup_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.write(paths["main"], arcname=main_filename)
|
||||
_zip_write_file(zf, paths["main"], arcname=main_filename)
|
||||
if os.path.exists(paths["wal"]):
|
||||
zf.write(paths["wal"], arcname=f"{main_filename}-wal")
|
||||
_zip_write_file(zf, paths["wal"], arcname=f"{main_filename}-wal")
|
||||
if os.path.exists(paths["shm"]):
|
||||
zf.write(paths["shm"], arcname=f"{main_filename}-shm")
|
||||
_zip_write_file(zf, paths["shm"], arcname=f"{main_filename}-shm")
|
||||
included = self._add_identity_storage_to_zip(
|
||||
zf,
|
||||
db_basenames,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
"test:e2e:install": "playwright install chromium",
|
||||
"test:mutation:frontend": "node scripts/mutation/run.mjs",
|
||||
"test:mutation:frontend:sample": "node scripts/mutation/run.mjs --source meshchatx/src/frontend/js/rnode/Capabilities.js --max-per-file 20",
|
||||
"electron-postinstall": "electron-builder install-app-deps && node scripts/ensure-micron-parser-package.js && node scripts/patch-electron-builder-fs.cjs && node scripts/patch-electron-installer-common.cjs",
|
||||
"electron-postinstall": "electron-builder install-app-deps && node scripts/ensure-micron-parser-package.js && node scripts/patch-electron-builder-fs.cjs && node scripts/patch-electron-installer-common.cjs && node scripts/bundle-electron-preload.cjs",
|
||||
"electron": "pnpm run electron-postinstall && pnpm run build && electron .",
|
||||
"dist": "pnpm run electron-postinstall && pnpm run build && electron-builder --publish=never",
|
||||
"dist:linux": "pnpm run electron-postinstall && cross-env PLATFORM=linux pnpm run build && electron-builder --linux AppImage deb --publish=never",
|
||||
|
|
@ -124,6 +124,7 @@
|
|||
"appimage": "1.0.2"
|
||||
},
|
||||
"files": [
|
||||
"package.json",
|
||||
"electron/",
|
||||
"electron/**"
|
||||
],
|
||||
|
|
|
|||
40
scripts/bundle-electron-preload.cjs
Normal file
40
scripts/bundle-electron-preload.cjs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Electron sandbox preloads cannot require() sibling modules. Bundle shellOrigin
|
||||
* into a single preload script for packaged AppImage / desktop builds.
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const root = path.join(__dirname, "..");
|
||||
const shellPath = path.join(root, "electron", "shellOrigin.js");
|
||||
const preloadPath = path.join(root, "electron", "preload.js");
|
||||
const outPath = path.join(root, "electron", "preload.bundle.js");
|
||||
|
||||
const shellSource = fs.readFileSync(shellPath, "utf8");
|
||||
const preloadSource = fs.readFileSync(preloadPath, "utf8");
|
||||
|
||||
const shellBody = shellSource
|
||||
.replace(/^"use strict";\s*/m, "")
|
||||
.replace(/\nmodule\.exports\s*=\s*\{[\s\S]*$/m, "")
|
||||
.trim();
|
||||
|
||||
const preloadBody = preloadSource
|
||||
.replace(/const \{ isTrustedShellOrigin \} = require\("\.\/shellOrigin"\);\s*/m, "")
|
||||
.trim();
|
||||
|
||||
const bundled = `// SPDX-License-Identifier: 0BSD
|
||||
// Generated by scripts/bundle-electron-preload.cjs. Do not edit by hand.
|
||||
|
||||
"use strict";
|
||||
|
||||
${shellBody}
|
||||
|
||||
${preloadBody}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(outPath, bundled, "utf8");
|
||||
console.log(`Bundled electron preload -> ${path.relative(root, outPath)}`);
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
* meshchatx/src/frontend/public/meshchatx-docs/en/platform-guides/raspberry-pi.md,
|
||||
* meshchatx/src/backend/data/licenses_backend.json (reticulum-meshchatx entry),
|
||||
* android/app/build.gradle,
|
||||
* electron/app-version.json,
|
||||
* pipx example, packaging/arch/PKGBUILD pkgver / printf fallback,
|
||||
* then runs scripts/bake_build_meta.js (git commit / channel overlay).
|
||||
*
|
||||
|
|
@ -137,6 +138,9 @@ patchFile("packaging/arch/.SRCINFO", (c) =>
|
|||
|
||||
console.log(`Synced version ${version} from package.json`);
|
||||
|
||||
const appVersionPath = path.join(root, "electron", "app-version.json");
|
||||
writeIfChanged(appVersionPath, `${JSON.stringify({ version }, null, 4)}\n`);
|
||||
|
||||
try {
|
||||
require("./bake_build_meta.js");
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -537,6 +537,70 @@ def test_pre_migration_backup_written_before_schema_upgrade(temp_dir):
|
|||
assert any("backup-pre-migrate" in row["name"] for row in backups)
|
||||
|
||||
|
||||
def test_pre_migration_backup_handles_pre_1980_identity_file_mtime(temp_dir):
|
||||
from meshchatx.src.backend.database.schema import DatabaseSchema
|
||||
|
||||
legacy = os.path.join(temp_dir, "legacy.dat")
|
||||
with open(legacy, "wb") as handle:
|
||||
handle.write(b"legacy")
|
||||
os.utime(legacy, (0, 0))
|
||||
|
||||
db_path = os.path.join(temp_dir, "test.db")
|
||||
db = Database(db_path)
|
||||
db.initialize()
|
||||
db.close_all()
|
||||
|
||||
prior = DatabaseSchema.LATEST_VERSION - 1
|
||||
if prior < 1:
|
||||
pytest.skip("No prior schema version to simulate")
|
||||
|
||||
provider = DatabaseProvider(db_path)
|
||||
provider.execute(
|
||||
"UPDATE config SET value = ? WHERE key = ?",
|
||||
(str(prior), "database_version"),
|
||||
)
|
||||
provider.close_all()
|
||||
|
||||
upgraded = Database(db_path)
|
||||
upgraded.initialize()
|
||||
backups = upgraded.list_auto_backups(temp_dir)
|
||||
upgraded.close_all()
|
||||
|
||||
assert any("backup-pre-migrate" in row["name"] for row in backups)
|
||||
|
||||
|
||||
def test_pre_migration_backup_handles_pre_1980_identity_file_mtime(temp_dir):
|
||||
from meshchatx.src.backend.database.schema import DatabaseSchema
|
||||
|
||||
legacy = os.path.join(temp_dir, "legacy.dat")
|
||||
with open(legacy, "wb") as handle:
|
||||
handle.write(b"legacy")
|
||||
os.utime(legacy, (0, 0))
|
||||
|
||||
db_path = os.path.join(temp_dir, "test.db")
|
||||
db = Database(db_path)
|
||||
db.initialize()
|
||||
db.close_all()
|
||||
|
||||
prior = DatabaseSchema.LATEST_VERSION - 1
|
||||
if prior < 1:
|
||||
pytest.skip("No prior schema version to simulate")
|
||||
|
||||
provider = DatabaseProvider(db_path)
|
||||
provider.execute(
|
||||
"UPDATE config SET value = ? WHERE key = ?",
|
||||
(str(prior), "database_version"),
|
||||
)
|
||||
provider.close_all()
|
||||
|
||||
upgraded = Database(db_path)
|
||||
upgraded.initialize()
|
||||
backups = upgraded.list_auto_backups(temp_dir)
|
||||
upgraded.close_all()
|
||||
|
||||
assert any("backup-pre-migrate" in row["name"] for row in backups)
|
||||
|
||||
|
||||
def test_pre_migration_backup_skipped_with_env(temp_dir, monkeypatch):
|
||||
from meshchatx.src.backend.database.schema import DatabaseSchema
|
||||
|
||||
|
|
|
|||
21
tests/backend/test_database_zip_timestamps.py
Normal file
21
tests/backend/test_database_zip_timestamps.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
from meshchatx.src.backend.database import _zip_write_file
|
||||
|
||||
|
||||
def test_zip_write_file_clamps_pre_1980_mtime(tmp_path):
|
||||
old_file = tmp_path / "legacy.dat"
|
||||
old_file.write_bytes(b"legacy")
|
||||
os.utime(old_file, (0, 0))
|
||||
|
||||
zip_path = tmp_path / "backup.zip"
|
||||
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
_zip_write_file(zf, str(old_file), arcname="legacy.dat")
|
||||
|
||||
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||
info = zf.getinfo("legacy.dat")
|
||||
assert info.date_time[0] >= 1980
|
||||
assert zf.read("legacy.dat") == b"legacy"
|
||||
8
tests/electron/appVersion.test.js
Normal file
8
tests/electron/appVersion.test.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { readPackagedAppVersion } from "../../electron/appVersion.js";
|
||||
|
||||
describe("electron/appVersion", () => {
|
||||
it("reads version from electron/app-version.json", () => {
|
||||
expect(readPackagedAppVersion("0.0.0")).toBe("4.8.3");
|
||||
});
|
||||
});
|
||||
16
tests/electron/preloadBundle.test.js
Normal file
16
tests/electron/preloadBundle.test.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const bundlePath = path.join(repoRoot, "electron/preload.bundle.js");
|
||||
|
||||
describe("electron/preload.bundle", () => {
|
||||
it("exists and inlines shellOrigin for sandbox preload", () => {
|
||||
expect(fs.existsSync(bundlePath)).toBe(true);
|
||||
const source = fs.readFileSync(bundlePath, "utf8");
|
||||
expect(source).toContain("function isTrustedShellOrigin");
|
||||
expect(source).not.toContain("require(\"./shellOrigin\")");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue