Skip to content

Run AppleScript in-process on macOS instead of spawning osascript - #19

Merged
LabyStudio merged 1 commit into
LabyStudio:masterfrom
FllipEis:fix/macos-in-process-applescript
Oct 3, 2026
Merged

LabyStudio merged 1 commit into
LabyStudio:masterfrom
FllipEis:fix/macos-in-process-applescript

Conversation

@FllipEis

@FllipEis FllipEis commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Problem

On macOS every AppleScript.execute spawned an osascript process: at least three per tick (track id, player state, position), six when the track changes.

macOS registers each of those processes with Launch Services as a foreground application, attributed to the application that started the JVM. When the JVM is a game started by a launcher (LabyMod with the Spotify addon on Minecraft 26.3), every poll adds another launcher icon to the Dock. That's several icons per second for as long as the game runs. The system log shows it for every osascript child:

launchservicesd: Application LSASN:{0x0-0x734133a} has already checked in and an child pid 12480 is checking in, so forcing a new application record to be created.
launchservicesd: CHECKIN:0x0-0x734233b 12480 net.labymod.launcher

Fix

AppleScriptEngine runs the script inside the JVM through the Open Scripting Architecture (Carbon, via JNA):

  • Each script is compiled once and executed per request, so no child processes are started.
  • Output uses kOSAModeDisplayForHumans, which matches osascript's output exactly (strings unquoted, reals, lists).
  • Errors carry the AppleScript error number and message.
  • It replaces AppleScript's default send and active procedures. By default AppleScript pumps the calling thread's event loop while it waits for a reply. Off the main thread that hangs until the Apple Event timeout whenever the main thread runs its own event loop, which is the case in any GUI app and even in the plain java launcher. AESendMessage waits on a reply port instead.
  • Each call is wrapped in an autorelease pool, because Java threads have none.
  • The component is shared and execute is synchronized, since the tick thread and media keys can call it concurrently.

If the native path can't be initialised, AppleScript falls back to osascript as before.

Testing (macOS 27, Apple Silicon, Spotify running)

  • In a test program, every value the API reads (track id, name, artist, duration, position, player state) matches osascript's output exactly. SpotifyAppleScript getters work. Runtime and compile errors are reported.
  • SpotifyAPIFactory.create() + initialize() ran for 6 s: onConnect, onTrackChanged and onPlayBackChanged fired, and 0 child processes were started (previously at least 3 per second).
  • About 8 ms per request instead of about 136 ms per osascript spawn.
  • 12,000 executions on a background thread: RSS stays flat, no native leak.
  • In the game: with this jar in place of 1.5.2, the LabyMod Spotify addon shows the current track. Over 60 s there were no osascript children and no additional Dock icons.

🤖 Generated with Claude Code

Every request (at least three per tick, six on a track change) spawned an
osascript process. macOS registers each of them with Launch Services as an
application of its own, attributed to the application that started the JVM.
Inside a game started by a launcher, every poll put another launcher icon into
the Dock, several per second.

The script now runs through the Open Scripting Architecture inside the JVM:
compiled once, executed per request, no child processes and about 8 ms per
request instead of about 130 ms. AppleScript's default send and active
procedures pump the calling thread's event loop, which hangs off the main
thread while the main thread runs its own loop, so replies are awaited with
AESendMessage instead. osascript remains the fallback if the native path is
unavailable.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@LabyStudio
LabyStudio merged commit 559ffca into LabyStudio:master Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants