package qz.installer; /** * @author Tres Finocchiaro * * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC * * LGPL 2.1 This is free software. This software and source code are released under * the "LGPL 2.1 License". A copy of this license should be distributed with * this software. http://www.gnu.org/licenses/lgpl-2.1.html */ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import qz.utils.FileUtilities; import qz.utils.ShellUtilities; import qz.utils.SystemUtilities; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.List; import static qz.common.Constants.*; public class MacInstaller extends Installer { protected static final Logger log = LogManager.getLogger(MacInstaller.class); private static final String PACKAGE_NAME = getPackageName(); public static final String LAUNCH_AGENT_PATH = String.format("/Library/LaunchAgents/%s.plist", MacInstaller.PACKAGE_NAME); private String destination = "/Applications/" + ABOUT_TITLE + ".app"; public Installer addAppLauncher() { // not needed; registered when "QZ Tray.app" is copied return this; } public Installer addStartupEntry() { File dest = new File(LAUNCH_AGENT_PATH); HashMap fieldMap = new HashMap<>(); // Dynamic fields fieldMap.put("%PACKAGE_NAME%", PACKAGE_NAME); fieldMap.put("%COMMAND%", String.format("%s/Contents/MacOS/%s", destination, ABOUT_TITLE)); fieldMap.put("%PARAM%", "--honorautostart"); try { FileUtilities.configureAssetFile("assets/mac-launchagent.plist.in", dest, fieldMap, MacInstaller.class); // Disable service until reboot if(SystemUtilities.isMac()) { ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH); } } catch(IOException e) { log.warn("Unable to write startup file: {}", dest, e); } return this; } public void setDestination(String destination) { this.destination = destination; } public String getDestination() { return destination; } public Installer addSystemSettings() { // Chrome protocol handler String plist = "/Library/Preferences/com.google.Chrome.plist"; if(ShellUtilities.execute(new String[] { "/usr/bin/defaults", "write", plist }, new String[] {DATA_DIR + "://*" }).isEmpty()) { ShellUtilities.execute("/usr/bin/defaults", "write", plist, "URLAllowlist", "-array-add", DATA_DIR +"://*"); } return this; } public Installer removeSystemSettings() { // Remove startup entry File dest = new File(LAUNCH_AGENT_PATH); dest.delete(); return this; } /** * Removes legacy (<= 2.0) startup entries */ public Installer removeLegacyStartup() { log.info("Removing startup entries for all users matching " + ABOUT_TITLE); String script = "tell application \"System Events\" to delete " + "every login item where name is \"" + ABOUT_TITLE + "\"" + " or name is \"" + PROPS_FILE + ".jar\""; // Run on background thread in case System Events is hung or slow to respond final String finalScript = script; new Thread(() -> { ShellUtilities.executeAppleScript(finalScript); }).run(); return this; } public static String getPackageName() { String packageName; String[] parts = ABOUT_URL.split("\\W"); if (parts.length >= 2) { // Parse io.qz.qz-print from Constants packageName = String.format("%s.%s.%s", parts[parts.length - 1], parts[parts.length - 2], PROPS_FILE); } else { // Fallback on something sane packageName = "local." + PROPS_FILE; } return packageName; } public void spawn(List args) throws Exception { if(SystemUtilities.isAdmin()) { // macOS unconventionally uses "$USER" during its install process String sudoer = System.getenv("USER"); if(sudoer == null || sudoer.isEmpty() || sudoer.equals("root")) { // Fallback, should only fire via Terminal + sudo sudoer = ShellUtilities.executeRaw("logname").trim(); } // Start directly without waitFor(...), avoids deadlocking Runtime.getRuntime().exec(new String[] { "su", sudoer, "-c", "\"" + StringUtils.join(args, "\" \"") + "\""}); } else { Runtime.getRuntime().exec(args.toArray(new String[args.size()])); } } }