Skip to content

Commit

Permalink
Initial support for Windows Service
Browse files Browse the repository at this point in the history
  • Loading branch information
tresf committed Jul 16, 2023
1 parent a3f2a1b commit 2544987
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 7 deletions.
37 changes: 36 additions & 1 deletion ant/windows/installer.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<project name="windows-installer" basedir="../../">
<property file="ant/project.properties"/>
<property file="${basedir}/ant/windows/windows.properties"/>
<import file="${basedir}/ant/version.xml"/>
<import file="${basedir}/ant/platform-detect.xml"/>
<property environment="env"/>

<target name="build-exe" depends="get-version,platform-detect">
<target name="build-exe" depends="get-version,platform-detect,download-nssm">
<!-- Get the os-preferred name for the target architecture -->
<condition property="windows.target.arch" value="arm64">
<isset property="target.arch.aarch64"/>
Expand Down Expand Up @@ -175,4 +176,38 @@
<arg line="&quot;${sign.win.file}&quot;"/>
</java>
</target>

<target name="nssm-exists">
<available file="${dist.dir}/utils/nssm.exe" property="nssm.exists"/>
</target>

<target name="download-nssm" depends="platform-detect,nssm-exists" unless="${nssm.exists}">
<delete includeemptydirs="true" defaultexcludes="false" failonerror="false">
<fileset dir="${dist.dir}/utils">
<include name="nssm.exe/"/>
</fileset>
</delete>
<mkdir dir="${dist.dir}/utils"/>

<property name="nssm.zip" value="${out.dir}/nssm.zip"/>

<echo level="info">Downloading nssm ${nssm.url}</echo>
<echo level="info">Temporarily saving nssm to ${nssm.zip}</echo>

<mkdir dir="${out.dir}"/>
<get src="${nssm.url}" verbose="true" dest="${nssm.zip}"/>

