Starting Java GUIs on macOS over SSH
On macOS, launching Java GUIs or running the OpenJDK Regression Harness jtreg over SSH is a bit of a head-scratcher. If I start a simple test program via SSH, I get the following error message even though a running GUI is accessible:
admin@test Desktop % java WindowTest.java
Exception in thread "main" java.awt.HeadlessException:
The application is not running in a desktop session,
but this program performed an operation which requires it.
at java.desktop/java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:166)
at java.desktop/java.awt.Window.<init>(Window.java:553)
at java.desktop/java.awt.Frame.<init>(Frame.java:428)
at java.desktop/javax.swing.JFrame.<init>(JFrame.java:224)
at WindowTest.main(WindowTest.java:5)
But do not despair! Running Java GUIs over SSH is possible on macOS.
First, let us figure out what Java complains about. When digging into the OpenJDK source, the relevant code block for macOS is a JNI call into native code:
// originally from java.base/macosx/native/libjava/java_props_macosx.c
// environment variable to bypass the aqua session check
char *ev = getenv("AWT_FORCE_HEADFUL");
if (ev && (strncasecmp(ev, "true", 4) == 0)) {
// if "true" then tell the caller we're in an Aqua session without
// actually checking
return JNI_TRUE;
}
// Is the WindowServer available?
SecuritySessionId session_id;
SessionAttributeBits session_info;
OSStatus status = SessionGetInfo(callerSecuritySession, &session_id, &session_info);
if (status == noErr) {
if (session_info & sessionHasGraphicAccess) {
return JNI_TRUE;
}
}
return JNI_FALSE;
The interesting part is the second half: sessionHasGraphicAccess
from the Security framework is queried to check whether the user has access to a GUI. This means Java requires a full Aqua session. As this message from 2013 to the OpenJDK mailing list by Apple’s Mike Swingler reveals, it was a conscious decision to require a full Aqua session:
The crux of the problem is that the session you get when you SSH in isn’t an ‘Aqua’ session - so while apps you launch may be able to connect to the window server and show a window, lots of subtle stuff is going to be broken that relies on the session:
- The connection to the pasteboard server -> DnD and some copy/paste operations won’t function properly
- App foreground activation doesn’t occur correctly (LaunchServices)
- The “robot” operations may also be busted, but I haven’t checked recently.
- Any AppleScript events to or from the process
Since these are design issues that are unlikely to ever be addressed by Apple (they have been present since OS X 10.0) we decided for Java 7+ that we will not further the misconception that you can fully interact with the logged in user from an SSH session.
The Solution
ssh andreas@example.com
, andreas
must be logged into the GUI either directly or via Screen Sharing (VNC). Otherwise, the GUI is inaccessible.
As we have learnt in Accessing the macOS GUI in Automation Contexts, we can change into an Aqua session with the help of Launch Services and open
:
-
Write a shell script that invokes Java and save it on disk (I saved it to
/Users/andreas/test.sh
). I will again run my simple test program (I use a single file source-code program, requires OpenJDK 11 or higher):#! /usr/bin/env bash /usr/bin/java /Users/andreas/WindowTest.java # Quit Terminal.app that is started to run *this* script. Otherwise, # the desktop becomes cluttered with Terminal windows. kill -9 $(ps -p $(ps -p $PPID -o ppid=) -o ppid=)
-
Make the script executable.
-
Start this script with the help of
open
andTerminal.app
:open --wait-apps --new --fresh -a Terminal.app /Users/andreas/test.sh
Thanks to Launch Services, the Bash script will be run within an Aqua session, and Java displays the JFrame
on the screen. It works with all current versions of Java (8, 11, 17).
Please see this section from “Accessing the macOS GUI in Automation Contexts” for a complete explanation of the technique.
Possible Workarounds
Now, you might think that the solution offered above is overly complex (I agree!), and maybe you have already spotted the workaround in OpenJDK code fragment above: You can override the check for the Aqua session by setting the environment variable AWT_FORCE_HEADFUL
to true
.
andreas@test ~ % AWT_FORCE_HEADFUL=true java WindowTest.java
And indeed, not only is the exception gone, but Java draws the JFrame on my desktop, even if the screen is locked.
But, be warned: As with all workarounds, this one might engulf your computers in flames or cause other unexpected side effects. Especially, you will not get an error if no GUI is available.
On the web, various other workarounds are recommended that do not work (anymore):
- Setting the system property
java.awt.headless=false
(source) becauseLWCToolkit
performs theisInAquaSession()
check once more. You only end up with a different exception. - Setting the environment variable
AWT_TOOLKIT=CToolkit
. That might have worked in a distant past (think Java 6 or 7).
Unblocking UI Automation
If you plan to run automated tests that interact with the UI by using macOS’ accessibility APIs, you have to explicitly allow screen automation starting with macOS 10.14. Go to System Preferences → Security & Privacy → Accessibility.
- If you start programs with the help of
open
, you only need to addTerminal
to the allowed programs. - If you go through SSH directly, you need to add
/usr/libexec/sshd-keygen-wrapper
(if you click on +, press ⌘ ⇧ G to go to/usr/libexec
so that you can selectsshd-keygen-wrapper
).
While you are at it, you might want to grant either tool Full Disk Access in the same pane if you have not already.
The Test Program
import javax.swing.JFrame;
public class WindowTest {
public static void main(String[] args) {
var window = new JFrame("Test");
window.setSize(640, 480);
window.setVisible(true);
}
}