├── .circleci └── config.yml ├── .classpath ├── .gitignore ├── .project ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── README.md ├── appveyor.yml ├── benchmark ├── README.md ├── benchmark.sh ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── zaxxer │ └── nuprocess │ └── benchmark │ └── NuProcessBenchmark.java ├── mvnvm.properties ├── pom.xml ├── shippable.yml └── src ├── example └── java │ └── com │ └── zaxxer │ └── nuprocess │ └── example │ ├── NuSchool.java │ ├── OldSchool.java │ └── SshExample.java ├── main └── java │ └── com │ └── zaxxer │ └── nuprocess │ ├── NuAbstractProcessHandler.java │ ├── NuProcess.java │ ├── NuProcessBuilder.java │ ├── NuProcessFactory.java │ ├── NuProcessHandler.java │ ├── codec │ ├── NuAbstractCharsetHandler.java │ ├── NuCharsetDecoder.java │ ├── NuCharsetDecoderHandler.java │ ├── NuCharsetEncoder.java │ └── NuCharsetEncoderHandler.java │ ├── internal │ ├── BaseEventProcessor.java │ ├── BasePosixProcess.java │ ├── Constants.java │ ├── HexDumpElf.java │ ├── IEventProcessor.java │ ├── LibC.java │ ├── LibJava10.java │ ├── LibJava8.java │ └── ReferenceCountedFileDescriptor.java │ ├── linux │ ├── EpollEvent.java │ ├── LibEpoll.java │ ├── LinProcessFactory.java │ ├── LinuxProcess.java │ └── ProcessEpoll.java │ ├── osx │ ├── LibKevent.java │ ├── OsxProcess.java │ ├── OsxProcessFactory.java │ └── ProcessKqueue.java │ ├── package.html │ └── windows │ ├── NuKernel32.java │ ├── NuWinBase32.java │ ├── NuWinNT.java │ ├── ProcessCompletions.java │ ├── WinProcessFactory.java │ ├── WindowsCreateProcessEscape.java │ └── WindowsProcess.java └── test └── java └── com └── zaxxer └── nuprocess ├── CatTest.java ├── DirectWriteTest.java ├── EnvironmentTest.java ├── FastExitingProcessTest.java ├── InterruptTest.java ├── RunOnlyOnUnix.java ├── RunOnlyOnWindows.java ├── RunTest.java ├── ThreadedTest.java ├── cat.exe ├── internal └── ConstantsTest.java └── linux └── EpollEventTest.java /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | build: # name of your job 3 | machine: true # executor type 4 | resource_class: brettwooldridge/ubuntu 5 | 6 | steps: 7 | # Commands run in a Linux virtual machine environment 8 | - checkout 9 | - run: mvn clean package 10 | 11 | -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /benchmark/target 2 | /target 3 | /.idea 4 | .settings/org.eclipse.core.resources.prefs 5 | .settings/org.eclipse.jdt.core.prefs 6 | .settings/org.eclipse.m2e.core.prefs 7 | *.iml 8 | *.ipr 9 | org.eclipse.pde.core.prefs 10 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | nuprocess 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.pde.PluginNature 21 | org.eclipse.jdt.core.javanature 22 | org.eclipse.m2e.core.maven2Nature 23 | 24 | 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: required 3 | dist: trusty 4 | cache: 5 | directories: 6 | - $HOME/.m2/repository 7 | 8 | os: 9 | - linux 10 | - osx 11 | 12 | matrix: 13 | include: 14 | - os: linux 15 | jdk: 16 | - openjdk8 17 | - openjdk11 18 | - oraclejdk8 19 | - oraclejdk11 20 | - os: osx 21 | osx_image: xcode8.3 22 | jdk: 23 | - oraclejdk8 24 | - os: osx 25 | osx_image: xcode10.1 26 | jdk: 27 | - oraclejdk11 28 | 29 | script: 30 | - mvn -Dmaven.javadoc.skip=true -e package -V 31 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Changes in 3.0.0 2 | * Drop support for Java 7. Artifact is now Java 8+ only. 3 | * Fix race condition for fast exiting processes (#158). 4 | * Alter thread name from "ProcessQueue" to "NuProcessQueue". 5 | 6 | Changes in 2.0.6 7 | 8 | * Fix environment variable injection vulnerability. This is a follow-up from CVE-2022-39243 and does not 9 | have its own CVE assigned 10 | 11 | Changes in 2.0.5 12 | 13 | * Fix command line injection vulnerability (CVE-2022-39243) (#143) 14 | 15 | Changes in 2.0.4 16 | 17 | * Add `Automatic-Module-Name` to `MANIFEST.MF` to better support Java 9+ modules (#142) 18 | 19 | Changes in 2.0.3 20 | 21 | * Add handling for `EINTR` during `epoll_wait` and `ESRCH` from `kill` to prevent zombie processes (#125, #131) 22 | * Remove static `EpollEvent` queue to avoid blocking under heavy load, which can lead to leaking zombie processes if a blocked thread is interrupted (#138) 23 | 24 | Changes in 2.0.2 25 | 26 | * Ensure pipes are closed after failing to start a process on Linux, to prevent leaking descriptors (#120) 27 | * Reduce pipe buffer size on Windows to increase process throughput to `stdout` and `stderr` (#118) 28 | * Update JNA dependency to v5.8.0 to pick up support for the Apple M1 (#123) 29 | 30 | Changes in 2.0.1 31 | 32 | * Fix race condition between the `onStdinReady()` and `userWantsWrite()` methods that causes the state to 33 | become incorrect, resulting in failure to continue processing wanted `stdin` writes (#113) 34 | 35 | Changes in 2.0.0 36 | 37 | * Update JNA dependency to v5.5.0 38 | * Add ability to run processes synchronously (#104) 39 | * Handle detecting Java versions when non-numbers are present (#105) 40 | 41 | Changes in 1.2.6 42 | 43 | * Fix linkage issues with Azul JVMs. Special thanks to Bryan Turner for this pull request and extensive testing across 44 | a wide variety of JVMs. (#107) 45 | 46 | Changes in 1.2.5 47 | 48 | * Handle `SystemRoot` case-insensitively (Windows) (#103). 49 | * Compatibility changes for JNA 5.2.0 (#99). 50 | 51 | Changes in 1.2.4 52 | 53 | * Fix structure alignment (`ALIGN_GNUC`) on various platforms (#94). 54 | * Add support for JDK 10/11. (#92) 55 | 56 | Changes in 1.2.3 57 | 58 | * Resolve launch issue on Linux/macOS w/Zulu JVM. 59 | 60 | Changes in 1.2.2 61 | 62 | * Fix race condition seen (once) on Linux after moving to JVM internal API for process spawning. 63 | 64 | Changes in 1.2.1 65 | 66 | * On macOS, continue to use `posix_spawnp` directly, `POSIX_SPAWN_START_SUSPENDED` solves a lot of potential race conditions that we have to contend with on Linux. 67 | 68 | Changes in 1.2.0 69 | 70 | * Spawn processes on Linux and macOS using JVM internal method `Java_java_lang_UNIXProcess_forkAndExec`. 71 | 72 | Changes in 1.1.3 73 | 74 | * Add constructor with `Pointer` to `LibKevent.Kevent` (#82) that reduces reflection code executed in JNA. 75 | * Change to Linux `epoll` implementation that massively decreases the amount of memory used. 76 | * Optimize `epoll_wait` loop to avoid memory churn. (#80) 77 | 78 | Changes in 1.1.2 79 | 80 | * Fix issue on macOS where pipes would leak after many calls to `closeStdin` 81 | 82 | Changes in 1.1.1 83 | 84 | * Fixed issue where calling `writeStdin` with a buffer larger than 65kb would hang. 85 | 86 | Changes in 1.1.0 87 | 88 | * Expose new `NuProcess.getPID()` method on all platforms. 89 | * Fix wrong position and limit value on `inBuffer` caused by incorrect `inBuffer` clear. 90 | * Fix source of memory leak and `DirectWriteTest` failure on macOS; clear `wantsWrite` 91 | queue on process exit. 92 | * Remove `System.exit()` call from unit test. 93 | 94 | Changes in 1.0.4 95 | 96 | * Update OSGi manifest with correct exports. 97 | * Fix moar Windows argument escaping. 98 | * Introduce 'force' parameter to `NuProcess.closeStdin()`, and enqueue special 99 | tombstone marker into the `pendingWrites` queue for non-forced closes. 100 | * Remove use of `sun.misc.Unsafe`. 101 | 102 | Changes in 1.0.3 103 | 104 | * Remove dependency on `jna-platform`. 105 | * Handle fast-exiting processes on Linux. 106 | * Fix Windows argument escaping. 107 | * Handle race condition in named pipe creation on Windows. 108 | 109 | Changes in 1.0.2 110 | 111 | * Process `cwd` support for Mac, Linux, and Win32. 112 | * Remove `ThreadLocal` usage for macOS. 113 | 114 | Changes in 1.0.1 115 | 116 | * Performance improvements for macOS `kqueue` / `kevent`. 117 | * Fix issue where `stdout` and `stderr` are empty for quick exiting processes. 118 | 119 | Changes in 1.0.0 120 | 121 | * Genesis. See the git history for previous evolution. 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NuProcess 2 | ========= 3 | 4 | [![][Build Status img]][Build Status] 5 | [![][Shippable Status img]][Shippable Status] 6 | [![][license img]][license] 7 | [![][Maven Central img]][Maven Central] 8 | [![][Javadocs img]][Javadocs] 9 | 10 | A low-overhead, non-blocking I/O, external Process execution implementation for Java. It is a replacement for 11 | ``java.lang.ProcessBuilder`` and ``java.lang.Process``. 12 | 13 | Have you ever been annoyed by the fact that whenever you spawn a process in Java you have to create two or three "pumper" 14 | threads (for every process) to pull data out of the ``stdout`` and ``stderr`` pipes and pump data into ``stdin``? If your 15 | code starts a lot of processes you can have dozens or hundreds of threads doing nothing but pumping data. 16 | 17 | NuProcess uses the JNA library to use platform-specific native APIs to achieve non-blocking I/O on the pipes between your 18 | Java process and the spawned processes: 19 | 20 | * Linux: uses epoll 21 | * MacOS X: uses kqueue/kevent 22 | * Windows: uses IO Completion Ports 23 | 24 | #### Maven 25 | 26 | com.zaxxer 27 | nuprocess 28 | 2.0.6 29 | compile 30 | 31 | 32 | :point_right: **Note: Versions 1.0.0 and above contain breaking API changes from 0.9.7.** 33 | 34 | #### It's mostly about the memory 35 | Speed-wise, there is not a significant difference between NuProcess and the standard Java Process class, even when running 36 | 500 concurrent processes. On some platforms such as MacOS X or Linux, NuProcess is 20% faster than ``java.lang.Process`` 37 | for large numbers of processes. 38 | 39 | However, when it comes to memory there is a significant difference. The overhead of 500 threads, for example, is quite 40 | large compared to the one or few threads employed by NuProcess. 41 | 42 | Additionally, on unix-based platforms such as Linux, when creating a new process ``java.lang.Process`` uses a fork()/exec() 43 | operation. This requires a temporary copy of the Java process (the fork), before the exec is performed. When running 44 | tests on Linux, in order to spawn 500 processes required setting the JVM max. memory to 3Gb (-Xmx3g). NuProcess uses a 45 | variant of ``fork()`` called ``vfork()``, which does not impose this overhead. NuProcess can comfortably spawn 500 processes 46 | even when running the JVM with only 128Mb. 47 | 48 | #### Example 49 | Like the Java ``ProcessBuilder``, NuProcess offers ``NuProcessBuilder``, so building a process is fairly simple. Let's make a simple example where we use the Unix "cat" command. When launched with no parameters, *cat* reads from STDIN and echos the output to STDOUT. We're going to start the *cat* process, write *"Hello world!"* to its STDIN, and read the echoed reply from STDOUT and print it. Let's build and start the process. 50 | ```java 51 | NuProcessBuilder pb = new NuProcessBuilder(Arrays.asList("/bin/cat")); 52 | ProcessHandler handler = new ProcessHandler(); 53 | pb.setProcessListener(handler); 54 | NuProcess process = pb.start(); 55 | process.wantWrite(); 56 | process.waitFor(0, TimeUnit.SECONDS); // when 0 is used for waitFor() the wait is infinite 57 | ``` 58 | You'll notice the ``ProcessHandler`` in code above. This is a class you provide which receives callbacks from the process to handle input, output, termination, etc. And notice the ``wantWrite()`` call, this expresses that we have something we want to write to the process, so our ``ProcessHandler`` will be called back to perform the write. Here's what ``ProcessHandler`` looks like for our example: 59 | ```java 60 | class ProcessHandler extends NuAbstractProcessHandler { 61 | private NuProcess nuProcess; 62 | 63 | @Override 64 | public void onStart(NuProcess nuProcess) { 65 | this.nuProcess = nuProcess; 66 | } 67 | 68 | @Override 69 | public boolean onStdinReady(ByteBuffer buffer) { 70 | buffer.put("Hello world!".getBytes()); 71 | buffer.flip(); 72 | return false; // false means we have nothing else to write at this time 73 | } 74 | 75 | @Override 76 | public void onStdout(ByteBuffer buffer, boolean closed) { 77 | if (!closed) { 78 | byte[] bytes = new byte[buffer.remaining()]; 79 | // You must update buffer.position() before returning (either implicitly, 80 | // like this, or explicitly) to indicate how many bytes your handler has consumed. 81 | buffer.get(bytes); 82 | System.out.println(new String(bytes)); 83 | 84 | // For this example, we're done, so closing STDIN will cause the "cat" process to exit 85 | nuProcess.closeStdin(true); 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | #### Synchronous Operation 92 | NuProcess does allow you to perform synchronous *writes* to the *stdin* of the spawned process. Even though the writes are *synchronous* they are *non-blocking*; meaning the write returns immediately. In this model, you do not use ``NuProcess.wantWrite()`` and your ``onStdinReady()`` method will not be called. If you extend the ``NuAbstractProcessHandler`` you do not need to provide an implementation of ``onStdinReady()``. Use the ``NuProcess.writeStdin()`` method to write data to the process. This method will return immediately and writes are queued and occur in order. Read the JavaDoc for the ``NuProcess.writeStdin()`` method for cautions and caveats. 93 | 94 | In the synchronous model, the above example would look like this: 95 | ```java 96 | NuProcessBuilder pb = new NuProcessBuilder(Arrays.asList("/bin/cat")); 97 | ProcessHandler handler = new ProcessHandler(); 98 | pb.setProcessListener(handler); 99 | NuProcess process = pb.start(); 100 | 101 | ByteBuffer buffer = ByteBuffer.wrap("Hello, World!".getBytes()); 102 | process.writeStdin(buffer); 103 | 104 | process.waitFor(0, TimeUnit.SECONDS); // when 0 is used for waitFor() the wait is infinite 105 | ``` 106 | 107 | And the handler: 108 | ```java 109 | class ProcessHandler extends NuAbstractProcessHandler { 110 | private NuProcess nuProcess; 111 | 112 | @Override 113 | public void onStart(NuProcess nuProcess) { 114 | this.nuProcess = nuProcess; 115 | } 116 | 117 | public void onStdout(ByteBuffer buffer, boolean closed) { 118 | if (!closed) { 119 | byte[] bytes = new byte[buffer.remaining()]; 120 | // You must update buffer.position() before returning (either implicitly, 121 | // like this, or explicitly) to indicate how many bytes your handler has consumed. 122 | buffer.get(bytes); 123 | System.out.println(new String(bytes)); 124 | 125 | // For this example, we're done, so closing STDIN will cause the "cat" process to exit 126 | nuProcess.closeStdin(true); 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | #### JavaDocs 133 | You can read the [JavaDoc here](http://brettwooldridge.github.io/NuProcess/apidocs/index.html). Make sure you read and fully understand the JavaDoc for the ``NuProcessHandler`` interface as it is your primary contract with NuProcess. 134 | 135 | #### Settings 136 | These are settings that can be defined as System properties that control various behaviors of the NuProcess library. *You typically do not need to modify these.* 137 | 138 | ##### ``com.zaxxer.nuprocess.threads`` 139 | This setting controls how many threads are used to handle the STDIN, STDOUT, STDERR streams of spawned processes. No 140 | matter how many processes are spawned, this setting will be the *maximum* number of threads used. Possible values are: 141 | 142 | * ``auto`` (default) - this sets the *maximum* number of threads to the number of CPU cores divided by 2. 143 | * ``cores`` - this sets the *maximum* number of threads to the number of CPU cores. 144 | * ```` - the sets the *maximum* number of threads to a specific number. Often ``1`` will provide good performance even for dozens of processes. 145 | 146 | The default is ``auto``, but in reality if your child processes are "bursty" in their output, rather than producing a 147 | constant stream of data, a single thread may provide equivalent performance even with hundreds of processes. 148 | 149 | ##### ``com.zaxxer.nuprocess.softExitDetection`` 150 | On Linux and Windows there is no method by which you can be notified in an asynchronous manner that a child process has 151 | exited. Rather than polling all child processes constantly NuProcess uses what we call "Soft Exit Detection". When a 152 | child process exits, the OS automatically closes all of it's open file handles; which *is* something about which we can be 153 | notified. So, on Linux and Windows when NuProcess determines that both the STDOUT and STDERR streams have been closed 154 | in the child process, that child process is put into a "dead pool". The processes in the dead pool are polled to 155 | determine when they have truly exited and what their exit status was. See ``com.zaxxer.nuprocess.deadPoolPollMs`` 156 | 157 | The default value for this property is ``true``. Setting this value to ``false`` will completely disable process exit 158 | detection, and the ``NuProcess.waitFor()" API __MUST__ be used. Failure to invoke this API on Linux will result in an 159 | ever-growing accumulation of "zombie" processes and eventually an inability to create new processes. There is very little 160 | reason to disable soft exit detection unless you have child process that itself closes the STDOUT and STDERR streams. 161 | 162 | ##### ``com.zaxxer.nuprocess.deadPoolPollMs`` 163 | On Linux and Windows, when Soft Exit Detection is enabled (the default), this property controls how often the processes in 164 | the dead pool are polled for their exit status. The default value is 250ms, and the minimum value is 100ms. 165 | 166 | ##### ``com.zaxxer.nuprocess.lingerTimeMs`` 167 | This property controls how long the processing thread(s) remains after the last executing child process has exited. In 168 | order to avoid the overhead of starting up another processing thread, if processes are frequently run it may be desirable 169 | for the processing thread to remain (linger) for some amount of time (default 2500ms). 170 | 171 | #### Related Projects 172 | Charles Duffy has developed a Clojure wrapper library [here](https://github.com/threatgrid/asynp). 173 | Julien Viet has developed a Vert.x 3 library [here](https://github.com/vietj/vertx-childprocess). 174 | 175 | #### Limitations 176 | The following limitations exist in NuProcess: 177 | * Currently only supports Linux, Windows, and MacOS X. 178 | * Java 7 and above 179 | * Linux support requires at least kernel version 2.6.17 or higher (kernels after June 2006) 180 | 181 | [Build Status]:https://circleci.com/gh/brettwooldridge/NuProcess` 182 | [Build Status img]:https://circleci.com/gh/brettwooldridge/NuProcess.svg?style=shield 183 | 184 | [Shippable Status]:https://app.shippable.com/github/brettwooldridge/NuProcess 185 | [Shippable Status img]:https://api.shippable.com/projects/5b4b6a3e6db3b807000a63d8/badge?branch=master 186 | 187 | [license]:LICENSE 188 | [license img]:https://img.shields.io/badge/license-Apache%202-blue.svg 189 | 190 | [Maven Central]:https://maven-badges.herokuapp.com/maven-central/com.zaxxer/nuprocess 191 | [Maven Central img]:https://maven-badges.herokuapp.com/maven-central/com.zaxxer/nuprocess/badge.svg 192 | 193 | [Javadocs]:http://javadoc.io/doc/com.zaxxer/nuprocess 194 | [Javadocs img]:http://javadoc.io/badge/com.zaxxer/nuprocess.svg 195 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | 3 | environment: 4 | matrix: 5 | - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 6 | 7 | os: Windows Server 2012 8 | 9 | install: 10 | - ps: | 11 | Add-Type -AssemblyName System.IO.Compression.FileSystem 12 | if (!(Test-Path -Path "C:\maven" )) { 13 | (new-object System.Net.WebClient).DownloadFile( 14 | 'http://www.us.apache.org/dist/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.zip', 15 | 'C:\maven-bin.zip' 16 | ) 17 | [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\maven-bin.zip", "C:\maven") 18 | } 19 | $env:Path +=";C:\maven\apache-maven-3.3.9\bin;%JAVA_HOME%\bin" 20 | 21 | build_script: 22 | - mvn test -DargLine="-Djna.nosys=true" 23 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | ## NuProcess JMH Benchmark 2 | 3 | Prior to excuting the benchmark, make sure that the primary (NuProcess) library has been built and/or installed. 4 | ```cli 5 | mvn clean install 6 | ``` 7 | 8 | Simple execution of the benchmark is as follows: 9 | ```cli 10 | cd ./benchmark 11 | ./benchmark.sh quick 12 | ``` 13 | 14 | The following command line options are available when running the ``benchmark.sh`` script: 15 | ```cli 16 | benchmark.sh [clean] [-t ] [quick|medium|long|profile|debug] 17 | ``` 18 | 19 | | Option | Description | 20 | |:-------:|:--------------------------------------------------------- | 21 | | clean | recompile the benchmark | 22 | | -t <threads> | execute JMH with the specified number of threads | 23 | | quick | a "quick" run of the benchmark; fewer iterations and forks | 24 | | medium | a "medium" run of the benchmark; more iterations and forks | 25 | | long | a "long" run of the benchmark; maximum iterations and forks | 26 | | profile | start JMH such as to allow attaching JProfiler (MacOS) | 27 | | debug | start JMH such as to allow the attachment of a debugger | 28 | -------------------------------------------------------------------------------- /benchmark/benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | JAVA_OPTIONS="-server -XX:-RestrictContended -XX:+AggressiveOpts -XX:+UseFastAccessorMethods -Xms1096m -Xmx1096m" 4 | JMH_OPTIONS="-prof gc -prof hs_thr" 5 | 6 | if [[ "clean" == "$1" || ! -f "./target/microbenchmarks.jar" ]]; then 7 | mvn clean package 8 | shift 9 | fi 10 | 11 | JMH_THREADS="-t 4" 12 | if [[ "$2" == "-t" ]]; then 13 | JMH_THREADS="-t $3" 14 | set -- "$1" "${@:4}" 15 | fi 16 | 17 | if [[ ! -f "/tmp/random.dat" ]]; then 18 | echo "" ; echo "Generating 250MB random data file into /tmp/random.dat ... this might take 15-30s." 19 | dd bs=1048576 count=250 /tmp/random.dat 20 | fi 21 | 22 | if [[ "quick" == "$1" ]]; then 23 | java -jar ./target/microbenchmarks.jar -jvmArgs "$JAVA_OPTIONS" $JMH_OPTIONS -wi 3 -i 8 $JMH_THREADS -f 2 $2 $3 $4 $5 $6 $7 $8 $9 24 | elif [[ "medium" == "$1" ]]; then 25 | java -jar ./target/microbenchmarks.jar -jvmArgs "$JAVA_OPTIONS" $JMH_OPTIONS -wi 3 -f 8 -i 6 $JMH_THREADS $2 $3 $4 $5 $6 $7 $8 $9 26 | elif [[ "profile" == "$1" ]]; then 27 | java -server $JAVA_OPTIONS -agentpath:/Applications/JProfiler.app/Contents/Resources/app/bin/macos/libjprofilerti.jnilib=port=8849 -jar ./target/microbenchmarks.jar -r 5 -wi 3 -i 8 $JMH_THREADS -f 0 $2 $3 $4 $5 $6 $7 $8 $9 28 | elif [[ "debug" == "$1" ]]; then 29 | java -server $JAVA_OPTIONS -Xdebug -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=y -jar ./target/microbenchmarks.jar -r 5 -wi 3 -i 8 -t 8 -f 0 $2 $3 $4 $5 $6 $7 $8 $9 30 | else # "long" 31 | java -jar ./target/microbenchmarks.jar -jvmArgs "$JAVA_OPTIONS" $JMH_OPTIONS -wi 3 -i 15 $JMH_THREADS $1 $2 $3 $4 $5 $6 $7 $8 $9 32 | fi 33 | -------------------------------------------------------------------------------- /benchmark/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.sonatype.oss 7 | oss-parent 8 | 7 9 | 10 | 11 | 12 | com.zaxxer 13 | nuprocess-benchmark 14 | 1.1.3-SNAPSHOT 15 | 16 | NuProcess Benchmarks 17 | 18 | 19 | UTF-8 20 | 21 | 1.19 22 | 23 | 24 | 25 | 26 | com.zaxxer 27 | nuprocess 28 | 2.1.0-SNAPSHOT 29 | 30 | 31 | 32 | org.openjdk.jmh 33 | jmh-core 34 | ${jmh.version} 35 | 36 | 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-compiler-plugin 43 | 3.7.0 44 | 45 | 46 | org.openjdk.jmh.generators.BenchmarkProcessor 47 | 48 | 49 | 50 | org.openjdk.jmh 51 | jmh-generator-annprocess 52 | ${jmh.version} 53 | 54 | 55 | 1.8 56 | 1.8 57 | 58 | 59 | 60 | 61 | org.apache.maven.plugins 62 | maven-shade-plugin 63 | 2.3 64 | 65 | 66 | package 67 | 68 | shade 69 | 70 | 71 | microbenchmarks 72 | 73 | 75 | org.openjdk.jmh.Main 76 | 77 | 78 | 79 | 80 | *:* 81 | 82 | META-INF/services/javax.annotation.processing.Processor 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /benchmark/src/main/java/com/zaxxer/nuprocess/benchmark/NuProcessBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.zaxxer.nuprocess.benchmark; 2 | 3 | import com.zaxxer.nuprocess.NuAbstractProcessHandler; 4 | import com.zaxxer.nuprocess.NuProcess; 5 | import com.zaxxer.nuprocess.NuProcessBuilder; 6 | import org.openjdk.jmh.annotations.Benchmark; 7 | import org.openjdk.jmh.profile.GCProfiler; 8 | import org.openjdk.jmh.profile.HotspotThreadProfiler; 9 | import org.openjdk.jmh.runner.Runner; 10 | import org.openjdk.jmh.runner.options.Options; 11 | import org.openjdk.jmh.runner.options.OptionsBuilder; 12 | 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.nio.ByteBuffer; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.nio.file.Paths; 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.zip.CRC32; 21 | 22 | /** 23 | * A simple performance benchmark which uses {@code cat} to write a file to {@code stdout}. 24 | *

