Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.

fix(client): show warning/error when app is using Rosetta 2 translation (running wrong arch) #2281

Merged
merged 15 commits into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion apps/client/src/components/app_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { NativeImage, TouchBar } from "electron";
import TouchBarComponent from "./touch_bar.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import { StartupChecks } from "./startup_checks.js";

interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget;
Expand Down Expand Up @@ -128,6 +129,7 @@ export type CommandMappings = {
openAboutDialog: CommandData;
hideFloatingButtons: {};
hideLeftPane: CommandData;
showCpuArchWarning: CommandData;
showLeftPane: CommandData;
hoistNote: CommandData & { noteId: string };
leaveProtectedSession: CommandData;
Expand Down Expand Up @@ -473,7 +475,14 @@ export class AppContext extends Component {
initComponents() {
this.tabManager = new TabManager();

this.components = [this.tabManager, new RootCommandExecutor(), new Entrypoints(), new MainTreeExecutors(), new ShortcutComponent()];
this.components = [
this.tabManager,
new RootCommandExecutor(),
new Entrypoints(),
new MainTreeExecutors(),
new ShortcutComponent(),
new StartupChecks()
];

if (utils.isMobile()) {
this.components.push(new MobileScreenSwitcherExecutor());
Expand Down
26 changes: 26 additions & 0 deletions apps/client/src/components/startup_checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import server from "../services/server";
import Component from "./component";

// TODO: Deduplicate.
interface CpuArchResponse {
isCpuArchMismatch: boolean;
}

export class StartupChecks extends Component {

constructor() {
super();
this.checkCpuArchMismatch();
}

async checkCpuArchMismatch() {
try {
const response = await server.get("system-checks") as CpuArchResponse;
if (response.isCpuArchMismatch) {
this.triggerCommand("showCpuArchWarning", {});
}
} catch (error) {
console.warn("Could not check CPU arch status:", error);
}
}
}
1 change: 1 addition & 0 deletions apps/client/src/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import electronContextMenu from "./menus/electron_context_menu.js";
import glob from "./services/glob.js";
import { t } from "./services/i18n.js";
import options from "./services/options.js";
import server from "./services/server.js";
import type ElectronRemote from "@electron/remote";
import type Electron from "electron";
import "./stylesheets/bootstrap.scss";
Expand Down
2 changes: 2 additions & 0 deletions apps/client/src/layouts/layout_commons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import ConfirmDialog from "../widgets/dialogs/confirm.js";
import RevisionsDialog from "../widgets/dialogs/revisions.js";
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
import InfoDialog from "../widgets/dialogs/info.js";
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";

export function applyModals(rootContainer: RootContainer) {
rootContainer
Expand All @@ -45,4 +46,5 @@ export function applyModals(rootContainer: RootContainer) {
.child(new InfoDialog())
.child(new ConfirmDialog())
.child(new PromptDialog())
.child(new IncorrectCpuArchDialog())
}
9 changes: 9 additions & 0 deletions apps/client/src/translations/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1918,5 +1918,14 @@
"title": "Appearance",
"word_wrapping": "Word wrapping",
"color-scheme": "Color scheme"
},
"cpu_arch_warning": {
"title": "Please download the ARM64 version",
"message_macos": "TriliumNext is currently running under Rosetta 2 translation, which means you're using the Intel (x64) version on Apple Silicon Mac. This will significantly impact performance and battery life.",
"message_windows": "TriliumNext is currently running emulation, which means you're using the Intel (x64) version on a Windows on ARM device. This will significantly impact performance and battery life.",
"recommendation": "For the best experience, please download the native ARM64 version of TriliumNext from our releases page.",
"download_link": "Download Native Version",
"continue_anyway": "Continue Anyway",
"dont_show_again": "Don't show this warning again"
}
}
59 changes: 59 additions & 0 deletions apps/client/src/widgets/dialogs/incorrect_cpu_arch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap";
import utils from "../../services/utils.js";
import { t } from "../../services/i18n.js";

const TPL = /*html*/`
<div class="cpu-arch-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${t("cpu_arch_warning.title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>${utils.isMac() ? t("cpu_arch_warning.message_macos") : t("cpu_arch_warning.message_windows")}</p>

<p>${t("cpu_arch_warning.recommendation")}</p>
</div>
<div class="modal-footer d-flex justify-content-between align-items-center">
<button class="download-correct-version-button btn btn-primary btn-lg me-2">
<span class="bx bx-download"></span>
${t("cpu_arch_warning.download_link")}
</button>

<button class="btn btn-secondary" data-bs-dismiss="modal">${t("cpu_arch_warning.continue_anyway")}</button>
</div>
</div>
</div>
</div>`;

export default class IncorrectCpuArchDialog extends BasicWidget {
private modal!: Modal;
private $downloadButton!: JQuery<HTMLElement>;

doRender() {
this.$widget = $(TPL);
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
this.$downloadButton = this.$widget.find(".download-correct-version-button");

this.$downloadButton.on("click", () => {
// Open the releases page where users can download the correct version
if (utils.isElectron()) {
const { shell } = utils.dynamicRequire("electron");
shell.openExternal("https://github.com/TriliumNext/Notes/releases/latest");
} else {
window.open("https://github.com/TriliumNext/Notes/releases/latest", "_blank");
}
});

// Auto-focus the download button when shown
this.$widget.on("shown.bs.modal", () => {
this.$downloadButton.trigger("focus");
});
}

showCpuArchWarningEvent() {
this.modal.show();
}
}
45 changes: 45 additions & 0 deletions apps/server/src/routes/api/system_info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { execSync } from "child_process";
import { isMac, isWindows } from "../../services/utils";
import { arch, cpus } from "os";

function systemChecks() {
return {
isCpuArchMismatch: isCpuArchMismatch()
}
}

/**
* Detects if the application is running under emulation on Apple Silicon or Windows on ARM.
* This happens when an x64 version of the app is run on an M1/M2/M3 Mac or on a Windows Snapdragon chip.
* @returns true if running on x86 emulation on ARM, false otherwise.
*/
export const isCpuArchMismatch = () => {
if (isMac) {
try {
// Use child_process to check sysctl.proc_translated
// This is the proper way to detect Rosetta 2 translation
const result = execSync("sysctl -n sysctl.proc_translated 2>/dev/null", {
encoding: "utf8",
timeout: 1000
}).trim();

// 1 means the process is being translated by Rosetta 2
// 0 means native execution
// If the sysctl doesn't exist (on Intel Macs), this will return empty/error
return result === "1";
} catch (error) {
// If sysctl fails or doesn't exist (Intel Macs), not running under Rosetta 2
return false;
}
} else if (isWindows && arch() === "x64") {
return cpus().some(cpu =>
cpu.model.includes('Microsoft SQ') ||
cpu.model.includes('Snapdragon'));
} else {
return false;
}
};

export default {
systemChecks
};
2 changes: 2 additions & 0 deletions apps/server/src/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import ollamaRoute from "./api/ollama.js";
import openaiRoute from "./api/openai.js";
import anthropicRoute from "./api/anthropic.js";
import llmRoute from "./api/llm.js";
import systemInfoRoute from "./api/system_info.js";

import etapiAuthRoutes from "../etapi/auth.js";
import etapiAppInfoRoutes from "../etapi/app_info.js";
Expand Down Expand Up @@ -238,6 +239,7 @@ function register(app: express.Application) {
apiRoute(PST, "/api/recent-notes", recentNotesRoute.addRecentNote);
apiRoute(GET, "/api/app-info", appInfoRoute.getAppInfo);
apiRoute(GET, "/api/metrics", metricsRoute.getMetrics);
apiRoute(GET, "/api/system-checks", systemInfoRoute.systemChecks);

// docker health check
route(GET, "/api/health-check", [], () => ({ status: "ok" }), apiResultHandler);
Expand Down