Skip to content

Commit 8b686d9

Browse files
Alexey Semenyukaivanov-jdk
authored andcommitted
8365790: Shutdown hook for application image does not work on Windows
Reviewed-by: almatvee, aivanov Backport-of: f7ce3a1b5f38143f17b5015ca5b714ec0e708f54
1 parent 1a69c6b commit 8b686d9

File tree

9 files changed

+396
-43
lines changed

9 files changed

+396
-43
lines changed

src/jdk.jpackage/windows/native/applauncher/WinLauncher.cpp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -256,6 +256,16 @@ bool needRestartLauncher(AppLauncher& appLauncher, CfgFile& cfgFile) {
256256
}
257257

258258

259+
void enableConsoleCtrlHandler(bool enable) {
260+
if (!SetConsoleCtrlHandler(NULL, enable ? FALSE : TRUE)) {
261+
JP_THROW(SysError(tstrings::any() << "SetConsoleCtrlHandler(NULL, "
262+
<< (enable ? "FALSE" : "TRUE")
263+
<< ") failed",
264+
SetConsoleCtrlHandler));
265+
}
266+
}
267+
268+
259269
void launchApp() {
260270
// [RT-31061] otherwise UI can be left in back of other windows.
261271
::AllowSetForegroundWindow(ASFW_ANY);
@@ -310,6 +320,19 @@ void launchApp() {
310320
exec.arg(arg);
311321
});
312322

323+
exec.afterProcessCreated([&](HANDLE pid) {
324+
//
325+
// Ignore Ctrl+C in the current process.
326+
// This will prevent child process termination without allowing
327+
// it to handle Ctrl+C events.
328+
//
329+
// Disable the default Ctrl+C handler *after* the child process
330+
// has been created as it is inheritable and we want the child
331+
// process to have the default handler.
332+
//
333+
enableConsoleCtrlHandler(false);
334+
});
335+
313336
DWORD exitCode = RunExecutorWithMsgLoop::apply(exec);
314337

315338
exit(exitCode);

src/jdk.jpackage/windows/native/common/Executor.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -161,6 +161,10 @@ UniqueHandle Executor::startProcess(UniqueHandle* threadHandle) const {
161161
}
162162
}
163163

164+
if (afterProcessCreatedCallback) {
165+
afterProcessCreatedCallback(processInfo.hProcess);
166+
}
167+
164168
// Return process handle.
165169
return UniqueHandle(processInfo.hProcess);
166170
}

src/jdk.jpackage/windows/native/common/Executor.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -26,6 +26,8 @@
2626
#ifndef EXECUTOR_H
2727
#define EXECUTOR_H
2828

29+
#include <functional>
30+
2931
#include "tstrings.h"
3032
#include "UniqueHandle.h"
3133

