From be5a29fa91d74a1b4473b2ccebf11b60cac46ed5 Mon Sep 17 00:00:00 2001 From: Sergey Simonchik Date: Thu, 22 Nov 2018 08:42:51 +0300 Subject: [PATCH] run sendctrlc.exe with Java (no new console window, simpler code) (#60, #55) Additionally, diagnostics is improved by capturing output stream. --- native/java-interface.cpp | 5 - native/java-interface.h | 8 -- native/winp.cpp | 57 ---------- native/winp.h | 2 - src/main/java/org/jvnet/winp/CtrlCSender.java | 106 ++++++++++++++++++ src/main/java/org/jvnet/winp/Native.java | 5 +- 6 files changed, 108 insertions(+), 75 deletions(-) create mode 100644 src/main/java/org/jvnet/winp/CtrlCSender.java diff --git a/native/java-interface.cpp b/native/java-interface.cpp index bbb369c..a9aa34c 100644 --- a/native/java-interface.cpp +++ b/native/java-interface.cpp @@ -7,11 +7,6 @@ JNIEXPORT jboolean JNICALL Java_org_jvnet_winp_Native_kill(JNIEnv* env, jclass c return KillProcessEx(pid, recursive); } -JNIEXPORT jboolean JNICALL Java_org_jvnet_winp_Native_sendCtrlC(JNIEnv* env, jclass clazz, jint pid, jstring sendctrlcExePath) { - const wchar_t* exePath = (wchar_t*)env->GetStringChars(sendctrlcExePath, NULL); - return SendCtrlC(env, clazz, pid, exePath); -} - JNIEXPORT jint JNICALL Java_org_jvnet_winp_Native_setPriority(JNIEnv* env, jclass clazz, jint pid, jint priority) { auto_handle hProcess = OpenProcess(PROCESS_SET_INFORMATION, FALSE, pid); if(hProcess && SetPriorityClass(hProcess, priority)) { diff --git a/native/java-interface.h b/native/java-interface.h index 91c72a0..36c7db3 100644 --- a/native/java-interface.h +++ b/native/java-interface.h @@ -15,14 +15,6 @@ extern "C" { JNIEXPORT jboolean JNICALL Java_org_jvnet_winp_Native_kill (JNIEnv *, jclass, jint, jboolean); -/* - * Class: org_jvnet_winp_Native - * Method: sendCtrlC - * Signature: (ILjava/lang/String)Z - */ -JNIEXPORT jboolean JNICALL Java_org_jvnet_winp_Native_sendCtrlC - (JNIEnv *, jclass, jint, jstring); - /* * Class: org_jvnet_winp_Native * Method: isCriticalProcess diff --git a/native/winp.cpp b/native/winp.cpp index 01751d0..f977d41 100644 --- a/native/winp.cpp +++ b/native/winp.cpp @@ -13,63 +13,6 @@ // This file uses a long buffer, because it prints executable paths in some cases. #define ERRMSG_SIZE 512 -//--------------------------------------------------------------------------- -// SendCtrlC -// -// Sends CTRL+C to the specified process. -// -// Parameters: -// dwProcessId - identifier of the process to terminate -// -// Returns: -// TRUE, if successful, FALSE - otherwise. -// When used from JNI, exceptions may be thrown instead -// -BOOL WINAPI SendCtrlC(JNIEnv* pEnv, jclass clazz, IN DWORD dwProcessId, const wchar_t* pExePath) { - char errorBuffer[ERRMSG_SIZE]; - STARTUPINFO si; - PROCESS_INFORMATION pi; - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - ZeroMemory(&pi, sizeof(pi)); - - std::wstring exepath(pExePath); - std::wstring cmd = L'"' + exepath + L"\" " + std::to_wstring(dwProcessId); - std::vector cmd_buffer(cmd.begin(), cmd.end()); // with C++17, could just use cmd.data() - - BOOL started = CreateProcessW(NULL, &cmd_buffer[0], NULL, NULL, - FALSE, 0, NULL, NULL, &si, &pi); - - BOOL success = FALSE; - if (started) { - // wait for termination if the process started, max. 5 secs - DWORD ret = WaitForSingleObject(pi.hProcess, 5000); - if (ret != WAIT_OBJECT_0) { - sprintf_s(errorBuffer, "Failed to send Ctrl+C to process with pid=%d. WaitForSingleObject exit code: %d (last error: d).", dwProcessId, ret, GetLastError()); - reportError(pEnv, errorBuffer); - } - - // then set success flag if the exit code was 0 - DWORD exit_code; - if (GetExitCodeProcess(pi.hProcess, &exit_code) != FALSE) { - success = (exit_code == 0); - if (exit_code != 0) { - sprintf_s(errorBuffer, "External Ctrl+C execution failed for process pid=%d. Ctrl+C process exited with code %d: %s.", dwProcessId, exit_code, - (exit_code == -1) ? "Wrong arguments" : "Failed to attach to the console (see the AttachConsole WinAPI call)"); - reportError(pEnv, errorBuffer); - } - } - } else { - sprintf_s(errorBuffer, "Failed to send Ctrl+C to process with pid=%d. Signal process did not start: %s.", dwProcessId, cmd); - reportError(pEnv, errorBuffer); - } - - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - - return success; -} - //--------------------------------------------------------------------------- // KillProcess // diff --git a/native/winp.h b/native/winp.h index 873fc66..8c438ae 100644 --- a/native/winp.h +++ b/native/winp.h @@ -6,8 +6,6 @@ #define reportError(env,msg) error(env,__FILE__,__LINE__,msg); void error(JNIEnv* env, const char* file, int line, const char* msg); -BOOL WINAPI SendCtrlC(JNIEnv* pEnv, jclass clazz, IN DWORD dwProcessId, const wchar_t* pExePath); - // // Kernel32.dll // diff --git a/src/main/java/org/jvnet/winp/CtrlCSender.java b/src/main/java/org/jvnet/winp/CtrlCSender.java new file mode 100644 index 0000000..be0425b --- /dev/null +++ b/src/main/java/org/jvnet/winp/CtrlCSender.java @@ -0,0 +1,106 @@ +package org.jvnet.winp; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.util.Locale; + +class CtrlCSender { + + private static final int TIMEOUT_MILLIS = 5000; + + static boolean sendCtrlC(int pid, String ctrlCExePath) { + ProcessBuilder builder = new ProcessBuilder(ctrlCExePath, String.valueOf(pid)); + builder.redirectErrorStream(true); + Process process; + try { + process = builder.start(); + } catch (IOException e) { + throw new WinpException(e); + } + StreamGobbler stdout = new StreamGobbler(new InputStreamReader(process.getInputStream(), Charset.defaultCharset())); + Integer exitCode = null; + try { + exitCode = waitFor(process); + } catch (InterruptedException ignored) { + } + stdout.stop(); + if (exitCode == null) { + process.destroy(); + throw new WinpException("Failed to send Ctrl+C to " + pid + ": " + TIMEOUT_MILLIS + " ms timeout exceeded"); + } + if (exitCode == 0) { + return true; + } + throw new WinpException("Failed to send Ctrl+C, " + new File(ctrlCExePath).getName() + + " terminated with exit code " + stringifyExitCode(exitCode) + ", output: " + stdout.getText()); + } + + private static String stringifyExitCode(int exitCode) { + if (exitCode >= 0xC0000000 && exitCode < 0xD0000000) { + // http://support.microsoft.com/kb/308558: + // If the result code has the "C0000XXX" format, the task did not complete successfully (the "C" indicates an error condition). + // The most common "C" error code is "0xC000013A: The application terminated as a result of a CTRL+C". + return exitCode + " (0x" + Integer.toHexString(exitCode).toUpperCase(Locale.ENGLISH) + ")"; + } + return String.valueOf(exitCode); + } + + private static Integer waitFor(Process process) throws InterruptedException { + long endTime = System.currentTimeMillis() + TIMEOUT_MILLIS; + int i = 0; + do { + try { + return process.exitValue(); + } + catch (IllegalThreadStateException ignore) { + Thread.sleep(i++ < 3 ? 10 : i < 5 ? 30 : 100); + } + } while (System.currentTimeMillis() < endTime); + return null; + } + + private static class StreamGobbler implements Runnable { + + private final Reader reader; + private final StringBuilder myBuffer = new StringBuilder(); + private final Thread thread; + private boolean isStopped = false; + + private StreamGobbler(Reader reader) { + this.reader = reader; + this.thread = new Thread(this, "sendctrlc.exe output reader"); + this.thread.start(); + } + + public void run() { + char[] buf = new char[8192]; + try { + int readCount; + while (!isStopped && (readCount = reader.read(buf)) >= 0) { + myBuffer.append(buf, 0, readCount); + } + if (isStopped) { + myBuffer.append("Failed to read output: force stopped"); + } + } + catch (Exception e) { + myBuffer.append("Failed to read output: ").append(e.getClass().getName()).append(" raised"); + } + } + + private void stop() { + try { + this.thread.join(1000); // await to read whole buffered output + } catch (InterruptedException ignored) { + } + this.isStopped = true; + } + + private String getText() { + return myBuffer.toString(); + } + } +} diff --git a/src/main/java/org/jvnet/winp/Native.java b/src/main/java/org/jvnet/winp/Native.java index ccf85a0..9455b73 100755 --- a/src/main/java/org/jvnet/winp/Native.java +++ b/src/main/java/org/jvnet/winp/Native.java @@ -24,7 +24,6 @@ class Native { public static final String CTRLCEXE_NAME = "64".equals(System.getProperty("sun.arch.data.model")) ? "sendctrlc.x64" : "sendctrlc"; native static boolean kill(int pid, boolean recursive); - native static boolean sendCtrlC(int pid, String sendctrlcExePath); native static boolean isCriticalProcess(int pid); native static boolean isProcessRunning(int pid); native static int setPriority(int pid, int value); @@ -75,7 +74,7 @@ class Native { /** * Sends Ctrl+C to the process. - * Due to the Windows platform specifics, this execution will spawn a separate thread to deliver the signal. + * Due to the Windows platform specifics, this execution will spawn a separate process to deliver the signal. * This process is expected to be executed within a 5-second timeout. * @param pid PID to receive the signal * @return {@code true} if the signal was delivered successfully @@ -87,7 +86,7 @@ public static boolean sendCtrlC(int pid) throws WinpException { LOGGER.log(Level.WARNING, "Cannot send the CtrlC signal to the process. Cannot find the executable {0}.dll", CTRLCEXE_NAME); return false; } - return sendCtrlC(pid, ctrlCExePath); + return CtrlCSender.sendCtrlC(pid, ctrlCExePath); } static {