mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] macos: native mouse-clamp addon to dodge the off-window mouse-release RootView::UpdateCursor crash
This commit is contained in:
@@ -1673,6 +1673,27 @@ function killBackend() {
|
||||
}
|
||||
}
|
||||
|
||||
// macOS only: dodge the Chromium RootView::UpdateCursor null-deref (a browser-process
|
||||
// SIGSEGV when the mouse is released OUTSIDE the window mid-drag, easy with a second
|
||||
// display) by snapping off-window releases to the window edge before Chromium hit-tests
|
||||
// them. The fault is upstream of our renderer so JS can't catch it; this native addon
|
||||
// sits on an AppKit local event monitor. Fail-open: any miss leaves behavior as today.
|
||||
function installMacMouseClamp() {
|
||||
if (process.platform !== 'darwin') return;
|
||||
try {
|
||||
const nodePath = isPackaged
|
||||
? path.join(process.resourcesPath, 'mouseclamp', 'mouseclamp.node')
|
||||
: path.join(__dirname, 'build-staging', 'mouseclamp', process.arch, 'mouseclamp.node');
|
||||
if (!fs.existsSync(nodePath)) {
|
||||
console.log('[mouseclamp] addon not present, skipping:', nodePath);
|
||||
return;
|
||||
}
|
||||
console.log('[mouseclamp] install =>', require(nodePath).install());
|
||||
} catch (e) {
|
||||
console.log('[mouseclamp] install failed (continuing):', e && e.message);
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// We made it here, so any prior update swap finished. Drop a stale updating.lock
|
||||
// (the watchdog never deletes it) so a real crash later isn't silently swallowed.
|
||||
@@ -1682,6 +1703,9 @@ app.whenReady().then(async () => {
|
||||
// watchdog itself prevent false-positive relaunches.
|
||||
spawnCrashWatchdog();
|
||||
|
||||
// Off-window mouse-release crash dodge (macOS). Safe to call before windows exist.
|
||||
installMacMouseClamp();
|
||||
|
||||
// Cold-launch: if the OS opened us via openswarm:// (Windows/Linux it's
|
||||
// in argv; macOS fires open-url AFTER whenReady which we handle above)
|
||||
// route through forwardDeepLinkToRenderer so the URL gets stashed under
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
build/
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "mouseclamp",
|
||||
"sources": ["mouseclamp.mm"],
|
||||
"xcode_settings": {
|
||||
"CLANG_ENABLE_OBJC_ARC": "NO",
|
||||
"OTHER_CFLAGS": ["-fobjc-exceptions"]
|
||||
},
|
||||
"libraries": ["-framework Cocoa"],
|
||||
"conditions": [["OS!=\"mac\"", {"type": "none"}]]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Neutralizes a Chromium browser-process crash: releasing the mouse OUTSIDE the
|
||||
// window during a drag (trivial with a second display) makes RootView::UpdateCursor
|
||||
// deref a null view (GetEventHandlerForPoint returns null for an off-widget point,
|
||||
// root_view.cc:852) and SIGSEGVs the whole app. We can't catch it in JS, it's
|
||||
// upstream of our renderer, so we sit on a supported AppKit local event monitor and
|
||||
// snap any off-window mouse-UP to the window edge before Chromium hit-tests it; the
|
||||
// lookup then always finds a view, so the null deref is unreachable. macOS-only.
|
||||
//
|
||||
// Fail-open in both directions: anything unexpected falls through to the original
|
||||
// event, and we only touch releases that land fully outside the window, so the
|
||||
// worst case is "behaves exactly like today".
|
||||
|
||||
#include <node_api.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include <math.h>
|
||||
|
||||
static id gMonitor = nil;
|
||||
|
||||
static NSEvent *ClampOffWindowRelease(NSEvent *event) {
|
||||
@try {
|
||||
NSEventType t = [event type];
|
||||
if (t != NSEventTypeLeftMouseUp && t != NSEventTypeRightMouseUp &&
|
||||
t != NSEventTypeOtherMouseUp) {
|
||||
return event;
|
||||
}
|
||||
NSWindow *win = [event window];
|
||||
NSView *content = [win contentView];
|
||||
if (!content) return event;
|
||||
NSPoint p = [event locationInWindow];
|
||||
NSSize ws = [win frame].size;
|
||||
// act only when the release is truly off the window (the crash case); any
|
||||
// release inside the window, titlebar included, is left exactly as-is
|
||||
if (NSPointInRect(p, NSMakeRect(0.0, 0.0, ws.width, ws.height))) return event;
|
||||
NSRect cb = [content frame];
|
||||
CGFloat x = fmin(fmax(p.x, NSMinX(cb) + 1.0), NSMaxX(cb) - 1.0);
|
||||
CGFloat y = fmin(fmax(p.y, NSMinY(cb) + 1.0), NSMaxY(cb) - 1.0);
|
||||
NSEvent *clamped =
|
||||
[NSEvent mouseEventWithType:t
|
||||
location:NSMakePoint(x, y)
|
||||
modifierFlags:[event modifierFlags]
|
||||
timestamp:[event timestamp]
|
||||
windowNumber:[event windowNumber]
|
||||
context:nil
|
||||
eventNumber:[event eventNumber]
|
||||
clickCount:[event clickCount]
|
||||
pressure:[event pressure]];
|
||||
return clamped ? clamped : event;
|
||||
} @catch (...) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
static napi_value Install(napi_env env, napi_callback_info info) {
|
||||
bool ok = false;
|
||||
@autoreleasepool {
|
||||
if (gMonitor == nil) {
|
||||
NSEventMask mask = NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp |
|
||||
NSEventMaskOtherMouseUp;
|
||||
gMonitor = [[NSEvent addLocalMonitorForEventsMatchingMask:mask
|
||||
handler:^NSEvent *(NSEvent *e) {
|
||||
return ClampOffWindowRelease(e);
|
||||
}] retain];
|
||||
ok = (gMonitor != nil);
|
||||
} else {
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
napi_value result;
|
||||
napi_get_boolean(env, ok, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
static napi_value Init(napi_env env, napi_value exports) {
|
||||
napi_value fn;
|
||||
napi_create_function(env, "install", NAPI_AUTO_LENGTH, Install, NULL, &fn);
|
||||
napi_set_named_property(env, exports, "install", fn);
|
||||
return exports;
|
||||
}
|
||||
|
||||
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "mouseclamp",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "macOS-only native addon: snaps off-window mouse releases to the window edge to dodge a Chromium RootView::UpdateCursor null-deref. See mouseclamp.mm.",
|
||||
"gypfile": true
|
||||
}
|
||||
@@ -148,6 +148,13 @@
|
||||
"**/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "build-staging/mouseclamp/${arch}",
|
||||
"to": "mouseclamp",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "build-staging/uv-bin/${arch}",
|
||||
"to": "backend/uv-bin",
|
||||
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Compile the macOS mouse-clamp native addon for one arch and stage it where
|
||||
# electron-builder's extraResources picks it up (build-staging/mouseclamp/<arch>).
|
||||
# See electron/native/mouseclamp/mouseclamp.mm for what it fixes.
|
||||
set -euo pipefail
|
||||
|
||||
ARCH="${1:?usage: build-mouseclamp.sh <arm64|x64>}"
|
||||
ELECTRON_TARGET="42.0.0"
|
||||
|
||||
HERE="$(cd "$(dirname "$0")/.." && pwd)" # electron/
|
||||
SRC="$HERE/native/mouseclamp"
|
||||
OUT="$HERE/build-staging/mouseclamp/$ARCH"
|
||||
NODE_GYP="$HERE/node_modules/.bin/node-gyp"
|
||||
[[ -x "$NODE_GYP" ]] || NODE_GYP="npx --yes node-gyp" # transitive dep usually, npx if not
|
||||
|
||||
echo "[mouseclamp] building for arch=$ARCH (electron $ELECTRON_TARGET)"
|
||||
cd "$SRC"
|
||||
rm -rf build
|
||||
$NODE_GYP rebuild \
|
||||
--target="$ELECTRON_TARGET" \
|
||||
--arch="$ARCH" \
|
||||
--dist-url=https://electronjs.org/headers
|
||||
|
||||
mkdir -p "$OUT"
|
||||
cp "build/Release/mouseclamp.node" "$OUT/mouseclamp.node"
|
||||
echo "[mouseclamp] staged -> $OUT/mouseclamp.node"
|
||||
file "$OUT/mouseclamp.node"
|
||||
@@ -459,6 +459,16 @@ cd "$PROJECT_ROOT/electron"
|
||||
# npm ci: lockfile-exact, no drift. See frontend note above.
|
||||
npm ci
|
||||
|
||||
# macOS mouse-clamp native addon: compile both arches into build-staging/mouseclamp/<arch>
|
||||
# so extraResources (mouseclamp/${arch}) is populated whichever target gets packed.
|
||||
# Cheap (~2s each); fails the build loudly if a slice can't compile rather than
|
||||
# silently shipping the crash. macOS-only.
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
echo "Building mouse-clamp native addon (arm64 + x64)..."
|
||||
bash scripts/build-mouseclamp.sh arm64
|
||||
bash scripts/build-mouseclamp.sh x64
|
||||
fi
|
||||
|
||||
# Node's default ~4 GB heap OOMs while codesign'ing the .app on dual-arch
|
||||
# publish runs (the .app is ~4.8 GB and electron-builder walks every file
|
||||
# to hash + sign, holding paths + metadata in memory). Bump the old-space
|
||||
|
||||
Reference in New Issue
Block a user