• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

evolvedbinary / elemental / 982

29 Apr 2025 08:34PM UTC coverage: 56.409% (+0.007%) from 56.402%
982

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

11.63
/exist-core/src/main/java/org/exist/launcher/WindowsServiceManager.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 *
24
 * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
25
 *       The original license header is included below.
26
 *
27
 * =====================================================================
28
 *
29
 * eXist-db Open Source Native XML Database
30
 * Copyright (C) 2001 The eXist-db Authors
31
 *
32
 * info@exist-db.org
33
 * http://www.exist-db.org
34
 *
35
 * This library is free software; you can redistribute it and/or
36
 * modify it under the terms of the GNU Lesser General Public
37
 * License as published by the Free Software Foundation; either
38
 * version 2.1 of the License, or (at your option) any later version.
39
 *
40
 * This library is distributed in the hope that it will be useful,
41
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
43
 * Lesser General Public License for more details.
44
 *
45
 * You should have received a copy of the GNU Lesser General Public
46
 * License along with this library; if not, write to the Free Software
47
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
48
 */
49
package org.exist.launcher;
50

51
import com.evolvedbinary.j8fu.OptionalUtil;
52
import com.evolvedbinary.j8fu.lazy.LazyValE;
53
import com.evolvedbinary.j8fu.tuple.Tuple2;
54
import net.jcip.annotations.NotThreadSafe;
55
import org.apache.logging.log4j.LogManager;
56
import org.apache.logging.log4j.Logger;
57
import org.exist.util.ConfigurationHelper;
58

59
import javax.annotation.Nullable;
60
import java.io.BufferedReader;
61
import java.io.IOException;
62
import java.io.InputStreamReader;
63
import java.nio.file.Files;
64
import java.nio.file.Path;
65
import java.nio.file.Paths;
66
import java.util.*;
67
import java.util.regex.Matcher;
68
import java.util.regex.Pattern;
69

70
import static com.evolvedbinary.j8fu.Either.Left;
71
import static com.evolvedbinary.j8fu.Either.Right;
72
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
73
import static java.nio.charset.StandardCharsets.UTF_8;
74
import static org.exist.launcher.ConfigurationUtility.LAUNCHER_PROPERTY_MAX_MEM;
75
import static org.exist.launcher.ConfigurationUtility.LAUNCHER_PROPERTY_MIN_MEM;
76

77
/**
78
 * @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
79
 */