@@ -97,6 +99,14 @@ class Executor {
9799
*/
98100
int execAndWaitForExit() const;
99101

102+
/**
103+
* Call provided function after the process hass been created.
104+
*/
105+
Executor& afterProcessCreated(const std::function<void(HANDLE)>& v) {
106+
afterProcessCreatedCallback = v;
107+
return *this;
108+
}
109+
100110
private:
101111
UniqueHandle startProcess(UniqueHandle* threadHandle=0) const;
102112

@@ -106,6 +116,7 @@ class Executor {
106116
HANDLE jobHandle;
107117
tstring_array argsArray;
108118
std::wstring appPath;
119+
std::function<void(HANDLE)> afterProcessCreatedCallback;
109120
};
110121

111122
#endif // #ifndef EXECUTOR_H
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
import java.io.IOException;
25+
import java.io.UncheckedIOException;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.nio.file.StandardOpenOption;
29+
import java.text.SimpleDateFormat;
30+
import java.util.Date;
31+
import java.util.List;
32+
33+
public class UseShutdownHook {
34+
35+
public static void main(String[] args) throws InterruptedException {
36+
trace("Started");
37+
38+
var outputFile = Path.of(args[0]);
39+
trace(String.format("Write output in [%s] file", outputFile));
40+
41+
var shutdownTimeoutSeconds = Integer.parseInt(args[1]);
42+
trace(String.format("Automatically shutdown the app in %ss", shutdownTimeoutSeconds));
43+
44+
Runtime.getRuntime().addShutdownHook(new Thread() {
45+
@Override
46+
public void run() {
47+
output(outputFile, "shutdown hook executed");
48+
}
49+
});
50+
51+
var startTime = System.currentTimeMillis();
52+
var lock = new Object();
53+
do {
54+
synchronized (lock) {
55+
lock.wait(shutdownTimeoutSeconds * 1000);
56+
}
57+
} while ((System.currentTimeMillis() - startTime) < (shutdownTimeoutSeconds * 1000));
58+
59+
output(outputFile, "exit");
60+
}
61+
62+
private static void output(Path outputFilePath, String msg) {
63+
64+
trace(String.format("Writing [%s] into [%s]", msg, outputFilePath));
65+
66+
try {
67+
Files.createDirectories(outputFilePath.getParent());
68+
Files.writeString(outputFilePath, msg, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
69+
} catch (IOException ex) {
70+
throw new UncheckedIOException(ex);
71+
}
72+
}
73+
74+
private static void trace(String msg) {
75+
Date time = new Date(System.currentTimeMillis());
76+
msg = String.format("UseShutdownHook [%s]: %s", SDF.format(time), msg);
77+
System.out.println(msg);
78+
try {
79+
Files.write(traceFile, List.of(msg), StandardOpenOption.APPEND, StandardOpenOption.CREATE);
80+
} catch (IOException ex) {
81+
throw new UncheckedIOException(ex);
82+
}
83+
}
84+
85+
private static final SimpleDateFormat SDF = new SimpleDateFormat("HH:mm:ss.SSS");
86+
87+
private static final Path traceFile = Path.of(System.getProperty("jpackage.test.trace-file"));
88+
}

test/jdk/tools/jpackage/helpers/jdk/jpackage/test/CfgFile.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -59,13 +59,18 @@ public String getValueUnchecked(String sectionName, String key) {
5959
}
6060
}
6161

62-
public void addValue(String sectionName, String key, String value) {
62+
public CfgFile addValue(String sectionName, String key, String value) {
6363
var section = getSection(sectionName);
6464
if (section == null) {
6565
section = new Section(sectionName, new ArrayList<>());
6666
data.add(section);
6767
}
6868
section.data.add(Map.entry(key, value));
69+
return this;
70+
}
71+
72+
public CfgFile add(CfgFile other) {
73+
return combine(this, other);
6974
}
7075

7176
public CfgFile() {
@@ -89,7 +94,7 @@ private CfgFile(List<Section> data, String id) {
8994
this.id = id;
9095
}
9196

92-
public void save(Path path) {
97+
public CfgFile save(Path path) {
9398
var lines = data.stream().flatMap(section -> {
9499
return Stream.concat(
95100
Stream.of(String.format("[%s]", section.name)),
@@ -98,6 +103,7 @@ public void save(Path path) {
98103
}));
99104
});
100105
TKit.createTextFile(path, lines);
106+
return this;
101107
}
102108

103109
private Section getSection(String name) {

test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.io.IOException;
3636
import java.nio.file.Files;
3737
import java.nio.file.Path;
38+
import java.time.Duration;
3839
import java.util.ArrayList;
3940
import java.util.Collection;
4041
import java.util.Collections;
@@ -296,7 +297,7 @@ PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa) {
296297
Files.deleteIfExists(appOutput);
297298

298299
List<String> expectedArgs = testRun.openFiles(testFiles);
299-
TKit.waitForFileCreated(appOutput, 7);
300+
TKit.waitForFileCreated(appOutput, Duration.ofSeconds(7), Duration.ofSeconds(3));
300301

301302
// Wait a little bit after file has been created to
302303
// make sure there are no pending writes into it.

test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@
4141
import java.nio.file.StandardWatchEventKinds;
4242
import java.nio.file.WatchEvent;
4343
import java.nio.file.WatchKey;
44-
import java.nio.file.WatchService;
4544
import java.text.SimpleDateFormat;
45+
import java.time.Duration;
46+
import java.time.Instant;
4647
import java.util.ArrayList;
4748
import java.util.Arrays;
4849
import java.util.Base64;
@@ -597,49 +598,56 @@ public static Path createRelativePathCopy(final Path file) {
597598
return file;
598599
}
599600

600-
static void waitForFileCreated(Path fileToWaitFor,
601-
long timeoutSeconds) throws IOException {
601+
public static void waitForFileCreated(Path fileToWaitFor,
602+
Duration timeout, Duration afterCreatedTimeout) throws IOException {
603+
waitForFileCreated(fileToWaitFor, timeout);
604+
// Wait after the file has been created to ensure it is fully written.
605+
ThrowingConsumer.<Duration>toConsumer(Thread::sleep).accept(afterCreatedTimeout);
606+
}
607+
608+
private static void waitForFileCreated(Path fileToWaitFor, Duration timeout) throws IOException {
602609

603610
trace(String.format("Wait for file [%s] to be available",
604611
fileToWaitFor.toAbsolutePath()));
605612

606-
WatchService ws = FileSystems.getDefault().newWatchService();
607-
608-
Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent();
609-
watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY);
610-
611-
long waitUntil = System.currentTimeMillis() + timeoutSeconds * 1000;
612-
for (;;) {
613-
long timeout = waitUntil - System.currentTimeMillis();
614-
assertTrue(timeout > 0, String.format(
615-
"Check timeout value %d is positive", timeout));
616-
617-
WatchKey key = ThrowingSupplier.toSupplier(() -> ws.poll(timeout,
618-
TimeUnit.MILLISECONDS)).get();
619-
if (key == null) {
620-
if (fileToWaitFor.toFile().exists()) {
621-
trace(String.format(
622-
"File [%s] is available after poll timeout expired",
623-
fileToWaitFor));
624-
return;
613+
try (var ws = FileSystems.getDefault().newWatchService()) {
614+
615+
Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent();
616+
watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY);
617+
618+
var waitUntil = Instant.now().plus(timeout);
619+
for (;;) {
620+
var remainderTimeout = Instant.now().until(waitUntil);
621+
assertTrue(remainderTimeout.isPositive(), String.format(
622+
"Check timeout value %dms is positive", remainderTimeout.toMillis()));
623+
624+
WatchKey key = ThrowingSupplier.toSupplier(() -> {
625+
return ws.poll(remainderTimeout.toMillis(), TimeUnit.MILLISECONDS);
626+
}).get();
627+
if (key == null) {
628+
if (Files.exists(fileToWaitFor)) {
629+
trace(String.format(
630+
"File [%s] is available after poll timeout expired",
631+
fileToWaitFor));
632+
return;
633+
}
634+
assertUnexpected(String.format("Timeout %dms expired", remainderTimeout.toMillis()));
625635
}
626-
assertUnexpected(String.format("Timeout expired", timeout));
627-
}
628636

629-
for (WatchEvent<?> event : key.pollEvents()) {
630-
if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
631-
continue;
632-
}
633-
Path contextPath = (Path) event.context();
634-
if (Files.isSameFile(watchDirectory.resolve(contextPath),
635-
fileToWaitFor)) {
636-
trace(String.format("File [%s] is available", fileToWaitFor));
637-
return;
637+
for (WatchEvent<?> event : key.pollEvents()) {
638+
if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
639+
continue;
640+
}
641+
Path contextPath = (Path) event.context();
642+
if (Files.exists(fileToWaitFor) && Files.isSameFile(watchDirectory.resolve(contextPath), fileToWaitFor)) {
643+
trace(String.format("File [%s] is available", fileToWaitFor));
644+
return;
645+
}
638646
}
639-
}
640647

641-
if (!key.reset()) {
642-
assertUnexpected("Watch key invalidated");
648+
if (!key.reset()) {
649+
assertUnexpected("Watch key invalidated");
650+
}
643651
}
644652
}
645653
}

0 commit comments

Comments
 (0)