Skip to content

Commit

Permalink
Merge pull request #32 from fizzed/feature/windows-processes
Browse files Browse the repository at this point in the history
dFeature/windows processes
  • Loading branch information
jjlauer authored Nov 4, 2024
2 parents 5532a86 + ff79ac7 commit 9e7ef5c
Show file tree
Hide file tree
Showing 20 changed files with 581 additions and 26 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/macos-arm64.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: MacOS arm64

on:
push

jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Set up Azul JDK 11
uses: actions/setup-java@v3
with:
java-version: 11
distribution: 'zulu'
cache: 'maven'
- name: Test in Maven
run: mvn test
2 changes: 1 addition & 1 deletion .github/workflows/macos-x64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:

jobs:
build:
runs-on: macos-11
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- name: Set up Azul JDK 11
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ blaze-lite/dependency-reduced-pom.xml
ssh/src/test/java/ssh.*
*.iml
examples/pom.xml
.idea
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
Blaze by Fizzed
===============

#### 1.6.0 - 2024-11-04

- New powershell blaze.ps1 to improve ctrl+c not prompting user to terminate a batch file (if you use blaze -i installer)
- exec() launched processes (and their descendant processes) will auto destroy when blaze JVM process shuts down (fixes
issue on Windows where by default the hierarchy of processes is not destroyed on CTRL+C)
- Bump maven-parent to v2.6.0
- Bump crux to v1.0.47
- Bump SLF4J to v2.0.13
- Bump config to v1.4.3
- Bump commons-io to v2.16.1
- Bump jsch to v0.2.18
- Bump jna to v5.13.0 (fixes ssh agent for arm64 platforms)

#### 1.5.1 - 2023-10-31

