diff --git a/build.cmd b/build.cmd index 13fd676..4d62f48 100644 --- a/build.cmd +++ b/build.cmd @@ -34,6 +34,10 @@ msbuild winp.vcxproj /t:Clean /p:Configuration=%configuration% /verbosity:minima if %errorlevel% neq 0 exit /b %errorlevel% msbuild winp.vcxproj /t:Clean /p:Configuration=%configuration% /verbosity:minimal /nologo /p:Platform="x64" if %errorlevel% neq 0 exit /b %errorlevel% +msbuild sendctrlc\sendctrlc.vcxproj /t:Clean /p:Configuration=Release /verbosity:minimal /nologo /p:Platform="Win32" +if %errorlevel% neq 0 exit /b %errorlevel% +msbuild sendctrlc\sendctrlc.vcxproj /t:Clean /p:Configuration=Release /verbosity:minimal /nologo /p:Platform="x64" +if %errorlevel% neq 0 exit /b %errorlevel% msbuild ..\native_test\testapp\testapp.vcxproj /t:Clean /p:Configuration=Release /verbosity:minimal /nologo /p:Platform="Win32" if %errorlevel% neq 0 exit /b %errorlevel% msbuild ..\native_test\testapp\testapp.vcxproj /t:Clean /p:Configuration=Release /verbosity:minimal /nologo /p:Platform="x64" @@ -48,6 +52,10 @@ msbuild winp.vcxproj /p:Configuration=%configuration% /nologo /p:Platform="Win32 if %errorlevel% neq 0 exit /b %errorlevel% msbuild winp.vcxproj /p:Configuration=%configuration% /nologo /p:Platform="x64" if %errorlevel% neq 0 exit /b %errorlevel% +msbuild sendctrlc\sendctrlc.vcxproj /p:Configuration=%configuration% /nologo /p:Platform="Win32" +if %errorlevel% neq 0 exit /b %errorlevel% +msbuild sendctrlc\sendctrlc.vcxproj /p:Configuration=%configuration% /nologo /p:Platform="x64" +if %errorlevel% neq 0 exit /b %errorlevel% echo ### Building test applications msbuild ..\native_test\testapp\testapp.vcxproj /verbosity:minimal /p:Configuration=Release /nologo /p:Platform="Win32" @@ -61,6 +69,10 @@ COPY native\%configuration%\winp.dll src\main\resources\winp.dll if %errorlevel% neq 0 exit /b %errorlevel% COPY native\x64\%configuration%\winp.dll src\main\resources\winp.x64.dll if %errorlevel% neq 0 exit /b %errorlevel% +COPY native\sendctrlc\Win32\%configuration%\sendctrlc.exe src\main\resources\sendctrlc.exe +if %errorlevel% neq 0 exit /b %errorlevel% +COPY native\sendctrlc\x64\%configuration%\sendctrlc.exe src\main\resources\sendctrlc.x64.exe +if %errorlevel% neq 0 exit /b %errorlevel% echo ### Build and Test winp.jar for %version% cd %BUIDROOT% diff --git a/native/java-interface.cpp b/native/java-interface.cpp index a9aa34c..7e72cdc 100644 --- a/native/java-interface.cpp +++ b/native/java-interface.cpp @@ -7,6 +7,11 @@ 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(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 aebbaf0..b85bfd1 100644 --- a/native/java-interface.h +++ b/native/java-interface.h @@ -15,6 +15,14 @@ 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/runtime.cpp b/native/runtime.cpp index f316f28..39a37ca 100644 --- a/native/runtime.cpp +++ b/native/runtime.cpp @@ -1,10 +1,13 @@ #include "stdafx.h" #include "winp.h" +#include +HANDLE hDllInst; LPFN_ISWOW64PROCESS fnIsWow64Process; extern "C" BOOL WINAPI DllMain(HANDLE hInst, ULONG dwReason, LPVOID lpReserved) { + hDllInst = hInst; fnIsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress( GetModuleHandle(TEXT("kernel32")), "IsWow64Process"); return TRUE; diff --git a/native/sendctrlc/main.cpp b/native/sendctrlc/main.cpp new file mode 100644 index 0000000..964f82e --- /dev/null +++ b/native/sendctrlc/main.cpp @@ -0,0 +1,24 @@ + +#define STRICT +#define WIN32_LEAN_AND_MEAN +#include + +#include +#include + +int main(int argc, char** argv) { + if (argc < 2) { + return 2; + } + + int pid = atoi(argv[1]); + + FreeConsole(); + if (!AttachConsole(pid)) { + return 1; + } + + SetConsoleCtrlHandler(NULL, TRUE); + GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); + return 0; +} diff --git a/native/sendctrlc/sendctrlc.vcxproj b/native/sendctrlc/sendctrlc.vcxproj new file mode 100644 index 0000000..396222b --- /dev/null +++ b/native/sendctrlc/sendctrlc.vcxproj @@ -0,0 +1,160 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {AF1488B6-BF5B-4F3F-8AB7-5114D5BF93B1} + Win32Proj + sendctrlc32 + sendctrlc + + + + Application + true + v120 + Unicode + + + Application + true + v120 + Unicode + + + Application + false + v120 + true + Unicode + + + Application + false + v120 + true + Unicode + + + + + + + + + + + + + + + + + + + true + $(MSBuildProjectDirectory)\$(Platform)\$(Configuration)\ + + + true + $(MSBuildProjectDirectory)\$(Platform)\$(Configuration)\ + + + false + $(MSBuildProjectDirectory)\$(Platform)\$(Configuration)\ + + + false + $(MSBuildProjectDirectory)\$(Platform)\$(Configuration)\ + + + + + + Level3 + Disabled + WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + + + Console + true + $(OutDir)$(TargetName)$(TargetExt) + + + + + + + Level3 + Disabled + WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + + + Console + true + $(OutDir)$(TargetName)$(TargetExt) + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + + + Console + true + true + true + $(OutDir)$(TargetName)$(TargetExt) + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) + true + + + Console + true + true + true + $(OutDir)$(TargetName)$(TargetExt) + + + + + + + + + \ No newline at end of file diff --git a/native/winp.cpp b/native/winp.cpp index b4eac97..bec3b67 100644 --- a/native/winp.cpp +++ b/native/winp.cpp @@ -6,6 +6,52 @@ #include "auto_handle.h" #include "java-interface.h" +#include +#include + +//--------------------------------------------------------------------------- +// SendCtrlC +// +// Sends CTRL+C to the specified process. +// +// Parameters: +// dwProcessId - identifier of the process to terminate +// +// Returns: +// TRUE, if successful, FALSE - otherwise. +// +BOOL WINAPI SendCtrlC(IN DWORD dwProcessId, const wchar_t* pExePath) { + 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 + WaitForSingleObject(pi.hProcess, 5000); + + // then set success flag if the exit code was 0 + DWORD exit_code; + if (GetExitCodeProcess(pi.hProcess, &exit_code) != FALSE) { + success = (exit_code == 0); + } + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + return success; +} + //--------------------------------------------------------------------------- // KillProcess // diff --git a/native/winp.h b/native/winp.h index 1c197c5..1bd5e80 100644 --- a/native/winp.h +++ b/native/winp.h @@ -6,6 +6,8 @@ #define reportError(env,msg) error(env,__FILE__,__LINE__,msg); void error(JNIEnv* env, const char* file, int line, const char* msg); +BOOL WINAPI SendCtrlC(IN DWORD dwProcessId, const wchar_t* pExePath); + // // Kernel32.dll // @@ -58,7 +60,7 @@ enum MBI_REGION_TYPE : DWORD { Private = MEM_PRIVATE }; -extern "C" NTSTATUS NTAPI ZwQueryInformationProcess(HANDLE hProcess, PROCESSINFOCLASS infoType, /*out*/ PVOID pBuf, /*sizeof pBuf*/ ULONG lenBuf, SIZE_T* /*PULONG*/ returnLength); +extern "C" NTSTATUS NTAPI ZwQueryInformationProcess(HANDLE hProcess, PROCESSINFOCLASS infoType, /*out*/ PVOID pBuf, /*sizeof pBuf*/ ULONG lenBuf, SIZE_T* /*PULONG*/ returnLength); #define SystemProcessesAndThreadsInformation 5 diff --git a/native/winp.sln b/native/winp.sln index 4e5c46e..5b9f63d 100644 --- a/native/winp.sln +++ b/native/winp.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 12.0.31101.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "winp", "winp.vcxproj", "{6B623E61-2427-4C6A-B028-35E9A397AC3A}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sendctrlc", "sendctrlc\sendctrlc.vcxproj", "{AF1488B6-BF5B-4F3F-8AB7-5114D5BF93B1}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "native_test", "native_test", "{3F7718C9-97FF-4839-8197-63A69FAD6E54}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "testapp", "..\native_test\testapp\testapp.vcxproj", "{AF1488B6-BF5B-4F3F-8AB7-5114D5BF93A1}" @@ -25,6 +27,13 @@ Global {6B623E61-2427-4C6A-B028-35E9A397AC3A}.Release|Win32.Build.0 = Release|Win32 {6B623E61-2427-4C6A-B028-35E9A397AC3A}.Release|x64.ActiveCfg = Release|x64 {6B623E61-2427-4C6A-B028-35E9A397AC3A}.Release|x64.Build.0 = Release|x64 + {AF1488B6-BF5B-4F3F-8AB7-5114D5BF93B1}.Debug|Win32.ActiveCfg = Debug|Win32 + {AF1488B6-BF5B-4F3F-8AB7-5114D5BF93B1}.Debug|Win32.Build.0 = Debug|Win32 + {AF1488B6-BF5B-4F3F-8AB7-5114D5BF93B1}.Debug|x64.ActiveCfg = Debug|Win32 + {AF1488B6-BF5B-4F3F-8AB7-5114D5BF93B1}.Release|Win32.ActiveCfg = Release|Win32 + {AF1488B6-BF5B-4F3F-8AB7-5114D5BF93B1}.Release|Win32.Build.0 = Release|Win32 + {AF1488B6-BF5B-4F3F-8AB7-5114D5BF93B1}.Release|x64.ActiveCfg = Release|x64 + {AF1488B6-BF5B-4F3F-8AB7-5114D5BF93B1}.Release|x64.Build.0 = Release|x64 {AF1488B6-BF5B-4F3F-8AB7-5114D5BF93A1}.Debug|Win32.ActiveCfg = Debug|Win32 {AF1488B6-BF5B-4F3F-8AB7-5114D5BF93A1}.Debug|Win32.Build.0 = Debug|Win32 {AF1488B6-BF5B-4F3F-8AB7-5114D5BF93A1}.Debug|x64.ActiveCfg = Debug|Win32 diff --git a/src/main/java/org/jvnet/winp/Native.java b/src/main/java/org/jvnet/winp/Native.java index 95ae60a..4d696cc 100755 --- a/src/main/java/org/jvnet/winp/Native.java +++ b/src/main/java/org/jvnet/winp/Native.java @@ -19,8 +19,10 @@ class Native { public static final String DLL_NAME = "64".equals(System.getProperty("sun.arch.data.model")) ? "winp.x64" : "winp"; + 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 int setPriority(int pid, int value); native static int getProcessId(int handle); @@ -66,8 +68,18 @@ class Native { private static final String DLL_TARGET = "winp.folder.preferred"; private static final String UNPACK_DLL_TO_PARENT_DIR = "winp.unpack.dll.to.parent.dir"; + private static String ctrlCExePath; + + public static boolean sendCtrlC(int pid) { + if (ctrlCExePath == null) { + return false; + } + return sendCtrlC(pid, ctrlCExePath); + } + static { - load(); + File exeFile = load(); + ctrlCExePath = (exeFile == null) ? null : exeFile.getPath(); } private static String md5(URL res) { @@ -90,16 +102,18 @@ private static String md5(URL res) { } } - private static void load() { + private static File load() { - final URL res = Native.class.getClassLoader().getResource(DLL_NAME + ".dll"); + final URL dllRes = Native.class.getClassLoader().getResource(DLL_NAME + ".dll"); try { - if (res != null) { - loadByUrl(res); + if (dllRes != null) { + final URL exeRes = Native.class.getClassLoader().getResource(CTRLCEXE_NAME + ".exe"); + return loadByUrl(dllRes, exeRes); } else { // we don't know where winp.dll is, so let's just hope the user put it somewhere System.loadLibrary(DLL_NAME); + return null; } } catch (Throwable cause) { @@ -109,32 +123,40 @@ private static void load() { } } - private static void loadByUrl(URL res) throws IOException { + private static File loadByUrl(URL dllRes, URL exeRes) throws IOException { - String url = res.toExternalForm(); + String dllUrl = dllRes.toExternalForm(); + if (dllUrl.startsWith("file:")) { + // during debug the files are on disk and not in a jar + if (!exeRes.toExternalForm().startsWith("file:")) { + LOGGER.log(Level.WARNING, "DLL and EXE are inconsistenly present on disk"); + } - if (url.startsWith("file:")) { - // during debug File f; try { - f = new File(res.toURI()); + f = new File(dllRes.toURI()); } catch (URISyntaxException e) { - f = new File(res.getPath()); + f = new File(dllRes.getPath()); } loadDll(f); - return; + + File exeFile = new File(f.getParentFile(), CTRLCEXE_NAME + ".exe"); + return exeFile; } try { - File dllFile = extractToStaticLocation(res); + File dllFile = extractToStaticLocation(dllRes); + File exeFile = extractExe(exeRes, dllFile.getParentFile()); loadDll(dllFile); - return; + return exeFile; } catch (Throwable e) { LOGGER.log(Level.WARNING, "Failed to load DLL from static location", e); } - File dllFile = extractToTmpLocation(res); + File dllFile = extractToTmpLocation(dllRes); + File exeFile = extractExe(exeRes, dllFile.getParentFile()); loadDll(dllFile); + return exeFile; } private static File extractToStaticLocation(URL url) throws IOException { @@ -160,6 +182,14 @@ private static File extractToTmpLocation(URL res) throws IOException { return tmpFile; } + private static File extractExe(URL res, File dir) throws IOException { + File destFile = new File(dir, CTRLCEXE_NAME + '.' + md5(res) + ".exe"); + if (!destFile.exists()) { + copyStream(res.openStream(), new FileOutputStream(destFile)); + } + return destFile; + } + private static File getJarFile(URL res) { String url = res.toExternalForm(); diff --git a/src/main/java/org/jvnet/winp/WinProcess.java b/src/main/java/org/jvnet/winp/WinProcess.java index 7b68a18..0fe7231 100755 --- a/src/main/java/org/jvnet/winp/WinProcess.java +++ b/src/main/java/org/jvnet/winp/WinProcess.java @@ -78,6 +78,21 @@ public void kill() { Native.kill(pid,false); } + public boolean sendCtrlC() { + if (LOGGER.isLoggable(FINE)) + LOGGER.fine(String.format("Attempting to send CTRL+C to pid=%d (%s)",pid,getCommandLine())); + return Native.sendCtrlC(pid); + } + + public boolean isRunning() { + try { + Native.getCmdLine(pid); + return true; + } catch (WinpException e) { + return false; + } + } + public boolean isCriticalProcess() { return Native.isCriticalProcess(pid); } diff --git a/src/test/java/org/jvnet/winp/NativeAPITest.java b/src/test/java/org/jvnet/winp/NativeAPITest.java index f697970..ab95225 100644 --- a/src/test/java/org/jvnet/winp/NativeAPITest.java +++ b/src/test/java/org/jvnet/winp/NativeAPITest.java @@ -119,7 +119,41 @@ public void testKill() throws Exception { Thread.sleep(100); wp.killRecursively(); } - + + @Test + public void testPingAsDelay() throws Exception { + Process p = spawnProcess("PING", "-n", "10", "127.0.0.1"); // run for 10 secs + + WinProcess wp = new WinProcess(p); + assertTrue(wp.isRunning()); + + Thread.sleep(4000); // just wait, don't send Ctrl+C + + assertTrue(wp.isRunning()); + wp.killRecursively(); + } + + @Test + public void testSendCtrlC() throws Exception { + Process p = spawnProcess("PING", "-n", "10", "127.0.0.1"); // run for 10 secs + + WinProcess wp = new WinProcess(p); + assertTrue(wp.isRunning()); + + // send Ctrl+C, then wait for a max of 4 secs + boolean sent = wp.sendCtrlC(); + assertTrue(sent); + for (int i = 0; i < 40; ++i) { + if (!wp.isRunning()) { + break; + } + Thread.sleep(100); + } + + assertTrue(!wp.isRunning()); + wp.killRecursively(); + } + @Test public void shouldFailForNonExistentProcess() { int nonExistentPid = Integer.MAX_VALUE;