made LockFile reentrant (multiple locks on the same file can be acquired in the same JVM instance)
CI / build (push) Has been cancelled

added argument file to JavaProcessBuilder
This commit is contained in:
2025-01-23 21:36:58 +08:00
parent d61cfc6ea7
commit cddd7889c7
8 changed files with 464 additions and 55 deletions
@@ -1,60 +1,167 @@
package net.woggioni.jwo;
import lombok.RequiredArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@RequiredArgsConstructor
@Getter
@Setter
public class JavaProcessBuilder {
private final Class<?> mainClass;
private static final Logger log = LoggerFactory.getLogger(JavaProcessBuilder.class);
private static final String PROCESS_BUILDER_PREFIX = "javaProcessBuilder";
/**
* Maximum number of characters to be used to create a command line
* (beyond which a Java argument file will be created instead), the actual limit is OS-specific,
* the current value is extremely conservative so that it is safe to use on most operating systems
*/
private static final int COMMAND_LINE_MAX_SIZE = 1024;
private static final String PATH_SEPARATOR = System.getProperty("path.separator");
@AllArgsConstructor
public static class JavaAgent {
Path jar;
String args;
}
private String mainClassName;
private Path executableJar;
private final String javaHome = System.getProperty("java.home");
private List<String> jvmArgs = new ArrayList<>();
private List<String> classpath = new ArrayList<>();
private String javaHome = System.getProperty("java.home");
private String classPath = System.getProperty("java.class.path");
private Properties properties = new Properties();
private String[] cliArgs = null;
public JavaProcessBuilder javaHome(String javaHome) {
this.javaHome = javaHome;
return this;
}
private List<String> cliArgs = new ArrayList<>();
public JavaProcessBuilder classPath(String classPath) {
this.classPath = classPath;
return this;
}
private List<JavaAgent> javaAgents = new ArrayList<>();
public JavaProcessBuilder properties(Properties properties) {
this.properties = properties;
return this;
}
public JavaProcessBuilder cliArgs(String ...cliArgs) {
this.cliArgs = cliArgs;
return this;
/**
* Generate the argument file string according to the grammar specified
* <a href="https://docs.oracle.com/en/java/javase/14/docs/specs/man/java.html#java-command-line-argument-files">here</a>
* @param strings list of command line arguments to be passed to the spawned JVM
* @return the Java argument file content as a string
*/
static String generateArgumentFileString(List<String> strings) {
StringBuilder sb = new StringBuilder();
int i = 0;
while(i < strings.size()) {
CharacterIterator it = new StringCharacterIterator(strings.get(i));
sb.append('"');
for (char c = it.first(); c != CharacterIterator.DONE; c = it.next()) {
switch (c) {
case '"':
sb.append("\\\"");
break;
case '\r':
sb.append("\\r");
break;
case '\n':
sb.append("\\n");
break;
case '\t':
sb.append("\\t");
break;
case '\\':
sb.append("\\\\");
break;
default:
sb.append(c);
break;
}
}
sb.append('"');
if(++i < strings.size()) {
sb.append(' ');
}
}
return sb.toString();
}
@SneakyThrows
public ProcessBuilder exec() {
public ProcessBuilder build() {
ArrayList<String> cmd = new ArrayList<>();
Path javaBin = Paths.get(javaHome, "bin", "java");
Stream<String> propertyStream = Optional.ofNullable(properties)
.map(p -> p.entrySet().stream())
.orElse(Stream.empty())
.map(entry -> String.format("-D%s=%s", entry.getKey(), entry.getValue()));
List<String> cmd = JWO.streamCat(
Stream.of(javaBin.toString(), "-cp", classPath),
propertyStream,
Stream.of(mainClass.getCanonicalName()),
Optional.ofNullable(cliArgs).map(Arrays::stream).orElse(Stream.empty()))
.collect(Collectors.toList());
return new ProcessBuilder(cmd);
cmd.add(javaBin.toString());
cmd.addAll(jvmArgs);
if(!classpath.isEmpty()) {
cmd.add("-cp");
cmd.add(String.join(PATH_SEPARATOR, classpath));
}
for(Map.Entry<Object, Object> entry : properties.entrySet()) {
cmd.add(String.format("-D%s=%s", entry.getKey(), entry.getValue()));
}
for(JavaAgent javaAgent : javaAgents) {
StringBuilder sb = new StringBuilder("-javaagent:").append(javaAgent.jar.toString());
String agentArguments = javaAgent.args;
if(agentArguments != null) {
sb.append('=');
sb.append(agentArguments);
}
cmd.add(sb.toString());
}
if(executableJar != null) {
cmd.add("-jar");
cmd.add(executableJar.toString());
} else if(mainClassName != null) {
cmd.add(mainClassName);
} else {
throw new IllegalArgumentException(
"Either a main class or the path to an executable jar file have to be specified");
}
cmd.addAll(cliArgs);
int cmdLength = 0;
for(String part : cmd) {
cmdLength += part.length();
}
//Add space between arguments
cmdLength += cmd.size() - 1;
if(log.isDebugEnabled()) {
log.debug("Spawning new process with command line: [{}]",
cmd.stream().map(s -> "\"" + s + "\"").collect(Collectors.joining(", ")));
}
int jvmVersion = Integer.parseInt(System.getProperty("java.vm.specification.version"));
if(jvmVersion < 9 /* Java versions 8 and earlier do not support argument files */ || cmdLength < COMMAND_LINE_MAX_SIZE || cmd.size() == 1) {
return new ProcessBuilder(cmd);
} else {
Path argumentFile = Files.createTempFile(PROCESS_BUILDER_PREFIX, ".arg");
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
Files.delete(argumentFile);
} catch (IOException ioe) {
ioe.printStackTrace();
}
}));
log.trace("Using Java argument file '{}'", argumentFile);
try(Writer writer = Files.newBufferedWriter(argumentFile)) {
writer.write(generateArgumentFileString(cmd.subList(1, cmd.size())));
}
return new ProcessBuilder(cmd.get(0), "@" + argumentFile);
}
}
}
+102 -16
View File
@@ -1,40 +1,126 @@
package net.woggioni.jwo;
import java.io.Closeable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockFile implements Closeable {
private final FileLock lock;
@RequiredArgsConstructor
public abstract class LockFile implements AutoCloseable {
@Getter
private static class LockFileMapValue {
private final ReadWriteLock threadLock;
private final AtomicInteger readerCount;
@Setter
private FileLock fileLock;
public LockFileMapValue(Path path) {
threadLock = new ReentrantReadWriteLock();
readerCount = new AtomicInteger(0);
fileLock = null;
}
}
private static Map<Path, LockFileMapValue> map = Collections.synchronizedMap(new HashMap<>());
private static FileChannel openFileChannel(Path path) throws IOException {
Files.createDirectories(path.getParent());
return FileChannel.open(path, EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE));
}
public static LockFile acquire(Path path, boolean shared) throws IOException {
FileChannel channel = openFileChannel(path);
return new LockFile(channel.lock(0L, Long.MAX_VALUE, shared));
private static LockFile acquireInternal(Path path, boolean shared, Boolean blocking) throws IOException {
LockFileMapValue lockFileMapValue = map.computeIfAbsent(path, LockFileMapValue::new);
if(shared) {
Lock lock = lockFileMapValue.getThreadLock().readLock();
if(blocking) {
lock.lock();
} else {
if(!lock.tryLock()) {
return null;
}
}
int readers = lockFileMapValue.getReaderCount().incrementAndGet();
if(readers == 1) {
FileChannel channel = openFileChannel(path);
FileLock fileLock;
if(blocking) {
fileLock = channel.lock(0L, Long.MAX_VALUE, true);
} else {
fileLock = channel.tryLock(0L, Long.MAX_VALUE, true);
if(fileLock == null) return null;
}
lockFileMapValue.setFileLock(fileLock);
}
return new LockFile() {
@Override
public void close() throws IOException {
int remainingReaders = lockFileMapValue.getReaderCount().decrementAndGet();
if(remainingReaders == 0) {
FileLock fileLock = lockFileMapValue.getFileLock();
fileLock.release();
fileLock.channel().close();
lockFileMapValue.setFileLock(null);
}
lock.unlock();
}
};
} else {
Lock lock = lockFileMapValue.getThreadLock().writeLock();
if(blocking) {
lock.lock();
} else {
if(!lock.tryLock()) {
return null;
}
}
FileLock fileLock;
FileChannel channel = openFileChannel(path);
if(blocking) {
fileLock = channel.lock(0L, Long.MAX_VALUE, false);
} else {
fileLock = channel.tryLock(0L, Long.MAX_VALUE, false);
if(fileLock == null) {
lock.unlock();
return null;
}
}
lockFileMapValue.setFileLock(fileLock);
final FileLock fl = fileLock;
return new LockFile() {
@Override
public void close() throws IOException {
fl.release();
fl.channel().close();
lockFileMapValue.setFileLock(null);
lock.unlock();
}
};
}
}
public static LockFile tryAcquire(Path path, boolean shared) throws IOException {
FileChannel channel = openFileChannel(path);
FileLock lock = channel.tryLock(0L, Long.MAX_VALUE, shared);
return (lock != null) ? new LockFile(lock) : null;
return acquireInternal(path, shared, false);
}
private LockFile(FileLock lock) {
this.lock = lock;
}
@Override
public void close() throws IOException {
lock.channel().close();
public static LockFile acquire(Path path, boolean shared) throws IOException {
return acquireInternal(path, shared, true);
}
}