From a4133ec4a3dfe77a7a2f30aebe78e39f709b7733 Mon Sep 17 00:00:00 2001 From: tlrx Date: Wed, 17 Dec 2014 18:00:53 +0100 Subject: [PATCH] Shutdown: Add support for Ctrl-Close event on Windows platforms to gracefully shutdown node This commit adds the support for the Ctrl-Close event on Windows using native system calls. This way, it is possible to catch the Ctrl-Close event sent by a 'taskill /pid' command (or when the user closes the console window where elasticsearch.bat was started) and gracefully close the node. Before this commit, the node was simply killed on taskkill/window closing. --- .../elasticsearch/bootstrap/Bootstrap.java | 34 ++--- .../common/jna/Kernel32Library.java | 129 ++++++++++++++++++ .../org/elasticsearch/common/jna/Natives.java | 19 +++ .../common/jna/Kernel32LibraryTests.java | 89 ++++++++++++ 4 files changed, 254 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/elasticsearch/common/jna/Kernel32Library.java create mode 100644 src/test/java/org/elasticsearch/common/jna/Kernel32LibraryTests.java diff --git a/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java b/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java index 0f39b536d02..04f397419a4 100644 --- a/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java +++ b/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java @@ -19,14 +19,12 @@ package org.elasticsearch.bootstrap; -import com.google.common.base.Charsets; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.Version; import org.elasticsearch.common.PidFile; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.CreationException; import org.elasticsearch.common.inject.spi.Message; -import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.common.jna.Natives; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; @@ -39,15 +37,13 @@ import org.elasticsearch.node.Node; import org.elasticsearch.node.NodeBuilder; import org.elasticsearch.node.internal.InternalSettingsPreparer; -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.nio.file.*; +import java.nio.file.Paths; import java.util.Locale; import java.util.Set; import java.util.concurrent.CountDownLatch; import static com.google.common.collect.Sets.newHashSet; +import static org.elasticsearch.common.jna.Kernel32Library.ConsoleCtrlHandler; import static org.elasticsearch.common.settings.ImmutableSettings.Builder.EMPTY_SETTINGS; /** @@ -62,11 +58,9 @@ public class Bootstrap { private static Bootstrap bootstrap; private void setup(boolean addShutdownHook, Tuple tuple) throws Exception { -// Loggers.getLogger(Bootstrap.class, tuple.v1().get("name")).info("heap_size {}/{}", JvmStats.jvmStats().mem().heapCommitted(), JvmInfo.jvmInfo().mem().heapMax()); if (tuple.v1().getAsBoolean("bootstrap.mlockall", false)) { Natives.tryMlockall(); } - tuple = setupJmx(tuple); NodeBuilder nodeBuilder = NodeBuilder.nodeBuilder().settings(tuple.v1()).loadConfigSettings(false); node = nodeBuilder.build(); @@ -78,16 +72,22 @@ public class Bootstrap { } }); } - } - private static Tuple setupJmx(Tuple tuple) { - // We disable JMX on by default, since we don't really want the overhead of RMI (and RMI GC...) -// if (tuple.v1().get(JmxService.SettingsConstants.CREATE_CONNECTOR) == null) { -// // automatically create the connector if we are bootstrapping -// Settings updated = settingsBuilder().put(tuple.v1()).put(JmxService.SettingsConstants.CREATE_CONNECTOR, true).build(); -// tuple = new Tuple(updated, tuple.v2()); -// } - return tuple; + if (tuple.v1().getAsBoolean("bootstrap.ctrlhandler", true)) { + Natives.addConsoleCtrlHandler(new ConsoleCtrlHandler() { + @Override + public boolean handle(int code) { + if (CTRL_CLOSE_EVENT == code) { + ESLogger logger = Loggers.getLogger(Bootstrap.class); + logger.info("running graceful exit on windows"); + + System.exit(0); + return true; + } + return false; + } + }); + } } private static void setupLogging(Tuple tuple) { diff --git a/src/main/java/org/elasticsearch/common/jna/Kernel32Library.java b/src/main/java/org/elasticsearch/common/jna/Kernel32Library.java new file mode 100644 index 00000000000..68005f1a3ca --- /dev/null +++ b/src/main/java/org/elasticsearch/common/jna/Kernel32Library.java @@ -0,0 +1,129 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.jna; + +import com.google.common.collect.ImmutableList; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.win32.StdCallLibrary; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Library for Windows/Kernel32 + */ +public class Kernel32Library { + + private static ESLogger logger = Loggers.getLogger(Kernel32Library.class); + + private Kernel32 internal; + + private List callbacks = new ArrayList<>(); + + private final static class Holder { + private final static Kernel32Library instance = new Kernel32Library(); + } + + private Kernel32Library() { + try { + internal = (Kernel32)Native.synchronizedLibrary((Kernel32)Native.loadLibrary("kernel32", Kernel32.class)); + logger.debug("windows/Kernel32 library loaded"); + } catch (NoClassDefFoundError e) { + logger.warn("JNA not found. native methods and handlers will be disabled."); + } catch (UnsatisfiedLinkError e) { + logger.warn("unable to link Windows/Kernel32 library. native methods and handlers will be disabled."); + } + } + + public static Kernel32Library getInstance() { + return Holder.instance; + } + + public boolean addConsoleCtrlHandler(ConsoleCtrlHandler handler) { + if (internal == null) { + throw new UnsupportedOperationException("windows/Kernel32 library not loaded, console ctrl handler cannot be set"); + } + boolean result = false; + if (handler != null) { + NativeHandlerCallback callback = new NativeHandlerCallback(handler); + result = internal.SetConsoleCtrlHandler(callback, true); + if (result) { + callbacks.add(callback); + } + } + return result; + } + + public ImmutableList getCallbacks() { + return ImmutableList.builder().addAll(callbacks).build(); + } + + interface Kernel32 extends Library { + + /** + * Registers a Console Ctrl Handler. + * + * @param handler + * @param add + * @return true if the handler is correctly set + */ + public boolean SetConsoleCtrlHandler(StdCallLibrary.StdCallCallback handler, boolean add); + } + + /** + * Handles consoles event with WIN API + *

+ * See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683242%28v=vs.85%29.aspx + */ + class NativeHandlerCallback implements StdCallLibrary.StdCallCallback { + + private final ConsoleCtrlHandler handler; + + public NativeHandlerCallback(ConsoleCtrlHandler handler) { + this.handler = handler; + } + + public boolean callback(long dwCtrlType) { + int event = (int) dwCtrlType; + if (logger.isDebugEnabled()) { + logger.debug("console control handler receives event [{}@{}]", event, dwCtrlType); + + } + return handler.handle(event); + } + } + + public interface ConsoleCtrlHandler { + + public static final int CTRL_CLOSE_EVENT = 2; + + /** + * Handles the Ctrl event. + * + * @param code the code corresponding to the Ctrl sent. + * @return true if the handler processed the event, false otherwise. If false, the next handler will be called. + */ + boolean handle(int code); + } +} diff --git a/src/main/java/org/elasticsearch/common/jna/Natives.java b/src/main/java/org/elasticsearch/common/jna/Natives.java index 8d5743d86cf..e1268b05561 100644 --- a/src/main/java/org/elasticsearch/common/jna/Natives.java +++ b/src/main/java/org/elasticsearch/common/jna/Natives.java @@ -20,6 +20,8 @@ package org.elasticsearch.common.jna; import com.sun.jna.Native; +import org.apache.lucene.util.Constants; +import org.elasticsearch.common.jna.Kernel32Library.ConsoleCtrlHandler; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; @@ -59,4 +61,21 @@ public class Natives { } } } + + public static void addConsoleCtrlHandler(ConsoleCtrlHandler handler) { + // The console Ctrl handler is necessary on Windows platforms only. + if (Constants.WINDOWS) { + try { + boolean result = Kernel32Library.getInstance().addConsoleCtrlHandler(handler); + if (result) { + logger.debug("console ctrl handler correctly set"); + } else { + logger.warn("unknown error " + Native.getLastError() + " when adding console ctrl handler:"); + } + } catch (UnsatisfiedLinkError e) { + // this will have already been logged by Kernel32Library, no need to repeat it + } + } + } + } diff --git a/src/test/java/org/elasticsearch/common/jna/Kernel32LibraryTests.java b/src/test/java/org/elasticsearch/common/jna/Kernel32LibraryTests.java new file mode 100644 index 00000000000..18497f64ae6 --- /dev/null +++ b/src/test/java/org/elasticsearch/common/jna/Kernel32LibraryTests.java @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.jna; + +import org.apache.lucene.util.Constants; +import org.elasticsearch.common.jna.Kernel32Library.ConsoleCtrlHandler; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; + +public class Kernel32LibraryTests extends ElasticsearchTestCase { + + /** + * Those properties are set by the JNA Api and if not ignored, + * lead to tests failure (see AbstractRandomizedTest#IGNORED_INVARIANT_PROPERTIES) + */ + private static final String[] JNA_INVARIANT_PROPERTIES = { + "jna.platform.library.path", + "jnidispatch.path" + }; + + private Map properties = new HashMap<>(); + + @Before + public void saveProperties() { + for (String p : JNA_INVARIANT_PROPERTIES) { + properties.put(p, System.getProperty(p)); + } + } + + @After + public void restoreProperties() { + for (String p : JNA_INVARIANT_PROPERTIES) { + if (properties.get(p) != null) { + System.setProperty(p, properties.get(p)); + } else { + System.clearProperty(p); + } + } + } + + @Test + public void testKernel32Library() { + ConsoleCtrlHandler handler = new ConsoleCtrlHandler() { + @Override + public boolean handle(int code) { + return false; + } + }; + + assertNotNull(Kernel32Library.getInstance()); + assertThat(Kernel32Library.getInstance().getCallbacks().size(), equalTo(0)); + + if (Constants.WINDOWS) { + assertTrue(Kernel32Library.getInstance().addConsoleCtrlHandler(handler)); + assertThat(Kernel32Library.getInstance().getCallbacks().size(), equalTo(1)); + } else { + try { + Kernel32Library.getInstance().addConsoleCtrlHandler(handler); + fail("should have thrown an unsupported operation exception"); + } catch (UnsupportedOperationException e) { + assertThat(e.getMessage(), e.getMessage().contains("console ctrl handler cannot be set"), equalTo(true)); + } + } + } +}