80
@NotThreadSafe
81
class WindowsServiceManager implements ServiceManager {
82

83
    /**
84
     * See <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html#BABHDABI">Java - Non-Standard Options</a>.
85
     */
86
    private static final Pattern JAVA_CMDLINE_MEMORY_STRING = Pattern.compile("([0-9]+)(g|G|m|M|k|K)?.*");
1✔
87

88
    private static final Logger LOG = LogManager.getLogger(WindowsServiceManager.class);
1✔
89
    private static final String PROCRUN_SRV_EXE = "prunsrv-x86_64.exe";
90
    private static final String SC_EXE = "sc.exe";
91

92
    private static final String SERVICE_NAME = "Elemental";
1✔
93

94
    private final Path existHome;
95
    private final LazyValE<Path, ServiceManagerException> prunsrvExe;
96

97
    private enum WindowsServiceState {
×
98
        UNINSTALLED,
×
99
        RUNNING,
×
100
        STOPPED,
×
101
        PAUSED
×
102
    }
103

104
    WindowsServiceManager() {
×
105
        this.prunsrvExe = new LazyValE<>(() ->
×
106
            OptionalUtil.toRight(() -> new ServiceManagerException("Could not detect EXIST_HOME when trying to find Procrun exe"), ConfigurationHelper.getExistHome())
×
107
                .map(base -> base.resolve("bin").resolve(PROCRUN_SRV_EXE))
×
108
                .flatMap(exe -> Files.exists(exe) ? Right(exe) : Left(new ServiceManagerException("Could not find Procrun at: " + exe)))
×
109
                .flatMap(exe -> Files.isExecutable(exe) ? Right(exe) : Left(new ServiceManagerException("Procrun is not executable at: " + exe)))
×
110
        );
111

112
        this.existHome = ConfigurationHelper.getExistHome().orElse(Paths.get("."));
×
113
    }
×
114

115
    @Override
116
    public void install() throws ServiceManagerException {
117
        if (getState() != WindowsServiceState.UNINSTALLED) {
×
118
            throw new ServiceManagerException("Service is already installed");
×
119
        }
120

121
        final Path configFile = ConfigurationHelper.getFromSystemProperty()
×
122
                .orElse(existHome.resolve("etc").resolve("conf.xml"));
×
123

124
        final Properties launcherProperties = ConfigurationUtility.loadProperties();
×
125
        final Optional<String> maxMemory = Optional.ofNullable(launcherProperties.getProperty(LAUNCHER_PROPERTY_MAX_MEM)).flatMap(WindowsServiceManager::asJavaCmdlineMemoryString);
×
126
        final Optional<String> minMemory = asJavaCmdlineMemoryString(launcherProperties.getProperty(LAUNCHER_PROPERTY_MIN_MEM, "128"));
×
127

128
        final StringBuilder jvmOptions = new StringBuilder();
×
129
        jvmOptions.append("-Dfile.encoding=UTF-8");
×
130
        for (final String propertyName : System.getProperties().stringPropertyNames()) {
×
131
            if (propertyName.startsWith("exist.") ||
×
132
                    propertyName.startsWith("jetty.") ||
×
133
                    propertyName.startsWith("log4j.")) {
×
134
                final String propertyValue = System.getProperty(propertyName);
×
135
                if (propertyValue != null) {
×
136
                    jvmOptions
×
137
                            .append(";-D").append(propertyName)
×
138
                            .append('=')
×
139
                            .append(propertyValue);
×
140
                }
141
            }
142
        }
143
        final Path exe = prunsrvExe.get();
×
144
        final List<String> args = newList(exe.toAbsolutePath().toString(), "install", SERVICE_NAME,
×
145
                "--DisplayName=" + SERVICE_NAME,
×
146
                "--Description=Elemental Server",
×
147
                "--StdError=auto",
×
148
                "--StdOutput=auto",
×
149
                "--LogPath=\"" + existHome.resolve("logs").toAbsolutePath().toString() + "\"",
×
150
                "--LogPrefix=service",
×
151
                "--PidFile=service.pid",
×
152
                "--Startup=auto",
×
153
                "--ServiceUser=LocalSystem",  // TODO(AR) this changed from `LocalSystem` to `NT Authority\LocalService` in procrun 1.2.0, however our service won't seem to start under that account... we need to investigate!
×
154
                "--Jvm=" + findJvm().orElse("auto"),
×
155
                "--Classpath=\"" + existHome.resolve("lib").toAbsolutePath().toString().replace('\\', '/') + "/*\"",
×
156
                "--StartMode=jvm",
×
157
                "--StartClass=org.exist.service.ExistDbDaemon",
×
158
                "--StartMethod=start",
×
159
                "--StopMode=jvm",
×
160
                "--StopClass=org.exist.service.ExistDbDaemon",
×
161
                "--StopMethod=stop",
×
162
                "--JvmOptions=\"" + jvmOptions + "\"",
×
163
                "--StartParams=\"" + configFile.toAbsolutePath().toString() + "\""
×
164
        );
165
        minMemory.flatMap(WindowsServiceManager::asPrunSrvMemoryString).ifPresent(xms -> args.add("--JvmMs=" + xms));
×
166
        maxMemory.flatMap(WindowsServiceManager::asPrunSrvMemoryString).ifPresent(xmx -> args.add("--JvmMx=" + xmx));
×
167

168
        try {
169
            final Tuple2<Integer, String> execResult = run(args, true);
×
170
            final int exitCode = execResult._1;
×
171
            final String result = execResult._2;
×
172

173
            if (exitCode != 0) {
×
174
                LOG.error("Could not install service, exitCode={}, output='{}'", exitCode, result);
×
175
                throw new ServiceManagerException("Could not install service, exitCode=" + exitCode + ", output='" + result + "'");
×
176
            }
177
        } catch (final IOException | InterruptedException e) {
×
178
            if (e instanceof InterruptedException) {
×
179
                Thread.currentThread().interrupt();
×
180
            }
181
            LOG.error("Could not install service: {}", e.getMessage(), e);
×
182
            throw new ServiceManagerException("Could not install service: " + e.getMessage(), e);
×
183
        }
184
    }
×
185

186
    private static <T> List<T> newList(final T... items) {
187
        final List<T> list = new ArrayList<>(items.length);
×
188
        list.addAll(Arrays.asList(items));
×
189
        return list;
×
190
    }
191

192

193
    @Override
194
    public boolean isInstalled() {
195
        try {
196
            return getState() != WindowsServiceState.UNINSTALLED;
×
197
        } catch (final ServiceManagerException e) {
×
198
            LOG.error("Could not determine if service is installed: {}", e.getMessage(), e);
×
199
            return false;
×
200
        }
201
    }
202

203
    @Override
204
    public void uninstall() throws ServiceManagerException {
205
        if (getState() == WindowsServiceState.UNINSTALLED) {
×
206
            throw new ServiceManagerException("Service is already uninstalled");
×
207
        }
208

209
        final Path exe = prunsrvExe.get();
×
210
        final List<String> args = Arrays.asList(exe.toAbsolutePath().toString(), "delete", SERVICE_NAME);
×
211
        try {
212
            final Tuple2<Integer, String> execResult = run(args, true);
×
213
            final int exitCode = execResult._1;
×
214
            final String result = execResult._2;
×
215

216
            if (exitCode != 0) {
×
217
                LOG.error("Could not uninstall service, exitCode={}, output='{}'", exitCode, result);
×
218
                throw new ServiceManagerException("Could not uninstall service, exitCode=" + exitCode + ", output='" + result + "'");
×
219
            }
220
        } catch (final IOException | InterruptedException e) {
×
221
            if (e instanceof InterruptedException) {
×
222
                Thread.currentThread().interrupt();
×
223
            }
224
            LOG.error("Could not uninstall service: {}", e.getMessage(), e);
×
225
            throw new ServiceManagerException("Could not uninstall service: " + e.getMessage(), e);
×
226
        }
227
    }
×
228

229
    @Override
230
    public void start() throws ServiceManagerException {
231
        final WindowsServiceState state = getState();
×
232
        if (state == WindowsServiceState.RUNNING || state == WindowsServiceState.PAUSED) {
×
233
            return;
×
234
        }
235

236
        if (state == WindowsServiceState.UNINSTALLED) {
×
237
            throw new ServiceManagerException("Cannot start service which is not yet installed");
×
238
        }
239

240
        final Path exe = prunsrvExe.get();
×
241
        final List<String> args = Arrays.asList(exe.toAbsolutePath().toString(), "start", SERVICE_NAME);
×
242
        try {
243
            final Tuple2<Integer, String> execResult = run(args, true);
×
244
            final int exitCode = execResult._1;
×
245
            final String result = execResult._2;
×
246

247
            if (exitCode != 0) {
×
248
                LOG.error("Could not start service, exitCode={}, output='{}'", exitCode, result);
×
249
                throw new ServiceManagerException("Could not start service, exitCode=" + exitCode + ", output='" + result + "'");
×
250
            }
251
        } catch (final IOException | InterruptedException e) {
×
252
            if (e instanceof InterruptedException) {
×
253
                Thread.currentThread().interrupt();
×
254
            }
255
            LOG.error("Could not start service: {}", e.getMessage(), e);
×
256
            throw new ServiceManagerException("Could not start service: " + e.getMessage(), e);
×
257
        }
258
    }
×
259

260
    @Override
261
    public boolean isRunning() {
262
        try {
263
            return getState() == WindowsServiceState.RUNNING;
×
264
        } catch (final ServiceManagerException e) {
×
265
            LOG.error("Could not determine if service is running: {}", e.getMessage(), e);
×
266
            return false;
×
267
        }
268
    }
269

270
    @Override
271
    public void stop() throws ServiceManagerException {
272
        final WindowsServiceState state = getState();
×
273
        if (state == WindowsServiceState.UNINSTALLED) {
×
274
            throw new ServiceManagerException("Cannot stop service which is not yet installed");
×
275
        }
276

277
        if (state != WindowsServiceState.RUNNING) {
×
278
            return;
×
279
        }
280

281
        final Path exe = prunsrvExe.get();
×
282
        final List<String> args = Arrays.asList(exe.toAbsolutePath().toString(), "stop", SERVICE_NAME);
×
283
        try {
284
            final Tuple2<Integer, String> execResult = run(args, true);
×
285
            final int exitCode = execResult._1;
×
286
            final String result = execResult._2;
×
287

288
            if (exitCode != 0) {
×
289
                LOG.error("Could not stop service, exitCode={}, output='{}'", exitCode, result);
×
290
                throw new ServiceManagerException("Could not stop service, exitCode=" + exitCode + ", output='" + result + "'");
×
291
            }
292
        } catch (final IOException | InterruptedException e) {
×
293
            if (e instanceof InterruptedException) {
×
294
                Thread.currentThread().interrupt();
×
295
            }
296
            LOG.error("Could not stop service: {}", e.getMessage(), e);
×
297
            throw new ServiceManagerException("Could not stop service: " + e.getMessage(), e);
×
298
        }
299
    }
×
300

301
    @Override
302
    public void showNativeServiceManagementConsole() throws UnsupportedOperationException, ServiceManagerException {
303
        final List<String> args = Arrays.asList("cmd.exe", "/c", "services.msc");
×
304
        final ProcessBuilder pb = new ProcessBuilder(args);
×
305
        try {
306
            pb.start();
×
307
        } catch (final IOException e) {
×
308
            throw new ServiceManagerException(e.getMessage(), e);
×
309
        }
310
    }
×
311

312
    /**
313
     * Try to find jvm.dll, which should either reside in `bin/client` or `bin/server` below
314
     * JAVA_HOME. Autodetection does not seem to work with OpenJDK-based Java distributions.
315
     *
316
     * @return Path to jvm.dll or empty Optional
317
     */
318
    private Optional<String> findJvm() {
319
        final Path javaHome = Paths.get(System.getProperty("java.home")).toAbsolutePath();
×
320
        Path jvm = javaHome.resolve("bin").resolve("client").resolve("jvm.dll");
×
321
        if (Files.exists(jvm)) {
×
322
            return Optional.of(jvm.toString());
×
323
        }
324
        jvm = javaHome.resolve("bin").resolve("server").resolve("jvm.dll");
×
325
        if (Files.exists(jvm)) {
×
326
            return Optional.of(jvm.toString());
×
327
        }
328
        return Optional.empty();
×
329
    }
330

331
    private WindowsServiceState getState() throws ServiceManagerException {
332
        try {
333
            final List<String> args = Arrays.asList(SC_EXE, "query", SERVICE_NAME);
×
334
            final Tuple2<Integer, String> execResult = run(args, false);
×
335
            final int exitCode = execResult._1;
×
336
            final String result = execResult._2;
×
337

338
            if (exitCode == 1060) {
×
339
                return WindowsServiceState.UNINSTALLED;
×
340
            }
341
            if (exitCode != 0) {
×
342
                throw new ServiceManagerException("Could not query service status, exitCode=" + exitCode + ", output='" + result + "'");
×
343
            }
344

345
            if (result.contains("STOPPED")) {
×
346
                return WindowsServiceState.STOPPED;
×
347
            }
348
            if (result.contains("RUNNING")) {
×
349
                return WindowsServiceState.RUNNING;
×
350
            }
351
            if (result.contains("PAUSED")) {
×
352
                return WindowsServiceState.PAUSED;
×
353
            }
354

355
            throw new ServiceManagerException("Could not determine service status, exitCode=" + exitCode + ", output='" + result + "'");
×
356

357
        } catch (final IOException | InterruptedException e) {
×
358
            if (e instanceof InterruptedException) {
×
359
                Thread.currentThread().interrupt();
×
360
            }
361
            throw new ServiceManagerException(e);
×
362
        }
363
    }
364

365
    private Tuple2<Integer, String> run(List<String> args, final boolean elevated) throws IOException, InterruptedException {
366

367
        if (elevated) {
×
368
            final List<String> elevatedArgs = new ArrayList<>();
×
369
            elevatedArgs.add("cmd.exe");
×
370
            elevatedArgs.add("/c");
×
371
            elevatedArgs.addAll(args);
×
372

373
            args = elevatedArgs;
×
374
        }
375

376
        if (LOG.isDebugEnabled()) {
×
377
            final StringBuilder buf = new StringBuilder("Executing: [");
×
378
            for (int i = 0; i < args.size(); i++) {
×
379
                buf.append('"');
×
380
                buf.append(args.get(i));
×
381
                buf.append('"');
×
382
                if (i != args.size() - 1) {
×
383
                    buf.append(", ");
×
384
                }
385
            }
386
            buf.append(']');
×
387
            LOG.debug(buf.toString());
×
388
        }
389

390
        final ProcessBuilder pb = new ProcessBuilder(args);
×
391
        pb.directory(existHome.toFile());
×
392
        pb.redirectErrorStream(true);
×
393

394
        final Process process = pb.start();
×
395
        final StringBuilder output = new StringBuilder();
×
396
        try (final BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(),
×
397
                UTF_8))) {
×
398
            String line;
399
            while ((line = reader.readLine()) != null) {
×
400
                output.append(System.getProperty("line.separator")).append(line);
×
401
            }
402
        }