<!-- Determine which architecture to extract -->
<condition property="nssm.subdir" value="win64" else="win32">
<isset property="target.arch.x86_64"/>
</condition>
<echo level="info">Copying ${nssm.subdir}/nssm.exe</echo>
<unzip src="${nssm.zip}" dest="${dist.dir}/utils" overwrite="true">
<patternset>
<include name="**/win64/nssm.exe"/>
</patternset>
<mapper type="flatten"/>
</unzip>
<delete file="${nssm.zip}"/>
</target>
</project>
1 change: 1 addition & 0 deletions ant/windows/windows.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nssm.url=https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip
1 change: 1 addition & 0 deletions src/qz/common/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class Constants {
public static final int BORDER_PADDING = 10;

public static final String ABOUT_TITLE = "QZ Tray";
public static final String ABOUT_DESCRIPTION = "Print and communicate with devices from a web browser";
public static final String ABOUT_EMAIL = "support@qz.io";
public static final String ABOUT_URL = "https://qz.io";
public static final String ABOUT_COMPANY = "QZ Industries, LLC";
Expand Down
8 changes: 6 additions & 2 deletions src/qz/installer/Installer.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ public enum PrivilegeLevel {
public abstract Installer addAppLauncher();
public abstract Installer addStartupEntry();
public abstract Installer addSystemSettings();
public abstract Installer addServiceRegistration(String user);
public abstract Installer removeSystemSettings();
public abstract Installer removeServiceRegistration();
public abstract void spawn(List<String> args) throws Exception;

public abstract void setDestination(String destination);
Expand Down Expand Up @@ -93,7 +95,8 @@ public static boolean preinstall() {
public static void install() throws Exception {
getInstance();
log.info("Installing to {}", instance.getDestination());
instance.removeLibs()
instance.removeServiceRegistration()
.removeLibs()
.deployApp()
.removeLegacyStartup()
.removeLegacyFiles()
Expand All @@ -110,6 +113,7 @@ public static void uninstall() {
log.info("Uninstalling from {}", instance.getDestination());
instance.removeSharedDirectory()
.removeSystemSettings()
.removeServiceRegistration()
.removeCerts();
}

Expand Down Expand Up @@ -177,8 +181,8 @@ public Installer removeLegacyFiles() {

// QZ Tray 2.0 files
dirs.add("demo/js/3rdparty");
dirs.add("utils");
dirs.add("auth");
files.add("utils/windows-cleanup.js");
files.add("demo/js/qz-websocket.js");
files.add("windows-icon.ico");

Expand Down
10 changes: 10 additions & 0 deletions src/qz/installer/LinuxInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -368,4 +368,14 @@ private static HashMap<String, String> getUserEnv(String matchingUser) {
return env;
}

@Override
public Installer addServiceRegistration(String user) {
throw new UnsupportedOperationException("This feature is not yet supported on Linux");
}

@Override
public Installer removeServiceRegistration() {
return this; // no-op
}

}
10 changes: 10 additions & 0 deletions src/qz/installer/MacInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,14 @@ public void spawn(List<String> args) throws Exception {
Runtime.getRuntime().exec(args.toArray(new String[args.size()]));
}
}

@Override
public Installer addServiceRegistration(String user) {
throw new UnsupportedOperationException("This feature is not yet supported on macOS");
}

@Override
public Installer removeServiceRegistration() {
return this; // no-op
}
}
78 changes: 75 additions & 3 deletions src/qz/installer/WindowsInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.WindowsUtilities;
import qz.utils.*;
import qz.ws.PrintSocketServer;

import javax.swing.*;
Expand Down Expand Up @@ -204,4 +202,78 @@ public void spawn(List<String> args) throws Exception {
}
ShellUtilities.execute(args.toArray(new String[args.size()]));
}

@Override
public Installer addServiceRegistration(String user) {
log.warn("Registering system service: {}", PROPS_FILE);
if(!SystemUtilities.isAdmin()) {
throw new UnsupportedOperationException("Installing a service requires elevation");
}

if(WindowsUtilities.serviceExists(PROPS_FILE)) {
log.warn("System service is already registered, removing.");
removeServiceRegistration();
}

Path nssm = SystemUtilities.getJarParentPath().resolve("utils/nssm.exe");
Path qz = SystemUtilities.getJarParentPath().resolve(PROPS_FILE + ".exe");
String servicePath = String.format("\"" + qz.toString() + "\" %s %s %s",
ArgValue.WAIT.getMatches()[0],
ArgValue.STEAL.getMatches()[0],
ArgValue.HEADLESS.getMatches()[0]);

// Install the service
if(ShellUtilities.execute(nssm.toString(), "install", PROPS_FILE,
qz.toString(),
ArgValue.WAIT.getMatches()[0],
ArgValue.STEAL.getMatches()[0],
ArgValue.HEADLESS.getMatches()[0])) {
ShellUtilities.execute(nssm.toString(), "set", PROPS_FILE, "DisplayName", ABOUT_TITLE);
ShellUtilities.execute(nssm.toString(), "set", PROPS_FILE, "Description", ABOUT_DESCRIPTION);
ShellUtilities.execute(nssm.toString(), "set", PROPS_FILE, "DependOnService", "Spooler");
log.info("Successfully registered system service: {}", PROPS_FILE);
if(user != null && !user.trim().isEmpty()) {
log.info("Setting service to run as {}", user);
if(!ShellUtilities.execute(nssm.toString(), "set", "ObjectName", user)) {
log.warn("Could not set service to run as {}, please configure manually.", user);
}
}
// Kill all running instances
TaskKiller.killAll();
// Instruct autostart to be ignored
FileUtilities.disableGlobalAutoStart();
log.info("Starting system service: {}", PROPS_FILE);
if(WindowsUtilities.startService(PROPS_FILE)) {
log.info("System system service started successfully.", PROPS_FILE);
return this;
}
}
throw new UnsupportedOperationException("An error occurred installing the service");
}

@Override
public Installer removeServiceRegistration() {
log.info("Removing system service: {}", PROPS_FILE);
if(!SystemUtilities.isAdmin()) {
throw new UnsupportedOperationException("Removing a service requires elevation");
}

if(WindowsUtilities.serviceExists(PROPS_FILE)) {
WindowsUtilities.stopService(PROPS_FILE);
Path nssm = SystemUtilities.getJarParentPath().resolve("utils/nssm.exe");
if(ShellUtilities.execute(nssm.toString(), "remove", PROPS_FILE, "confirm")) {
// Old tutorials used "QZ Tray" as the service name
ShellUtilities.execute(nssm.toString(), "remove", ABOUT_TITLE, "confirm");
// Restore default autostart settings by deleting the preference file
FileUtils.deleteQuietly(FileUtilities.SHARED_DIR.resolve(AUTOSTART_FILE).toFile());
log.info("System service successfully removed: {}", PROPS_FILE);
} else {
log.error("An error occurred removing system service: {}", PROPS_FILE);
}
} else {
log.info("System service was not found, skipping.");
}

return this;
}
}
6 changes: 6 additions & 0 deletions src/qz/utils/ArgParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ public ExitStatus processInstallerArgs(ArgValue argValue, List<String> args) {
case UNINSTALL:
Installer.uninstall();
return SUCCESS;
case SERVICE:
if(hasFlag(REMOVE)) {
Installer.getInstance().removeServiceRegistration();
} else {
Installer.getInstance().addServiceRegistration(valueOf(RUNAS));
}
case SPAWN:
args.remove(0); // first argument is "spawn", remove it
Installer.getInstance().spawn(args);
Expand Down
12 changes: 11 additions & 1 deletion src/qz/utils/ArgValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public enum ArgValue {
"--honorautostart", "-A"),
STEAL(OPTION, "Ask other running instance to stop so that this instance can take precedence.", null,
"--steal", Constants.DATA_DIR + ":steal"),
WAIT(OPTION, "Wait for launcher to terminate (Windows only)", null,
"--wait"),
HEADLESS(OPTION, "Force startup \"headless\" without graphical interface or interactive components.", null,
"--headless"),

Expand All @@ -45,6 +47,8 @@ public enum ArgValue {
"certgen"),
UNINSTALL(INSTALLER, "Perform all uninstall tasks: Stop instances, delete files, unregister settings.", null,
"uninstall"),
SERVICE(INSTALLER, "Installs as system service (Windows only).", "service [--user jdoe] [--remove]",
"service"),
SPAWN(INSTALLER, "Spawn an instance of the specified program as the logged-in user, avoiding starting as the root user if possible.", "spawn [program params ...]",
"spawn"),

Expand Down Expand Up @@ -122,7 +126,13 @@ public enum ArgValueOption {
PFX(ArgValue.CERTGEN, "Path to a paired HTTPS private key and certificate in PKCS#12 format.",
"--pfx", "--pkcs12"),
PASS(ArgValue.CERTGEN, "Password for decoding private key.",
"--pass", "-p");
"--pass", "-p"),

// service
RUNAS(ArgValue.SERVICE, "Username to run the system service as (Windows only)",
"--runas", "--user", "-u"),
REMOVE(ArgValue.SERVICE, "Remove the system service as (Windows only)",
"--remove", "-r");

ArgValue parent;
String description;
Expand Down
14 changes: 14 additions & 0 deletions src/qz/utils/FileUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.text.SimpleDateFormat;
Expand All @@ -48,6 +49,7 @@
import java.util.zip.ZipOutputStream;

import static qz.common.Constants.ALLOW_FILE;
import static qz.common.Constants.AUTOSTART_FILE;

/**
* Common static file i/o utilities
Expand Down Expand Up @@ -798,6 +800,18 @@ public static boolean isAutostart() {
}
}

public static boolean disableGlobalAutoStart() {
Path autostart = SHARED_DIR.resolve(AUTOSTART_FILE);
try {
Files.write(autostart, "0".getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE_NEW, StandardOpenOption.TRUNCATE_EXISTING);
return true;
}
catch(IOException e) {
log.warn("Unable to write a zero to the global autostart file: {}", autostart);
}
return false;
}

/**
* Configures the given embedded resource file using qz.common.Constants combined with the provided
* HashMap and writes to the specified location
Expand Down
44 changes: 44 additions & 0 deletions src/qz/utils/WindowsUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -451,4 +451,48 @@ public static boolean isWow64() {
}
return isWow64;
}

public static boolean stopService(String serviceName) {
try {
W32ServiceManager serviceManager = new W32ServiceManager();
serviceManager.open(Winsvc.SC_MANAGER_ALL_ACCESS);
W32Service service = serviceManager.openService(serviceName, Winsvc.SC_MANAGER_ALL_ACCESS);
service.stopService();
service.close();
return true;
} catch(Throwable t) {
log.warn("Could not stop service {} using JNA, will fallback to command line.", serviceName);
}

// Start the newly registered service
return ShellUtilities.execute("net", "stop", Constants.PROPS_FILE);
}

public static boolean startService(String serviceName) {
try {
W32ServiceManager serviceManager = new W32ServiceManager();
serviceManager.open(Winsvc.SC_MANAGER_ALL_ACCESS);
W32Service service = serviceManager.openService(serviceName, Winsvc.SC_MANAGER_ALL_ACCESS);
service.startService();
service.close();
return true;
} catch(Throwable t) {
log.warn("Could not start service {} using JNA, will fallback to command line.", serviceName);
}

// Start the newly registered service
return ShellUtilities.execute("net", "start", Constants.PROPS_FILE);
}

public static boolean serviceExists(String serviceName) {
try {
W32ServiceManager serviceManager = new W32ServiceManager();
serviceManager.open(Winsvc.SC_MANAGER_ALL_ACCESS);
W32Service service = serviceManager.openService(serviceName, Winsvc.SC_MANAGER_ALL_ACCESS);
return true;
} catch(Win32Exception e) {
return false;
} catch(Throwable t) {}
return ShellUtilities.execute("sc", "query", serviceName);
}
}

0 comments on commit 2544987

Please sign in to comment.