-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathToolProgram.java
More file actions
163 lines (148 loc) · 5.98 KB
/
ToolProgram.java
File metadata and controls
163 lines (148 loc) · 5.98 KB
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/*
* Copyright (c) 2024 Christian Stein
* Licensed under the Universal Permissive License v 1.0 -> https://opensource.org/license/upl
*/
package run.bach;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.spi.ToolProvider;
/**
* An implementation of the tool provider interface running operating system programs.
*
* @param name the name of this tool program
* @param command the list containing the executable program and its fixed arguments
* @param processBuilderTweaker the tweaker of the process builder instance created before starting
* @param processWaiter the waiter waits for the started process to finish and returns an exit value
* @see ToolProvider#name()
*/
public record ToolProgram(
String name,
List<String> command,
ProcessBuilderTweaker processBuilderTweaker,
ProcessWaiter processWaiter)
implements ToolProvider {
/**
* {@return an instance of a tool program launching a Java application via the {@code java} tool}
*
* <p>Example for using {@code java} with an array of fixed arguments:
*
* <pre>{@code
* // java[.exe] --limit-modules java.base --list-modules
* var base = ToolProgram.java("--limit-modules", "java.base");
* var tool = Tool.of(base);
* tool.run("--list-modules");
* }</pre>
*
* @param args zero or more fixed arguments
* @see <a href="https://docs.oracle.com/en/java/javase/22/docs/specs/man/java.html">The java
* Command</a>
*/
public static ToolProgram java(String... args) {
var name = "java";
var tool = findJavaDevelopmentKitTool(name, args);
if (tool.isPresent()) return tool.get();
throw new ToolNotFoundException("Tool not found for name: " + name);
}
/**
* {@return an instance of a JDK tool program for the given name, if found}
*
* @param name the name of the JDK tool program to look up
* @param args the fixed arguments
* @see <a href="https://docs.oracle.com/en/java/javase/22/docs/specs/man/">Java® Development Kit
* Version 22 Tool Specifications</a>
*/
public static Optional<ToolProgram> findJavaDevelopmentKitTool(String name, String... args) {
var bin = Path.of(System.getProperty("java.home", ""), "bin");
return findInFolder(name, bin, args);
}
/**
* {@return an operating system program for the given name in the specified folder, if found}
*
* @param name the name of the operating system program to lookup
* @param folder the directory to look up the name in
* @param args the fixed arguments
*/
public static Optional<ToolProgram> findInFolder(String name, Path folder, String... args) {
if (!Files.isDirectory(folder)) return Optional.empty();
var win = System.getProperty("os.name", "").toLowerCase().startsWith("win");
var file = name + (win && !name.endsWith(".exe") ? ".exe" : "");
try {
var path = folder.resolve(file);
return findExecutable(name, path, args);
} catch (InvalidPathException exception) {
return Optional.empty();
}
}
/**
* {@return an operating system program for the given file path, if it is executable}
*
* @param name the name of the operating system program to lookup
* @param file the file to look up the name in
* @param args the fixed arguments
* @see Files#isExecutable(Path)
*/
public static Optional<ToolProgram> findExecutable(String name, Path file, String... args) {
if (!Files.isExecutable(file)) return Optional.empty();
var command = new ArrayList<String>();
command.add(file.toString());
command.addAll(List.of(args));
return Optional.of(new ToolProgram(name, List.copyOf(command)));
}
public ToolProgram(String name, List<String> command) {
this(name, command, x -> x, Process::waitFor);
}
public ToolProgram withProcessBuilderTweaker(ProcessBuilderTweaker processBuilderTweaker) {
return new ToolProgram(name, command, processBuilderTweaker, processWaiter);
}
public ToolProgram withProcessWaiter(ProcessWaiter processWaiter) {
return new ToolProgram(name, command, processBuilderTweaker, processWaiter);
}
public Tool tool() {
var path = Path.of(command.getFirst()).normalize();
var namespace = path.getNameCount() == 0 ? "" : path.getParent().toString().replace('\\', '/');
var file = path.getFileName() == null ? "<null>" : path.getFileName().toString();
var name = file.endsWith(".exe") ? file.substring(0, file.length() - 4) : file;
var identifier = Tool.Identifier.of(namespace, name, null);
return Tool.of(identifier, this);
}
@Override
public int run(PrintWriter out, PrintWriter err, String... arguments) {
var processBuilder = new ProcessBuilder(new ArrayList<>(command));
processBuilder.command().addAll(List.of(arguments));
try {
var process = processBuilderTweaker.tweak(processBuilder).start();
var threadBuilder = Thread.ofVirtual();
threadBuilder.name(name + "-out").start(new LinePrinter(process.getInputStream(), out));
threadBuilder.name(name + "-err").start(new LinePrinter(process.getErrorStream(), err));
return process.isAlive() ? processWaiter().waitFor(process) : process.exitValue();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
return -1;
} catch (Exception exception) {
exception.printStackTrace(err);
return 1;
}
}
@FunctionalInterface
public interface ProcessBuilderTweaker {
ProcessBuilder tweak(ProcessBuilder builder);
}
@FunctionalInterface
public interface ProcessWaiter {
int waitFor(Process process) throws InterruptedException;
}
private record LinePrinter(InputStream stream, PrintWriter writer) implements Runnable {
@Override
public void run() {
new BufferedReader(new InputStreamReader(stream)).lines().forEach(writer::println);
}
}
}