Skip to content

Commit 78564fd

Browse files
committed
8365790: Shutdown hook for application image does not work on Windows
Reviewed-by: mbaesken Backport-of: 569d08bdf1350ec423369b507c5d8775499a141a
1 parent bc6cb61 commit 78564fd

File tree

9 files changed

+405
-43
lines changed

9 files changed

+405
-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
@@ -210,6 +210,16 @@ class RunExecutorWithMsgLoop {
210210
};
211211

212212

213+
void enableConsoleCtrlHandler(bool enable) {
214+
if (!SetConsoleCtrlHandler(NULL, enable ? FALSE : TRUE)) {
215+
JP_THROW(SysError(tstrings::any() << "SetConsoleCtrlHandler(NULL, "
216+
<< (enable ? "FALSE" : "TRUE")
217+
<< ") failed",
218+
SetConsoleCtrlHandler));
219+
}
220+
}
221+
222+
213223
void launchApp() {
214224
// [RT-31061] otherwise UI can be left in back of other windows.
215225
::AllowSetForegroundWindow(ASFW_ANY);
@@ -256,6 +266,19 @@ void launchApp() {
256266
exec.arg(arg);
257267
});
258268

269+
exec.afterProcessCreated([&](HANDLE pid) {
270+
//
271+
// Ignore Ctrl+C in the current process.
272+
// This will prevent child process termination without allowing
273+
// it to handle Ctrl+C events.
274+
//
275+
// Disable the default Ctrl+C handler *after* the child process
276+
// has been created as it is inheritable and we want the child
277+
// process to have the default handler.
278+
//
279+
enableConsoleCtrlHandler(false);
280+
});
281+
259282
DWORD exitCode = RunExecutorWithMsgLoop::apply(exec);
260283

261284
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
@@ -27,6 +27,7 @@
2727
import java.io.IOException;
2828
import java.nio.file.Files;
2929
import java.nio.file.Path;
30+
import java.time.Duration;
3031
import java.util.ArrayList;
3132
import java.util.Collection;
3233
import java.util.Collections;
@@ -270,7 +271,7 @@ PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa,
270271
TKit.trace(String.format("Use desktop to open [%s] file",
271272
testFile));
272273
Desktop.getDesktop().open(testFile.toFile());
273-
TKit.waitForFileCreated(appOutput, 7);
274+
TKit.waitForFileCreated(appOutput, Duration.ofSeconds(7), Duration.ofSeconds(3));
274275

275276
List<String> expectedArgs = new ArrayList<>(List.of(
276277
faLauncherDefaultArgs));

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

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@
3535
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
3636
import java.nio.file.WatchEvent;
3737
import java.nio.file.WatchKey;
38-
import java.nio.file.WatchService;
3938
import java.text.SimpleDateFormat;
39+
import java.time.Duration;
40+
import java.time.Instant;
4041
import java.util.ArrayList;
4142
import java.util.Arrays;
4243
import java.util.Collection;
@@ -242,6 +243,13 @@ public static void createPropertiesFile(Path propsFilename,
242243
trace("Done");
243244
}
244245

246+
public static void traceFileContents(Path path, String label) throws IOException {
247+
assertFileExists(path);
248+
trace(String.format("Dump [%s] %s...", path, label));
249+
Files.readAllLines(path).forEach(TKit::trace);
250+
trace("Done");
251+
}
252+
245253
public static void createPropertiesFile(Path propsFilename,
246254
Map.Entry<String, String>... props) {
247255
createPropertiesFile(propsFilename, List.of(props));
@@ -521,49 +529,57 @@ public static Path createRelativePathCopy(final Path file) {
521529
return file;
522530
}
523531

524-
static void waitForFileCreated(Path fileToWaitFor,
525-
long timeoutSeconds) throws IOException {
532+
public static void waitForFileCreated(Path fileToWaitFor,
533+
Duration timeout, Duration afterCreatedTimeout) throws IOException {
534+
waitForFileCreated(fileToWaitFor, timeout);
535+
// Wait after the file has been created to ensure it is fully written.
536+
ThrowingConsumer.<Long>toConsumer(Thread::sleep).accept(afterCreatedTimeout.getSeconds());
537+
}
538+
539+
private static void waitForFileCreated(Path fileToWaitFor, Duration timeout) throws IOException {
526540

527541
trace(String.format("Wait for file [%s] to be available",
528542
fileToWaitFor.toAbsolutePath()));
529543

530-
WatchService ws = FileSystems.getDefault().newWatchService();
531-
532-
Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent();
533-
watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY);
534-
535-
long waitUntil = System.currentTimeMillis() + timeoutSeconds * 1000;
536-
for (;;) {
537-
long timeout = waitUntil - System.currentTimeMillis();
538-
assertTrue(timeout > 0, String.format(
539-
"Check timeout value %d is positive", timeout));
540-
541-
WatchKey key = ThrowingSupplier.toSupplier(() -> ws.poll(timeout,
542-
TimeUnit.MILLISECONDS)).get();
543-
if (key == null) {
544-
if (fileToWaitFor.toFile().exists()) {
545-
trace(String.format(
546-
"File [%s] is available after poll timeout expired",
547-
fileToWaitFor));
548-
return;
544+
try (var ws = FileSystems.getDefault().newWatchService()) {
545+
546+
Path watchDirectory = fileToWaitFor.toAbsolutePath().getParent();
547+
watchDirectory.register(ws, ENTRY_CREATE, ENTRY_MODIFY);
548+
549+
var waitUntil = Instant.now().plus(timeout);
550+
for (;;) {
551+
Instant n = Instant.now();
552+
Duration remainderTimeout = Duration.between(n, waitUntil);
553+
assertTrue(!remainderTimeout.isNegative() && !remainderTimeout.isZero(), String.format(
554+
"Check timeout value %dms is positive", remainderTimeout.toMillis()));
555+
556+
WatchKey key = ThrowingSupplier.toSupplier(() -> {
557+
return ws.poll(remainderTimeout.toMillis(), TimeUnit.MILLISECONDS);
558+
}).get();
559+
if (key == null) {
560+
if (Files.exists(fileToWaitFor)) {
561+
trace(String.format(
562+
"File [%s] is available after poll timeout expired",
563+
fileToWaitFor));
564+
return;
565+
}
566+
assertUnexpected(String.format("Timeout %dms expired", remainderTimeout.toMillis()));
549567
}
550-
assertUnexpected(String.format("Timeout expired", timeout));
551-
}
552568

553-
for (WatchEvent<?> event : key.pollEvents()) {
554-
if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
555-
continue;
556-
}
557-
Path contextPath = (Path) event.context();
558-
if (Files.isSameFile(watchDirectory.resolve(contextPath),
559-
fileToWaitFor)) {
560-
trace(String.format("File [%s] is available", fileToWaitFor));
561-
return;
569+
for (WatchEvent<?> event : key.pollEvents()) {
570+
if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
571+
continue;
572+
}
573+
Path contextPath = (Path) event.context();
574+
if (Files.exists(fileToWaitFor) && Files.isSameFile(watchDirectory.resolve(contextPath), fileToWaitFor)) {
575+
trace(String.format("File [%s] is available", fileToWaitFor));
576+
return;
577+
}
562578
}
563-
}
564579

565-
if (!key.reset()) {
566-
assertUnexpected("Watch key invalidated");
580+
if (!key.reset()) {
581+
assertUnexpected("Watch key invalidated");
582+
}
567583
}
568584
}
569585
}

0 commit comments

Comments
 (0)