25 | * This benchmark requires a test file to use. One method to generate a test file is: 26 | *

 27 |  * head -c 250M /dev/urandom >/tmp/random.dat
 28 |  * 
29 | *

30 | * The CRC32 for the file is also required to allow the benchmark to apply a spot check on the contents, after 31 | * they've been pumped through {@code cat}, to verify that the contents were not corrupted during processing. A 32 | * CRC32 checksum is used in preference to algorithms like SHA-256 because the speed of the hashing algorithm is 33 | * not the focus of this benchmark. 34 | *

35 | * After generating a test file, you can compute the CRC32 to update the benchmark by either running it once and 36 | * checking the value in the error message, or running: 37 | *

 38 |  * crc32 /tmp/random.dat
 39 |  * 
40 | * That will display the CRC32 checksum in hex, which can then be converted to decimal to match {@code CRC32}'s 41 | * {@code getValue()}. (The calculator on most operating systems can convert hex to decimal.) 42 | */ 43 | public class NuProcessBenchmark { 44 | 45 | private static final long TEST_CRC32; 46 | private static final String TEST_FILE = "/tmp/random.dat"; 47 | 48 | static { 49 | final Path path = Paths.get(TEST_FILE); 50 | if (!Files.exists(path)) { 51 | throw new IllegalStateException(TEST_FILE + " does not exist."); 52 | } 53 | 54 | final CRC32 crc32 = new CRC32(); 55 | final byte[] buf = new byte[8192]; 56 | try (InputStream is = Files.newInputStream(path)) { 57 | do { 58 | int rc = is.read(buf); 59 | if (rc < 0) { 60 | break; 61 | } 62 | crc32.update(buf, 0, rc); 63 | } while (true); 64 | TEST_CRC32 = crc32.getValue(); 65 | } 66 | catch (IOException e) { 67 | throw new RuntimeException(e); 68 | } 69 | } 70 | 71 | public static void main(String[] args) throws Exception { 72 | //System.setProperty("com.zaxxer.nuprocess.threads", "cores"); // set when running with forks(0) below 73 | 74 | Options options = new OptionsBuilder() 75 | .addProfiler(GCProfiler.class) 76 | .addProfiler(HotspotThreadProfiler.class) 77 | //.forks(0) // set when running with JProfiler to simplify profiling 78 | .forks(2) 79 | //.jvmArgsAppend("-Dcom.zaxxer.nuprocess.threads=cores") // to adjust the pump thread count 80 | .include(NuProcessBenchmark.class.getSimpleName()) 81 | .measurementIterations(50) 82 | .threads(10) 83 | .timeUnit(TimeUnit.SECONDS) 84 | .warmupIterations(1) 85 | .build(); 86 | 87 | new Runner(options).run(); 88 | } 89 | 90 | @Benchmark 91 | public void cat() throws Exception { 92 | NuProcessBuilder builder = new NuProcessBuilder("cat", TEST_FILE); 93 | builder.setProcessListener(new NuAbstractProcessHandler() { 94 | 95 | private final CRC32 crc32 = new CRC32(); 96 | 97 | @Override 98 | public void onExit(int statusCode) { 99 | long crc = crc32.getValue(); 100 | if (crc != TEST_CRC32) { 101 | System.err.println("Incorrect CRC32 checksum (" + crc + "); file corruption?"); 102 | } 103 | } 104 | 105 | @Override 106 | public void onStdout(ByteBuffer buffer, boolean closed) { 107 | // the contents of the file are run through CRC32 just to "do" something with them 108 | // note that update(ByteBuffer) requires Java 8. it's been selected because it doesn't 109 | // inflate the measured allocation rate by allocating buffers internally 110 | crc32.update(buffer); 111 | } 112 | }); 113 | 114 | NuProcess process = builder.start(); 115 | 116 | int exitCode = process.waitFor(5L, TimeUnit.MINUTES); 117 | if (exitCode == Integer.MIN_VALUE) { 118 | process.destroy(false); 119 | 120 | throw new AssertionError(process + " took longer than 5 minutes to complete"); 121 | } 122 | if (exitCode != 0) { 123 | throw new AssertionError(process + " failed (Exit code: " + exitCode + ")"); 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /mvnvm.properties: -------------------------------------------------------------------------------- 1 | mvn_version=3.9.6 2 | -------------------------------------------------------------------------------- /shippable.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | build: 7 | ci: 8 | - mvn install 9 | #Create folders for test and code coverage 10 | - mkdir -p shippable/testresults 11 | - cp -r target/surefire-reports/* shippable/testresults 12 | 13 | runtime: 14 | nodePool: shippable_shared_aarch64 15 | -------------------------------------------------------------------------------- /src/example/java/com/zaxxer/nuprocess/example/NuSchool.java: -------------------------------------------------------------------------------- 1 | package com.zaxxer.nuprocess.example; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.Arrays; 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.zip.Adler32; 7 | 8 | import com.zaxxer.nuprocess.NuAbstractProcessHandler; 9 | import com.zaxxer.nuprocess.NuProcess; 10 | import com.zaxxer.nuprocess.NuProcessBuilder; 11 | 12 | /** 13 | * This class demonstrates how one might use the NuProcess classes to run 5000 14 | * processes (in batches of 500, for 10 iterations). It is used as kind of a 15 | * benchmark to compare to the conventional method of accomplishing the same 16 | * (see the OldSchool example). 17 | * 18 | * @author Brett Wooldridge 19 | */ 20 | public class NuSchool 21 | { 22 | public static void main(String... args) 23 | { 24 | if (args.length < 1) 25 | { 26 | System.err.println("Usage: java com.zaxxer.nuprocess.example.NuSchool "); 27 | System.exit(0); 28 | } 29 | 30 | int PROCESSES = Integer.valueOf(args[0]); 31 | 32 | String command = "/bin/cat"; 33 | if (System.getProperty("os.name").toLowerCase().contains("win")) 34 | { 35 | command = "src\\test\\java\\org\\nuprocess\\cat.exe"; 36 | } 37 | 38 | long start = System.currentTimeMillis(); 39 | long maxFreeMem = 0; 40 | 41 | NuProcessBuilder pb = new NuProcessBuilder(Arrays.asList(command)); 42 | for (int times = 0; times < 10; times++) 43 | { 44 | NuProcess[] processes = new NuProcess[PROCESSES]; 45 | LottaProcessHandler[] handlers = new LottaProcessHandler[PROCESSES]; 46 | 47 | for (int i = 0; i < PROCESSES; i++) 48 | { 49 | handlers[i] = new LottaProcessHandler(); 50 | pb.setProcessListener(handlers[i]); 51 | processes[i] = pb.start(); 52 | } 53 | 54 | // Kick all of the processes to start going 55 | for (NuProcess process : processes) 56 | { 57 | process.wantWrite(); 58 | } 59 | 60 | for (NuProcess process : processes) 61 | { 62 | try 63 | { 64 | maxFreeMem = Math.max(maxFreeMem, Runtime.getRuntime().freeMemory()); 65 | process.waitFor(0, TimeUnit.SECONDS); 66 | } 67 | catch (InterruptedException e) 68 | { 69 | System.exit(-1); 70 | } 71 | } 72 | 73 | for (LottaProcessHandler handler : handlers) 74 | { 75 | if (handler.getAdler() != 4237270634l) 76 | { 77 | System.err.println("Adler32 mismatch between written and read"); 78 | System.exit(-1); 79 | } 80 | else if (handler.getExitCode() != 0) 81 | { 82 | System.err.println("Exit code not zero (0)"); 83 | System.exit(-1); 84 | } 85 | } 86 | } 87 | 88 | System.out.printf("Maximum memory used: %d\n", Runtime.getRuntime().totalMemory() - maxFreeMem); 89 | System.out.printf("Total execution time (ms): %d\n", (System.currentTimeMillis() - start)); 90 | System.exit(0); 91 | } 92 | 93 | private static class LottaProcessHandler extends NuAbstractProcessHandler 94 | { 95 | private static final int WRITES = 100; 96 | private static final int LIMIT; 97 | private static final byte[] bytes; 98 | 99 | private NuProcess nuProcess; 100 | private int writes; 101 | private int size; 102 | private int exitCode; 103 | 104 | private Adler32 readAdler32 = new Adler32(); 105 | 106 | static 107 | { 108 | // Create 600K of data. 109 | StringBuffer sb = new StringBuffer(); 110 | for (int i = 0; i < 6000; i++) 111 | { 112 | sb.append("1234567890"); 113 | } 114 | bytes = sb.toString().getBytes(); 115 | LIMIT = WRITES * bytes.length; 116 | } 117 | 118 | @Override 119 | public void onStart(final NuProcess nuProcess) 120 | { 121 | this.nuProcess = nuProcess; 122 | } 123 | 124 | @Override 125 | public void onExit(int statusCode) 126 | { 127 | exitCode = statusCode; 128 | } 129 | 130 | @Override 131 | public void onStdout(ByteBuffer buffer, boolean closed) 132 | { 133 | size += buffer.remaining(); 134 | 135 | byte[] bytes = new byte[buffer.remaining()]; 136 | buffer.get(bytes); 137 | readAdler32.update(bytes); 138 | 139 | if (size == LIMIT || closed) 140 | { 141 | nuProcess.closeStdin(true); 142 | } 143 | } 144 | 145 | @Override 146 | public boolean onStdinReady(ByteBuffer buffer) 147 | { 148 | buffer.put(bytes); 149 | buffer.flip(); 150 | return (++writes < WRITES); 151 | } 152 | 153 | int getExitCode() 154 | { 155 | return exitCode; 156 | } 157 | 158 | long getAdler() 159 | { 160 | return readAdler32.getValue(); 161 | } 162 | }; 163 | } 164 | -------------------------------------------------------------------------------- /src/example/java/com/zaxxer/nuprocess/example/OldSchool.java: -------------------------------------------------------------------------------- 1 | package com.zaxxer.nuprocess.example; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.BufferedOutputStream; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.util.concurrent.CyclicBarrier; 8 | import java.util.concurrent.LinkedBlockingQueue; 9 | import java.util.concurrent.ThreadPoolExecutor; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.zip.Adler32; 12 | 13 | /** 14 | * This class demonstrates how one might use the conventional java.lang.Process 15 | * class to run 5000 processes (in batches of 500, for 10 iterations). It is 16 | * used as kind of a benchmark to compare to the NuProcess method of accomplishing 17 | * the same (see the NuSchool example). 18 | * 19 | * Execution notes: 20 | * 21 | * Linux (CentOS) 22 | * In order to run this test, java was run with the following parameters: 23 | * -Xmx2048m -Xss256k 24 | * 25 | * The following had to be added to /etc/security/limits.conf: 26 | * #domain type item value 27 | * 28 | * [user] soft nofile 4096 29 | * [user] hard nofile 8192 30 | * [user] soft nproc 4096 31 | * [user] soft nproc 4096 32 | * where [user] is the username of the executing user (you). 33 | * 34 | * The following change was made to /etc/security/limits.d/90-nproc.conf: 35 | * * soft nproc 1024 36 | * was changed to: 37 | * * soft nproc 8024 38 | * 39 | * @author Brett Wooldridge 40 | */ 41 | public class OldSchool 42 | { 43 | private static volatile CyclicBarrier startBarrier; 44 | 45 | public static void main(String... args) 46 | { 47 | if (args.length < 1) 48 | { 49 | System.err.println("Usage: java com.zaxxer.nuprocess.example.OldSchool "); 50 | System.exit(0); 51 | } 52 | 53 | int PROCESSES = Integer.valueOf(args[0]); 54 | 55 | ThreadPoolExecutor outExecutor = new ThreadPoolExecutor(PROCESSES, PROCESSES, 10, TimeUnit.SECONDS, new LinkedBlockingQueue()); 56 | ThreadPoolExecutor inExecutor = new ThreadPoolExecutor(PROCESSES, PROCESSES, 10, TimeUnit.SECONDS, new LinkedBlockingQueue()); 57 | 58 | String command = "/bin/cat"; 59 | if (System.getProperty("os.name").toLowerCase().contains("win")) 60 | { 61 | command = "src\\test\\java\\org\\nuprocess\\cat.exe"; 62 | } 63 | 64 | ProcessBuilder pb = new ProcessBuilder(command); 65 | pb.redirectErrorStream(true); 66 | 67 | long start = System.currentTimeMillis(); 68 | long maxFreeMem = 0; 69 | 70 | for (int times = 0; times < 10; times++) 71 | { 72 | 73 | Process[] processes = new Process[PROCESSES]; 74 | InPumper[] inPumpers = new InPumper[PROCESSES]; 75 | OutPumper[] outPumpers = new OutPumper[PROCESSES]; 76 | 77 | startBarrier = new CyclicBarrier(PROCESSES); 78 | try 79 | { 80 | for (int i = 0; i < PROCESSES; i++) 81 | { 82 | Process process = pb.start(); 83 | processes[i] = process; 84 | 85 | outPumpers[i] = new OutPumper(new BufferedInputStream(process.getInputStream(), 65536)); 86 | inPumpers[i] = new InPumper(new BufferedOutputStream(process.getOutputStream(), 65536)); 87 | 88 | outExecutor.execute(outPumpers[i]); 89 | inExecutor.execute(inPumpers[i]); 90 | } 91 | 92 | for (Process process : processes) 93 | { 94 | maxFreeMem = Math.max(maxFreeMem, Runtime.getRuntime().freeMemory()); 95 | if (process.waitFor() != 0) 96 | { 97 | System.err.println("Exit code not zero (0)"); 98 | System.exit(-1); 99 | } 100 | } 101 | 102 | for (OutPumper pumper : outPumpers) 103 | { 104 | if (pumper.getAdler() != 4237270634l) 105 | { 106 | System.err.println("Adler32 mismatch between written and read"); 107 | System.exit(-1); 108 | } 109 | } 110 | } 111 | catch (Exception e) 112 | { 113 | e.printStackTrace(System.err); 114 | System.exit(-1); 115 | } 116 | } 117 | 118 | System.out.printf("Maximum memory used: %d\n", Runtime.getRuntime().totalMemory() - maxFreeMem); 119 | System.out.printf("Total execution time (ms): %d\n", (System.currentTimeMillis() - start)); 120 | System.exit(0); 121 | } 122 | 123 | public static class InPumper implements Runnable 124 | { 125 | private static final byte[] bytes; 126 | private OutputStream outputStream; 127 | 128 | static 129 | { 130 | // Create 600K of data. 131 | StringBuffer sb = new StringBuffer(); 132 | for (int i = 0; i < 6000; i++) 133 | { 134 | sb.append("1234567890"); 135 | } 136 | bytes = sb.toString().getBytes(); 137 | } 138 | 139 | public InPumper(OutputStream outputStream) 140 | { 141 | this.outputStream = outputStream; 142 | } 143 | 144 | @Override 145 | public void run() 146 | { 147 | try 148 | { 149 | startBarrier.await(); 150 | for (int i = 0; i < 100; i++) 151 | { 152 | outputStream.write(bytes); 153 | } 154 | 155 | outputStream.close(); 156 | } 157 | catch (Exception e) 158 | { 159 | System.err.println(e); 160 | return; 161 | } 162 | } 163 | } 164 | 165 | public static class OutPumper implements Runnable 166 | { 167 | private InputStream inputStream; 168 | private Adler32 readAdler32; 169 | 170 | OutPumper(InputStream inputStream) 171 | { 172 | this.inputStream = inputStream; 173 | 174 | this.readAdler32 = new Adler32(); 175 | } 176 | 177 | @Override 178 | public void run() 179 | { 180 | try 181 | { 182 | byte[] buf = new byte[65536]; 183 | while (true) 184 | { 185 | int rc = inputStream.read(buf); 186 | if (rc == -1) 187 | { 188 | break; 189 | } 190 | 191 | readAdler32.update(buf, 0, rc); 192 | } 193 | 194 | inputStream.close(); 195 | } 196 | catch (Exception e) 197 | { 198 | System.err.println(e); 199 | return; 200 | } 201 | } 202 | 203 | long getAdler() 204 | { 205 | return readAdler32.getValue(); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/example/java/com/zaxxer/nuprocess/example/SshExample.java: -------------------------------------------------------------------------------- 1 | package com.zaxxer.nuprocess.example; 2 | 3 | import java.io.PrintWriter; 4 | import java.nio.ByteBuffer; 5 | import java.util.Arrays; 6 | import java.util.LinkedList; 7 | import java.util.concurrent.Semaphore; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | import com.zaxxer.nuprocess.NuAbstractProcessHandler; 11 | import com.zaxxer.nuprocess.NuProcess; 12 | import com.zaxxer.nuprocess.NuProcessBuilder; 13 | 14 | public class SshExample 15 | { 16 | private PrintWriter writer = new PrintWriter(System.out); 17 | private String host; 18 | 19 | public static void main(String[] args) 20 | { 21 | if (args.length < 1) 22 | { 23 | System.err.println("Usage: java com.zaxxer.nuprocess.example.SshExample "); 24 | System.exit(0); 25 | } 26 | 27 | SshExample ssh = new SshExample(args[0]); 28 | ssh.execute(); 29 | } 30 | 31 | private SshExample(String host) 32 | { 33 | this.host = host; 34 | } 35 | 36 | private void execute() 37 | { 38 | NuProcessBuilder pb = new NuProcessBuilder(Arrays.asList("ssh", host)); 39 | ProcessHandler processHandler = new ProcessHandler(); 40 | pb.setProcessListener(processHandler); 41 | NuProcess np = pb.start(); 42 | 43 | processHandler.write("cd"); 44 | processHandler.write("ls -l"); 45 | processHandler.write("exit"); 46 | 47 | processHandler.awaitDisconnection(); 48 | } 49 | 50 | class ProcessHandler extends NuAbstractProcessHandler 51 | { 52 | private NuProcess nuProcess; 53 | private LinkedList cmdList = new LinkedList(); 54 | private Semaphore disconnected = new Semaphore(0); 55 | 56 | @Override 57 | public void onStart(NuProcess nuProcess) 58 | { 59 | this.nuProcess = nuProcess; 60 | } 61 | 62 | public void awaitDisconnection() 63 | { 64 | disconnected.acquireUninterruptibly(1); 65 | } 66 | 67 | public void clear() 68 | { 69 | cmdList.clear(); 70 | } 71 | 72 | public void close() 73 | { 74 | nuProcess.destroy(false); 75 | } 76 | 77 | //Send key event 78 | public void sendKey(String ch) 79 | { 80 | clear(); 81 | nuProcess.writeStdin(ByteBuffer.wrap(ch.getBytes())); 82 | } 83 | 84 | //Send command 85 | public void write(String stack) 86 | { 87 | cmdList.add(stack + "\n"); 88 | //nuProcess.hasPendingWrites(); 89 | nuProcess.wantWrite(); 90 | } 91 | 92 | public synchronized Boolean isPending() 93 | { 94 | return nuProcess.hasPendingWrites(); 95 | } 96 | 97 | @Override 98 | public boolean onStdinReady(ByteBuffer buffer) 99 | { 100 | if (!cmdList.isEmpty()) 101 | { 102 | String cmd = cmdList.poll(); 103 | buffer.put(cmd.getBytes()); 104 | buffer.flip(); 105 | } 106 | 107 | return !cmdList.isEmpty(); 108 | } 109 | 110 | @Override 111 | public void onStdout(ByteBuffer buffer, boolean closed) 112 | { 113 | int remaining = buffer.remaining(); 114 | byte[] bytes = new byte[remaining]; 115 | buffer.get(bytes); 116 | 117 | writer.print(new String(bytes)); 118 | writer.flush(); 119 | 120 | if (closed) 121 | { 122 | disconnected.release(); 123 | } 124 | 125 | // nuProcess.wantWrite(); 126 | // We're done, so closing STDIN will cause the "cat" process to exit 127 | //nuProcess.closeStdin(true); 128 | } 129 | 130 | @Override 131 | public void onStderr(ByteBuffer buffer, boolean closed) 132 | { 133 | this.onStdout(buffer, false); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/NuAbstractProcessHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess; 18 | 19 | import java.nio.ByteBuffer; 20 | 21 | /** 22 | * Convenience base class for a {@link NuProcessHandler} with default methods 23 | * that can be overridden. 24 | * 25 | * @author Brett Wooldridge 26 | */ 27 | public abstract class NuAbstractProcessHandler implements NuProcessHandler 28 | { 29 | /** {@inheritDoc} */ 30 | @Override 31 | public void onPreStart(NuProcess nuProcess) 32 | { 33 | } 34 | 35 | /** {@inheritDoc} */ 36 | @Override 37 | public void onStart(NuProcess nuProcess) 38 | { 39 | } 40 | 41 | /** {@inheritDoc} */ 42 | @Override 43 | public void onExit(int statusCode) 44 | { 45 | } 46 | 47 | /** {@inheritDoc} */ 48 | @Override 49 | public void onStdout(ByteBuffer buffer, boolean closed) 50 | { 51 | // Ensure we consume the entire buffer in case it's not used. 52 | buffer.position(buffer.limit()); 53 | } 54 | 55 | /** {@inheritDoc} */ 56 | @Override 57 | public void onStderr(ByteBuffer buffer, boolean closed) 58 | { 59 | // Ensure we consume the entire buffer in case it's not used. 60 | buffer.position(buffer.limit()); 61 | } 62 | 63 | /** {@inheritDoc} */ 64 | @Override 65 | public boolean onStdinReady(ByteBuffer buffer) 66 | { 67 | return false; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/NuProcess.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess; 18 | 19 | import java.nio.ByteBuffer; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | /** 23 | * The {@link NuProcessBuilder#start()} method creates a native process and 24 | * returns an instance of a platform-specific implementation of 25 | * {@link NuProcess} that can be used to control the process and obtain 26 | * information about it. The {@link NuProcess} interface provides methods for 27 | * expressing desire to perform output to the process, waiting for the process 28 | * to complete, checking the status of the process, and destroying (killing) the 29 | * process. 30 | *

