Introduction
One project required the development of a Windows service that could perform a number of actions using the Windows API, Websocket and standard Java tools. The article below gives the description of the steps that have been taken to create such a service.
The need for the Windows service arose because of the necessity to have a software with the following features:
- it should be constantly running,
- it should be run by a system user,
- it should automatically start when the system starts,
- it should be difficult to be terminated by an ordinary user.
Creating a minimalized JRE version
As GraalVM still does not support creation of executable files for Windows, it was decided to take advantage of other capabilities offered by the Java ecosystem, specifically, creation of a minimalized JRE version.
In order to create the minimized JRE version, first it is needed to know the dependencies on certain packages that will be included in the JRE.
First of all, it is necessary to create the “fat jar” file with all dependencies.
Then run the jdeps -s <path to jar file> command to get a list of all dependencies. For example:
jdeps -s application.jar
application.jar -> java.base
application.jar -> java.datatransfer
application.jar -> java.desktop
application.jar -> java.logging
application.jar -> java.net.http
application.jar -> java.sql
application.jar -> java.xml
application.jar -> jdk.unsupported
application.jar -> not found
After that, create your version of JRE with these dependencies:
jlink –module-path <path to the jmods folder, which is located in jdk> –add-modules
java.base, java.datatransfer, java.desktop, java.logging, java.net.http, java.sql, java.xml, jdk.unsupported –strip-debug –compress 2 –no-header-files –no-man-pages –output <name of the folder that will contain the generated JRE>
Note that the listed packages for –add-modules option should be separated by a comma with no spaces between them. The other options are responsible for compressing and removing files and other information that is not useful for software execution.
After performing these actions, the JRE will take around 30 mb instead of hundreds.
Creating a Windows service from any application
Java does not have standard services creation tools, so third-party tools were studied and WinSW has been selected because it is free and easy to use.
WinSW
WinSW is a utility that allows to start and wrap any process as a Windows service. In order to start working with it, you need to download executable and configuration files through this link https://github.com/kohsuke/winsw/releases.
Place these two files in the directory. Rename the executable file at your discretion and give the same name to the configuration file, then place the application’s jar file and the created JRE in this directory.
Write a minimum configuration in the configuration file:
<configuration>
<!– ID of the service. It should be unique across the Windows system–>
<id>идентификатор службы</id>
<!– Display name of the service –>
<name>имя</name>
<!– Service description –>
<description>Описание</description>
<!– Path to the executable, which should be started –>
<executable>jre\bin\java.exe</executable>
<arguments>-jar application.jar</arguments>
</configuration>
jre\bin\java.exe – the relative path inside our folder to our JRE executable file.
After these actions, you can install the service by running the command as the administrator:
winsw.exe install
The list of commands is available here.
Interaction between Java and Windows API
To use Windows functions (such as creating a new process or adding registry keys) we used JNA in our application.
JNA (Java Native Access) provides Java software with easy access to libraries written in another language without writing anything but Java code. JNA allows calling directly native functions using natural Java method invocation. Most methods require no special handling or configuration; no boilerplate or generated code is required.
Connecting and working with JNA is very easy, you need to download the jar file or connect the dependency to a project builder, which is Maven in our case:
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>5.0.0</version>
</dependency>
In our project we used JNA to achieve the following goals: disable and enable the task manager 1) through the Ctrl+Shift+Esc combination, and 2) in the menu available by the Ctrl+Alt+Del combination.
To achieve these goals, we used the Advapi32Util class (an easy-to-use wrapping around the advapi32.dll library) and the WinReg interface with useful constants which provide functionality to make changes to the Windows registry (Figure 1. TaskManager class with enable() and disable() methods to modify the task manager registry keys).
- Create a new process on behalf of a specific Windows user. For this purpose we used the CreateProcessAsUser() method of the Advapi32 interface. The following parameters should be transferred to the method:
- hToken – a handle to the primary token that represents a user for whom we start the process.
- lpApplicationName – the name of the module to be executed.
- lpCommandLine – the command line to be executed.
- lpProcessAttributes – a pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new process object and determines whether child processes can inherit the returned handle to the process.
- lpThreadAttributes – a pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread object and determines whether child processes can inherit the returned handle to the thread.
- bInheritHandles – if this parameter is TRUE, each inheritable handle in the calling process is inherited by the new process. If the parameter is FALSE, the processes are not inherited.
- dwCreationFlags – the flags that control the priority class and create the process.
- lpEnvironment – a pointer to an environment block for the new process. If the parameter is NULL, the new process uses the environment of the calling process. An environment block consists of a null-terminated block of null-terminated strings. Each string is in the following form: name = value \ 0.
- lpCurrentDirectory – the full path to the current directory for the process. The string can also specify a UNC (universal naming convention) path.
- lpStartupInfo – a pointer to a STARTUPINFO or STARTUPINFOEX.lpProcessInformation structure – a pointer to a PROCESS_INFORMATION structure that receives identification information about the new process.
- Receive an active user token, as it is necessary to create a process from the specific user.
Working with processes
For working and tracking processes on Windows, we used the ProcessHandle class added to Java 9. ProcessHandle allows receiving and make various manipulations with processes. In particular, when solving the task, it was required to collect PIDs of processes, filter processes based on the name and abort necessary processes.
Interaction with other system components
WebSocket
For Java there is a standard API for working with WebSocket.
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
But just the API was not enough, so one of its implementations Tyrus was chosen to run the code.
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-server</artifactId>
<version>1.14</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-container-grizzly-server</artifactId>
<version>1.14</version>
</dependency>
Then you can create a minimalistic server and specify handlers (EndPoints).
var server = new Server(
“localhost”,
8080,
“/endpoints”,
null,
EndPoint1.class,
EndPoint2.class,
…);
try {
server.start();
Thread.currentThread().join();
} catch (Exception e) {
log.error(“Ooops! “, e);
} finally {
server.stop();
log.info(“Finally…”);
}
A handler skeleton is looking as follows:
@ServerEndpoint(value = “endpoint/url”)
public class SampleEndpoint {
@OnOpen
public void onOpen(Session session) throws IOException {
// Called when a new connection is initialized
}
@OnMessage
public void onMessage(Session session, Message message) throws IOException {
// Called when a message from the client is received
}
@OnClose
public void onClose(Session session) throws IOException {
// Called when a connection is closed
}
@OnError
public void onError(Session session, Throwable throwable) {
// Called when errors appear
}
}
HTTP Client
The release of the 11th version of Java brought us a convenient HTTP client, so there is no need for third-party clients.
To create a client instance, you need to use a builder. In the simplest case:
var client = HttpClient .newBuilder() .build()
After that, you need to create a request, for example:
var request = HttpRequest.newBuilder()
.uri(URI.create(“https://myserver.com”))
.timeout(Duration.ofMillis(1000))
.header(“Content-Type”, “application/json”)
.POST(bean.toJSON())
.build();
Then this request can be used for sending to the server:
var response = client.send(closeSession(sessionId, token),
HttpResponse.BodyHandlers.ofString())
Conclusion
Due to the modular organization of Java 9 and higher, WinSW utility, updated Process API for interaction with operating system processes and JNA (Java Native Access) library, which provides Java programs with easy access to native libraries, we were able to create a Windows service using Java language which was also used for the server part implementation. As the result, this allowed us not to introduce a new language into the development process.