Changeset 31650
- Timestamp:
- 2017-05-05T21:39:34+12:00 (7 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
main/trunk/gli/src/org/greenstone/gatherer/util/SafeProcess.java
r31646 r31650 12 12 import java.util.Arrays; 13 13 import java.util.Scanner; 14 import java.util.Stack; 14 15 15 16 import com.sun.jna.*; … … 28 29 // to avoid blocking problems that can arise from a Process' input and output streams. 29 30 31 // On Windows, Perl could launch processes as proper ProcessTrees: http://search.cpan.org/~gsar/libwin32-0.191/ 32 // Then killing the root process will kill child processes naturally. 33 30 34 public class SafeProcess { 31 //public static int DEBUG = 0;35 public static int DEBUG = 0; 32 36 33 37 public static final int STDERR = 0; 34 38 public static final int STDOUT = 1; 35 39 public static final int STDIN = 2; 36 40 // can't make this variable final and init in a static block, because it needs to use other SafeProcess static methods which rely on this in turn: 41 public static String WIN_KILL_CMD; 42 43 37 44 // charset for reading process stderr and stdout streams 38 45 //public static final String UTF8 = "UTF-8"; … … 111 118 } 112 119 120 // In future, think of changing the method doRuntimeExec() over to using ProcessBuilder 121 // instead of Runtime.exec(). ProcessBuilder seems to have been introduced from Java 5. 122 // https://docs.oracle.com/javase/7/docs/api/java/lang/ProcessBuilder.html 123 // https://zeroturnaround.com/rebellabs/how-to-deal-with-subprocesses-in-java/ 124 // which suggests using Apache Common Exec to launch processes and says what will be forthcoming in Java 9 113 125 private Process doRuntimeExec() throws IOException { 114 126 Process prcs = null; … … 122 134 123 135 // http://stackoverflow.com/questions/5283444/convert-array-of-strings-into-a-string-in-java 124 log("SafeProcess running: " + Arrays.toString(command_args)); 136 //log("SafeProcess running:" + Arrays.toString(command_args)); 137 StringBuffer cmdDisplay = new StringBuffer(); 138 for(int i = 0; i < command_args.length; i++) { 139 cmdDisplay.append(" ").append(command_args[i]); 140 } 141 log("SafeProcess running: [" + cmdDisplay + "]"); 142 cmdDisplay = null; // let the GC have it 143 125 144 126 145 if(this.envp == null) { … … 231 250 232 251 // Run a very basic process: with no reading from or writing to the Process' iostreams, 233 // this just execs the process and waits for it to return 252 // this just execs the process and waits for it to return. 253 // Don't call this method but the zero-argument runProcess() instead if your process will 254 // output stuff to its stderr and stdout streams but you don't need to monitory these. 255 // Because, as per a comment in GLI's GS3ServerThread.java, 256 // in Java 6, it wil block if you don't handle a process' streams when the process is 257 // outputting something. (Java 7+ won't block if you don't bother to handle the output streams) 234 258 public int runBasicProcess() { 235 259 try { … … 399 423 400 424 401 425 // 4. kick off the stream gobblers 402 426 this.exitValue = waitForWithStreams(inputGobbler, outputGobbler, errorGobbler); 403 427 … … 450 474 } 451 475 452 // stackoverflow.com/questions/1835885/send-ctrl-c-to-process-open-by-java 453 // (Taskkill command can kill all running perl. But what if we kill perl instances not launched by GS?) 454 // stackoverflow.com/questions/912889/how-to-send-interrupt-key-sequence-to-a-java-process 455 // Searching for: "forcibly terminate external process launched by Java on Windows" 456 static void killAnyPerl() { 457 if(!Utility.isWindows()) return; 458 459 // only on Windows do we need to still kill perl despite process.destroy() 460 try{ 461 //Runtime.getRuntime().exec("taskkill /f /im perl.exe"); 462 SafeProcess proc = new SafeProcess("taskkill /f /im perl.exe"); 463 // /f Specifies to forcefully terminate the process(es). 464 // /t Terminates the specified process and any child processes which were started by it. 465 // /im is the image name (the name of the program), see Image Name column in Win Task Manager. 466 int exitValue = proc.runBasicProcess(); // don't care about IOstreams 467 } catch(Exception e) { 468 log("@@@ Exception attempting to stop perl " + e.getMessage(), e); 469 } 470 } 476 /* 477 478 On Windows, p.destroy() terminates process p that Java launched, 479 but does not terminate any processes that p may have launched. Presumably since they didn't form a proper process tree. 480 https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/e3cb7532-87f6-4ae3-9d80-a3afc8b9d437/how-to-kill-a-process-tree-in-cc-on-windows-platform?forum=vclanguage 481 https://msdn.microsoft.com/en-us/library/windows/desktop/ms684161(v=vs.85).aspx 482 483 Searching for: "forcibly terminate external process launched by Java on Windows" 484 Not possible: stackoverflow.com/questions/1835885/send-ctrl-c-to-process-open-by-java 485 But can use taskkill or tskill or wmic commands to terminate a process by processID 486 stackoverflow.com/questions/912889/how-to-send-interrupt-key-sequence-to-a-java-process 487 Taskkill command can kill by Image Name, such as all running perl, e.g. taskkill /f /im perl.exe 488 But what if we kill perl instances not launched by GS? 489 /f Specifies to forcefully terminate the process(es). We need this flag switched on to kill childprocesses. 490 /t Terminates the specified process and any child processes which were started by it. 491 /t didn't work to terminate subprocesses. Maybe since the process wasn't launched as 492 a properly constructed processtree. 493 /im is the image name (the name of the program), see Image Name column in Win Task Manager. 494 495 We don't want to kill all perl running processes. 496 Another option is to use wmic, available since Windows XP, to kill a process based on its command 497 which we sort of know (SafeProcess.command) and which can be seen in TaskManager under the 498 "Command Line" column of the Processes tab. 499 https://superuser.com/questions/52159/kill-a-process-with-a-specific-command-line-from-command-line 500 The following works kill any Command Line that matches -site localsite lucene-jdbm-demo 501 C:>wmic PATH win32_process Where "CommandLine like '%-site%localsite%%lucene-jdbm-demo%'" Call Terminate 502 "WMIC Wildcard Search using 'like' and %" 503 https://codeslammer.wordpress.com/2009/02/21/wmic-wildcard-search-using-like-and/ 504 However, we're not even guaranteed that every perl command GS launches will contain the collection name 505 Nor do we want to kill all perl processes that GS launches with bin\windows\perl\bin\perl, though this works: 506 wmic PATH win32_process Where "CommandLine like '%bin%windows%perl%bin%perl%'" Call Terminate 507 The above could kill GS perl processes we don't intend to terminate, as they're not spawned by the particular 508 Process we're trying to terminate from the root down. 509 510 Solution: We can use taskkill or the longstanding tskill or wmic to kill a process by ID. Since we can 511 kill an external process that SafeProcess launched OK, and only have trouble killing any child processes 512 it launched, we need to know the pids of the child processes. 513 514 We can use Windows' wmic to discover the childpids of a process whose id we know. 515 And we can use JNA to get the process ID of the external process that SafeProcess launched. 516 517 To find the processID of the process launched by SafeProcess, 518 need to use Java Native Access (JNA) jars, available jna.jar and jna-platform.jar. 519 http://stackoverflow.com/questions/4750470/how-to-get-pid-of-process-ive-just-started-within-java-program 520 http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-process-id 521 http://www.golesny.de/p/code/javagetpid 522 https://github.com/java-native-access/jna/blob/master/www/GettingStarted.md 523 We're using JNA v 4.1.0, https://mvnrepository.com/artifact/net.java.dev.jna/jna 524 525 WMIC can show us a list of parent process id and process id of running processes, and then we can 526 kill those child processes with a specific process id. 527 https://superuser.com/questions/851692/track-which-program-launches-a-certain-process 528 http://stackoverflow.com/questions/7486717/finding-parent-process-id-on-windows 529 WMIC can get us the pids of all childprocesses launched by parent process denoted by parent pid. 530 And vice versa: 531 if you know the parent pid and want to know all the pids of the child processes spawned: 532 wmic process where (parentprocessid=596) get processid 533 if you know a child process id and want to know the parent's id: 534 wmic process where (processid=180) get parentprocessid 535 536 The above is the current solution. 537 538 Eventually, instead of running a windows command to kill the process ourselves, consider changing over to use 539 https://github.com/flapdoodle-oss/de.flapdoodle.embed.process/blob/master/src/main/java/de/flapdoodle/embed/process/runtime/Processes.java 540 (works with Apache license, http://www.apache.org/licenses/LICENSE-2.0) 541 This is a Java class that uses JNA to terminate processes. It also has the getProcessID() method. 542 543 Linux ps equivalent on Windows is "tasklist", see 544 http://stackoverflow.com/questions/4750470/how-to-get-pid-of-process-ive-just-started-within-java-program 545 546 */ 471 547 472 548 // http://stackoverflow.com/questions/4750470/how-to-get-pid-of-process-ive-just-started-within-java-program 473 // http://www.golesny.de/p/code/javagetpid 474 // Need to use JNA (Java Native Access), http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-process-id 475 // Got 4.1.0 from https://mvnrepository.com/artifact/net.java.dev.jna/jna/4.1.0 and https://mvnrepository.com/artifact/net.java.dev.jna/jna-platform/4.1.0 476 // (see also https://mvnrepository.com/artifact/net.java.dev.jna/jna), since github's GettingStarted doc talks about downloading jna.jar but never provides it. 477 // See https://github.com/java-native-access/jna/blob/master/www/GettingStarted.md 549 // Uses Java Native Access, JNA 478 550 public static long getProcessID(Process p) 479 551 { … … 511 583 } 512 584 513 // UNUSED - only kills toplevel process, same as p.destroy().514 // Call destroyProcess(p) instead to kill any childprocesses launched515 static void killProcessIfOnWindows(Process p) {516 if(!Utility.isWindows()) return;517 518 // only on Windows do we need to still kill perl despite process.destroy()519 // http://stackoverflow.com/questions/4750470/how-to-get-pid-of-process-ive-just-started-within-java-program520 // http://www.golesny.de/p/code/javagetpid521 long processID = getProcessID(p);522 523 SafeProcess.killWinProcessWithID(processID);524 }525 526 527 // Eventually move to using https://github.com/flapdoodle-oss/de.flapdoodle.embed.process/blob/master/src/main/java/de/flapdoodle/embed/process/runtime/Processes.java ?528 // Works with Apache license, http://www.apache.org/licenses/LICENSE-2.0529 530 585 531 586 // stackoverflow.com/questions/1835885/send-ctrl-c-to-process-open-by-java … … 535 590 static void killWinProcessWithID(long processID) { 536 591 537 // check if we have taskkill or tskill 538 String cmd = null; 539 if (SafeProcess.isAvailable("wmic")) { 540 // https://isc.sans.edu/diary/Windows+Command-Line+Kung+Fu+with+WMIC/1229 541 cmd = "wmic process " + processID + " delete"; // like "kill -9" on Windows 542 } else if(SafeProcess.isAvailable("taskkill")) { 543 cmd = "taskkill /f /t /PID " + processID; 544 } else { //if(SafeProcess.isAvailable("tskill")) { can't check availability since "which tskill" doesn't ever succeed 545 cmd = "tskill " + processID; // https://ss64.com/nt/tskill.html 546 } 547 cmd = "tskill " + processID; 592 String cmd = SafeProcess.getWinProcessKillCmd(processID); 593 if (cmd == null) return; 548 594 549 595 try{ 550 // Despite the /T flag, the following still doesn't kill subprocesses. Taskkill is also not available 551 // universally. Try alternates tskill and pskill, though they too don't kill further processes launched by p. 552 log("Attempting to taskkil process with ID " + processID); 553 //Runtime.getRuntime().exec("taskkill /f /t /PID " + processID); 554 SafeProcess proc = new SafeProcess(cmd); 555 // /f Specifies to forcefully terminate the process(es). 556 // /t Terminates the specified process and any child processes which were started by it. 557 int exitValue = proc.runProcess(); // no IOstreams for Taskkill, but for "wmic process pid delete" there is output that needs flushing 596 log("\tAttempting to terminate Win subprocess with pid: " + processID); 597 SafeProcess proc = new SafeProcess(cmd); 598 int exitValue = proc.runProcess(); // no IOstreams for Taskkill, but for "wmic process pid delete" 599 // there is output that needs flushing, so don't use runBasicProcess() 558 600 559 601 } catch(Exception e) { … … 562 604 } 563 605 606 564 607 // On linux and mac, p.destroy() suffices to kill processes launched by p as well. 565 // On Windows we need to do more work, since processes launched by p remain around executing until termination otherwise.566 // e.g. full-import.pl may be terminated with p.destroy(), but it launches import.pl which is left running until termination.608 // On Windows we need to do more work, since otherwise processes launched by p remain around executing until they naturally terminate. 609 // e.g. full-import.pl may be terminated with p.destroy(), but it launches import.pl which is left running until it naturally terminates. 567 610 static void destroyProcess(Process p) { 568 //killProcessIfOnWindows(p); // doesn't work: only terminates first level process launched, same as p.destroy() 569 //p.destroy(); 570 //killAnyPerl(); // kills any running perl, even those not launched by GS 571 611 // If it isn't windows, process.destroy() terminates any child processes too 572 612 if(!Utility.isWindows()) { 573 613 p.destroy(); 574 614 return; 575 } 576 577 578 579 // We don't want to kill all perl running processes. Just the ones whose command we sort of know and can see in TaskManager 580 // under the "Command Line" column of the Processes tab. 581 // This is possible with wmic, available since xp. First check wmic is really present on this Windows machine 582 // https://superuser.com/questions/52159/kill-a-process-with-a-specific-command-line-from-command-line 615 } 616 583 617 if(!SafeProcess.isAvailable("wmic")) { 584 log("wmic used to kill subprocessesis not available. Unable to terminate subprocesses...");618 log("wmic, used to kill subprocesses, is not available. Unable to terminate subprocesses..."); 585 619 log("Kill them manually from the TaskManager or they will proceed to run to termination"); 586 620 … … 588 622 p.destroy(); 589 623 return; 590 } 591 592 593 // get the process id of the process we launched 594 long processID = SafeProcess.getProcessID(p); 595 596 597 // What we want to be able to do on Windows is to kill a process by its command line as seen in Task Manager for that process. 598 // full-import.pl launches import.pl, full-build will launch buildcol.pl and activate.pl. 599 // These tend to have the collection name still at the end, so we can terminate a process knowing the collection name, since 600 // we can use wildcards in our wmic termination command. 601 // However, lucene_passes.pl is launched by buildcol.pl and doesn't have the collection name in its launching cmd line. 602 603 // Works beatufilly to kill any CommandLine (as seen in TaskManager) that matches 604 //C:>wmic PATH win32_process Where "CommandLine like '%-site%localsite%%lucene-jdbm-demo%'" Call Terminate 605 // https://codeslammer.wordpress.com/2009/02/21/wmic-wildcard-search-using-like-and/ 606 // "WMIC Wildcard Search using like and %" 607 // All subsequent perl processes launched by Greenstone are launched with bin\windows\perl\bin\perl. So this works well: 608 // wmic PATH win32_process Where "CommandLine like '%bin%windows%perl%bin%perl%'" Call Terminate 609 // However do we want to terminate all perl processes launched by Greenstone, or only those that we can infer were spawned by 610 // the SafeProcess that we've been running? 611 612 /* 613 // Works in terminal, but when run through GLI, GLI is terminated too... 614 try{ 615 616 log("Attempting to use Windows to terminate sub processes"); 617 Runtime.getRuntime().exec("wmic PATH win32_process Where \"CommandLine like '%bin%windows%perl%bin%perl%'\" Call Terminate"); 618 } catch(Exception e) { 619 log("@@@ Exception attempting to stop perl " + e.getMessage(), e); 620 } 621 */ 622 623 // https://superuser.com/questions/851692/track-which-program-launches-a-certain-process 624 // Can get parent process id and process id, and then kill those with child process id 625 // http://stackoverflow.com/questions/7486717/finding-parent-process-id-on-windows 626 // Can get the pids of all childprocesses launched by parent process with parent pid. 627 // And vice versa: 628 // e.g. if you know the parent pid and want to know all the pids of the child processes spawned: 629 // wmic process where (parentprocessid=596) get processid 630 // if you know a child process id and want to know the parent's id: 631 // wmic process where (processid=180) get parentprocessid 632 633 String[] child_pids = null; 634 635 636 log("Attempting to use Windows to terminate sub processes"); 637 SafeProcess proc = new SafeProcess("wmic process where (parentprocessid="+processID+") get processid"); 624 } 625 626 // get the process id of the process we launched, 627 // so we can use it to find the pids of any subprocesses it launched in order to terminate those too. 628 629 long processID = SafeProcess.getProcessID(p); 630 log("Attempting to terminate sub processes of Windows process with pid " + processID); 631 terminateSubProcessesRecursively(processID, p); 632 633 } 634 635 // Helper function. Only for Windows. 636 // Counterintuitively, we're be killing all parent processess and then all child procs and all their descendants 637 // as soon as we discover any further process each (sub)process has launched. The parent processes are killed 638 // first in each case for 2 reasons: 639 // 1. on Windows, killing the parent process leaves the child running as an orphan anyway, so killing the 640 // parent is an independent action, the child process is not dependent on the parent; 641 // 2. Killing a parent process prevents it from launching further processes while we're killing off each child process 642 private static void terminateSubProcessesRecursively(long parent_pid, Process p) { 643 644 // Use Windows wmic to find the pids of any sub processes launched by the process denoted by parent_pid 645 SafeProcess proc = new SafeProcess("wmic process where (parentprocessid="+parent_pid+") get processid"); 638 646 proc.setSplitStdOutputNewLines(true); // since this is windows, splits lines by \r\n 647 int exitValue = proc.runProcess(); // exitValue (%ERRORLEVEL%) is 0 either way. 648 //log("@@@@ Return value from proc: " + exitValue); 649 650 // need output from both stdout and stderr: stderr will say there are no pids, stdout will contain pids 651 String stdOutput = proc.getStdOutput(); 652 String stdErrOutput = proc.getStdError(); 653 654 655 // Now we know the pids of the immediate subprocesses, we can get rid of the parent process 656 if(p != null) { // we're the top level process, terminate java way 657 p.destroy(); 658 } else { // terminate windows way 659 SafeProcess.killWinProcessWithID(parent_pid); // get rid off current pid 660 } 661 662 // parse the output to get the sub processes' pids 639 663 // Output looks like: 640 664 // ProcessId … … 646 670 // If no children, then STDERR output starts with the following, possibly succeeded by empty lines: 647 671 // No Instance(s) Available. 648 int exitValue = proc.runProcess(); // exitValue (%ERRORLEVEL%) is 0 either way. 649 log("@@@@ Return value from proc: " + exitValue); 650 String stdOutput = proc.getStdOutput(); 651 // http://stackoverflow.com/questions/691184/scanner-vs-stringtokenizer-vs-string-split 652 653 log("@@@@ Got output:\n" + stdOutput); 654 if(stdOutput.indexOf("No Instance(s) Available.") == -1) { 655 //int start = stdOutput.indexOf("\n"); // skip first line, which is the column name "ProcessId" 656 //stdOutput = stdOutput.substring(start+1); 657 //child_pids = stdOutput.split("/(\\r?\\n/)+"); // get rid of newline, so we should only have a list of Long numbers 658 659 // Now we can get rid of the top level process 660 p.destroy(); 672 673 // base step of the recursion 674 if(stdErrOutput.indexOf("No Instance(s) Available.") != -1) { 675 //log("@@@@ Got output on stderr: " + stdErrOutput); 676 // No further child processes. And we already terminated the parent process, so we're done 677 return; 678 } else { 679 //log("@@@@ Got output on stdout:\n" + stdOutput); 680 681 // http://stackoverflow.com/questions/691184/scanner-vs-stringtokenizer-vs-string-split 682 661 683 // find all childprocesses for that pid and terminate them too: 684 Stack<Long> subprocs = new Stack<Long>(); 662 685 Scanner sc = new Scanner(stdOutput); 663 686 while (sc.hasNext()) { … … 666 689 } else { 667 690 long child_pid = sc.nextLong(); 668 log("Attempting to terminate child pid: " + child_pid); 669 SafeProcess.killWinProcessWithID(child_pid); 691 subprocs.push(new Long(child_pid)); 670 692 } 671 693 } 672 sc.close(); 673 } else { 674 // No child process. Only the top level process needs to be got rid of: 675 p.destroy(); 676 677 } 678 679 /* 680 681 // Now we can get rid of the top level process 682 p.destroy(); 683 684 685 // Then kill each subprocess. 686 687 if(child_pids != null) { 688 for(int i = 0; i < child_pids.length; i++) { 689 log("Attempting to terminate child pid: |" + child_pids[i] + "|"); 690 SafeProcess.killWinProcessWithID(Long.parseLong(child_pids[i].trim())); 694 sc.close(); 695 696 // recursion step if subprocs is not empty (but if it is empty, then it's another base step) 697 if(!subprocs.empty()) { 698 long child_pid = subprocs.pop().longValue(); 699 terminateSubProcessesRecursively(child_pid, null); 700 } 701 } 702 } 703 704 // This method should only be called on a Windows OS 705 private static String getWinProcessKillCmd(Long processID) { 706 // check if we first need to init WIN_KILL_CMD. We do this only once, but can't do it in a static codeblock 707 708 if(WIN_KILL_CMD == null) { 709 if(SafeProcess.isAvailable("wmic")) { 710 // https://isc.sans.edu/diary/Windows+Command-Line+Kung+Fu+with+WMIC/1229 711 WIN_KILL_CMD = "wmic process _PROCID_ delete"; // like "kill -9" on Windows 691 712 } 692 } 693 */ 694 713 else if(SafeProcess.isAvailable("taskkill")) { // check if we have taskkill or else use the longstanding tskill 714 715 WIN_KILL_CMD = "taskkill /f /t /PID _PROCID_"; // need to forcefully /f terminate the process 716 // /t "Terminates the specified process and any child processes which were started by it." 717 // But despite the /T flag, the above doesn't kill subprocesses. 718 } 719 else { //if(SafeProcess.isAvailable("tskill")) { can't check availability since "which tskill" doesn't ever succeed 720 WIN_KILL_CMD = "tskill _PROCID_"; // https://ss64.com/nt/tskill.html 721 } 722 } 723 724 if(WIN_KILL_CMD == null) { // can happen if none of the above cmds were available 725 return null; 726 } 727 return WIN_KILL_CMD.replace( "_PROCID_", Long.toString(processID) ); 695 728 } 696 729 … … 973 1006 // logger and DebugStream print commands are synchronized, therefore thread safe. 974 1007 public static void log(String msg) { 1008 if(DEBUG == 0) return; 975 1009 //logger.info(msg); 976 1010 … … 981 1015 982 1016 public static void log(String msg, Exception e) { // Print stack trace on the exception 1017 if(DEBUG == 0) return; 983 1018 //logger.error(msg, e); 984 1019 … … 991 1026 992 1027 public static void log(Exception e) { 1028 if(DEBUG == 0) return; 993 1029 //logger.error(e); 994 1030
Note:
See TracChangeset
for help on using the changeset viewer.