Skip to content

Commit

Permalink
Initial support for Window Service
Browse files Browse the repository at this point in the history
  • Loading branch information
tresf committed Sep 10, 2021
1 parent a3cf427 commit 300c871
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 4 deletions.
41 changes: 41 additions & 0 deletions ant/windows/installer.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<project name="windows-installer" basedir="../../">
<property file="ant/project.properties"/>
<property file="ant/windows/windows.properties"/>
<import file="${basedir}/ant/platform-detect.xml"/>
<import file="${basedir}/ant/version.xml"/>
<property environment="env"/>

Expand Down Expand Up @@ -167,4 +169,43 @@
<arg line="${sign.win.file}"/>
</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}"/>

<!-- Resign nssm.exe using our own signature -->
<antcall target="sign-win">
<param name="sign.win.file" value="${dist.dir}/utils/nssm.exe"/>
</antcall>
</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
4 changes: 4 additions & 0 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 @@ -97,6 +99,7 @@ public static void install() throws Exception {
.deployApp()
.removeLegacyStartup()
.removeLegacyFiles()
.removeServiceRegistration()
.addSharedDirectory()
.addAppLauncher()
.addStartupEntry()
Expand All @@ -110,6 +113,7 @@ public static void uninstall() {
log.info("Uninstalling from {}", instance.getDestination());
instance.removeSharedDirectory()
.removeSystemSettings()
.removeServiceRegistration()
.removeCerts();
}

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 @@ -277,4 +277,14 @@ public void spawn(List<String> args) throws Exception {
Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp);
}

@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
}
}
63 changes: 60 additions & 3 deletions src/qz/installer/WindowsInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.WindowsUtilities;
import qz.common.Constants;
import qz.utils.*;
import qz.ws.PrintSocketServer;

import javax.swing.*;
Expand All @@ -29,8 +28,11 @@

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;


Expand Down Expand Up @@ -201,4 +203,59 @@ public void spawn(List<String> args) throws Exception {
}
ShellUtilities.execute(args.toArray(new String[args.size()]));
}

@Override
public Installer addServiceRegistration(String user) {
if(!SystemUtilities.isAdmin()) {
throw new UnsupportedOperationException("Installing a service requires elevation");
}

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, servicePath)) {
ShellUtilities.execute(nssm.toString(), "set", "DisplayName", ABOUT_TITLE);
ShellUtilities.execute(nssm.toString(), "set", "Description", ABOUT_DESCRIPTION);
ShellUtilities.execute(nssm.toString(), "set", "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();
if(WindowsUtilities.startService(PROPS_FILE)) {
return this;
}
}
throw new UnsupportedOperationException("An error occurred installing the service");
}

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

WindowsUtilities.stopService(PROPS_FILE);
Path nssm = SystemUtilities.getJarParentPath().resolve("utils/nssm.exe");
if(ShellUtilities.execute(nssm.toString(), "remove", PROPS_FILE)) {
// 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: {}, please try to remove manually using 'sc ", PROPS_FILE);
}

return this;
}
}
3 changes: 3 additions & 0 deletions src/qz/utils/ArgParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ public ExitStatus processInstallerArgs(ArgValue argValue, List<String> args) {
case UNINSTALL:
Installer.uninstall();
return SUCCESS;
case SERVICE:
Installer.getInstance().addServiceRegistration(valueOf(RUNAS));
return SUCCESS;
case SPAWN:
args.remove(0); // first argument is "spawn", remove it
Installer.getInstance().spawn(args);
Expand Down
10 changes: 9 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,11 @@ 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");

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 @@ -38,6 +38,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 @@ -47,6 +48,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 @@ -159,6 +161,18 @@ public static Path inheritParentPermissions(Path filePath) {

}

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

private static final String[] badExtensions = new String[] {
"exe", "pif", "paf", "application", "msi", "com", "cmd", "bat", "lnk", // Windows Executable program or script
"gadget", // Windows desktop gadget
Expand Down
33 changes: 33 additions & 0 deletions src/qz/utils/WindowsUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.Map;

import static com.sun.jna.platform.win32.WinReg.*;
import static qz.common.Constants.PROPS_FILE;
import static qz.utils.SystemUtilities.*;

import static java.nio.file.attribute.AclEntryPermission.*;
Expand Down Expand Up @@ -343,4 +344,36 @@ 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", 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", PROPS_FILE);
}
}

0 comments on commit 300c871

Please sign in to comment.