Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for Windows Service #1157

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}