Skip to content

Commit 569d08b

Browse files
committed
8365790: Shutdown hook for application image does not work on Windows
Reviewed-by: mbaesken Backport-of: 8b686d9ee4669d31b306d7a3337a0b57e95c9a4c
1 parent 1302534 commit 569d08b

File tree

9 files changed

+397
-43
lines changed

9 files changed

+397
-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
@@ -226,6 +226,16 @@ class RunExecutorWithMsgLoop {
226226
};
227227

228228

229+
void enableConsoleCtrlHandler(bool enable) {
230+
if (!SetConsoleCtrlHandler(NULL, enable ? FALSE : TRUE)) {
231+
JP_THROW(SysError(tstrings::any() << "SetConsoleCtrlHandler(NULL, "
232+
<< (enable ? "FALSE" : "TRUE")
233+
<< ") failed",
234+
SetConsoleCtrlHandler));
235+
}
236+
}
237+
238+
229239
void launchApp() {
230240
// [RT-31061] otherwise UI can be left in back of other windows.
231241
::AllowSetForegroundWindow(ASFW_ANY);
@@ -279,6 +289,19 @@ void launchApp() {
279289
exec.arg(arg);
280290
});
281291

292+
exec.afterProcessCreated([&](HANDLE pid) {
293+
//
294+
// Ignore Ctrl+C in the current process.
295+
// This will prevent child process termination without allowing
296+
// it to handle Ctrl+C events.
297+
//
298+
// Disable the default Ctrl+C handler *after* the child process
299+
// has been created as it is inheritable and we want the child
300+
// process to have the default handler.
301+
//
302+
enableConsoleCtrlHandler(false);
303+
});
304+
282305
DWORD exitCode = RunExecutorWithMsgLoop::apply(exec);
283306

284307
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
@@ -26,6 +26,7 @@
2626
import java.io.IOException;
2727
import java.nio.file.Files;
2828
import java.nio.file.Path;
29+
import java.time.Duration;
2930
import java.util.ArrayList;
3031
import java.util.Collection;
3132
import java.util.Collections;
@@ -273,7 +274,7 @@ PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa) {
273274
Files.deleteIfExists(appOutput);
274275

275276
List<String> expectedArgs = testRun.openFiles(testFiles);
276-
TKit.waitForFileCreated(appOutput, 7);
277+
TKit.waitForFileCreated(appOutput, Duration.ofSeconds(7), Duration.ofSeconds(3));
277278

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

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

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@
3636
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
3737
import java.nio.file.WatchEvent;
3838
import java.nio.file.WatchKey;
39-
import java.nio.file.WatchService;
4039
import java.text.SimpleDateFormat;
40+
import java.time.Duration;
41+
import java.time.Instant;
4142
import java.util.ArrayList;
4243
import java.util.Arrays;
4344
import java.util.Collection;
@@ -531,49 +532,57 @@ public static Path createRelativePathCopy(final Path file) {
531532
return file;
532533
}
533534

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

537544
trace(String.format("Wait for file [%s] to be available",
538545
fileToWaitFor.toAbsolutePath()));
539546

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

563-
for (WatchEvent<?> event : key.pollEvents()) {
564-
if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
565-
continue;
566-
}
567-
Path contextPath = (Path) event.context();
568-
if (Files.isSameFile(watchDirectory.resolve(contextPath),
569-
fileToWaitFor)) {
570-
trace(String.format("File [%s] is available", fileToWaitFor));
571-
return;
572+
for (WatchEvent<?> event : key.pollEvents()) {
573+
if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
574+
continue;
575+
}
576+
Path contextPath = (Path) event.context();
577+
if (Files.exists(fileToWaitFor) && Files.isSameFile(watchDirectory.resolve(contextPath), fileToWaitFor)) {
578+
trace(String.format("File [%s] is available", fileToWaitFor));
579+
return;
580+
}
572581
}
573-
}
574582

575-
if (!key.reset()) {
576-
assertUnexpected("Watch key invalidated");
583+
if (!key.reset()) {
584+
assertUnexpected("Watch key invalidated");
585+
}
577586
}
578587
}
579588
}

0 commit comments

Comments
 (0)