403
        final int exitValue = process.waitFor();
×
404
        return Tuple(exitValue, output.toString());
×
405
    }
406

407
    /**
408
     * Transform the supplied memory string into a string
409
     * that is compatible with the Java command line arguments for -Xms and -Xmx.
410
     *
411
     * See <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html#BABHDABI">Java - Non-Standard Options</a>.
412
     *
413
     * @param memoryString the memory string.
414
     *
415
     * @return a memory string compatible with java.exe.
416
     */
417
    static Optional<String> asJavaCmdlineMemoryString(final String memoryString) {
418
        // should optionally end in g|G|m|M|k|K
419
        final Matcher mtcJavaCmdlineMemoryString = JAVA_CMDLINE_MEMORY_STRING.matcher(memoryString);
1✔
420
        if (!mtcJavaCmdlineMemoryString.matches()) {
1✔
421
            // invalid java cmdline memory string
422
            return Optional.empty();
1✔
423
        }
424

425
        final String value = mtcJavaCmdlineMemoryString.group(1);
1✔
426
        @Nullable final String mnemonic = mtcJavaCmdlineMemoryString.group(2);
1✔
427

428
        if (mnemonic == null) {
1✔
429
            // no mnemonic supplied, assume `m` for megabytes
430
            return Optional.of(value + "m");
1✔
431
        }
432

433
        // valid mnemonic supplied, so return as is (excluding any additional cruft)
434
        return Optional.of(value + mnemonic);
1✔
435
    }