31 | * The methods that create processes may not work well for special processes on 32 | * certain native platforms, such as native windowing processes, daemon 33 | * processes, Win16/DOS processes on Microsoft Windows, or shell scripts. 34 | *

35 | * See the {@link NuProcessHandler} interface for asynchronous callbacks related 36 | * to start, exit, and input/output notifications and operations for the 37 | * process. 38 | * 39 | * @author Brett Wooldridge 40 | */ 41 | public interface NuProcess 42 | { 43 | int BUFFER_CAPACITY = 65536; 44 | 45 | /** 46 | * Waits for the process to exit in a blocking fashion. See 47 | * {@link NuProcessHandler#onExit} for the non-blocking method process exit 48 | * notification and exit code retrieval. If the process is terminated by the 49 | * {@link #destroy(boolean)} method, the exit code is non-deterministic. 50 | * 51 | * @param timeout a timeout value, 0 indicates an infinite wait 52 | * @param timeUnit the unit of time indicator for the timeout value 53 | * @return the exit code of the process, or {@code Integer.MIN_VALUE} if the 54 | * timeout is reached 55 | * @throws InterruptedException thrown if the thread is interrupted while 56 | * waiting 57 | */ 58 | int waitFor(long timeout, TimeUnit timeUnit) throws InterruptedException; 59 | 60 | /** 61 | * Express a desire to write data to the STDIN stream of the process. Calling 62 | * this method will result in the {@link NuProcessHandler#onStdinReady} 63 | * callback method of the process handler being called when space is 64 | * available in the STDIN pipe. 65 | *

66 | * This method will throw a {@link IllegalStateException} if the 67 | * {@link #closeStdin} method has already been called. 68 | */ 69 | void wantWrite(); 70 | 71 | /** 72 | * Performs a "direct write" rather than expressing a desire to write using 73 | * {@link #wantWrite} and performing the write in a callback. Be careful 74 | * mixing this paradigm with the asynchronous paradigm imposed by 75 | * {@link #wantWrite()}. 76 | *

77 | * This method returns immediately and the write of the data occurs on the 78 | * asynchronous processing thread. You can perform multiple 79 | * {@code writeStdin()} calls without regard to the size of the interprocess 80 | * pipe; the writes will be queued and written asynchronously as space is 81 | * available in the pipe. 82 | *

83 | * Note that if the client process is not pulling data out of the pipe, 84 | * calling this method repeatedly will result is the accumulation of 85 | * unwritten ByteBuffers in the Java process and possibly an eventual out of 86 | * memory condition. 87 | *

88 | * Using a Direct ByteBuffer will provide performance improvements. Note that 89 | * NuProcess will not flip the buffer for you; after writing your data into 90 | * the {@code buffer} you must flip the buffer before returning from this 91 | * method. 92 | * 93 | * @param buffer the {@link ByteBuffer} to write to the STDIN stream of the 94 | * process 95 | */ 96 | void writeStdin(ByteBuffer buffer); 97 | 98 | /** 99 | * This method is used to close the STDIN pipe between the Java process and 100 | * the spawned process. 101 | *

102 | * If {@code force} is {@code true}, the STDIN pipe is immediately closed regardless of 103 | * pending unwritten data, and even data that has been written into the pipe 104 | * will be immediately discarded. 105 | *

106 | * Otherwise, STDIN will be closed only after all pending writes 107 | * have completed. 108 | * 109 | * @param force if true is passed, STDIN will be immediately closed 110 | * even if there are pending writes; otherwise, it will be closed 111 | * after all pending writes are completed 112 | */ 113 | void closeStdin(boolean force); 114 | 115 | /** 116 | * This method is most useful in combination with {@link #writeStdin}. This 117 | * method returns true if there is outstanding data to be written to the 118 | * stdin stream, and false if there are no pending writes. 119 | * 120 | * @return true if there are pending writes or STDIN is pending 121 | * close, false otherwise 122 | */ 123 | boolean hasPendingWrites(); 124 | 125 | /** 126 | * Terminates the process.
127 | *
128 | * If {@code force} is {@code false}, the process will be terminated 129 | * gracefully (i.e. its shutdown logic will be allowed to execute), assuming 130 | * the OS supports such behavior. Note that the process may not actually 131 | * terminate, as its cleanup logic may fail or it may choose to ignore the 132 | * termination request. If a guarantee of termination is required, call this 133 | * method with {@code force} equal to {@code true} instead. 134 | *

135 | * You can also call {@link #waitFor(long, TimeUnit)} after calling this 136 | * method with {@code force=false}, to give the process an opportunity to 137 | * terminate gracefully. If the timeout expires, you can then call this 138 | * method again with {@code force=true} to ensure the process is terminated. 139 | *

140 | * If {@code force} is true, the process is guaranteed to terminate, but 141 | * whether it is terminated gracefully or not is OS-dependent. Note that it 142 | * may take the OS a moment to terminate the process, so {@link #isRunning()} 143 | * may return true for a brief period after calling this method. You can use 144 | * {@link #waitFor(long, TimeUnit)} if you want to wait until the process has 145 | * actually been terminated.
146 | *
147 | * When this method is called with force, the exit code returned by 148 | * {@link #waitFor} or passed to the {@link NuProcessHandler#onExit} callback 149 | * method is non-deterministic. 150 | * 151 | * @param force if true is passed, the process will be forcibly 152 | * killed 153 | */ 154 | void destroy(boolean force); 155 | 156 | /** 157 | * Tests whether or not the process is still running or has exited. 158 | * 159 | * @return true if the process is still running, false if it has exited 160 | */ 161 | boolean isRunning(); 162 | 163 | /** 164 | * Sets a new process handler for this {@link NuProcess} instance. This 165 | * method is only safe to call from within one of the callback methods of the 166 | * existing {@link NuProcessHandler}. 167 | * 168 | * @param processHandler the new {@link NuProcessHandler} 169 | */ 170 | void setProcessHandler(NuProcessHandler processHandler); 171 | 172 | int getPID(); 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/NuProcessBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess; 18 | 19 | import java.nio.file.Path; 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Map.Entry; 25 | import java.util.TreeMap; 26 | 27 | /** 28 | * This class is used to create operating system processes. 29 | *

30 | * Each {@code NuProcessBuilder} instance manages a collection of process 31 | * attributes. The {@link #start()} method creates a new {@link NuProcess} 32 | * instance with those attributes. The {@link #start()} method can be invoked 33 | * repeatedly from the same instance to create new subprocesses with identical 34 | * or related attributes. 35 | *

36 | * Each {@code NuProcessBuilder} manages these attributes: 37 | *

    38 | *
  • a command, a list of strings which signifies the external 39 | * program file to be invoked and its arguments, if any. Which string lists 40 | * represent a valid operating system command is system-dependent. For example, 41 | * it is common for each conceptual argument to be an element in this list, but 42 | * there are operating systems where programs are expected to tokenize command 43 | * line strings themselves - on such a system a Java implementation might 44 | * require commands to contain exactly two elements.
  • 45 | * 46 | *
  • an environment, which is a system-dependent mapping from 47 | * variables to values. The initial value is a copy of the environment of the 48 | * current process. See {@link System#getenv()}.
  • 49 | *
50 | *

51 | * Modifying a process builder's attributes will affect processes subsequently 52 | * started by that object's {@link #start()} method, but will never affect 53 | * previously started processes or the Java process itself. 54 | * 55 | * @author Brett Wooldridge 56 | */ 57 | public class NuProcessBuilder 58 | { 59 | private static final NuProcessFactory factory; 60 | 61 | private final List command; 62 | private final TreeMap environment; 63 | private Path cwd; 64 | private NuProcessHandler processListener; 65 | 66 | static { 67 | String factoryClassName = null; 68 | String osname = System.getProperty("os.name").toLowerCase(); 69 | if (osname.contains("mac") || osname.contains("freebsd")) { 70 | factoryClassName = "com.zaxxer.nuprocess.osx.OsxProcessFactory"; 71 | } 72 | else if (osname.contains("win")) { 73 | factoryClassName = "com.zaxxer.nuprocess.windows.WinProcessFactory"; 74 | } 75 | else if (osname.contains("linux")) { 76 | factoryClassName = "com.zaxxer.nuprocess.linux.LinProcessFactory"; 77 | } 78 | else if (osname.contains("sunos")) { 79 | factoryClassName = "com.zaxxer.nuprocess.solaris.SolProcessFactory"; 80 | } 81 | 82 | if (factoryClassName == null) { 83 | throw new RuntimeException("Unsupported operating system: " + osname); 84 | } 85 | 86 | try { 87 | Class forName = Class.forName(factoryClassName); 88 | factory = (NuProcessFactory) forName.newInstance(); 89 | } 90 | catch (Exception e) { 91 | throw new RuntimeException(e); 92 | } 93 | } 94 | 95 | /** 96 | * Constructs a process builder with the specified operating system program 97 | * and arguments. This constructor makes a copy of the command list. Invokers 98 | * of this constructor must later call 99 | * {@link #setProcessListener(NuProcessHandler)} in order to set a 100 | * {@link NuProcessHandler} instance. 101 | * 102 | * @param commands a {@link List} of commands 103 | * @param environment The environment for the process 104 | */ 105 | public NuProcessBuilder(List commands, Map environment) 106 | { 107 | if (commands == null || commands.isEmpty()) { 108 | throw new IllegalArgumentException("List of commands may not be null or empty"); 109 | } 110 | 111 | this.environment = new TreeMap(environment); 112 | this.command = new ArrayList(commands); 113 | } 114 | 115 | /** 116 | * Constructs a process builder with the specified operating system program 117 | * and arguments. This constructor makes a copy of the command list. Invokers 118 | * of this constructor must later call 119 | * {@link #setProcessListener(NuProcessHandler)} in order to set a 120 | * {@link NuProcessHandler} instance. 121 | * 122 | * @param commands a {@link List} of commands 123 | */ 124 | public NuProcessBuilder(List commands) 125 | { 126 | if (commands == null || commands.isEmpty()) { 127 | throw new IllegalArgumentException("List of commands may not be null or empty"); 128 | } 129 | 130 | this.environment = new TreeMap(System.getenv()); 131 | this.command = new ArrayList(commands); 132 | } 133 | 134 | /** 135 | * Constructs a process builder with the specified operating system program 136 | * and arguments. Invokers of this constructor must later call 137 | * {@link #setProcessListener(NuProcessHandler)} in order to set a 138 | * {@link NuProcessHandler} instance. 139 | * 140 | * @param commands a list of commands 141 | */ 142 | public NuProcessBuilder(String... commands) 143 | { 144 | if (commands == null || commands.length == 0) { 145 | throw new IllegalArgumentException("List of commands may not be null or empty"); 146 | } 147 | 148 | this.environment = new TreeMap(System.getenv()); 149 | this.command = new ArrayList(Arrays.asList(commands)); 150 | } 151 | 152 | /** 153 | * Constructs a process builder with the specified {@link NuProcessHandler} 154 | * and operating system program and arguments. 155 | * 156 | * @param nuProcessHandler a {@link NuProcessHandler} instance 157 | * @param commands a list of commands 158 | */ 159 | public NuProcessBuilder(NuProcessHandler nuProcessHandler, String... commands) 160 | { 161 | this(commands); 162 | 163 | if (nuProcessHandler == null) { 164 | throw new IllegalArgumentException("A NuProcessListener must be specified"); 165 | } 166 | 167 | this.processListener = nuProcessHandler; 168 | } 169 | 170 | /** 171 | * Constructs a process builder with the specified {@link NuProcessHandler} 172 | * and operating system program and arguments. This constructor makes a copy 173 | * of the command list. 174 | * 175 | * @param nuProcessHandler a {@link NuProcessHandler} instance 176 | * @param commands a {@link List} of commands 177 | */ 178 | public NuProcessBuilder(NuProcessHandler nuProcessHandler, List commands) 179 | { 180 | this(commands); 181 | 182 | if (nuProcessHandler == null) { 183 | throw new IllegalArgumentException("A NuProcessListener must be specified"); 184 | } 185 | 186 | this.processListener = nuProcessHandler; 187 | } 188 | 189 | /** 190 | * Get the {@link List} of commands that were used to construct this 191 | * {@link NuProcessBuilder}. 192 | * 193 | * @return a {@link List} of commands 194 | */ 195 | public List command() 196 | { 197 | return command; 198 | } 199 | 200 | /** 201 | * Returns a string map view of this process builder's environment. Whenever 202 | * a process builder is created, the environment is initialized to a copy of 203 | * the current process environment. Subprocesses subsequently started by this 204 | * object's {@link #start()} method will use this map as their environment. 205 | *

206 | * The returned object may be modified using ordinary Map operations prior to 207 | * invoking the {@link #start()} method. The returned map is typically 208 | * case-sensitive on all platforms. 209 | * 210 | * @return This process builder's environment 211 | */ 212 | public Map environment() 213 | { 214 | return environment; 215 | } 216 | 217 | /** 218 | * Set the {@link NuProcessHandler} instance that will be used for the next 219 | * and subsequent launch of a {@link NuProcess} when calling the 220 | * {@link #start()} method. 221 | * 222 | * @param listener a {@link NuProcessHandler} instance 223 | */ 224 | public void setProcessListener(NuProcessHandler listener) 225 | { 226 | if (listener == null) { 227 | throw new IllegalArgumentException("A NuProcessListener must be specified"); 228 | } 229 | 230 | this.processListener = listener; 231 | } 232 | 233 | /** 234 | * Set the {@link Path} to which the current working directory (cwd) of the 235 | * subsequent launch of a {@link NuProcess} will be set when calling the 236 | * {@link #start()} method. 237 | * 238 | * @param cwd a {@link Path} to use for the process's current working 239 | * directory, or {@code null} to disable setting the cwd of 240 | * subsequently launched proceses 241 | */ 242 | public void setCwd(Path cwd) 243 | { 244 | this.cwd = cwd; 245 | } 246 | 247 | /** 248 | * Spawn the child process with the configured commands, environment, and 249 | * {@link NuProcessHandler}. 250 | * 251 | * @return a {@link NuProcess} instance or {@code null} if there is an 252 | * immediately detectable launch failure 253 | */ 254 | public NuProcess start() 255 | { 256 | ensureNoNullCharacters(command); 257 | ensureListener(); 258 | String[] env = prepareEnvironment(); 259 | 260 | return factory.createProcess(command, env, processListener, cwd); 261 | } 262 | 263 | /** 264 | * Spawn the child process with the configured commands, environment, and {@link NuProcessHandler} 265 | * and wait for it to complete running. 266 | * 267 | * @since 1.3 268 | */ 269 | public void run() 270 | { 271 | ensureNoNullCharacters(command); 272 | ensureListener(); 273 | String[] env = prepareEnvironment(); 274 | 275 | factory.runProcess(command, env, processListener, cwd); 276 | } 277 | 278 | private void ensureListener() 279 | { 280 | if (processListener == null) { 281 | throw new IllegalArgumentException("NuProcessHandler not specified"); 282 | } 283 | } 284 | 285 | private void ensureNoNullCharacters(List commands) { 286 | for (String command : commands) { 287 | if (command.indexOf('\u0000') >= 0) { 288 | throw new IllegalArgumentException("Commands may not contain null characters"); 289 | } 290 | } 291 | } 292 | 293 | private void ensureNoNullCharacters(String environment) { 294 | if (environment.indexOf('\u0000') >= 0) { 295 | throw new IllegalArgumentException("Environment may not contain null characters"); 296 | } 297 | } 298 | 299 | private String[] prepareEnvironment() 300 | { 301 | String[] env = new String[environment.size()]; 302 | int i = 0; 303 | for (Entry entrySet : environment.entrySet()) { 304 | String key = entrySet.getKey(); 305 | String value = entrySet.getValue(); 306 | ensureNoNullCharacters(key); 307 | ensureNoNullCharacters(value); 308 | env[i++] = key + "=" + value; 309 | } 310 | 311 | return env; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/NuProcessFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess; 18 | 19 | import java.nio.file.Path; 20 | import java.util.List; 21 | 22 | /** 23 | * This is an internal class. Instances of this interface create and 24 | * start processes in a platform-specific fashion. 25 | * 26 | * @author Brett Wooldridge 27 | */ 28 | public interface NuProcessFactory 29 | { 30 | NuProcess createProcess(List commands, String[] env, NuProcessHandler processListener, Path cwd); 31 | 32 | /** 33 | * Runs the process synchronously. 34 | * 35 | * Pumping is done on the calling thread, and this method will not return until the process has exited. 36 | * 37 | * @since 1.3 38 | */ 39 | void runProcess(List commands, String[] env, NuProcessHandler processListener, Path cwd); 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/NuProcessHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess; 18 | 19 | import java.nio.ByteBuffer; 20 | 21 | /** 22 | * Executors of a {@link NuProcess} must provide an implementation of this class 23 | * to the {@link ProcessBuilder} prior to calling {@link ProcessBuilder#start()} 24 | * . This interface provides the primary means of asynchronous interaction 25 | * between the running child process and your code. 26 | *

27 | * Methods specified by this interface will be invoked by the asynchronous 28 | * processing thread. It is up to the implementation of this interface to handle 29 | * the resulting state changes or process the data passed to it. 30 | *

31 | * Note that the processing thread that executes these callbacks on behalf of a 32 | * NuProcess is the same thread that handles processing for other instances of 33 | * NuProcess, and as a result you should never perform a blocking or 34 | * time-consuming operation on the callback thread. Doing so will block or 35 | * severely hinder the processing of other NuProcess instances. Any operation 36 | * that requires more than single-digit millisecond processing should be handed 37 | * off to another thread so that the processing thread can return to immediately 38 | * service other NuProcess instances. 39 | * 40 | * @author Brett Wooldridge 41 | */ 42 | public interface NuProcessHandler 43 | { 44 | /** 45 | * This method is invoked when you call the {@link ProcessBuilder#start()} 46 | * method. This is an opportunity to store away the {@code NuProcess} 47 | * instance, possibly in your listener, so that it can be used for 48 | * interaction within other callbacks in this interface. 49 | *

50 | * Unlike the {@link #onStart(NuProcess)} method, this method is invoked 51 | * before the process is spawned, and is guaranteed to be invoked before any 52 | * other methods are called. 53 | * 54 | * @param nuProcess The {@link NuProcess} that is starting. Note that the 55 | * instance is not yet initialized, so it is not legal to call any of 56 | * its methods, and doing so will result in undefined behavior. If you 57 | * need to call any of the instance's methods, use 58 | * {@link #onStart(NuProcess)} instead. 59 | */ 60 | void onPreStart(NuProcess nuProcess); 61 | 62 | /** 63 | * This method is invoked when you call the {@link ProcessBuilder#start()} 64 | * method. This is an opportunity to store away the {@code NuProcess} 65 | * instance, possibly in your listener, so that it can be used for 66 | * interaction within other callbacks in this interface. 67 | *

68 | * Note that this method is called at some point after the process is 69 | * spawned. It is possible for other methods (even {@link #onExit(int)}) to 70 | * be called first. If you need a guarantee that no other methods will be 71 | * called first, use {@link #onPreStart(NuProcess)} instead. 72 | * 73 | * @param nuProcess the {@link NuProcess} that is starting 74 | */ 75 | void onStart(NuProcess nuProcess); 76 | 77 | /** 78 | * This method is invoked when the process exits. This method is also invoked 79 | * immediately in the case of a failure to launch the child process. 80 | *

81 | * There are two special values, besides ordinary process exit codes, that 82 | * may be passed to this method. A value of {@link Integer#MIN_VALUE} 83 | * indicates some kind of launch failure. A value of 84 | * {@link Integer#MAX_VALUE} indicates an unknown or expected failure mode. 85 | * 86 | * @param exitCode the exit code of the process, or a special value 87 | * indicating unexpected failures 88 | */ 89 | void onExit(int exitCode); 90 | 91 | /** 92 | * This method is invoked when there is stdout data to process or an the 93 | * end-of-file (EOF) condition has been reached. In the case of EOF, the 94 | * {@code closed} parameter will be {@code true}; this is your signal that 95 | * EOF has been reached. 96 | *

97 | * You do not own the {@link ByteBuffer} provided to you. You should not 98 | * retain a reference to this buffer. 99 | *

100 | * Upon returning from this method, if any bytes are left in the buffer 101 | * (i.e., {@code buffer.hasRemaining()} returns {@code true}), then the 102 | * buffer will be {@link ByteBuffer#compact() compacted} after returning. Any 103 | * unused data will be kept at the start of the buffer and passed back to you 104 | * as part of the next invocation of this method (which might be when EOF is 105 | * reached and {@code closed} is {@code true}). 106 | *

107 | * Exceptions thrown out from your method will be ignored, but your method 108 | * should handle all exceptions itself. 109 | * 110 | * @param buffer a {@link ByteBuffer} containing received stdout data 111 | * @param closed {@code true} if EOF has been reached 112 | */ 113 | void onStdout(ByteBuffer buffer, boolean closed); 114 | 115 | /** 116 | * This method is invoked when there is stderr data to process or an the 117 | * end-of-file (EOF) condition has been reached. In the case of EOF, the 118 | * {@code closed} parameter will be {@code true}; this is your signal that 119 | * EOF has been reached. 120 | *

121 | * You do not own the {@link ByteBuffer} provided to you. You should not 122 | * retain a reference to this buffer. 123 | *

124 | * Upon returning from this method, if any bytes are left in the buffer 125 | * (i.e., {@code buffer.hasRemaining()} returns {@code true}), then the 126 | * buffer will be {@link ByteBuffer#compact() compacted} after returning. Any 127 | * unused data will be kept at the start of the buffer and passed back to you 128 | * as part of the next invocation of this method (which might be when EOF is 129 | * reached and {@code closed} is {@code true}). 130 | *

131 | * Users wishing to merge stderr into stdout should simply delegate this 132 | * callback to {@link #onStdout(ByteBuffer, boolean)} when invoked, like so: 133 | * 134 | *

135 |     *    public void onStderr(ByteBuffer buffer, closed) {
136 |     *       if (!closed) {
137 |     *          onStdout(buffer, closed);
138 |     *       }
139 |     *    }
140 |     * 
141 | *

142 | * Notice that an EOF check is performed. If you merge streams in this way, 143 | * and you do not check for EOF here, then your 144 | * {@link #onStdout(ByteBuffer, boolean)} method will be called twice for an 145 | * EOF condition; once when the stdout stream closes, and once when the 146 | * stderr stream closes. If you check for EOF as above, your 147 | * {@link #onStdout(ByteBuffer, boolean)} method would only be called once 148 | * (for the close of stdout). 149 | *

150 | * Exceptions thrown out from your method will be ignored, but your method 151 | * should handle all exceptions itself. 152 | * 153 | * @param buffer a {@link ByteBuffer} containing received stderr data 154 | * @param closed {@code true} if EOF has been reached 155 | */ 156 | void onStderr(ByteBuffer buffer, boolean closed); 157 | 158 | /** 159 | * This method is invoked after you have expressed a desire to write to stdin 160 | * by first calling {@link NuProcess#wantWrite()}. When this method is 161 | * invoked, your code should write data to be sent to the stdin of the child 162 | * process into the provided {@link ByteBuffer}. After writing data into the 163 | * {@code buffer} your code must {@link ByteBuffer#flip() flip} the 164 | * buffer before returning. 165 | *

166 | * If not all of the data needed to be written will fit in the provided 167 | * {@code buffer}, this method can return {@code true} to indicate a desire 168 | * to write more data. If there is no more data to be written at the time 169 | * this method is invoked, then {@code false} should be returned from this 170 | * method. It is always possible to call {@link NuProcess#wantWrite()} later 171 | * if data becomes available to be written. 172 | * 173 | * @param buffer a {@link ByteBuffer} into which your stdin-bound data should 174 | * be written 175 | * @return true if you have more data to write immediately, false otherwise 176 | */ 177 | boolean onStdinReady(ByteBuffer buffer); 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/codec/NuAbstractCharsetHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Ben Hamilton 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.codec; 18 | 19 | import com.zaxxer.nuprocess.NuProcess; 20 | import com.zaxxer.nuprocess.NuProcessHandler; 21 | 22 | import java.nio.ByteBuffer; 23 | import java.nio.CharBuffer; 24 | import java.nio.charset.Charset; 25 | import java.nio.charset.CharsetDecoder; 26 | import java.nio.charset.CharsetEncoder; 27 | import java.nio.charset.CoderResult; 28 | 29 | /** 30 | * Convenience implementation of {@link NuProcessHandler} which decodes stdin, 31 | * stdout, and stderr bytes to and from Java UTF-16 string data using a 32 | * {@link Charset} or separate {@link CharsetDecoder} and {@link CharsetEncoder 33 | * CharsetEncoders}. 34 | *

35 | * Subclass this and override any of {@link #onStdinCharsReady(CharBuffer)}, 36 | * {@link #onStdoutChars(CharBuffer, boolean, CoderResult)}, and/or 37 | * {@link #onStderrChars(CharBuffer, boolean, CoderResult)} to process encoded 38 | * and decoded string data. 39 | *

40 | * You can also override any of the methods from {@link NuProcessHandler}) to 41 | * customize process handling behavior. 42 | * 43 | * @author Ben Hamilton 44 | */ 45 | public abstract class NuAbstractCharsetHandler implements NuProcessHandler 46 | { 47 | private final NuCharsetEncoder stdinEncoder; 48 | private final NuCharsetDecoder stdoutDecoder; 49 | private final NuCharsetDecoder stderrDecoder; 50 | 51 | private class StdinEncoderHandler implements NuCharsetEncoderHandler 52 | { 53 | @Override 54 | public boolean onStdinReady(CharBuffer buffer) 55 | { 56 | return onStdinCharsReady(buffer); 57 | } 58 | 59 | @Override 60 | public void onEncoderError(CoderResult result) 61 | { 62 | onStdinEncoderError(result); 63 | } 64 | } 65 | 66 | private class StdoutDecoderHandler implements NuCharsetDecoderHandler 67 | { 68 | @Override 69 | public void onDecode(CharBuffer buffer, boolean closed, CoderResult decoderResult) 70 | { 71 | onStdoutChars(buffer, closed, decoderResult); 72 | } 73 | } 74 | 75 | private class StderrDecoderHandler implements NuCharsetDecoderHandler 76 | { 77 | @Override 78 | public void onDecode(CharBuffer buffer, boolean closed, CoderResult decoderResult) 79 | { 80 | onStderrChars(buffer, closed, decoderResult); 81 | } 82 | } 83 | 84 | /** 85 | * Constructor which encodes and decodes stdin, stdout, and stderr bytes 86 | * using the given {@link Charset}. 87 | * 88 | * @param charset The {@link Charset} with which to encode and decode stdin, 89 | * stdout, and stderr bytes 90 | */ 91 | protected NuAbstractCharsetHandler(Charset charset) 92 | { 93 | this(charset.newEncoder(), charset.newDecoder(), charset.newDecoder()); 94 | } 95 | 96 | /** 97 | * Constructor which encodes and decodes stdin, stdout, and stderr bytes 98 | * using specific {@link CharsetEncoder} and {@link CharsetDecoder 99 | * CharsetDecoders}, then invokes {@link #onStdinCharsReady(CharBuffer)}, 100 | * {@link #onStdoutChars(CharBuffer, boolean, CoderResult)}, and 101 | * {@link #onStderrChars(CharBuffer, boolean, CoderResult)} to process the 102 | * encoded and decoded string data. 103 | * 104 | * @param stdinEncoder The {@link CharsetEncoder} with which to encode stdin 105 | * bytes 106 | * @param stdoutDecoder The {@link CharsetDecoder} with which to decode 107 | * stdout bytes 108 | * @param stderrDecoder The {@link CharsetDecoder} with which to decode 109 | * stderr bytes 110 | */ 111 | protected NuAbstractCharsetHandler(CharsetEncoder stdinEncoder, CharsetDecoder stdoutDecoder, CharsetDecoder stderrDecoder) 112 | { 113 | this.stdinEncoder = new NuCharsetEncoder(new StdinEncoderHandler(), stdinEncoder); 114 | this.stdoutDecoder = new NuCharsetDecoder(new StdoutDecoderHandler(), stdoutDecoder); 115 | this.stderrDecoder = new NuCharsetDecoder(new StderrDecoderHandler(), stderrDecoder); 116 | } 117 | 118 | /** 119 | * Override this to provide Unicode Java string data to stdin. 120 | * 121 | * @param buffer The {@link CharBuffer} into which you should write string 122 | * data to be fed to stdin 123 | * @return {@code true} if you have more string data to feed to stdin 124 | */ 125 | protected boolean onStdinCharsReady(CharBuffer buffer) 126 | { 127 | return false; 128 | } 129 | 130 | /** 131 | * Override this to handle errors encoding string data received from 132 | * {@link #onStdinCharsReady(CharBuffer)}. 133 | * 134 | * @param result The {@link CoderResult} indicating encoder error 135 | */ 136 | protected void onStdinEncoderError(CoderResult result) 137 | { 138 | } 139 | 140 | /** 141 | * Override this to receive decoded Unicode Java string data read from 142 | * stdout. 143 | *

144 | * Make sure to set the {@link CharBuffer#position() position} of 145 | * {@code buffer} to indicate how much data you have read before returning. 146 | * 147 | * @param buffer The {@link CharBuffer} receiving Unicode string data. 148 | */ 149 | protected void onStdoutChars(CharBuffer buffer, boolean closed, CoderResult coderResult) 150 | { 151 | // Consume the entire buffer by default. 152 | buffer.position(buffer.limit()); 153 | } 154 | 155 | /** 156 | * Override this to receive decoded Unicode Java string data read from 157 | * stderr. 158 | *

159 | * Make sure to set the {@link CharBuffer#position() position} of 160 | * {@code buffer} to indicate how much data you have read before returning. 161 | * 162 | * @param buffer The {@link CharBuffer} receiving Unicode string data. 163 | */ 164 | protected void onStderrChars(CharBuffer buffer, boolean closed, CoderResult coderResult) 165 | { 166 | // Consume the entire buffer by default. 167 | buffer.position(buffer.limit()); 168 | } 169 | 170 | /** {@inheritDoc} */ 171 | @Override 172 | public void onPreStart(NuProcess nuProcess) 173 | { 174 | } 175 | 176 | /** {@inheritDoc} */ 177 | @Override 178 | public void onStart(NuProcess nuProcess) 179 | { 180 | } 181 | 182 | /** {@inheritDoc} */ 183 | @Override 184 | public void onExit(int exitCode) 185 | { 186 | } 187 | 188 | /** {@inheritDoc} */ 189 | @Override 190 | public final void onStdout(ByteBuffer buffer, boolean closed) 191 | { 192 | stdoutDecoder.onOutput(buffer, closed); 193 | } 194 | 195 | /** {@inheritDoc} */ 196 | @Override 197 | public final void onStderr(ByteBuffer buffer, boolean closed) 198 | { 199 | stderrDecoder.onOutput(buffer, closed); 200 | } 201 | 202 | /** {@inheritDoc} */ 203 | @Override 204 | public final boolean onStdinReady(ByteBuffer buffer) 205 | { 206 | return stdinEncoder.onStdinReady(buffer); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/codec/NuCharsetDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Ben Hamilton 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.codec; 18 | 19 | import com.zaxxer.nuprocess.NuProcess; 20 | import com.zaxxer.nuprocess.NuProcessHandler; 21 | 22 | import java.nio.ByteBuffer; 23 | import java.nio.CharBuffer; 24 | import java.nio.charset.Charset; 25 | import java.nio.charset.CharsetDecoder; 26 | import java.nio.charset.CoderResult; 27 | 28 | /** 29 | * Implementation of {@link NuProcessHandler#onStdout(ByteBuffer, boolean)} or 30 | * {@link NuProcessHandler#onStderr(ByteBuffer, boolean)} which handles decoding 31 | * of stdout or stderr bytes to Java UTF-16 string data. 32 | * 33 | * Calls back into a {@link NuCharsetDecoderHandler} with decoded stdout or 34 | * stderr string data. 35 | * 36 | * This class is not intended to be subclassed. 37 | * 38 | * @author Ben Hamilton 39 | */ 40 | public final class NuCharsetDecoder 41 | { 42 | private final NuCharsetDecoderHandler handler; 43 | private final CharsetDecoder decoder; 44 | private final CharBuffer charBuffer; 45 | 46 | /** 47 | * Creates a decoder which uses a single {@link Charset} to decode output 48 | * data. 49 | * 50 | * @param handler {@link NuCharsetDecoderHandler} called back with decoded 51 | * string data 52 | * @param charset {@link Charset} used to decode output data 53 | */ 54 | public NuCharsetDecoder(NuCharsetDecoderHandler handler, Charset charset) 55 | { 56 | this(handler, charset.newDecoder()); 57 | } 58 | 59 | /** 60 | * Creates a decoder which uses a {@link CharsetDecoder CharsetDecoders} to 61 | * decode output data. 62 | * 63 | * @param handler {@link NuCharsetDecoderHandler} called back with decoded 64 | * string data 65 | * @param decoder {@link CharsetDecoder} used to decode stdout bytes to 66 | * string data 67 | */ 68 | public NuCharsetDecoder(NuCharsetDecoderHandler handler, CharsetDecoder decoder) 69 | { 70 | this.handler = handler; 71 | this.decoder = decoder; 72 | this.charBuffer = CharBuffer.allocate(NuProcess.BUFFER_CAPACITY); 73 | } 74 | 75 | /** 76 | * Implementation of {@link NuProcessHandler#onStdout(ByteBuffer, boolean)} 77 | * or {@link NuProcessHandler#onStderr(ByteBuffer, boolean)} which decodes 78 | * output data and forwards it to {@code handler}. 79 | * 80 | * @param buffer {@link ByteBuffer} which received bytes from stdout or 81 | * stderr 82 | * @param closed true if stdout or stderr was closed, false otherwise 83 | */ 84 | public void onOutput(ByteBuffer buffer, boolean closed) 85 | { 86 | CoderResult coderResult = decoder.decode(buffer, charBuffer, /* endOfInput */ closed); 87 | charBuffer.flip(); 88 | this.handler.onDecode(charBuffer, closed, coderResult); 89 | charBuffer.compact(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/codec/NuCharsetDecoderHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Ben Hamilton 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.codec; 18 | 19 | import com.zaxxer.nuprocess.NuProcessHandler; 20 | 21 | import java.nio.CharBuffer; 22 | import java.nio.charset.CoderResult; 23 | 24 | /** 25 | * Callbacks invoked by {@link NuCharsetDecoder} with decoded string data. 26 | * 27 | * @see NuProcessHandler 28 | */ 29 | public interface NuCharsetDecoderHandler 30 | { 31 | /** 32 | * This method is invoked when there is decoded data to process or an the 33 | * end-of-file (EOF) condition has been reached. In the case of EOF, the 34 | * {@code closed} parameter will be {@code true}; this is your signal that 35 | * EOF has been reached. 36 | *

37 | * You do not own the {@link CharBuffer} provided to you. You should not 38 | * retain a reference to this buffer. 39 | *

40 | * Upon returning from this method, if any characters are left in the buffer 41 | * (i.e., {@code buffer.hasRemaining()} returns {@code true}), then the 42 | * buffer will be {@link CharBuffer#compact() compacted} after returning. Any 43 | * unused data will be kept at the start of the buffer and passed back to you 44 | * as part of the next invocation of this method (which might be when EOF is 45 | * reached and {@code closed} is {@code true}). 46 | *

47 | * Exceptions thrown out from your method will be ignored, but your method 48 | * should handle all exceptions itself. 49 | * 50 | * @param buffer a {@link CharBuffer} containing received and decoded data 51 | * @param closed {@code true} if EOF has been reached 52 | * @param decoderResult a {@link CoderResult} signifying whether an error was 53 | * encountered decoding stdout bytes 54 | */ 55 | void onDecode(CharBuffer buffer, boolean closed, CoderResult decoderResult); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/codec/NuCharsetEncoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Ben Hamilton 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.codec; 18 | 19 | import com.zaxxer.nuprocess.NuProcess; 20 | import com.zaxxer.nuprocess.NuProcessHandler; 21 | 22 | import java.nio.ByteBuffer; 23 | import java.nio.CharBuffer; 24 | import java.nio.charset.Charset; 25 | import java.nio.charset.CharsetEncoder; 26 | import java.nio.charset.CoderResult; 27 | 28 | /** 29 | * Implementation of {@link NuProcessHandler#onStdinReady(ByteBuffer)} which 30 | * handles encoding of stdin bytes to Java UTF-16 string data. 31 | * 32 | * Calls back into a {@link NuCharsetEncoderHandler} with a {@link CharBuffer} 33 | * whose contents will be encoded with a specified {@link Charset} or 34 | * {@link CharsetEncoder} then passed to the stdin of a process. 35 | * 36 | * This class is not intended to be subclassed. 37 | * 38 | * @author Ben Hamilton 39 | */ 40 | public final class NuCharsetEncoder 41 | { 42 | private final NuCharsetEncoderHandler handler; 43 | private final CharsetEncoder encoder; 44 | private final CharBuffer charBuffer; 45 | 46 | /** 47 | * Creates an encoder which uses a single {@link Charset} to encode input 48 | * data. 49 | * 50 | * @param handler {@link NuCharsetEncoderHandler} called back with a string 51 | * buffer to be encoded and fed to stdin 52 | * @param charset {@link Charset} used to encode stdin data to bytes 53 | */ 54 | public NuCharsetEncoder(NuCharsetEncoderHandler handler, Charset charset) 55 | { 56 | this(handler, charset.newEncoder()); 57 | } 58 | 59 | /** 60 | * Creates an encoder which uses a {@link CharsetEncoder} to encode input 61 | * data. 62 | * 63 | * @param handler {@link NuCharsetEncoderHandler} called back with a string 64 | * buffer into which the caller writes string data to be written to 65 | * stdin 66 | * @param encoder {@link CharsetEncoder} used to encode stdin string data to 67 | * bytes 68 | */ 69 | public NuCharsetEncoder(NuCharsetEncoderHandler handler, CharsetEncoder encoder) 70 | { 71 | this.handler = handler; 72 | this.encoder = encoder; 73 | this.charBuffer = CharBuffer.allocate(NuProcess.BUFFER_CAPACITY); 74 | } 75 | 76 | /** 77 | * Implementation of {@link NuProcessHandler#onStdinReady(ByteBuffer)} which 78 | * calls {@link handler} with a string buffer then encodes it to bytes and 79 | * feeds it to the process's stdin. 80 | * 81 | * @param buffer The {@link ByteBuffer} passed to 82 | * {@link NuProcessHandler#onStdinReady(ByteBuffer)} 83 | * @return true if more data needs to be passed to stdin, false otherwise 84 | */ 85 | public boolean onStdinReady(ByteBuffer buffer) 86 | { 87 | // TODO: Should we avoid invoking onStdinReady() when it returned false previously? 88 | boolean endOfInput = !this.handler.onStdinReady(charBuffer); 89 | CoderResult encoderResult = encoder.encode(charBuffer, buffer, endOfInput); 90 | buffer.flip(); 91 | charBuffer.compact(); 92 | if (encoderResult.isError()) { 93 | this.handler.onEncoderError(encoderResult); 94 | } 95 | if (encoderResult.isOverflow()) { 96 | return true; 97 | } 98 | else if (endOfInput) { 99 | CoderResult flushResult = encoder.flush(buffer); 100 | return flushResult.isOverflow(); 101 | } 102 | else { 103 | return true; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/codec/NuCharsetEncoderHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Ben Hamilton 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.codec; 18 | 19 | import java.nio.CharBuffer; 20 | import java.nio.charset.CoderResult; 21 | 22 | import com.zaxxer.nuprocess.NuProcess; 23 | import com.zaxxer.nuprocess.NuProcessHandler; 24 | 25 | /** 26 | * Callbacks invoked by {@link NuCharsetEncoder} with decoded string data. 27 | * 28 | * @see NuProcessHandler 29 | */ 30 | public interface NuCharsetEncoderHandler 31 | { 32 | /** 33 | * This method is invoked after you have expressed a desire to write to stdin 34 | * by first calling {@link NuProcess#wantWrite()}. When this method is 35 | * invoked, your code should write data to be sent to the stdin of the child 36 | * process into the provided {@link CharBuffer}. After writing data into the 37 | * {@code buffer} your code must {@link CharBuffer#flip() flip} the 38 | * buffer before returning. 39 | *

40 | * If not all of the data needed to be written will fit in the provided 41 | * {@code buffer}, this method can return {@code true} to indicate a desire 42 | * to write more data. If there is no more data to be written at the time 43 | * this method is invoked, then {@code false} should be returned from this 44 | * method. It is always possible to call {@link NuProcess#wantWrite()} later 45 | * if data becomes available to be written. 46 | *

47 | * Note that this method can be invoked one more time after you return 48 | * {@code false}, in case the encoded {@link CharBuffer} did not fit inside a 49 | * byte buffer. 50 | * 51 | * @param buffer a {@link CharBuffer} into which your stdin-bound data should 52 | * be written 53 | * @return true if you have more data to write immediately, false otherwise 54 | */ 55 | boolean onStdinReady(CharBuffer buffer); 56 | 57 | /** 58 | * This method is invoked immediately after {@link #onStdinReady(CharBuffer)} 59 | * returns if encoding the {@link CharBuffer} to bytes fails. 60 | * 61 | * @param result The {@link CoderResult} indicating the encoding failure 62 | */ 63 | void onEncoderError(CoderResult result); 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/internal/BaseEventProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.internal; 18 | 19 | import java.util.Collection; 20 | import java.util.Map; 21 | import java.util.concurrent.ConcurrentHashMap; 22 | import java.util.concurrent.CyclicBarrier; 23 | import java.util.concurrent.atomic.AtomicBoolean; 24 | import java.util.logging.Level; 25 | import java.util.logging.Logger; 26 | 27 | import com.sun.jna.ptr.IntByReference; 28 | 29 | /** 30 | * @author Brett Wooldridge 31 | */ 32 | public abstract class BaseEventProcessor implements IEventProcessor 33 | { 34 | public static final int LINGER_TIME_MS; 35 | 36 | protected static final int DEADPOOL_POLL_INTERVAL; 37 | protected static final int LINGER_ITERATIONS; 38 | 39 | private static final Logger LOGGER = Logger.getLogger(BaseEventProcessor.class.getCanonicalName()); 40 | 41 | private final int lingerIterations; 42 | 43 | protected Map pidToProcessMap; 44 | protected Map fildesToProcessMap; 45 | 46 | protected volatile boolean shutdown; 47 | 48 | private CyclicBarrier startBarrier; 49 | private AtomicBoolean isRunning; 50 | 51 | static { 52 | LINGER_TIME_MS = Math.max(1000, Integer.getInteger("com.zaxxer.nuprocess.lingerTimeMs", 2500)); 53 | 54 | DEADPOOL_POLL_INTERVAL = Math.min(LINGER_TIME_MS, Math.max(100, Integer.getInteger("com.zaxxer.nuprocess.deadPoolPollMs", 250))); 55 | 56 | LINGER_ITERATIONS = LINGER_TIME_MS / DEADPOOL_POLL_INTERVAL; 57 | } 58 | 59 | public BaseEventProcessor() 60 | { 61 | this(LINGER_ITERATIONS); 62 | } 63 | 64 | public BaseEventProcessor(int lingerIterations) 65 | { 66 | this.lingerIterations = lingerIterations; 67 | pidToProcessMap = new ConcurrentHashMap(); 68 | fildesToProcessMap = new ConcurrentHashMap(); 69 | isRunning = new AtomicBoolean(); 70 | } 71 | 72 | /** 73 | * The primary run loop of the event processor. 74 | */ 75 | @Override 76 | public void run() 77 | { 78 | try { 79 | // If the process is running synchronously, startBarrier will be null 80 | if (startBarrier != null) { 81 | startBarrier.await(); 82 | } 83 | 84 | int idleCount = 0; 85 | while (!isRunning.compareAndSet(idleCount > lingerIterations && pidToProcessMap.isEmpty(), false)) { 86 | idleCount = (!shutdown && process()) ? 0 : (idleCount + 1); 87 | } 88 | } 89 | catch (Exception e) { 90 | // TODO: how to handle this error? 91 | LOGGER.log(Level.WARNING, "Aborting processing loop after unexpected exception (" + 92 | pidToProcessMap.size() + " processes running)", e); 93 | isRunning.set(false); 94 | } 95 | finally { 96 | if (startBarrier == null) { 97 | // If the process is running synchronously, when the run loop ends give the subclass 98 | // an opportunity to close any descriptors it might have been using 99 | close(); 100 | } 101 | } 102 | } 103 | 104 | /** {@inheritDoc} */ 105 | @Override 106 | public CyclicBarrier getSpawnBarrier() 107 | { 108 | startBarrier = new CyclicBarrier(2); 109 | return startBarrier; 110 | } 111 | 112 | /** {@inheritDoc} */ 113 | @Override 114 | public boolean checkAndSetRunning() 115 | { 116 | return isRunning.compareAndSet(false, true); 117 | } 118 | 119 | /** {@inheritDoc} */ 120 | @Override 121 | public void shutdown() 122 | { 123 | shutdown = true; 124 | Collection processes = pidToProcessMap.values(); 125 | IntByReference exitCode = new IntByReference(); 126 | for (T process : processes) { 127 | LibC.kill(process.getPid(), LibC.SIGTERM); 128 | process.onExit(Integer.MAX_VALUE - 1); 129 | LibC.waitpid(process.getPid(), exitCode, LibC.WNOHANG); 130 | } 131 | } 132 | 133 | /** 134 | * Closes the processor, freeing up any resources (such as file descriptors) it was using. 135 | *

136 | * Note: This method is only called for processors that are used to pump synchronous processes. 137 | * Processors used to pump asynchronous processes are never closed; while their threads may be stopped, 138 | * when there are no processes to pump, they are restarted if a new asynchronous process is started and will 139 | * reuse the same resources. 140 | * 141 | * @since 1.3 142 | */ 143 | protected abstract void close(); 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/internal/Constants.java: -------------------------------------------------------------------------------- 1 | package com.zaxxer.nuprocess.internal; 2 | 3 | public class Constants 4 | { 5 | public static final int NUMBER_OF_THREADS; 6 | public static final int JVM_MAJOR_VERSION; 7 | static final OperatingSystem OS; 8 | 9 | enum OperatingSystem 10 | { 11 | MAC, 12 | LINUX, 13 | SOLARIS 14 | } 15 | 16 | static int getJavaMajorVersion(String versionString) { 17 | String[] parts = versionString.split("\\."); 18 | // Make sure we handle versions like '11-ea' which ships with centos7 19 | int major = Integer.parseInt(parts[0].split("\\D")[0]); 20 | if (major == 1) { 21 | major = Integer.parseInt(parts[1].split("\\D")[0]); 22 | } 23 | return major; 24 | } 25 | 26 | static { 27 | 28 | JVM_MAJOR_VERSION = getJavaMajorVersion(System.getProperty("java.version")); 29 | 30 | final String osname = System.getProperty("os.name").toLowerCase(); 31 | if (osname.contains("mac") || osname.contains("freebsd")) 32 | OS = OperatingSystem.MAC; 33 | else if (osname.contains("linux")) 34 | OS = OperatingSystem.LINUX; 35 | else 36 | OS = OperatingSystem.SOLARIS; 37 | 38 | final String threads = System.getProperty("com.zaxxer.nuprocess.threads", "auto"); 39 | if ("auto".equals(threads)) { 40 | NUMBER_OF_THREADS = Math.max(1, Runtime.getRuntime().availableProcessors() / 2); 41 | } 42 | else if ("cores".equals(threads)) { 43 | NUMBER_OF_THREADS = Runtime.getRuntime().availableProcessors(); 44 | } 45 | else { 46 | NUMBER_OF_THREADS = Math.max(1, Integer.parseInt(threads)); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/internal/HexDumpElf.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.zaxxer.nuprocess.internal; 17 | 18 | import java.util.Formatter; 19 | 20 | /** 21 | * Hex Dump Elf 22 | * 23 | * xxxx: 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff ................ 24 | */ 25 | public final class HexDumpElf 26 | { 27 | private static final int MAX_VISIBLE = 127; 28 | private static final int MIN_VISIBLE = 31; 29 | 30 | private HexDumpElf() 31 | { 32 | // private constructor 33 | } 34 | 35 | /** 36 | * Dump an array of bytes in hexadecimal. 37 | * 38 | * @param displayOffset the display offset (left column) 39 | * @param data the byte array of data 40 | * @param offset the offset to start dumping in the byte array 41 | * @param len the length of data to dump 42 | * @return the dump string 43 | */ 44 | public static String dump(final int displayOffset, final byte[] data, final int offset, final int len) 45 | { 46 | StringBuilder sb = new StringBuilder(); 47 | Formatter formatter = new Formatter(sb); 48 | StringBuilder ascii = new StringBuilder(); 49 | 50 | int dataNdx = offset; 51 | final int maxDataNdx = offset + len; 52 | final int lines = (len + 16) / 16; 53 | for (int i = 0; i < lines; i++) { 54 | ascii.append(" |"); 55 | formatter.format("%08x ", displayOffset + (i * 16)); 56 | 57 | for (int j = 0; j < 16; j++) { 58 | if (dataNdx < maxDataNdx) { 59 | byte b = data[dataNdx++]; 60 | formatter.format("%02x ", b); 61 | ascii.append((b > MIN_VISIBLE && b < MAX_VISIBLE) ? (char) b : ' '); 62 | } 63 | else { 64 | sb.append(" "); 65 | } 66 | 67 | if (j == 7) { 68 | sb.append(' '); 69 | } 70 | } 71 | 72 | ascii.append('|'); 73 | sb.append(ascii).append('\n'); 74 | ascii.setLength(0); 75 | } 76 | 77 | formatter.close(); 78 | return sb.toString(); 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/internal/IEventProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.internal; 18 | 19 | import java.util.concurrent.CyclicBarrier; 20 | 21 | /** 22 | * This class is internal. 23 | * 24 | * @author Brett Wooldridge 25 | * 26 | * @param a subclass of {@link BasePosixProcess} 27 | */ 28 | public interface IEventProcessor extends Runnable 29 | { 30 | /** 31 | * Check whether the processor instance is currently running, and if not set 32 | * it to the running state. 33 | * 34 | * @return true if the processor was already running, false otherwise 35 | */ 36 | boolean checkAndSetRunning(); 37 | 38 | /** 39 | * Get the CyclicBarrier that this thread should join, along with the 40 | * NuProcess start thread that is starting this processor. Used to cause the 41 | * OsxProcess to wait until the processor is up and running before returning 42 | * from start() to the user. 43 | * 44 | * @return the CyclicBarrier to join to ensure the processor is running 45 | * before registering processes with it 46 | */ 47 | CyclicBarrier getSpawnBarrier(); 48 | 49 | /** 50 | * Register a process for handling by the event processor. 51 | * 52 | * @param process the process to register 53 | */ 54 | void registerProcess(T process); 55 | 56 | /** 57 | * Queues read handling for the process's STDOUT and STDERR streams. 58 | *

59 | * Prior to 2.1, this was performed by {@link #registerProcess}, but it was moved to a separate method 60 | * to help avoid a race condition when starting a new process where soft exit detection could result in 61 | * a fast-running process exiting before start was called on its process handler. 62 | * 63 | * @param process the process from which STDOUT and STDERR should be read 64 | * @since 2.1 65 | */ 66 | void queueRead(T process); 67 | 68 | /** 69 | * Express that the client desires to write data into the STDIN stream as 70 | * soon as possible. 71 | * 72 | * @param process the process that wants to write to STDIN 73 | */ 74 | void queueWrite(T process); 75 | 76 | /** 77 | * Close the process's STDIN pipe. 78 | * 79 | * @param process the process whose STDIN pipe should be closed 80 | */ 81 | void closeStdin(T process); 82 | 83 | /** 84 | * Called by the event-loop to process asynchronous I/O events. 85 | * 86 | * @return true if events were processed, false if an idle timeout occurred 87 | */ 88 | boolean process(); 89 | 90 | /** 91 | * Cleanly shutdown the processors and cleanup all resources. 92 | */ 93 | void shutdown(); 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/internal/LibC.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.internal; 18 | 19 | import com.sun.jna.*; 20 | import com.sun.jna.ptr.IntByReference; 21 | import com.zaxxer.nuprocess.internal.Constants.OperatingSystem; 22 | 23 | import java.nio.ByteBuffer; 24 | 25 | import static com.zaxxer.nuprocess.internal.Constants.OS; 26 | 27 | @SuppressWarnings("WeakerAccess") 28 | public class LibC 29 | { 30 | static { 31 | if (OS == OperatingSystem.MAC) { 32 | O_NONBLOCK = 0x0004; // MacOS X, Freebsd 33 | } 34 | else { 35 | O_NONBLOCK = 2048; // Linux 36 | } 37 | 38 | Native.register(NativeLibrary.getProcess()); 39 | } 40 | 41 | public static native int pipe(int[] fildes); 42 | 43 | public static native int posix_spawnattr_init(Pointer posix_spawnattr_t); 44 | 45 | public static native int posix_spawnattr_destroy(Pointer posix_spawnattr_t); 46 | 47 | public static native int posix_spawnattr_setflags(Pointer posix_spawnattr_t, short flags); 48 | 49 | public static native int posix_spawn_file_actions_init(Pointer posix_spawn_file_actions_t); 50 | 51 | public static native int posix_spawn_file_actions_destroy(Pointer posix_spawn_file_actions_t); 52 | 53 | public static native int posix_spawn_file_actions_addclose(Pointer actions, int filedes); 54 | 55 | public static native int posix_spawn_file_actions_adddup2(Pointer actions, int fildes, int newfildes); 56 | 57 | public static native int posix_spawnp(IntByReference restrict_pid, String restrict_path, Pointer file_actions, 58 | Pointer /*const posix_spawnattr_t*/ restrict_attrp, StringArray /*String[]*/ argv, Pointer /*String[]*/ envp); 59 | 60 | public static native int fcntl(int fildes, int cmd); 61 | 62 | public static native int fcntl(int fildes, int cmd, long argO); 63 | 64 | public static native int close(int fildes); 65 | 66 | public static native int write(int fildes, ByteBuffer buf, int nbyte); 67 | 68 | public static native int read(int fildes, ByteBuffer buf, int nbyte); 69 | 70 | public static native int getpid(); 71 | 72 | public static native int kill(int pid, int sig); 73 | 74 | public static native int waitpid(int pid, IntByReference status, int options); 75 | 76 | public static native Pointer signal(int signal, Pointer func); 77 | 78 | public static native String getcwd(Pointer buf, int size); 79 | 80 | // from /usr/include/sys/syscall.h 81 | // We can't use JNA direct mapping for syscall(), since it takes varargs. 82 | public interface SyscallLibrary extends Library 83 | { 84 | public static final int SYS___pthread_chdir = 348; 85 | 86 | int syscall(int syscall_number, Object... args); 87 | } 88 | 89 | public static SyscallLibrary SYSCALL = (SyscallLibrary) Native.loadLibrary(Platform.C_LIBRARY_NAME, SyscallLibrary.class); 90 | 91 | public static final int F_GETFL = 3; 92 | public static final int F_SETFL = 4; 93 | 94 | public static final int O_NONBLOCK; 95 | 96 | // from /usr/include/asm-generic/errno-base.h 97 | public static final int ESRCH = 3; /* No such process */ 98 | public static final int EINTR = 4; /* Interrupted system call */ 99 | public static final int ECHILD = 10; /* No child processes */ 100 | 101 | // from /usr/include/sys/wait.h 102 | public static final int WNOHANG = 0x00000001; 103 | 104 | // from /usr/include/sys/spawn.h 105 | public static final short POSIX_SPAWN_START_SUSPENDED = 0x0080; 106 | public static final short POSIX_SPAWN_CLOEXEC_DEFAULT = 0x4000; 107 | 108 | // From /usr/include/sys/signal.h 109 | public static final int SIGKILL = 9; 110 | public static final int SIGTERM = 15; 111 | public static final int SIGCONT = 19; 112 | public static final int SIGUSR2 = 31; 113 | 114 | public static final Pointer SIG_IGN = Pointer.createConstant(1); 115 | 116 | /* If WIFEXITED(STATUS), the low-order 8 bits of the status. */ 117 | public static int WEXITSTATUS(int status) 118 | { 119 | return (((status) & 0xff00) >> 8); 120 | } 121 | 122 | /* If WIFSIGNALED(STATUS), the terminating signal. */ 123 | public static int WTERMSIG(int status) 124 | { 125 | return ((status) & 0x7f); 126 | } 127 | 128 | /* If WIFSTOPPED(STATUS), the signal that stopped the child. */ 129 | public static int WSTOPSIG(int status) 130 | { 131 | return WEXITSTATUS(status); 132 | } 133 | 134 | /* Nonzero if STATUS indicates normal termination. */ 135 | public static boolean WIFEXITED(int status) 136 | { 137 | return ((status) & 0x7f) == 0; 138 | } 139 | 140 | /* Nonzero if STATUS indicates termination by a signal. */ 141 | public static boolean WIFSIGNALED(int status) 142 | { 143 | return (((byte) (((status) & 0x7f) + 1) >> 1) > 0); 144 | } 145 | 146 | /* Nonzero if STATUS indicates the child is stopped. */ 147 | public static boolean WIFSTOPPED(int status) 148 | { 149 | return WTERMSIG(status) != 0; 150 | } 151 | 152 | public static int W_EXITCODE(int ret, int sig) 153 | { 154 | return ((ret) << 8 | (sig)); 155 | } 156 | 157 | public static int W_STOPCODE(int sig) 158 | { 159 | return ((sig) << 8 | 0x7f); 160 | } 161 | 162 | public interface SignalFunction extends Callback 163 | { 164 | void invoke(int signal); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/internal/LibJava10.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.internal; 18 | 19 | import com.sun.jna.JNIEnv; 20 | import com.sun.jna.Library; 21 | import com.sun.jna.Native; 22 | import com.sun.jna.NativeLibrary; 23 | 24 | import java.io.IOException; 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | 28 | import static com.zaxxer.nuprocess.internal.Constants.JVM_MAJOR_VERSION; 29 | import static com.zaxxer.nuprocess.internal.Constants.OS; 30 | 31 | public class LibJava10 32 | { 33 | static { 34 | Map options = new HashMap<>(); 35 | options.put(Library.OPTION_ALLOW_OBJECTS, Boolean.TRUE); 36 | 37 | if (OS != Constants.OperatingSystem.MAC) { 38 | Native.register(NativeLibrary.getInstance("java", options)); 39 | } 40 | else { 41 | Native.register(NativeLibrary.getProcess(options)); 42 | } 43 | 44 | Java_java_lang_ProcessImpl_init(JNIEnv.CURRENT, BasePosixProcess.class); 45 | } 46 | 47 | public static native void Java_java_lang_ProcessImpl_init(JNIEnv jniEnv, Object clazz); 48 | 49 | /** 50 | * JNIEXPORT jint JNICALL 51 | * Java_java_lang_ProcessImpl_forkAndExec(JNIEnv *env, 52 | * jobject process, 53 | * jint mode, 54 | * jbyteArray helperpath, 55 | * jbyteArray prog, 56 | * jbyteArray argBlock, jint argc, 57 | * jbyteArray envBlock, jint envc, 58 | * jbyteArray dir, 59 | * jintArray std_fds, 60 | * jboolean redirectErrorStream) 61 | * 62 | * @return the PID of the process 63 | */ 64 | public static native int Java_java_lang_ProcessImpl_forkAndExec( 65 | JNIEnv jniEnv, 66 | Object process, 67 | int mode, 68 | Object helperpath, 69 | Object prog, 70 | Object argBlock, int argc, 71 | Object envBlock, int envc, 72 | Object dir, 73 | Object fds, 74 | byte redirectErrorStream 75 | ) throws IOException; 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/internal/LibJava8.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Ben Hamilton, Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.internal; 18 | 19 | import com.sun.jna.JNIEnv; 20 | import com.sun.jna.Library; 21 | import com.sun.jna.Native; 22 | import com.sun.jna.NativeLibrary; 23 | 24 | import java.io.IOException; 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | 28 | import static com.zaxxer.nuprocess.internal.Constants.OS; 29 | 30 | public class LibJava8 31 | { 32 | static { 33 | Map options = new HashMap<>(); 34 | options.put(Library.OPTION_ALLOW_OBJECTS, Boolean.TRUE); 35 | 36 | if (OS != Constants.OperatingSystem.MAC) { 37 | Native.register(NativeLibrary.getInstance("java", options)); 38 | } 39 | else { 40 | Native.register(NativeLibrary.getProcess(options)); 41 | } 42 | 43 | Java_java_lang_UNIXProcess_init(JNIEnv.CURRENT, BasePosixProcess.class); 44 | } 45 | 46 | public static native void Java_java_lang_UNIXProcess_init(JNIEnv jniEnv, Object clazz); 47 | 48 | /** 49 | * JNIEXPORT jint JNICALL 50 | * Java_java_lang_UNIXProcess_forkAndExec(JNIEnv *env, 51 | * jobject process, 52 | * jint mode, 53 | * jbyteArray helperpath, 54 | * jbyteArray prog, 55 | * jbyteArray argBlock, jint argc, 56 | * jbyteArray envBlock, jint envc, 57 | * jbyteArray dir, 58 | * jintArray std_fds, 59 | * jboolean redirectErrorStream) 60 | * 61 | * @return the PID of the process 62 | */ 63 | public static native int Java_java_lang_UNIXProcess_forkAndExec( 64 | JNIEnv jniEnv, 65 | Object process, 66 | int mode, 67 | Object helperpath, 68 | Object prog, 69 | Object argBlock, int argc, 70 | Object envBlock, int envc, 71 | Object dir, 72 | Object fds, 73 | byte redirectErrorStream 74 | ) throws IOException; 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/internal/ReferenceCountedFileDescriptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Ben Hamilton 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.internal; 18 | 19 | import com.sun.jna.LastErrorException; 20 | 21 | /** 22 | * Encapsulates a file descriptor plus a reference count to ensure close requests 23 | * only close the file descriptor once the last reference to the file descriptor 24 | * is released. 25 | * 26 | * If not explicitly closed, the file descriptor will be closed when 27 | * this object is finalized. 28 | */ 29 | public class ReferenceCountedFileDescriptor { 30 | private int fd; 31 | private int fdRefCount; 32 | private boolean closePending; 33 | 34 | public ReferenceCountedFileDescriptor(int fd) { 35 | this.fd = fd; 36 | this.fdRefCount = 0; 37 | this.closePending = false; 38 | } 39 | 40 | protected void finalize() { 41 | close(); 42 | } 43 | 44 | public synchronized int acquire() { 45 | fdRefCount++; 46 | return fd; 47 | } 48 | 49 | public synchronized void release() { 50 | fdRefCount--; 51 | if (fdRefCount == 0 && closePending && fd != -1) { 52 | doClose(); 53 | } 54 | } 55 | 56 | public synchronized void close() { 57 | if (fd == -1 || closePending) { 58 | return; 59 | } 60 | 61 | if (fdRefCount == 0) { 62 | doClose(); 63 | } else { 64 | // Another thread has the FD. We'll close it when they release the reference. 65 | closePending = true; 66 | } 67 | } 68 | 69 | private void doClose() { 70 | try { 71 | LibC.close(fd); 72 | fd = -1; 73 | } catch (LastErrorException e) { 74 | throw new RuntimeException(e); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/linux/EpollEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.linux; 18 | 19 | import java.util.Arrays; 20 | import java.util.List; 21 | 22 | import com.sun.jna.*; 23 | 24 | class EpollEvent 25 | { 26 | private static final int eventsOffset; 27 | private static final int fdOffset; 28 | private static final int size; 29 | 30 | static { 31 | EpollEventPrototype event = new EpollEventPrototype(); 32 | eventsOffset = event.getFieldOffset("events"); 33 | fdOffset = event.getFieldOffset("data"); 34 | size = event.size(); 35 | } 36 | 37 | private final Pointer pointer; 38 | 39 | EpollEvent() { 40 | pointer = new Memory(size); 41 | } 42 | 43 | int getEvents() { 44 | return pointer.getInt(eventsOffset); 45 | } 46 | 47 | void setEvents(final int mask) { 48 | pointer.setInt(eventsOffset, mask); 49 | } 50 | 51 | void setFileDescriptor(final int fd) { 52 | pointer.setInt(fdOffset, fd); 53 | } 54 | 55 | int getFileDescriptor() { 56 | return pointer.getInt(fdOffset); 57 | } 58 | 59 | Pointer getPointer() { 60 | return pointer; 61 | } 62 | 63 | int size() { 64 | return size; 65 | } 66 | 67 | public static class EpollEventPrototype extends Structure 68 | { 69 | /* 70 | struct epoll_event 71 | { 72 | uint32_t events; // Epoll events 73 | epoll_data_t data; // User data variable 74 | } __EPOLL_PACKED; 75 | 76 | sizeof(struct epoll_event) is 12 on x86 and x86_64, but is 16 on other 64-bit platforms 77 | */ 78 | 79 | public int events; 80 | public EpollData data; 81 | 82 | EpollEventPrototype() { 83 | super(detectAlignment()); 84 | 85 | data = new EpollData(); 86 | data.setType("fd"); 87 | } 88 | 89 | int getFieldOffset(String field) 90 | { 91 | return fieldOffset(field); 92 | } 93 | 94 | @SuppressWarnings("rawtypes") 95 | @Override 96 | protected List getFieldOrder() { 97 | return Arrays.asList("events", "data"); 98 | } 99 | 100 | /** 101 | * Uses the OS architecture to reproduce the following logic from the epoll header: 102 | *

103 |        * #ifdef __x86_64__
104 |        * #define EPOLL_PACKED __attribute__((packed))
105 |        * #else
106 |        * #define EPOLL_PACKED
107 |        * #endif
108 |        * 
109 | * 110 | * On x86-64 (amd64) platforms, {@code ALIGN_NONE} is used (to emulate {@code __attribute__((packed))}), 111 | * and on all other platforms {@code ALIGN_GNUC} is used. 112 | */ 113 | private static int detectAlignment() { 114 | return Platform.isIntel() && Platform.is64Bit() ? ALIGN_NONE : ALIGN_GNUC; 115 | } 116 | 117 | /* 118 | typedef union epoll_data 119 | { 120 | void *ptr; 121 | int fd; 122 | uint32_t u32; 123 | uint64_t u64; 124 | } epoll_data_t; 125 | */ 126 | 127 | @SuppressWarnings("unused") // unused fields are part of the union's C definition 128 | public static class EpollData extends Union { 129 | // technically this union should have a "Pointer ptr" field, but, for how EpollData is actually 130 | // used, only referencing the "fd" field, it's nothing but overhead. JNA will end up constructing 131 | // them as part of ProcessEpoll's execution, but they never get used 132 | //public Pointer ptr; 133 | public int fd; 134 | public int u32; 135 | public long u64; // must be included to make this union's size 8 bytes 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/linux/LibEpoll.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.linux; 18 | 19 | import com.sun.jna.Native; 20 | import com.sun.jna.Platform; 21 | import com.sun.jna.Pointer; 22 | 23 | /** 24 | * @author Brett Wooldridge 25 | */ 26 | public class LibEpoll 27 | { 28 | static { 29 | Native.register(Platform.C_LIBRARY_NAME); 30 | } 31 | 32 | public static native int sigignore(int signal); 33 | 34 | public static native int epoll_create(int size); 35 | 36 | public static native int epoll_ctl(int epfd, int op, int fd, Pointer event); 37 | 38 | // We only ever call this API with maxevents=1. However, if calling with maxevents > 1, 39 | // care must be taken to ensure that the "events" Pointer actually points to a 40 | // contiguous block of memory large enough to handle maxevents number of EpollEvent 41 | // mappings. 42 | // 43 | // EpollEvent would likely need to be updated to add a convenience method that 44 | // allocates a block of memory and returns an array of EpollEvents mapped into it. The 45 | // EpollEvent.getPointer() of the first array element could then be passed to this API. 46 | public static native int epoll_wait(int epfd, Pointer events, int maxevents, int timeout); 47 | 48 | public static final int SIGPIPE = 13; 49 | 50 | /* from /usr/include/sys/epoll.h */ 51 | public static final int EPOLL_CTL_ADD = 1; /* Add a file descriptor to the interface. */ 52 | public static final int EPOLL_CTL_DEL = 2; /* Remove a file descriptor from the interface. */ 53 | public static final int EPOLL_CTL_MOD = 3; /* Change file descriptor epoll_event structure. */ 54 | 55 | public static final int EPOLLIN = 0x001; 56 | public static final int EPOLLOUT = 0x004; 57 | public static final int EPOLLERR = 0x008; 58 | public static final int EPOLLHUP = 0x010; 59 | public static final int EPOLLRDHUP = 0x2000; 60 | public static final int EPOLLONESHOT = (1 << 30); 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/linux/LinProcessFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.linux; 18 | 19 | import java.nio.file.Path; 20 | 21 | import java.util.List; 22 | 23 | import com.zaxxer.nuprocess.NuProcess; 24 | import com.zaxxer.nuprocess.NuProcessFactory; 25 | import com.zaxxer.nuprocess.NuProcessHandler; 26 | 27 | /** 28 | * Linux process factory. Creates and starts a process. 29 | * 30 | * @author Brett Wooldridge 31 | */ 32 | public class LinProcessFactory implements NuProcessFactory 33 | { 34 | /** {@inheritDoc} */ 35 | @Override 36 | public NuProcess createProcess(List commands, String[] env, NuProcessHandler processListener, Path cwd) 37 | { 38 | LinuxProcess process = new LinuxProcess(processListener); 39 | synchronized (LinProcessFactory.class) { 40 | process.start(commands, env, cwd); 41 | } 42 | return process; 43 | } 44 | 45 | /** {@inheritDoc} */ 46 | @Override 47 | public void runProcess(List commands, String[] env, NuProcessHandler processListener, Path cwd) 48 | { 49 | LinuxProcess process = new LinuxProcess(processListener); 50 | process.run(commands, env, cwd); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/linux/LinuxProcess.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.linux; 18 | 19 | import com.sun.jna.JNIEnv; 20 | import com.zaxxer.nuprocess.NuProcess; 21 | import com.zaxxer.nuprocess.NuProcessHandler; 22 | import com.zaxxer.nuprocess.internal.BasePosixProcess; 23 | import com.zaxxer.nuprocess.internal.IEventProcessor; 24 | import com.zaxxer.nuprocess.internal.LibC; 25 | 26 | import java.io.IOException; 27 | import java.nio.file.Path; 28 | import java.util.List; 29 | import java.util.logging.Level; 30 | 31 | import static com.zaxxer.nuprocess.internal.Constants.JVM_MAJOR_VERSION; 32 | 33 | /** 34 | * @author Brett Wooldridge 35 | */ 36 | public class LinuxProcess extends BasePosixProcess 37 | { 38 | private final EpollEvent epollEvent; 39 | 40 | static { 41 | LibEpoll.sigignore(LibEpoll.SIGPIPE); 42 | 43 | // TODO: install signal handler for SIGCHLD, and call onExit() when received, call the default (JVM) hook if the PID is not ours 44 | 45 | for (int i = 0; i < processors.length; i++) { 46 | processors[i] = new ProcessEpoll(); 47 | } 48 | } 49 | 50 | @SuppressWarnings("unused") 51 | private enum LaunchMechanism { 52 | // order IS important! 53 | FORK, 54 | POSIX_SPAWN, 55 | VFORK 56 | } 57 | 58 | LinuxProcess(NuProcessHandler processListener) { 59 | super(processListener); 60 | 61 | epollEvent = new EpollEvent(); 62 | } 63 | 64 | @Override 65 | public NuProcess start(List command, String[] environment, Path cwd) { 66 | callPreStart(); 67 | 68 | try { 69 | prepareProcess(command, environment, cwd); 70 | if (pid == -1) { 71 | return null; 72 | } 73 | 74 | initializeBuffers(); 75 | 76 | afterStart(); 77 | 78 | // Registration must happen prior to calling NuProcessHandler.onStart to allow handlers 79 | // to call wantWrite (which calls myProcessor.queueWrite) 80 | registerProcess(); 81 | 82 | callStart(); 83 | 84 | // Queueing read handling for stdout and stderr happens after start has been called 85 | // to ensure fast-exiting processes don't call NuProcessHandler.onExit before onStart 86 | myProcessor.queueRead(this); 87 | } 88 | catch (Exception e) { 89 | // TODO remove from event processor pid map? 90 | LOGGER.log(Level.WARNING, "Failed to start process", e); 91 | onExit(Integer.MIN_VALUE); 92 | return null; 93 | } 94 | 95 | return this; 96 | } 97 | 98 | @Override 99 | public void run(List command, String[] environment, Path cwd) 100 | { 101 | callPreStart(); 102 | 103 | try { 104 | prepareProcess(command, environment, cwd); 105 | if (pid == -1) { 106 | return; 107 | } 108 | 109 | initializeBuffers(); 110 | 111 | afterStart(); 112 | 113 | myProcessor = (IEventProcessor) new ProcessEpoll(this); 114 | 115 | callStart(); 116 | 117 | myProcessor.run(); 118 | } 119 | catch (Exception e) { 120 | LOGGER.log(Level.WARNING, "Failed to start process", e); 121 | onExit(Integer.MIN_VALUE); 122 | } 123 | } 124 | 125 | /** 126 | * An {@link EpollEvent} struct, which may be used when registering for events for this process. Each process has 127 | * its own struct to avoid concurrency issues in {@link ProcessEpoll#registerProcess} when multiple processes are 128 | * registered at once (e.g. multiple threads are all starting new processes concurrently). 129 | * 130 | * @return this process's {@link EpollEvent} struct 131 | */ 132 | EpollEvent getEpollEvent() { 133 | return epollEvent; 134 | } 135 | 136 | private void prepareProcess(List command, String[] environment, Path cwd) throws IOException 137 | { 138 | String[] cmdarray = command.toArray(new String[0]); 139 | 140 | // See https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/classes/java/lang/ProcessImpl.java#L71-L83 141 | byte[][] args = new byte[cmdarray.length - 1][]; 142 | int size = args.length; // For added NUL bytes 143 | for (int i = 0; i < args.length; i++) { 144 | args[i] = cmdarray[i + 1].getBytes(); 145 | size += args[i].length; 146 | } 147 | byte[] argBlock = new byte[size]; 148 | int i = 0; 149 | for (byte[] arg : args) { 150 | System.arraycopy(arg, 0, argBlock, i, arg.length); 151 | i += arg.length + 1; 152 | // No need to write NUL bytes explicitly 153 | } 154 | 155 | // See https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/classes/java/lang/ProcessImpl.java#L86 156 | byte[] envBlock = toEnvironmentBlock(environment); 157 | 158 | createPipes(); 159 | try { 160 | // createPipes() returns the parent ends of the pipes, but forkAndExec requires the child ends 161 | int[] child_fds = {stdinWidow, stdoutWidow, stderrWidow}; 162 | 163 | if (JVM_MAJOR_VERSION >= 10) { 164 | pid = com.zaxxer.nuprocess.internal.LibJava10.Java_java_lang_ProcessImpl_forkAndExec( 165 | JNIEnv.CURRENT, 166 | this, 167 | LaunchMechanism.VFORK.ordinal() + 1, 168 | toCString(System.getProperty("java.home") + "/lib/jspawnhelper"), // used on Linux 169 | toCString(cmdarray[0]), 170 | argBlock, args.length, 171 | envBlock, environment.length, 172 | (cwd != null ? toCString(cwd.toString()) : null), 173 | child_fds, 174 | (byte) 0 /*redirectErrorStream*/); 175 | } 176 | else { 177 | // See https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/classes/java/lang/UNIXProcess.java#L247 178 | // Native source code: https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/native/java/lang/UNIXProcess_md.c#L566 179 | pid = com.zaxxer.nuprocess.internal.LibJava8.Java_java_lang_UNIXProcess_forkAndExec( 180 | JNIEnv.CURRENT, 181 | this, 182 | LaunchMechanism.VFORK.ordinal() + 1, 183 | toCString(System.getProperty("java.home") + "/lib/jspawnhelper"), // used on Linux 184 | toCString(cmdarray[0]), 185 | argBlock, args.length, 186 | envBlock, environment.length, 187 | (cwd != null ? toCString(cwd.toString()) : null), 188 | child_fds, 189 | (byte) 0 /*redirectErrorStream*/); 190 | } 191 | } 192 | finally { 193 | // If we call createPipes, even if launching the process then fails, we need to ensure 194 | // the child side of the pipes are closed. The parent side will be closed in onExit 195 | closePipes(); 196 | } 197 | } 198 | 199 | private void closePipes() 200 | { 201 | // Close the child end of the pipes in our process 202 | LibC.close(stdinWidow); 203 | LibC.close(stdoutWidow); 204 | LibC.close(stderrWidow); 205 | } 206 | 207 | private static byte[] toCString(String s) { 208 | if (s == null) 209 | return null; 210 | byte[] bytes = s.getBytes(); 211 | byte[] result = new byte[bytes.length + 1]; 212 | System.arraycopy(bytes, 0, 213 | result, 0, 214 | bytes.length); 215 | result[result.length-1] = (byte)0; 216 | return result; 217 | } 218 | 219 | private static byte[] toEnvironmentBlock(String[] environment) { 220 | int count = environment.length; // This implicitly adds an extra null byte for each entry 221 | for (String entry : environment) { 222 | count += entry.getBytes().length; 223 | } 224 | 225 | byte[] block = new byte[count]; 226 | 227 | int i = 0; 228 | for (String entry : environment) { 229 | byte[] bytes = entry.getBytes(); 230 | System.arraycopy(bytes, 0, block, i, bytes.length); 231 | i += bytes.length + 1; 232 | // No need to write NUL byte explicitly 233 | //block[i++] = (byte) '\u0000'; 234 | } 235 | 236 | return block; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/osx/LibKevent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.osx; 18 | 19 | import java.util.Arrays; 20 | import java.util.List; 21 | 22 | import com.sun.jna.Native; 23 | import com.sun.jna.NativeLibrary; 24 | import com.sun.jna.NativeLong; 25 | import com.sun.jna.Pointer; 26 | import com.sun.jna.Structure; 27 | 28 | /** 29 | * @author Brett Wooldridge 30 | */ 31 | public class LibKevent 32 | { 33 | static { 34 | Native.register(NativeLibrary.getProcess()); 35 | } 36 | 37 | public static native int kqueue(); 38 | 39 | public static native int kevent(int kq, Pointer changeList, int nchanges, Pointer eventList, int nevents, TimeSpec timespec); 40 | 41 | public static class TimeSpec extends Structure 42 | { 43 | public long tv_sec; 44 | public long tv_nsec; 45 | 46 | @SuppressWarnings("rawtypes") 47 | @Override 48 | protected List getFieldOrder() 49 | { 50 | return Arrays.asList("tv_sec", "tv_nsec"); 51 | } 52 | } 53 | 54 | public static class Kevent extends Structure 55 | { 56 | public NativeLong ident; 57 | public short filter; 58 | public short flags; 59 | public int fflags; 60 | public NativeLong data; 61 | public Pointer udata; 62 | 63 | public Kevent() { 64 | super(); 65 | } 66 | 67 | public Kevent(Pointer p) { 68 | super(p); 69 | } 70 | 71 | @SuppressWarnings("rawtypes") 72 | @Override 73 | protected List getFieldOrder() 74 | { 75 | return Arrays.asList("ident", "filter", "flags", "fflags", "data", "udata"); 76 | } 77 | 78 | Kevent EV_SET(long ident, int filter, int flags, int fflags, long data, Pointer udata) 79 | { 80 | this.ident.setValue(ident); 81 | this.filter = (short) filter; 82 | this.flags = (short) flags; 83 | this.fflags = fflags; 84 | this.data.setValue(data); 85 | this.udata = udata; 86 | return this; 87 | } 88 | 89 | /* actions */ 90 | public static final int EV_ADD = 0x0001; /* add event to kq (implies enable) */ 91 | public static final int EV_DELETE = 0x0002; /* delete event from kq */ 92 | public static final int EV_ENABLE = 0x0004; /* enable event */ 93 | public static final int EV_DISABLE = 0x0008; /* disable event (not reported) */ 94 | public static final int EV_RECEIPT = 0x0040; /* force EV_ERROR on success, data == 0 */ 95 | 96 | /* flags */ 97 | public static final int EV_ONESHOT = 0x0010; /* only report one occurrence */ 98 | public static final int EV_CLEAR = 0x0020; /* clear event state after reporting */ 99 | public static final int EV_DISPATCH = 0x0080; /* disable event after reporting */ 100 | 101 | public static final int EV_SYSFLAGS = 0xF000; /* reserved by system */ 102 | public static final int EV_FLAG0 = 0x1000; /* filter-specific flag */ 103 | public static final int EV_FLAG1 = 0x2000; /* filter-specific flag */ 104 | 105 | /* returned values */ 106 | public static final int EV_EOF = 0x8000; /* EOF detected */ 107 | public static final int EV_ERROR = 0x4000; /* error, data contains errno */ 108 | 109 | /* filters */ 110 | public static final int EVFILT_READ = (-1); 111 | public static final int EVFILT_WRITE = (-2); 112 | public static final int EVFILT_AIO = (-3); /* attached to aio requests */ 113 | public static final int EVFILT_VNODE = (-4); /* attached to vnodes */ 114 | public static final int EVFILT_PROC = (-5); /* attached to struct proc */ 115 | public static final int EVFILT_SIGNAL = (-6); /* attached to struct proc */ 116 | public static final int EVFILT_TIMER = (-7); /* timers */ 117 | public static final int EVFILT_MACHPORT = (-8); /* Mach portsets */ 118 | public static final int EVFILT_FS = (-9); /* Filesystem events */ 119 | public static final int EVFILT_USER = (-10); /* User events */ 120 | /* (-11) unused */ 121 | public static final int EVFILT_VM = (-12); /* Virtual memory events */ 122 | 123 | /* data/hint fflags for EVFILT_PROC */ 124 | public static final int NOTE_EXIT = 0x80000000; /* process exited */ 125 | public static final int NOTE_REAP = 0x10000000; /* process exited */ 126 | public static final int NOTE_EXITSTATUS = 0x04000000; /* exit status to be returned, valid for child process only */ 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/osx/OsxProcess.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.osx; 18 | 19 | import com.sun.jna.Memory; 20 | import com.sun.jna.Native; 21 | import com.sun.jna.Pointer; 22 | import com.sun.jna.StringArray; 23 | import com.sun.jna.ptr.IntByReference; 24 | import com.zaxxer.nuprocess.NuProcess; 25 | import com.zaxxer.nuprocess.NuProcessHandler; 26 | import com.zaxxer.nuprocess.internal.BasePosixProcess; 27 | import com.zaxxer.nuprocess.internal.IEventProcessor; 28 | import com.zaxxer.nuprocess.internal.LibC; 29 | import com.zaxxer.nuprocess.internal.LibC.SyscallLibrary; 30 | 31 | import java.nio.file.Path; 32 | import java.util.List; 33 | import java.util.logging.Level; 34 | 35 | /** 36 | * @author Brett Wooldridge 37 | */ 38 | class OsxProcess extends BasePosixProcess 39 | { 40 | static { 41 | for (int i = 0; i < processors.length; i++) { 42 | processors[i] = new ProcessKqueue(); 43 | } 44 | 45 | // Setup a private signal for waking up the kqueue processing threads 46 | LibC.signal(LibC.SIGUSR2, LibC.SIG_IGN); 47 | } 48 | 49 | OsxProcess(NuProcessHandler processListener) { 50 | super(processListener); 51 | } 52 | 53 | @Override 54 | public NuProcess start(List command, String[] environment, Path cwd) { 55 | callPreStart(); 56 | 57 | String[] commands = command.toArray(new String[0]); 58 | 59 | Pointer posix_spawn_file_actions = createPosixPipes(); 60 | Pointer posix_spawnattr = createPosixSpawnAttributes(); 61 | 62 | try { 63 | int rc = prepareProcess(environment, cwd, commands, posix_spawn_file_actions, posix_spawnattr); 64 | checkReturnCode(rc, "Invocation of posix_spawn() failed"); 65 | 66 | afterStart(); 67 | 68 | // On macOS, registration can be immediately followed by queueing read handling for stdout 69 | // and stderr without risking a racy exit because processes are launched suspended and are 70 | // only resumed after NuProcessHandler.onStart is called 71 | registerProcess(); 72 | myProcessor.queueRead(this); 73 | 74 | callStart(); 75 | 76 | singleProcessContinue(); 77 | } 78 | catch (RuntimeException e) { 79 | // TODO remove from event processor pid map? 80 | LOGGER.log(Level.WARNING, "Exception thrown from handler", e); 81 | onExit(Integer.MIN_VALUE); 82 | return null; 83 | } 84 | finally { 85 | LibC.posix_spawnattr_destroy(posix_spawnattr); 86 | LibC.posix_spawn_file_actions_destroy(posix_spawn_file_actions); 87 | 88 | // After we've spawned, close the unused ends of our pipes (that were dup'd into the child process space) 89 | closePipes(); 90 | } 91 | 92 | return this; 93 | } 94 | 95 | @Override 96 | public void run(List command, String[] environment, Path cwd) 97 | { 98 | callPreStart(); 99 | 100 | String[] commands = command.toArray(new String[0]); 101 | 102 | Pointer posix_spawn_file_actions = createPosixPipes(); 103 | Pointer posix_spawnattr = createPosixSpawnAttributes(); 104 | 105 | try { 106 | int rc = prepareProcess(environment, cwd, commands, posix_spawn_file_actions, posix_spawnattr); 107 | checkReturnCode(rc, "Invocation of posix_spawn() failed"); 108 | 109 | afterStart(); 110 | 111 | myProcessor = (IEventProcessor) new ProcessKqueue(this); 112 | 113 | callStart(); 114 | 115 | singleProcessContinue(); 116 | 117 | myProcessor.run(); 118 | } 119 | catch (RuntimeException e) { 120 | // TODO remove from event processor pid map? 121 | LOGGER.log(Level.WARNING, "Exception thrown from handler", e); 122 | onExit(Integer.MIN_VALUE); 123 | } 124 | finally { 125 | LibC.posix_spawnattr_destroy(posix_spawnattr); 126 | LibC.posix_spawn_file_actions_destroy(posix_spawn_file_actions); 127 | 128 | // After we've spawned, close the unused ends of our pipes (that were dup'd into the child process space) 129 | closePipes(); 130 | } 131 | } 132 | 133 | private int prepareProcess(String[] environment, Path cwd, String[] commands, 134 | Pointer posix_spawn_file_actions, Pointer posix_spawnattr) 135 | { 136 | int rc = LibC.posix_spawnattr_init(posix_spawnattr); 137 | checkReturnCode(rc, "Internal call to posix_spawnattr_init() failed"); 138 | 139 | LibC.posix_spawnattr_setflags(posix_spawnattr, (short)(LibC.POSIX_SPAWN_START_SUSPENDED | LibC.POSIX_SPAWN_CLOEXEC_DEFAULT)); 140 | 141 | IntByReference restrict_pid = new IntByReference(); 142 | StringArray commandsArray = new StringArray(commands); 143 | StringArray environmentArray = new StringArray(environment); 144 | if (cwd != null) { 145 | rc = spawnWithCwd(restrict_pid, commands[0], posix_spawn_file_actions, posix_spawnattr, commandsArray, environmentArray, cwd); 146 | } 147 | else { 148 | rc = LibC.posix_spawnp(restrict_pid, commands[0], posix_spawn_file_actions, posix_spawnattr, commandsArray, environmentArray); 149 | } 150 | 151 | pid = restrict_pid.getValue(); 152 | 153 | initializeBuffers(); 154 | 155 | return rc; 156 | } 157 | 158 | private void closePipes() 159 | { 160 | LibC.close(stdinWidow); 161 | LibC.close(stdoutWidow); 162 | LibC.close(stderrWidow); 163 | } 164 | 165 | private int spawnWithCwd(final IntByReference restrict_pid, 166 | final String restrict_path, 167 | final Pointer file_actions, 168 | final Pointer /*const posix_spawnattr_t*/ restrict_attrp, 169 | final StringArray /*String[]*/ argv, 170 | final Pointer /*String[]*/ envp, 171 | final Path cwd) 172 | { 173 | int cwdBufSize = 1024; 174 | long peer = Native.malloc(cwdBufSize); 175 | Pointer oldCwd = new Pointer(peer); 176 | LibC.getcwd(oldCwd, cwdBufSize); 177 | String newCwd = cwd.toAbsolutePath().toString(); 178 | int rc = LibC.SYSCALL.syscall(SyscallLibrary.SYS___pthread_chdir, newCwd); 179 | checkReturnCode(rc, "syscall(SYS__pthread_chdir) failed to set current directory"); 180 | 181 | try { 182 | return LibC.posix_spawnp(restrict_pid, restrict_path, file_actions, restrict_attrp, argv, envp); 183 | } 184 | finally { 185 | rc = LibC.SYSCALL.syscall(SyscallLibrary.SYS___pthread_chdir, oldCwd); 186 | Native.free(Pointer.nativeValue(oldCwd)); 187 | checkReturnCode(rc, "syscall(SYS__pthread_chdir) failed to restore current directory"); 188 | } 189 | } 190 | 191 | private void singleProcessContinue() 192 | { 193 | // Signal the spawned process to continue (unsuspend) 194 | LibC.kill(pid, LibC.SIGCONT); 195 | } 196 | 197 | private Pointer createPosixPipes() 198 | { 199 | int rc; 200 | 201 | Pointer posix_spawn_file_actions = createPosixSpawnFileActions(); 202 | 203 | try { 204 | int[] fds = createPipes(); 205 | 206 | // Create spawn file actions 207 | rc = LibC.posix_spawn_file_actions_init(posix_spawn_file_actions); 208 | checkReturnCode(rc, "Internal call to posix_spawn_file_actions_init() failed"); 209 | 210 | // Dup the reading end of the pipe into the sub-process, and close our end 211 | rc = LibC.posix_spawn_file_actions_adddup2(posix_spawn_file_actions, stdinWidow, 0); 212 | checkReturnCode(rc, "Internal call to posix_spawn_file_actions_adddup2() failed"); 213 | 214 | rc = LibC.posix_spawn_file_actions_addclose(posix_spawn_file_actions, fds[0]); 215 | checkReturnCode(rc, "Internal call to posix_spawn_file_actions_addclose() failed"); 216 | 217 | // Dup the writing end of the pipe into the sub-process, and close our end 218 | rc = LibC.posix_spawn_file_actions_adddup2(posix_spawn_file_actions, stdoutWidow, 1); 219 | checkReturnCode(rc, "Internal call to posix_spawn_file_actions_adddup2() failed"); 220 | 221 | rc = LibC.posix_spawn_file_actions_addclose(posix_spawn_file_actions, fds[1]); 222 | checkReturnCode(rc, "Internal call to posix_spawn_file_actions_addclose() failed"); 223 | 224 | // Dup the writing end of the pipe into the sub-process, and close our end 225 | rc = LibC.posix_spawn_file_actions_adddup2(posix_spawn_file_actions, stderrWidow, 2); 226 | checkReturnCode(rc, "Internal call to posix_spawn_file_actions_adddup2() failed"); 227 | 228 | rc = LibC.posix_spawn_file_actions_addclose(posix_spawn_file_actions, fds[2]); 229 | checkReturnCode(rc, "Internal call to posix_spawn_file_actions_addclose() failed"); 230 | 231 | return posix_spawn_file_actions; 232 | } 233 | catch (RuntimeException e) { 234 | LOGGER.log(Level.WARNING, "Exception creating posix pipe actions", e); 235 | LibC.posix_spawn_file_actions_destroy(posix_spawn_file_actions); 236 | throw e; 237 | } 238 | } 239 | 240 | private Pointer createPosixSpawnFileActions() 241 | { 242 | return new Memory(Native.POINTER_SIZE); 243 | } 244 | 245 | private Pointer createPosixSpawnAttributes() 246 | { 247 | return new Memory(Native.POINTER_SIZE); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/osx/OsxProcessFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.osx; 18 | 19 | import java.nio.file.Path; 20 | 21 | import java.util.List; 22 | 23 | import com.zaxxer.nuprocess.NuProcess; 24 | import com.zaxxer.nuprocess.NuProcessFactory; 25 | import com.zaxxer.nuprocess.NuProcessHandler; 26 | 27 | public class OsxProcessFactory implements NuProcessFactory 28 | { 29 | /** {@inheritDoc} */ 30 | @Override 31 | public NuProcess createProcess(List commands, String[] env, NuProcessHandler processListener, Path cwd) 32 | { 33 | OsxProcess process = new OsxProcess(processListener); 34 | process.start(commands, env, cwd); 35 | return process; 36 | } 37 | 38 | /** {@inheritDoc} */ 39 | @Override 40 | public void runProcess(List commands, String[] env, NuProcessHandler processListener, Path cwd) 41 | { 42 | OsxProcess process = new OsxProcess(processListener); 43 | process.run(commands, env, cwd); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Provides the primary classes and interfaces for executing and interacting with 4 | processes in an asynchronous manner. 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/windows/NuKernel32.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess.windows; 18 | 19 | import java.nio.ByteBuffer; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | 23 | import com.sun.jna.Native; 24 | import com.sun.jna.NativeLibrary; 25 | import com.sun.jna.Pointer; 26 | import com.sun.jna.Structure; 27 | import com.sun.jna.WString; 28 | import com.sun.jna.ptr.IntByReference; 29 | import com.sun.jna.ptr.PointerByReference; 30 | import com.sun.jna.win32.W32APIOptions; 31 | import com.zaxxer.nuprocess.windows.NuWinNT.DWORD; 32 | import com.zaxxer.nuprocess.windows.NuWinNT.HANDLE; 33 | import com.zaxxer.nuprocess.windows.NuWinNT.SECURITY_ATTRIBUTES; 34 | import com.zaxxer.nuprocess.windows.NuWinNT.PROCESS_INFORMATION; 35 | import com.zaxxer.nuprocess.windows.NuWinNT.STARTUPINFO; 36 | import com.zaxxer.nuprocess.windows.NuWinNT.ULONG_PTRByReference; 37 | import com.zaxxer.nuprocess.windows.NuWinNT.ULONG_PTR; 38 | 39 | public class NuKernel32 40 | { 41 | static { 42 | NativeLibrary nativeLibrary = NativeLibrary.getInstance("kernel32", W32APIOptions.UNICODE_OPTIONS); 43 | Native.register(NuKernel32.class, nativeLibrary); 44 | } 45 | 46 | public static native int GetProcessId(HANDLE hObject); 47 | 48 | public static native boolean CloseHandle(HANDLE hObject); 49 | 50 | public static native HANDLE CreateIoCompletionPort(HANDLE fileHandle, HANDLE existingCompletionPort, ULONG_PTR completionKey, int numberOfThreads); 51 | 52 | public static native boolean CreateProcessW(WString lpApplicationName, char[] lpCommandLine, SECURITY_ATTRIBUTES lpProcessAttributes, 53 | SECURITY_ATTRIBUTES lpThreadAttributes, boolean bInheritHandles, DWORD dwCreationFlags, Pointer lpEnvironment, 54 | char[] lpCurrentDirectory, STARTUPINFO lpStartupInfo, PROCESS_INFORMATION lpProcessInformation); 55 | 56 | public static native boolean TerminateProcess(HANDLE hProcess, int exitCode); 57 | 58 | public static native HANDLE CreateFile(WString lpFileName, int dwDesiredAccess, int dwShareMode, SECURITY_ATTRIBUTES lpSecurityAttributes, 59 | int dwCreationDisposition, int dwFlagsAndAttributes, HANDLE hTemplateFile); 60 | 61 | public static native HANDLE CreateEvent(SECURITY_ATTRIBUTES lpEventAttributes, boolean bManualReset, boolean bInitialState, String lpName); 62 | 63 | public static native int WaitForSingleObject(HANDLE hHandle, int dwMilliseconds); 64 | 65 | public static native int GetQueuedCompletionStatus(HANDLE completionPort, IntByReference numberOfBytes, ULONG_PTRByReference completionKey, 66 | PointerByReference lpOverlapped, int dwMilliseconds); 67 | 68 | public static native boolean PostQueuedCompletionStatus(HANDLE completionPort, int dwNumberOfBytesTransferred, ULONG_PTR dwCompletionKey, 69 | OVERLAPPED lpOverlapped); 70 | 71 | public static native HANDLE CreateNamedPipeW(WString name, int dwOpenMode, int dwPipeMode, int nMaxInstances, int nOutBufferSize, int nInBufferSize, 72 | int nDefaultTimeOut, SECURITY_ATTRIBUTES securityAttributes); 73 | 74 | public static native int ConnectNamedPipe(HANDLE hNamedPipe, OVERLAPPED lpo); 75 | 76 | public static native boolean DisconnectNamedPipe(HANDLE hNamedPipe); 77 | 78 | public static native DWORD ResumeThread(HANDLE hThread); 79 | 80 | public static native boolean GetExitCodeProcess(HANDLE hProcess, IntByReference exitCode); 81 | 82 | public static native int ReadFile(HANDLE hFile, ByteBuffer lpBuffer, int nNumberOfBytesToRead, IntByReference lpNumberOfBytesRead, 83 | NuKernel32.OVERLAPPED lpOverlapped); 84 | 85 | public static native int WriteFile(HANDLE hFile, ByteBuffer lpBuffer, int nNumberOfBytesToWrite, IntByReference lpNumberOfBytesWritten, 86 | NuKernel32.OVERLAPPED lpOverlapped); 87 | 88 | /** 89 | * The OVERLAPPED structure contains information used in 90 | * asynchronous (or overlapped) input and output (I/O). 91 | */ 92 | public static class OVERLAPPED extends Structure 93 | { 94 | public ULONG_PTR Internal; 95 | public ULONG_PTR InternalHigh; 96 | public int Offset; 97 | public int OffsetHigh; 98 | public HANDLE hEvent; 99 | 100 | public OVERLAPPED() 101 | { 102 | super(); 103 | } 104 | 105 | @Override 106 | protected List getFieldOrder() 107 | { 108 | return Arrays.asList("Internal", "InternalHigh", "Offset", "OffsetHigh", "hEvent"); 109 | } 110 | } 111 | 112 | public static final int PIPE_ACCESS_DUPLEX = 0x00000003; 113 | public static final int PIPE_ACCESS_INBOUND = 0x00000002; 114 | public static final int PIPE_ACCESS_OUTBOUND = 0x00000001; 115 | 116 | public static final int FILE_FLAG_OVERLAPPED = 0x40000000; 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/windows/NuWinBase32.java: -------------------------------------------------------------------------------- 1 | package com.zaxxer.nuprocess.windows; 2 | 3 | import com.sun.jna.Native; 4 | import com.sun.jna.NativeLibrary; 5 | import com.sun.jna.win32.W32APIOptions; 6 | 7 | public class NuWinBase32 8 | { 9 | static { 10 | NativeLibrary nativeLibrary = NativeLibrary.getInstance("winbase", W32APIOptions.UNICODE_OPTIONS); 11 | Native.register(nativeLibrary); 12 | } 13 | 14 | public static native boolean HasOverlappedIoCompleted(NuKernel32.OVERLAPPED lpOverlapped); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/windows/NuWinNT.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Ben Hamilton 3 | * 4 | * Originally from JNA com.sun.jna.platform.win32 package (Apache 5 | * License, Version 2.0) 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package com.zaxxer.nuprocess.windows; 21 | 22 | import java.util.Arrays; 23 | import java.util.List; 24 | 25 | import com.sun.jna.*; 26 | import com.sun.jna.ptr.ByReference; 27 | import com.sun.jna.ptr.ByteByReference; 28 | 29 | /** 30 | * Constants and structures for Windows APIs, borrowed from com.sun.jna.platform.win32 31 | * to avoid pulling in a dependency on that package. 32 | */ 33 | @SuppressWarnings("serial") 34 | public interface NuWinNT 35 | { 36 | int CREATE_SUSPENDED = 0x00000004; 37 | int CREATE_UNICODE_ENVIRONMENT = 0x00000400; 38 | int CREATE_NO_WINDOW = 0x08000000; 39 | 40 | int ERROR_SUCCESS = 0; 41 | int ERROR_BROKEN_PIPE = 109; 42 | int ERROR_PIPE_NOT_CONNECTED = 233; 43 | int ERROR_PIPE_CONNECTED = 535; 44 | int ERROR_IO_PENDING = 997; 45 | 46 | int FILE_ATTRIBUTE_NORMAL = 0x00000080; 47 | int FILE_FLAG_OVERLAPPED = 0x40000000; 48 | int FILE_SHARE_READ = 0x00000001; 49 | int FILE_SHARE_WRITE = 0x00000002; 50 | 51 | int GENERIC_READ = 0x80000000; 52 | int GENERIC_WRITE = 0x40000000; 53 | 54 | int OPEN_EXISTING = 3; 55 | 56 | int STATUS_PENDING = 0x00000103; 57 | int STILL_ACTIVE = STATUS_PENDING; 58 | 59 | int STARTF_USESTDHANDLES = 0x100; 60 | 61 | HANDLE INVALID_HANDLE_VALUE = new HANDLE(HANDLE.INVALID); 62 | 63 | class HANDLE extends PointerType 64 | { 65 | static final Pointer INVALID = Pointer.createConstant(Native.POINTER_SIZE == 8 ? -1 : 0xFFFFFFFFL); 66 | 67 | public HANDLE() 68 | { 69 | } 70 | 71 | public HANDLE(Pointer p) 72 | { 73 | setPointer(p); 74 | } 75 | 76 | @Override 77 | public Object fromNative(Object nativeValue, FromNativeContext context) 78 | { 79 | if (nativeValue == null) { 80 | return null; 81 | } 82 | 83 | Pointer ptr = (Pointer) nativeValue; 84 | if (INVALID.equals(ptr)) { 85 | return INVALID_HANDLE_VALUE; 86 | } 87 | return new HANDLE(ptr); 88 | } 89 | } 90 | 91 | class WORD extends IntegerType 92 | { 93 | public static final int SIZE = 2; 94 | 95 | public WORD() 96 | { 97 | this(0); 98 | } 99 | 100 | public WORD(long value) 101 | { 102 | super(SIZE, value, true); 103 | } 104 | } 105 | 106 | class DWORD extends IntegerType 107 | { 108 | public static final int SIZE = 4; 109 | 110 | public DWORD() 111 | { 112 | this(0); 113 | } 114 | 115 | public DWORD(long value) 116 | { 117 | super(SIZE, value, true); 118 | } 119 | } 120 | 121 | class ULONG_PTR extends IntegerType 122 | { 123 | public ULONG_PTR() 124 | { 125 | this(0); 126 | } 127 | 128 | public ULONG_PTR(long value) 129 | { 130 | super(Native.POINTER_SIZE, value, true); 131 | } 132 | } 133 | 134 | class ULONG_PTRByReference extends ByReference 135 | { 136 | public ULONG_PTRByReference() 137 | { 138 | this(new ULONG_PTR(0)); 139 | } 140 | 141 | public ULONG_PTRByReference(ULONG_PTR value) 142 | { 143 | super(Native.POINTER_SIZE); 144 | setValue(value); 145 | } 146 | 147 | public void setValue(ULONG_PTR value) 148 | { 149 | if (Native.POINTER_SIZE == 4) { 150 | getPointer().setInt(0, value.intValue()); 151 | } 152 | else { 153 | getPointer().setLong(0, value.longValue()); 154 | } 155 | } 156 | 157 | public ULONG_PTR getValue() 158 | { 159 | return new ULONG_PTR(Native.POINTER_SIZE == 4 ? getPointer().getInt(0) : getPointer().getLong(0)); 160 | } 161 | } 162 | 163 | class SECURITY_ATTRIBUTES extends Structure 164 | { 165 | public DWORD dwLength; 166 | public Pointer lpSecurityDescriptor; 167 | public boolean bInheritHandle; 168 | 169 | @Override 170 | protected List getFieldOrder() 171 | { 172 | return Arrays.asList("dwLength", "lpSecurityDescriptor", "bInheritHandle"); 173 | } 174 | } 175 | 176 | class STARTUPINFO extends Structure 177 | { 178 | public DWORD cb; 179 | public String lpReserved; 180 | public String lpDesktop; 181 | public String lpTitle; 182 | public DWORD dwX; 183 | public DWORD dwY; 184 | public DWORD dwXSize; 185 | public DWORD dwYSize; 186 | public DWORD dwXCountChars; 187 | public DWORD dwYCountChars; 188 | public DWORD dwFillAttribute; 189 | public int dwFlags; 190 | public WORD wShowWindow; 191 | public WORD cbReserved2; 192 | public ByteByReference lpReserved2; 193 | public HANDLE hStdInput; 194 | public HANDLE hStdOutput; 195 | public HANDLE hStdError; 196 | 197 | public STARTUPINFO() 198 | { 199 | cb = new DWORD(size()); 200 | } 201 | 202 | @Override 203 | protected List getFieldOrder() 204 | { 205 | return Arrays.asList("cb", "lpReserved", "lpDesktop", "lpTitle", "dwX", "dwY", "dwXSize", "dwYSize", 206 | "dwXCountChars", "dwYCountChars", "dwFillAttribute", "dwFlags", "wShowWindow", "cbReserved2", 207 | "lpReserved2", "hStdInput", "hStdOutput", "hStdError"); 208 | } 209 | } 210 | 211 | class PROCESS_INFORMATION extends Structure 212 | { 213 | public HANDLE hProcess; 214 | public HANDLE hThread; 215 | public DWORD dwProcessId; 216 | public DWORD dwThreadId; 217 | 218 | @Override 219 | protected List getFieldOrder() 220 | { 221 | return Arrays.asList("hProcess", "hThread", "dwProcessId", "dwThreadId"); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/windows/WinProcessFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * 19 | */ 20 | package com.zaxxer.nuprocess.windows; 21 | 22 | import java.nio.file.Path; 23 | 24 | import java.util.List; 25 | 26 | import com.zaxxer.nuprocess.NuProcess; 27 | import com.zaxxer.nuprocess.NuProcessFactory; 28 | import com.zaxxer.nuprocess.NuProcessHandler; 29 | 30 | /** 31 | * Windows process factory. Creates and starts a process. 32 | * 33 | * @author Brett Wooldridge 34 | */ 35 | public class WinProcessFactory implements NuProcessFactory 36 | { 37 | /** {@inheritDoc} */ 38 | @Override 39 | public NuProcess createProcess(List commands, String[] env, NuProcessHandler processListener, Path cwd) 40 | { 41 | WindowsProcess process = new WindowsProcess(processListener); 42 | process.start(commands, env, cwd); 43 | return process; 44 | } 45 | 46 | /** {@inheritDoc} */ 47 | @Override 48 | public void runProcess(List commands, String[] env, NuProcessHandler processListener, Path cwd) 49 | { 50 | WindowsProcess process = new WindowsProcess(processListener); 51 | process.run(commands, env, cwd); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/zaxxer/nuprocess/windows/WindowsCreateProcessEscape.java: -------------------------------------------------------------------------------- 1 | // This is free and unencumbered software released into the public domain. 2 | // 3 | // Anyone is free to copy, modify, publish, use, compile, sell, or 4 | // distribute this software, either in source code form or as a compiled 5 | // binary, for any purpose, commercial or non-commercial, and by any 6 | // means. 7 | // 8 | // In jurisdictions that recognize copyright laws, the author or authors 9 | // of this software dedicate any and all copyright interest in the 10 | // software to the public domain. We make this dedication for the benefit 11 | // of the public at large and to the detriment of our heirs and 12 | // successors. We intend this dedication to be an overt act of 13 | // relinquishment in perpetuity of all present and future rights to this 14 | // software under copyright law. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | // OTHER DEALINGS IN THE SOFTWARE. 23 | // 24 | // For more information, please refer to 25 | 26 | package com.zaxxer.nuprocess.windows; 27 | 28 | /** 29 | * Helps quote arguments when launching a process on Windows. 30 | * 31 | * Unix's process spawning syscall accepts an array of arguments, but Windows' 32 | * equivalent accepts a single string and splits the strings according to some 33 | * different rules. 34 | */ 35 | class WindowsCreateProcessEscape 36 | { 37 | private WindowsCreateProcessEscape() 38 | { 39 | } 40 | 41 | /** 42 | * Same as {@link #quote(String)} except appends to a {@code StringBuilder}. 43 | */ 44 | public static void quote(StringBuilder buf, String arg) 45 | { 46 | if (!mightNeedQuotes(arg)) { 47 | buf.append(arg); 48 | return; 49 | } 50 | buf.append('"'); 51 | 52 | // The length of the current run of backslashes. 53 | int nPending = 0; 54 | 55 | for (int i = 0; i < arg.length(); i++) { 56 | char c = arg.charAt(i); 57 | 58 | if (c == '\\') { 59 | nPending++; 60 | } 61 | else { 62 | if (c == '"') { 63 | // Escape all the backslashes we've collected up till now. 64 | for (int j = 0; j < nPending; j++) { 65 | buf.append('\\'); 66 | } 67 | // Escape the quote. 68 | buf.append('\\'); 69 | } 70 | nPending = 0; 71 | } 72 | 73 | buf.append(c); 74 | } 75 | 76 | // Escape all the backslashes that appear before the final closing quote. 77 | for (int j = 0; j < nPending; j++) { 78 | buf.append('\\'); 79 | } 80 | 81 | buf.append('"'); 82 | } 83 | 84 | /** 85 | * Given a string X, this function returns a string that, when passed through 86 | * the Windows implementation of Java's {@link Runtime#exec(String[])} or 87 | * {@link java.lang.ProcessBuilder}, will appear to the spawned process as X. 88 | * 89 | * @param arg 90 | * The argument to quote. 91 | * 92 | * @return 93 | * The quote version of 'arg'. 94 | */ 95 | public static String quote(String arg) 96 | { 97 | StringBuilder buf = new StringBuilder(2 + arg.length() * 2); 98 | quote(buf, arg); 99 | return buf.toString(); 100 | } 101 | 102 | private static boolean mightNeedQuotes(String arg) 103 | { 104 | if (arg.length() == 0) { 105 | return true; 106 | } 107 | 108 | for (int i = 0; i < arg.length(); i++) { 109 | char c = arg.charAt(i); 110 | if (c == '"' || c == ' ' || c == '\t') { 111 | return true; 112 | } 113 | } 114 | 115 | return false; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/com/zaxxer/nuprocess/DirectWriteTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess; 18 | 19 | import java.nio.ByteBuffer; 20 | import java.util.concurrent.Semaphore; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | import java.util.zip.Adler32; 24 | 25 | import org.junit.Assert; 26 | import org.junit.Before; 27 | import org.junit.Test; 28 | 29 | import static com.zaxxer.nuprocess.CatTest.getLotsOfBytes; 30 | 31 | /** 32 | * @author Brett Wooldridge 33 | */ 34 | public class DirectWriteTest 35 | { 36 | private String command; 37 | 38 | @Before 39 | public void setup() 40 | { 41 | command = "/bin/cat"; 42 | if (System.getProperty("os.name").toLowerCase().contains("win")) { 43 | command = "src\\test\\java\\com\\zaxxer\\nuprocess\\cat.exe"; 44 | } 45 | } 46 | 47 | @Test 48 | public void testDirectWrite() throws InterruptedException 49 | { 50 | ProcessHandler1 processListener = new ProcessHandler1(); 51 | NuProcessBuilder pb = new NuProcessBuilder(processListener, command); 52 | NuProcess nuProcess = pb.start(); 53 | nuProcess.waitFor(10, TimeUnit.SECONDS); 54 | Assert.assertEquals("Results did not match", "This is a test", processListener.result); 55 | } 56 | 57 | @Test 58 | public void testDirectWriteBig() throws InterruptedException 59 | { 60 | ProcessHandler2 processListener = new ProcessHandler2(); 61 | NuProcessBuilder pb = new NuProcessBuilder(processListener, command); 62 | NuProcess nuProcess = pb.start(); 63 | nuProcess.waitFor(10, TimeUnit.SECONDS); 64 | Assert.assertEquals("Checksums did not match", processListener.checksum, processListener.checksum2); 65 | } 66 | 67 | @Test 68 | public void testFewWrites() throws InterruptedException 69 | { 70 | final AtomicInteger count = new AtomicInteger(); 71 | 72 | NuProcessHandler processListener = new NuAbstractProcessHandler() { 73 | @Override 74 | public void onStdout(ByteBuffer buffer, boolean closed) { 75 | count.addAndGet(buffer.remaining()); 76 | buffer.position(buffer.limit()); 77 | } 78 | }; 79 | 80 | NuProcessBuilder pb = new NuProcessBuilder(processListener, command); 81 | NuProcess nuProcess = pb.start(); 82 | 83 | ByteBuffer buffer = ByteBuffer.allocate(64); 84 | buffer.put("This is a test".getBytes()); 85 | buffer.flip(); 86 | nuProcess.writeStdin(buffer); 87 | 88 | Thread.sleep(500); 89 | 90 | nuProcess.closeStdin(true); 91 | nuProcess.waitFor(10, TimeUnit.SECONDS); 92 | Assert.assertEquals("Count did not match", 14, count.get()); 93 | } 94 | 95 | @Test 96 | public void testConsecutiveWrites() throws InterruptedException 97 | { 98 | final AtomicInteger count = new AtomicInteger(); 99 | 100 | NuProcessHandler processListener = new NuAbstractProcessHandler() { 101 | @Override 102 | public void onStdout(ByteBuffer buffer, boolean closed) 103 | { 104 | count.addAndGet(buffer.remaining()); 105 | buffer.position(buffer.limit()); 106 | } 107 | }; 108 | 109 | NuProcessBuilder pb = new NuProcessBuilder(processListener, command); 110 | NuProcess nuProcess = pb.start(); 111 | // TODO: given a large i (e.g. 1,000, 10,000), this unit test (testConsecutiveWrites) will 112 | // produce a side-effect on InterruptTest (has problem on Mac OS X, but works on Linux and Win32). 113 | // We do not reuse fork on surefire (reuseForks=false) to address this issue for now. 114 | for (int i = 0; i < 1000; i++) { 115 | ByteBuffer buffer = ByteBuffer.allocate(64); 116 | buffer.put("This is a test".getBytes()); 117 | buffer.flip(); 118 | nuProcess.writeStdin(buffer); 119 | } 120 | 121 | Thread.sleep(500); 122 | 123 | nuProcess.closeStdin(true); 124 | nuProcess.waitFor(20, TimeUnit.SECONDS); 125 | Assert.assertEquals("Count did not match", 14000, count.get()); 126 | } 127 | 128 | 129 | @Test 130 | public void lotOfDataSync() throws Exception 131 | { 132 | System.err.println("Starting test lotOfDataSync()"); 133 | final byte[] bytes = getLotsOfBytes(34632); 134 | Adler32 adler = new Adler32(); 135 | adler.update(bytes); 136 | long expectedHash = adler.getValue(); 137 | 138 | for (int i = 0; i < 100; i++) { 139 | final Semaphore semaphore = new Semaphore(0); 140 | 141 | final AtomicInteger readLength = new AtomicInteger(); 142 | 143 | final Adler32 readAdler = new Adler32(); 144 | NuAbstractProcessHandler processListener = new NuAbstractProcessHandler() { 145 | 146 | @Override 147 | public void onStdout(ByteBuffer buffer, boolean closed) { 148 | byte[] buf = new byte[buffer.remaining()]; 149 | buffer.get(buf); 150 | readAdler.update(buf); 151 | 152 | if (readLength.addAndGet(buf.length) >= bytes.length) { 153 | semaphore.release(); 154 | } 155 | } 156 | }; 157 | 158 | NuProcessBuilder pb = new NuProcessBuilder(processListener, command); 159 | NuProcess process = pb.start(); 160 | 161 | process.writeStdin(ByteBuffer.wrap(bytes)); 162 | process.closeStdin(false); 163 | 164 | if (!semaphore.tryAcquire(10, TimeUnit.SECONDS)) 165 | { 166 | Assert.fail("Timed out waiting for process (iteration " + i + "), " + readLength.get() + " bytes read"); 167 | } 168 | 169 | Assert.assertEquals("Adler32 mismatch between written and read (iteration " + i + ")", expectedHash, readAdler.getValue()); 170 | } 171 | 172 | System.err.println("Completed test lotOfDataSync()"); 173 | } 174 | 175 | private static class ProcessHandler1 extends NuAbstractProcessHandler 176 | { 177 | private NuProcess nuProcess; 178 | String result; 179 | 180 | @Override 181 | public void onStart(NuProcess nuProcess) 182 | { 183 | this.nuProcess = nuProcess; 184 | 185 | ByteBuffer buffer = ByteBuffer.allocate(256); 186 | buffer.put("This is a test".getBytes()); 187 | buffer.flip(); 188 | 189 | System.out.println("Writing: This is a test"); 190 | nuProcess.writeStdin(buffer); 191 | } 192 | 193 | @Override 194 | public void onStdout(ByteBuffer buffer, boolean closed) { 195 | if (buffer.hasRemaining()) { 196 | byte[] chars = new byte[buffer.remaining()]; 197 | buffer.get(chars); 198 | result = new String(chars); 199 | System.out.println("Read: " + result); 200 | } 201 | nuProcess.closeStdin(true); 202 | } 203 | } 204 | 205 | private static class ProcessHandler2 extends NuAbstractProcessHandler 206 | { 207 | private NuProcess nuProcess; 208 | int checksum; 209 | int checksum2; 210 | 211 | @Override 212 | public void onStart(NuProcess nuProcess) 213 | { 214 | this.nuProcess = nuProcess; 215 | 216 | ByteBuffer buffer = ByteBuffer.allocate(1024 * 128); 217 | for (int i = 0; i < buffer.capacity(); i++) { 218 | byte b = (byte) (i % 256); 219 | buffer.put(b); 220 | checksum += b; 221 | } 222 | 223 | buffer.flip(); 224 | 225 | System.out.println("Writing: 128K of data, waiting for checksum " + checksum); 226 | nuProcess.writeStdin(buffer); 227 | } 228 | 229 | @Override 230 | public void onStdout(ByteBuffer buffer, boolean closed) 231 | { 232 | while (buffer.hasRemaining()) { 233 | checksum2 += buffer.get(); 234 | } 235 | 236 | System.out.println("Reading. Current checksum " + checksum2); 237 | if (checksum2 == checksum) { 238 | System.out.println("Checksums matched, exiting."); 239 | nuProcess.closeStdin(true); 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/test/java/com/zaxxer/nuprocess/EnvironmentTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess; 18 | 19 | import java.io.IOException; 20 | import java.nio.CharBuffer; 21 | import java.nio.charset.Charset; 22 | import java.nio.charset.CoderResult; 23 | import java.util.Arrays; 24 | import java.util.Collections; 25 | import java.util.LinkedHashSet; 26 | import java.util.Map; 27 | import java.util.Scanner; 28 | import java.util.Set; 29 | import java.util.concurrent.TimeUnit; 30 | import org.junit.Assert; 31 | import org.junit.Before; 32 | import org.junit.Test; 33 | 34 | import com.zaxxer.nuprocess.codec.NuAbstractCharsetHandler; 35 | 36 | public class EnvironmentTest { 37 | private String[] command; 38 | private static boolean isWin = System.getProperty("os.name").toLowerCase().contains("win"); 39 | 40 | @Before 41 | public void setup() 42 | { 43 | if (isWin) { 44 | command = new String[] { "cmd", "/C", "set" }; 45 | } else { 46 | command = new String[] { "env" }; 47 | } 48 | } 49 | 50 | @Test 51 | public void emptyEnv() throws InterruptedException, IOException 52 | { 53 | Set javaResult = runJavaProcess(command, Collections.emptyMap()); 54 | Set nuResult = runNuProcess(command, Collections.emptyMap()); 55 | Assert.assertEquals(javaResult, nuResult); 56 | } 57 | 58 | @Test 59 | public void defaultEnv() throws InterruptedException, IOException 60 | { 61 | Set javaResult = runJavaProcess(command, System.getenv()); 62 | Set nuResult = runNuProcess(command, System.getenv()); 63 | Assert.assertEquals(javaResult, nuResult); 64 | } 65 | 66 | private Set runJavaProcess(String[] command, Map env) throws IOException, InterruptedException 67 | { 68 | ProcessBuilder pb = new ProcessBuilder(command); 69 | pb.environment().clear(); 70 | pb.environment().putAll(env); 71 | Process process = pb.start(); 72 | Set result = new LinkedHashSet<>(); 73 | Scanner s = new Scanner(process.getInputStream(), "UTF-8").useDelimiter(System.lineSeparator()); 74 | while (s.hasNext()) { 75 | result.add(s.next()); 76 | } 77 | Assert.assertEquals(0, process.waitFor()); 78 | return result; 79 | } 80 | 81 | private Set runNuProcess(String[] command, Map env) throws InterruptedException 82 | { 83 | ProcessHandler processListener = new ProcessHandler(); 84 | NuProcessBuilder pb = new NuProcessBuilder(Arrays.asList(command), env); 85 | pb.setProcessListener(processListener); 86 | NuProcess process = pb.start(); 87 | process.waitFor(10, TimeUnit.SECONDS); 88 | Set result = new LinkedHashSet<>(); 89 | Scanner s = new Scanner(processListener.getStdOut()).useDelimiter(System.lineSeparator()); 90 | while (s.hasNext()) { 91 | result.add(s.next()); 92 | } 93 | return result; 94 | } 95 | 96 | private static class ProcessHandler extends NuAbstractCharsetHandler 97 | { 98 | ProcessHandler() 99 | { 100 | super(Charset.forName("UTF-8")); 101 | } 102 | 103 | private final StringBuilder stdOut = new StringBuilder(); 104 | 105 | @Override 106 | public void onStdoutChars(CharBuffer buffer, boolean closed, CoderResult coderResult) 107 | { 108 | stdOut.append(buffer); 109 | buffer.position(buffer.limit()); 110 | } 111 | 112 | String getStdOut() 113 | { 114 | return stdOut.toString(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/com/zaxxer/nuprocess/FastExitingProcessTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Ben Hamilton 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess; 18 | 19 | import static org.hamcrest.Matchers.equalTo; 20 | import static org.hamcrest.Matchers.is; 21 | import static org.hamcrest.Matchers.nullValue; 22 | import static org.junit.Assert.assertThat; 23 | 24 | import java.io.ByteArrayOutputStream; 25 | import java.nio.ByteBuffer; 26 | import java.nio.channels.Channels; 27 | import java.nio.channels.WritableByteChannel; 28 | import java.util.concurrent.TimeUnit; 29 | 30 | import org.junit.Test; 31 | 32 | import com.sun.jna.Platform; 33 | 34 | public class FastExitingProcessTest 35 | { 36 | private static class Handler extends NuAbstractProcessHandler 37 | { 38 | final ByteArrayOutputStream stdoutBytes = new ByteArrayOutputStream(); 39 | final WritableByteChannel stdoutBytesChannel = Channels.newChannel(stdoutBytes); 40 | int exitCode = -1; 41 | Exception stdoutException; 42 | 43 | @Override 44 | public void onExit(int exitCode) 45 | { 46 | this.exitCode = exitCode; 47 | } 48 | 49 | @Override 50 | public void onStdout(ByteBuffer buffer, boolean closed) 51 | { 52 | try { 53 | stdoutBytesChannel.write(buffer); 54 | } 55 | catch (Exception e) { 56 | stdoutException = e; 57 | } 58 | } 59 | } 60 | 61 | @Test 62 | public void whenProcessWritesToStdoutThenExitsThenHandlerReceivesOutput() throws Exception 63 | { 64 | System.setProperty("nuprocess.test.afterStartSleep", "" + TimeUnit.MILLISECONDS.toNanos(50)); 65 | try { 66 | for (int i = 0; i < 250; i++) { 67 | Handler handler = new Handler(); 68 | NuProcess process; 69 | if (Platform.isWindows()) { 70 | // We can't use an argument with a space here because the following command: 71 | // cmd /c echo "Hello world!" 72 | // literally echoes the quotes around "Hello world!" to stdout. 73 | process = new NuProcessBuilder(handler, "cmd.exe", "/c", "echo", "hello").start(); 74 | } else { 75 | process = new NuProcessBuilder(handler, "echo", "hello").start(); 76 | } 77 | int retVal = process.waitFor(Long.MAX_VALUE, TimeUnit.SECONDS); 78 | assertThat("Process should exit cleanly", retVal, equalTo(0)); 79 | assertThat("Process callback should indicate clean exit", handler.exitCode, equalTo(0)); 80 | assertThat("No exceptions thrown writing to stdout", handler.stdoutException, is(nullValue())); 81 | assertThat("Stdout should contain expected output", handler.stdoutBytes.toString("UTF-8"), equalTo(String.format("hello%n"))); 82 | } 83 | } 84 | finally { 85 | System.clearProperty("nuprocess.test.afterStartSleep"); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/zaxxer/nuprocess/InterruptTest.java: -------------------------------------------------------------------------------- 1 | package com.zaxxer.nuprocess; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.ArrayList; 5 | import java.util.LinkedList; 6 | import java.util.List; 7 | import java.util.concurrent.Semaphore; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | 15 | import com.zaxxer.nuprocess.internal.BasePosixProcess; 16 | import com.zaxxer.nuprocess.internal.LibC; 17 | import com.zaxxer.nuprocess.windows.WindowsProcess; 18 | 19 | public class InterruptTest 20 | { 21 | private String command; 22 | 23 | @Before 24 | public void setup() 25 | { 26 | command = "/bin/cat"; 27 | if (System.getProperty("os.name").toLowerCase().contains("win")) { 28 | command = "src\\test\\java\\com\\zaxxer\\nuprocess\\cat.exe"; 29 | } 30 | } 31 | 32 | @Test 33 | public void testDestroy() throws InterruptedException 34 | { 35 | testDestroy(false); 36 | } 37 | 38 | @Test 39 | public void testDestroyForcibly() throws InterruptedException 40 | { 41 | testDestroy(true); 42 | } 43 | 44 | private void testDestroy(boolean forceKill) throws InterruptedException 45 | { 46 | final Semaphore semaphore = new Semaphore(0); 47 | final AtomicInteger exitCode = new AtomicInteger(); 48 | final AtomicInteger count = new AtomicInteger(); 49 | 50 | NuProcessHandler processListener = new NuAbstractProcessHandler() { 51 | @Override 52 | public void onStart(NuProcess nuProcess) 53 | { 54 | nuProcess.wantWrite(); 55 | } 56 | 57 | @Override 58 | public void onExit(int statusCode) 59 | { 60 | exitCode.set(statusCode); 61 | semaphore.release(); 62 | } 63 | 64 | @Override 65 | public void onStdout(ByteBuffer buffer, boolean closed) 66 | { 67 | count.addAndGet(buffer.remaining()); 68 | buffer.position(buffer.limit()); 69 | } 70 | 71 | @Override 72 | public boolean onStdinReady(ByteBuffer buffer) 73 | { 74 | buffer.put("This is a test".getBytes()); 75 | return true; 76 | } 77 | }; 78 | 79 | NuProcessBuilder pb = new NuProcessBuilder(processListener, command); 80 | NuProcess process = pb.start(); 81 | while (true) { 82 | if (count.getAndIncrement() > 100) { 83 | process.destroy(forceKill); 84 | break; 85 | } 86 | Thread.sleep(20); 87 | } 88 | 89 | semaphore.acquireUninterruptibly(); 90 | int exit = process.waitFor(2, TimeUnit.SECONDS); 91 | Assert.assertNotEquals("Process exit code did not match", 0, exit); 92 | } 93 | 94 | @Test 95 | public void chaosMonkey() throws InterruptedException 96 | { 97 | NuProcessHandler processListener = new NuAbstractProcessHandler() { 98 | 99 | @Override 100 | public void onStart(NuProcess nuProcess) 101 | { 102 | nuProcess.wantWrite(); 103 | } 104 | 105 | @Override 106 | public void onStdout(ByteBuffer buffer, boolean closed) 107 | { 108 | buffer.position(buffer.limit()); 109 | } 110 | 111 | @Override 112 | public boolean onStdinReady(ByteBuffer buffer) 113 | { 114 | buffer.put("This is a test".getBytes()); 115 | return true; 116 | } 117 | 118 | }; 119 | 120 | NuProcessBuilder pb = new NuProcessBuilder(processListener, command); 121 | List processes = new LinkedList(); 122 | for (int times = 0; times < 1; times++) { 123 | for (int i = 0; i < 50; i++) { 124 | processes.add(pb.start()); 125 | } 126 | 127 | List deadProcs = new ArrayList(); 128 | while (true) { 129 | Thread.sleep(20); 130 | int dead = (int) (Math.random() * processes.size()); 131 | if (!System.getProperty("os.name").toLowerCase().contains("win")) { 132 | BasePosixProcess bpp = (BasePosixProcess) processes.remove(dead); 133 | if (bpp == null) { 134 | continue; 135 | } 136 | deadProcs.add(bpp); 137 | LibC.kill(bpp.getPid(), LibC.SIGKILL); 138 | } 139 | else { 140 | WindowsProcess wp = (WindowsProcess) processes.remove(dead); 141 | if (wp == null) { 142 | continue; 143 | } 144 | deadProcs.add(wp); 145 | wp.destroy(false); 146 | } 147 | 148 | if (processes.isEmpty()) { 149 | for (int i = 0; i < 50; i++) { 150 | int exit = deadProcs.get(i).waitFor(2, TimeUnit.SECONDS); 151 | Assert.assertTrue("Process exit code did not match", (exit != 0 || exit == Integer.MAX_VALUE)); 152 | } 153 | break; 154 | } 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/test/java/com/zaxxer/nuprocess/RunOnlyOnUnix.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess; 18 | 19 | import org.junit.runner.notification.RunNotifier; 20 | import org.junit.runners.BlockJUnit4ClassRunner; 21 | import org.junit.runners.model.InitializationError; 22 | 23 | public class RunOnlyOnUnix extends BlockJUnit4ClassRunner 24 | { 25 | public RunOnlyOnUnix(Class klass) throws InitializationError 26 | { 27 | super(klass); 28 | } 29 | 30 | @Override 31 | public void run(RunNotifier notifier) 32 | { 33 | if (System.getProperty("os.name").toLowerCase().contains("linux") || System.getProperty("os.name").toLowerCase().contains("mac") 34 | || System.getProperty("os.name").toLowerCase().contains("solaris") || System.getProperty("os.name").toLowerCase().contains("freebsd")) { 35 | super.run(notifier); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/zaxxer/nuprocess/RunOnlyOnWindows.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Brett Wooldridge 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.zaxxer.nuprocess; 18 | 19 | import org.junit.runner.notification.RunNotifier; 20 | import org.junit.runners.BlockJUnit4ClassRunner; 21 | import org.junit.runners.model.InitializationError; 22 | 23 | public class RunOnlyOnWindows extends BlockJUnit4ClassRunner 24 | { 25 | public RunOnlyOnWindows(Class klass) throws InitializationError 26 | { 27 | super(klass); 28 | } 29 | 30 | @Override 31 | public void run(RunNotifier notifier) 32 | { 33 | if (System.getProperty("os.name").toLowerCase().contains("win")) { 34 | super.run(notifier); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/zaxxer/nuprocess/ThreadedTest.java: -------------------------------------------------------------------------------- 1 | package com.zaxxer.nuprocess; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.concurrent.TimeUnit; 7 | import java.util.zip.Adler32; 8 | 9 | import org.junit.Assert; 10 | import org.junit.Assume; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | 14 | public class ThreadedTest 15 | { 16 | @Before 17 | public void unixOnly() 18 | { 19 | Assume.assumeTrue(!System.getProperty("os.name").startsWith("Windows")); 20 | } 21 | 22 | @Test 23 | public void threadTest1() throws InterruptedException 24 | { 25 | System.err.println("Starting threadTest1()"); 26 | 27 | ArrayList threads = new ArrayList(); 28 | int threadCount = 4; 29 | int procCount = 20; 30 | 31 | for (int ii = 0; ii < threadCount; ii++) { 32 | MyThread mt = new MyThread(ii, procCount); 33 | mt.start(); 34 | threads.add(mt); 35 | System.err.printf(" started thread: %d\n", ii); 36 | } 37 | 38 | for (Thread th : threads) { 39 | th.join(TimeUnit.SECONDS.toMillis(20)); 40 | MyThread mt = (MyThread) th; 41 | if (mt.failure != null) { 42 | Assert.fail(mt.failure); 43 | } 44 | } 45 | 46 | System.err.println("Completed threadTest1()"); 47 | } 48 | 49 | static class MyThread extends Thread 50 | { 51 | private int procCount; 52 | private int id; 53 | private String failure; 54 | 55 | public MyThread(int id, int procCount) 56 | { 57 | this.id = id; 58 | this.procCount = procCount; 59 | } 60 | 61 | public void startProcess(int PROCESSES) 62 | { 63 | String command = "/bin/cat"; 64 | 65 | long start = System.currentTimeMillis(); 66 | long maxFreeMem = 0; 67 | 68 | NuProcessBuilder pb = new NuProcessBuilder(Arrays.asList(command)); 69 | 70 | for (int times = 0; times < 10; times++) { 71 | NuProcess[] processes = new NuProcess[PROCESSES]; 72 | LottaProcessHandler[] handlers = new LottaProcessHandler[PROCESSES]; 73 | 74 | for (int i = 0; i < PROCESSES; i++) { 75 | handlers[i] = new LottaProcessHandler(); 76 | pb.setProcessListener(handlers[i]); 77 | processes[i] = pb.start(); 78 | // System.err.printf(" thread %d starting process %d: %s\n", id, i + 1, processes[i].toString()); 79 | } 80 | 81 | // Kick all of the processes to start going 82 | for (NuProcess process : processes) { 83 | // System.err.printf(" Thread %d calling wantWrite() on process: %s\n", id, process.toString()); 84 | process.wantWrite(); 85 | } 86 | 87 | for (NuProcess process : processes) { 88 | try { 89 | maxFreeMem = Math.max(maxFreeMem, Runtime.getRuntime().freeMemory()); 90 | process.waitFor(90, TimeUnit.SECONDS); 91 | } 92 | catch (InterruptedException ex) { 93 | ex.printStackTrace(); 94 | } 95 | } 96 | 97 | for (LottaProcessHandler handler : handlers) { 98 | if (handler.getAdler() != 4237270634l) { 99 | System.err.println("Adler32 mismatch between written and read"); 100 | failure = "Adler32 mismatch between written and read"; 101 | break; 102 | } 103 | else if (handler.getExitCode() != 0) { 104 | System.err.println("Exit code not zero (0)"); 105 | failure = "Exit code not zero (0)"; 106 | break; 107 | } 108 | } 109 | } 110 | 111 | System.out.printf(this.id + " Maximum memory used: %d\n", Runtime.getRuntime().totalMemory() - maxFreeMem); 112 | System.out.printf(this.id + " Total execution time (ms): %d\n", (System.currentTimeMillis() - start)); 113 | } 114 | 115 | @Override 116 | public void run() 117 | { 118 | this.startProcess(this.procCount); 119 | } 120 | } 121 | 122 | private static class LottaProcessHandler extends NuAbstractProcessHandler 123 | { 124 | private static final int WRITES = 100; 125 | private static final int LIMIT; 126 | private static final byte[] bytes; 127 | 128 | private NuProcess nuProcess; 129 | private int writes; 130 | private int size; 131 | private int exitCode; 132 | 133 | private Adler32 readAdler32 = new Adler32(); 134 | 135 | static { 136 | // Create 600K of data. 137 | StringBuffer sb = new StringBuffer(); 138 | for (int i = 0; i < 6000; i++) { 139 | sb.append("1234567890"); 140 | } 141 | bytes = sb.toString().getBytes(); 142 | LIMIT = WRITES * bytes.length; 143 | } 144 | 145 | @Override 146 | public void onStart(final NuProcess nuProcess) 147 | { 148 | this.nuProcess = nuProcess; 149 | } 150 | 151 | @Override 152 | public void onExit(int statusCode) 153 | { 154 | exitCode = statusCode; 155 | } 156 | 157 | @Override 158 | public void onStdout(ByteBuffer buffer, boolean closed) 159 | { 160 | size += buffer.remaining(); 161 | 162 | byte[] bytes = new byte[buffer.remaining()]; 163 | buffer.get(bytes); 164 | readAdler32.update(bytes); 165 | 166 | if (size == LIMIT) 167 | nuProcess.closeStdin(true); 168 | } 169 | 170 | @Override 171 | public boolean onStdinReady(ByteBuffer buffer) 172 | { 173 | buffer.put(bytes); 174 | buffer.flip(); 175 | return (++writes < WRITES); 176 | } 177 | 178 | int getExitCode() 179 | { 180 | return exitCode; 181 | } 182 | 183 | long getAdler() 184 | { 185 | return readAdler32.getValue(); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/test/java/com/zaxxer/nuprocess/cat.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettwooldridge/NuProcess/7b3a958c22abc1228a34d6fb190b16ac27fafa17/src/test/java/com/zaxxer/nuprocess/cat.exe -------------------------------------------------------------------------------- /src/test/java/com/zaxxer/nuprocess/internal/ConstantsTest.java: -------------------------------------------------------------------------------- 1 | package com.zaxxer.nuprocess.internal; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | public class ConstantsTest { 7 | @Test 8 | public void parsesVersionStrings() { 9 | Assert.assertEquals(8, Constants.getJavaMajorVersion("1.8.0_foo")); 10 | Assert.assertEquals(8, Constants.getJavaMajorVersion("1.8_foo")); 11 | Assert.assertEquals(11, Constants.getJavaMajorVersion("11_foo")); 12 | Assert.assertEquals(8, Constants.getJavaMajorVersion("1.8.0-foo")); 13 | Assert.assertEquals(8, Constants.getJavaMajorVersion("1.8-foo")); 14 | Assert.assertEquals(11, Constants.getJavaMajorVersion("11-foo")); 15 | Assert.assertEquals(8, Constants.getJavaMajorVersion("1.8.0")); 16 | Assert.assertEquals(8, Constants.getJavaMajorVersion("1.8")); 17 | Assert.assertEquals(11, Constants.getJavaMajorVersion("11")); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/zaxxer/nuprocess/linux/EpollEventTest.java: -------------------------------------------------------------------------------- 1 | package com.zaxxer.nuprocess.linux; 2 | 3 | import com.sun.jna.Platform; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | public class EpollEventTest 8 | { 9 | // ensure EpollEvent's size matches the platform: 10 | // - 12 bytes on all 32-bit architectures (4 byte aligned) 11 | // - 12 bytes on x86-64, where it's compiled with __attribute__((packed)) (1 byte aligned) 12 | // - 16 bytes on all other 64-bit architectures (8 byte aligned) 13 | @Test 14 | public void testSize() 15 | { 16 | // 64-bit architectures use a 16 byte struct, except on AMD/Intel, where the struct is 12 bytes 17 | // on both 32- and 64-bit. The struct is 12 bytes on all 32-bit architectures 18 | int expectedSize = (Platform.is64Bit() && !Platform.isIntel()) ? 16 : 12; 19 | 20 | EpollEvent event = new EpollEvent(); 21 | Assert.assertEquals(expectedSize, event.size()); 22 | } 23 | } 24 | --------------------------------------------------------------------------------