- Add Systems.remove() methods back (was causing backwards incompatiblity issues)
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Blaze by Fizzed
[![Java 21](https://img.shields.io/github/actions/workflow/status/fizzed/blaze/linux-java21.yaml?branch=master&label=Java%2021&style=flat-square)](https://github.com/fizzed/blaze/actions/workflows/linux-java21.yaml)

[![Linux](https://img.shields.io/github/actions/workflow/status/fizzed/blaze/linux-java8.yaml?branch=master&label=Linux&style=flat-square)](https://github.com/fizzed/blaze/actions/workflows/linux-java8.yaml)
[![MacOS](https://img.shields.io/github/actions/workflow/status/fizzed/blaze/macos-x64.yaml?branch=master&label=MacOS&style=flat-square)](https://github.com/fizzed/blaze/actions/workflows/macos-x64.yaml)
[![MacOS x64](https://img.shields.io/github/actions/workflow/status/fizzed/blaze/macos-x64.yaml?branch=master&label=MacOS&style=flat-square)](https://github.com/fizzed/blaze/actions/workflows/macos-x64.yaml)
[![MacOS arm64](https://img.shields.io/github/actions/workflow/status/fizzed/blaze/macos-arm64.yaml?branch=master&label=MacOS&style=flat-square)](https://github.com/fizzed/blaze/actions/workflows/macos-arm64.yaml)
[![Windows](https://img.shields.io/github/actions/workflow/status/fizzed/blaze/windows-x64.yaml?branch=master&label=Windows&style=flat-square)](https://github.com/fizzed/blaze/actions/workflows/macos-x64.yaml)

## Overview
Expand Down
14 changes: 13 additions & 1 deletion blaze-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>


<dependency>
<groupId>com.fizzed</groupId>
<artifactId>crux-util</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.fizzed</groupId>
<artifactId>jne</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ static public List<Path> installBlazeBinaries(Path installDir) throws MessageOnl
}

if (!Files.isWritable(installDir)) {
throw new MessageOnlyException("Install directory " + installDir + " is not writable (run this as an Administor or with sudo?)");
throw new MessageOnlyException("Install directory " + installDir + " is not writable (run this as an Administrator or with sudo?)");
}

List<Path> installedFiles = new ArrayList<>();
Expand All @@ -48,6 +48,11 @@ static public List<Path> installBlazeBinaries(Path installDir) throws MessageOnl
Path blazeBatFile = installDir.resolve("blaze.bat");
installResource("/bin/blaze.bat", blazeBatFile);
installedFiles.add(blazeBatFile);

// install blaze.ps1 (powershell)
Path blazePs1File = installDir.resolve("blaze.ps1");
installResource("/bin/blaze.ps1", blazePs1File);
installedFiles.add(blazePs1File);
}

// for ming32 compat also install the *nix version
Expand Down
53 changes: 41 additions & 12 deletions blaze-core/src/main/java/com/fizzed/blaze/local/LocalExec.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import com.fizzed.blaze.core.UnexpectedExitValueException;
import com.fizzed.blaze.util.CommandLines;
import com.fizzed.blaze.util.ProcessReaper;
import org.zeroturnaround.exec.InvalidExitValueException;
import org.zeroturnaround.exec.ProcessExecutor;
import com.fizzed.blaze.system.Exec;
Expand All @@ -36,6 +38,7 @@
import com.fizzed.blaze.util.Streamables;
import java.io.InputStream;
import org.zeroturnaround.exec.ProcessResult;
import org.zeroturnaround.exec.StartedProcess;
import org.zeroturnaround.exec.stream.PumpStreamHandler;

public class LocalExec extends Exec<LocalExec> {
Expand Down Expand Up @@ -82,15 +85,15 @@ protected Exec.Result doRun() throws BlazeException {
final ProcessExecutor executor = new ProcessExecutor();


if (this.environment.size() > 0) {
this.environment.forEach((k,v) -> executor.environment(k, v));
if (!this.environment.isEmpty()) {
this.environment.forEach(executor::environment);
}

if (this.workingDirectory != null) {
executor.directory(this.workingDirectory.toFile());
}

if (this.exitValues != null && this.exitValues.size() > 0) {
if (this.exitValues != null && !this.exitValues.isEmpty()) {
executor.exitValues(this.exitValues.toArray(new Integer[0]));
}

Expand Down Expand Up @@ -170,15 +173,41 @@ public void stop() {
executor
.command(finalCommand)
.streams(streams);

try {
ProcessResult processResult = executor.execute();

return new Exec.Result(this, processResult.getExitValue());
} catch (InvalidExitValueException e) {
throw new com.fizzed.blaze.core.UnexpectedExitValueException("Process exited with unexpected value", this.exitValues, e.getExitValue());
} catch (IOException | InterruptedException | TimeoutException e) {
throw new BlazeException("Unable to cleanly execute process", e);
final StartedProcess startedProcess = executor.start();

// register process for reaping (cleaning up...)
ProcessReaper.INSTANCE.register(startedProcess.getProcess());
try {
ProcessResult processResult = startedProcess.getFuture().get();

return new Exec.Result(this, processResult.getExitValue());
} finally {
ProcessReaper.INSTANCE.unregister(startedProcess.getProcess());
}
} catch (Throwable t) {
if (t instanceof ExecutionException) {
ExecutionException ee = (ExecutionException)t;
if (ee.getCause() instanceof InvalidExitValueException) {
// this is actually what we want to process
t = ee.getCause();
}
}

if (t instanceof InvalidExitValueException) {
InvalidExitValueException ievae = (InvalidExitValueException)t;

// this can happen IF we're in the process of being shutdown and we actually don't want to throw an exception
if (ProcessReaper.INSTANCE.isShuttingDown()) {
log.trace("Shutting down, ignoring invalid exit code on exec()");
return new Exec.Result(this, ievae.getExitValue());
}

throw new UnexpectedExitValueException("Process exited with unexpected value", this.exitValues, ievae.getExitValue());
}

throw new BlazeException("Unable to cleanly execute process", t);
} finally {
// close all the output streams (input stream closed above)
Streamables.close(os);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.fizzed.blaze.util;

import java.lang.reflect.Method;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Uses reflection to allow for Java 8 compiled source to leverage Java 9+ process API.
*/
public class ProcessHandleReflected {

// reflected methods we'll store for faster access
static private volatile boolean attempted = false;
static private final Object lock = new Object();
static private Class<?> processHandleClass = null;
static private Method processClassToHandleMethod;
static private Method processHandleClassCurrentMethod;
static private Method processHandleClassPidMethod;
static private Method processHandleClassIsAliveMethod;
static private Method processHandleClassDestroyMethod;
static private Method processHandleClassDestroyForciblyMethod;
static private Method processHandleClassDescendantsMethod;

static public boolean isAvailable() {
if (!attempted) {
synchronized (lock) {
if (!attempted) {
try {
processHandleClass = Class.forName("java.lang.ProcessHandle");
processClassToHandleMethod = Process.class.getMethod("toHandle");
processHandleClassCurrentMethod = processHandleClass.getMethod("current");
processHandleClassPidMethod = processHandleClass.getMethod("pid");
processHandleClassIsAliveMethod = processHandleClass.getMethod("isAlive");
processHandleClassDestroyMethod = processHandleClass.getMethod("destroy");
processHandleClassDestroyForciblyMethod = processHandleClass.getMethod("destroyForcibly");
processHandleClassDescendantsMethod = processHandleClass.getMethod("descendants");
} catch (Exception e) {
processHandleClass = null;
}
attempted = true;
}
}
}
return processHandleClass != null;
}

private final Object instance;

static public ProcessHandleReflected from(Process process) {
if (!isAvailable()) {
throw new UnsupportedOperationException("Process API is not available on this JVM (are you running Java 9+ ?)");
}

try {
final Object handle = processClassToHandleMethod.invoke(process);

return new ProcessHandleReflected(handle);
} catch (ReflectiveOperationException | SecurityException e) {
throw new UnsupportedOperationException(e);
}
}

static public ProcessHandleReflected current() {
if (!isAvailable()) {
throw new UnsupportedOperationException("Process API is not available on this JVM (are you running Java 9+ ?)");
}

try {
final Object handle = processHandleClassCurrentMethod.invoke(null);

return new ProcessHandleReflected(handle);
} catch (ReflectiveOperationException | SecurityException e) {
throw new UnsupportedOperationException(e);
}
}

public ProcessHandleReflected(Object instance) {
this.instance = instance;
}

public long pid() {
try {
final Object value = processHandleClassPidMethod.invoke(this.instance);

return (long) value;
} catch (ReflectiveOperationException | SecurityException e) {
throw new RuntimeException(e);
}
}

public boolean isAlive() {
try {
final Object value = processHandleClassIsAliveMethod.invoke(this.instance);

return (boolean) value;
} catch (ReflectiveOperationException | SecurityException e) {
throw new RuntimeException(e);
}
}

public boolean destroy() {
try {
final Object value = processHandleClassDestroyMethod.invoke(this.instance);

return (boolean) value;
} catch (ReflectiveOperationException | SecurityException e) {
throw new RuntimeException(e);
}
}

public boolean destroyForcibly() {
try {
final Object value = processHandleClassDestroyForciblyMethod.invoke(this.instance);

return (boolean) value;
} catch (ReflectiveOperationException | SecurityException e) {
throw new RuntimeException(e);
}
}

public List<ProcessHandleReflected> descendants() {
try {
final Stream<?> valueStream = (Stream<?>)processHandleClassDescendantsMethod.invoke(this.instance);

return valueStream.map(ProcessHandleReflected::new).collect(Collectors.toList());
} catch (ReflectiveOperationException | SecurityException e) {
throw new RuntimeException(e);
}
}
}
17 changes: 17 additions & 0 deletions blaze-core/src/main/java/com/fizzed/blaze/util/ProcessHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.fizzed.blaze.util;

public interface ProcessHelper {

void destroy(Process process, long normalTerminationTimeoutMillis) throws InterruptedException;

void destroyWithDescendants(Process process, long normalTerminationTimeoutMillis) throws InterruptedException;

static ProcessHelper get() {
if (ProcessHelper9.isAvailable()) {
return new ProcessHelper9();
} else {
return new ProcessHelper8();
}
}

}
30 changes: 30 additions & 0 deletions blaze-core/src/main/java/com/fizzed/blaze/util/ProcessHelper8.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.fizzed.blaze.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ProcessHelper8 implements ProcessHelper {
static private final Logger log = LoggerFactory.getLogger(ProcessHelper8.class);

@Override
public void destroy(Process process, long normalTerminationTimeoutMillis) throws InterruptedException {
if (process.isAlive()) {
log.debug("Destroying/killing process w/ normal termination {} (will wait {} ms)", process, normalTerminationTimeoutMillis);
// try to destroy process normally, then wait till timeout
process.destroy();
boolean killed = new WaitFor(() -> !process.isAlive()).await(normalTerminationTimeoutMillis, 100L);
if (!killed) {
log.debug("Normal termination timed out. Destroying/killing process {} forcibly", process);
process.destroyForcibly();
}
}
}

@Override
public void destroyWithDescendants(Process process, long normalTerminationTimeoutMillis) throws InterruptedException {
// this is not supported on java 8, so we'll only do the main process
log.debug("Destroying processes with descendants is only supported on Java 9+ (so we will only destroy parent process instead)");
this.destroy(process, normalTerminationTimeoutMillis);
}

}
Loading

0 comments on commit 9e7ef5c

Please sign in to comment.