436

437
    /**
438
     * Converts a memory string for the Java command line arguments -Xms or -Xmx, into
439
     * a memory string that is understood by prunsrv.exe.
440
     * prunsrv.exe expects an integer in megabytes.
441
     *
442
     * @param javaCmdlineMemoryString the memory strig as would be given to the Java command line.
443
     *
444
     * @return a memory string suitable for use with prunsrv.exe.
445
     */
446
    static Optional<String> asPrunSrvMemoryString(final String javaCmdlineMemoryString) {
447
        // should optionally end in g|G|m|M|k|K
448
        final Matcher mtcJavaCmdlineMemoryString = JAVA_CMDLINE_MEMORY_STRING.matcher(javaCmdlineMemoryString);
1✔
449
        if (!mtcJavaCmdlineMemoryString.matches()) {
1✔
450
            // invalid java cmdline memory string
451
            return Optional.empty();
1✔
452
        }
453

454
        long value = Integer.valueOf(mtcJavaCmdlineMemoryString.group(1)).longValue();
1✔
455
        @Nullable String mnemonic = mtcJavaCmdlineMemoryString.group(2);
1✔
456
        if (mnemonic == null) {
1✔
457
            mnemonic = "m";
1✔
458
        }
459

460
        switch (mnemonic.toLowerCase()) {
1✔
461
            case "k":
462
                value = value / 1024;
1✔
463
                break;
1✔
464

465
            case "g":
466
                value = value * 1024;
1✔
467
                break;
1✔
468

469
            case "m":
470
            default:
471
                // do nothing, megabytes is the default!
472
                break;
473
        }
474

475
        return Optional.of(Long.toString(value));
1✔
476
    }
477
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc