Skip to content

Latest commit

 

History

History
193 lines (133 loc) · 13.5 KB

File metadata and controls

193 lines (133 loc) · 13.5 KB

macOS notes

Like Linux and Windows, macOS has some specific concerns and idiosynchrasies that Jaunch must accommodate.

The main thread's CoreFoundation event loop

Many GUI paradigms have some kind of event loop to organize all of the operations happening in the interface. Java has its AWT Event Dispatch Thread (EDT), Qt has QEventLoop, the X Window System has the Xlib event loop (see also the XInitThreads discussion in LINUX.md)... but none of them have challenged this developer nearly so much as macOS's requirement that a Core Foundation event loop be running on the process's main thread. Apple tries to make this requirement as invisible as possible... if you are writing an Objective-C program with XCode. But Jaunch is a cross-platform launcher written in pure C, so it must handle this event loop management itself.

The RUNLOOP directive

Jaunch provides the RUNLOOP directive to control how the macOS main thread event loop is handled. This directive accepts four modes:

  • main: Calls NSApplicationLoad to initialize the NSApplication, then launches the runtime on the main thread. This mode approximates Java's -XstartOnFirstThread behavior and is intended for use with non-AWT toolkits like SWT that do their own event loop management.

  • park: Runs the CoreFoundation event loop on the main thread using CFRunLoopRunInMode in a loop, while launching the runtime on a separate pthread. This mode approximates Java's default launch behavior and is what's needed for AWT applications and other frameworks that require the main thread event loop.

  • none: No special main thread handling; launches the runtime directly on the main thread with no event loop initialization. This mode behaves the same as direct launch mechanisms like python.

  • auto: Automatically selects the appropriate mode based on the runtime configuration and detected frameworks. This setting, which is Jaunch's default, attempts to behave as closely as possible to the runtime's standard launcher:

    • For JVM runtime launches, park mode will be used, because it's what the java launcher does.
    • For other runtime launches, none mode is used; e.g., python performs no special handling of the main thread.

Combining Java AWT and Python Qt

The RUNLOOP directive enables powerful combinations of Java and Python GUI frameworks that can be otherwise problematic. For example, Java AWT and Python Qt can be successfully combined by using PyQt's QApplication.exec() function on the main thread while launching Java on a separate thread:

import sys
import threading

import jpype

from pathlib import Path

from qtpy.QtWidgets import QApplication
from qtpy.QtCore import Qt


def launch_app(libjvm_path, jvm_args, main_class, main_args):
    """Launch application in a background thread."""

    # Pass the JVM path to JPype.
    jvmpath = str(Path(libjvm_path).absolute())
    jpype.startJVM(*jvm_args, jvmpath=jvmpath)

    MainClass = jpype.JClass(main_class)
    MainClass.main(main_args)

    # Block this thread until app is disposed.
    from time import sleep
    while not app_disposed():
        sleep(0.1)

    # Signal main thread to quit Qt when the application closes.
    app.quit()


# CRITICAL: Qt must run on main thread on macOS.

# Configure Qt for macOS before any QApplication creation
QApplication.setAttribute(Qt.AA_MacPluginApplication, True)
QApplication.setAttribute(Qt.AA_PluginApplication, True)
QApplication.setAttribute(Qt.AA_DisableSessionManager, True)

# Create QApplication on main thread.
app = QApplication(sys.argv)

# Prevent Qt from quitting when last Qt window closes; we want the application to stay running.
app.setQuitOnLastWindowClosed(False)

libjvm_path = ...
jvm_args = ...
main_class = ...
main_args = ...
app_thread = threading.Thread(
    target=lambda: launch_app(libjvm_path, jvm_args, main_class, main_args),
    daemon=False
)
app_thread.start()

# Run Qt event loop on main thread.
app.exec()

# Wait for app cleanup.
if app_thread.is_alive():
    app_thread.join()

This approach allows Qt to maintain its required main thread event loop while Java AWT operates safely on its own thread.

Code signing

How macOS protects users from malware

  • The Gatekeeper security layer checks every program you launch to discern whether it is safe to run.
  • If the program is not crypticographically signed by its developer, the program launch is rejected1.
  • If the program is code-signed but the developer has not submitted it to Apple for so-called notarization, the program launch is rejected1.
  • If the program has been notarized, but the program or developer is no longer in good standing with Apple, the program launch is rejected.
  • After launching, if the program attempts to perform certain actions without having requested corresponding so-called entitlements that signal its need to do so, the program will crash.2

For this system to work:

1 Garbage programs

If you distribute your application unsigned or unnotarized, macOS will literally tell your users that the program is garbage, saying the app "is damaged and can't be opened. You should move it to the Trash/Bin." Users can disable Gatekeeper with a Terminal command, at least as of macOS Sequoia, but instructing users of your applications to do so is unlikely to be reassuring for them.

2 Entitlements rantchallenges

For example, if your application tries to load a shared library signed with a different signature than yours, and your app has not declared the com.apple.security.cs.disable-library-validation entitlement during the code signing process, the program will crash. And then, if your app did not declare the com.apple.security.cs.debugger entitlement, all attempts to debug why it crashed will fail, because no debugger will be able to be attached, and Apple's Console tool will not report the real reason for the crash. Even with the aforementioned entitlements set, loading of unsigned libraries is right out: there is no entitlement to make that possible.

How Jaunch deals with Gatekeeper

Jaunch addresses the macOS code-signing + notarization issue in two ways:

  1. Pre-signed binaries usable out of the box. Jaunch releases are distributed code-signed and notarized by the lead Jaunch developer, with all the entitlements needed to run Python and Java programs. If you do not need to customize your application's Info.plist manifest, and do not need to give your application an icon, and that set of entitlements is sufficient, you can use Jaunch's prebuilt binaries directly.

  2. Tooling to ease app construction and signing. In cases where your application needs to have its own icon, or customize its manifest, or request different entitlements, you will need to construct your own .app and re-sign and re-notarize the result with your own developer certificate. To help you do that more easily, Jaunch includes a suite of shell scripts to ease the process.

The next section offers step-by-step instructions for the second path: signing and notarizing your customized app launcher.

How to sign your application's Jaunch launcher

  1. Construct your .app bundle by following the instructions in SETUP.md. Make sure the application works on your local computer before proceeding further.

  2. Join the Apple Developer Program:

    • In your web browser, navigate to developer.apple.com.
    • Click the "Account" link on the right side of the top menu.
    • Sign in with your Apple account, or click "Create yours now" if you don't already have one.
    • Once you are signed in, figure out how to join the Apple Developer Program. It will cost you $99 USD.
  3. Create and install an application certificate:

    • Navigate back to developer.apple.com/account.
    • In the Certificates, IDs & Profiles section, click the "Certificates" link.
    • Take note of your developer ID code, shown in the top right corner of this page under your name; it will look something like Penelope Realperson - XY1Q234ABC.
    • Create a new certificate, selecting "Developer ID Application" under the "Software" section.
    • Follow the prompts to generate and download the certificate. You will need to create a certificate signing request using the Keychain Access tool.
    • After downloading the certificate, double-click the .cer file to install it into your Keychain. Be sure to choose the login keychain, rather than System.
    • IMPORTANT NOTE: This .cer file is only the public key. If you later want to migrate this certificate key pair to new machine, you will need to select the certificate and private key in Keychain Access on the old machine, right-click, and "Export 2 items" to a .p12 file, which you will then need to transfer to the new machine and import again. Or else start fresh with a new application certificate.
  4. Create an app-specific password:

    • Navigate to your Apple Account security settings.
    • Click into the "App-Specific Passwords" section.
    • Click the + to add a password named notarytool.
    • Write down the app-specific password; it will be of the form abcd-efgh-ijkl-mnop.
  5. Store the app-specific password into your Keychain:

    xcrun notarytool store-credentials notarytool-password \
        --apple-id penelope@realperson.name \
        --team-id XY1Q234ABC \
        --password abcd-efgh-ijkl-mnop

    replacing penelope@realperson.name with your actual Apple ID, and replacing XY1Q234ABC with your actual team ID from step 3, and replacing abcd-efgh-ijkl-mnop with your actual app-specific password from step 4. Be aware that app-specific passwords eventually expire, at which point you will need to repeat steps 4 and 5.

  6. Run Jaunch's code-signing script:

    export DEV_ID="Penelope Realperson (XY1Q234ABC)"
    ~/jaunch-2.1.1/bin/sign.sh /path/to/MyApp.app

    Where your actual DEV_ID value is your developer ID info from step 3, and /path/to/MyApp.app is the location of the .app bundle generated in step 1.

    Note: The sign.sh script assumes your app-specific password is stored under the credential notarytool-password, as shown in step 5. If you deviate from this name, you must modify the sign.sh script accordingly.

    If all goes well, the script will code-sign, verify, and notarize your application.

P.S. Here are links to Apple documentation about this process:

Path randomization

Starting in OS X v10.12, you can no longer provide external code or data alongside your code-signed app in a zip archive or unsigned disk image. An app distributed outside the Mac App Store runs from a randomized path when it is launched and so cannot access such external resources.

—"What's New in OS X" circa 2016

In addition to Gatekeeper's requirement that apps be code-signed and notarized, it employs a security feature known as path randomization when launching an app downloaded from the Internet. The app is copied into a random directory just before launching, with the goal of making it unable to access sibling resources. Unfortunately, such resources (e.g. the TOML configuration) are exactly what Jaunch needs to successfully launch the application.

To work around this difficulty, Jaunch includes logic to "untranslocate" itself upon first launch. It works by calling the internal SecTranslocateIsTranslocatedURL and SecTranslocateCreateOriginalPathForURL functions, which are part of the macOS security framework. The latter function reports the original location of the app rather than the transient translocated location; with this information, Jaunch can then remove the com.apple.quarantine attribute from the original app bundle, then relaunch it, thus avoiding future translocation by Gatekeeper.