org.conqat.lib.commons.io.DotNetUtils Maven / Gradle / Ivy
Show all versions of teamscale-lib-commons Show documentation
/*
* Copyright (c) CQSE GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.conqat.lib.commons.io;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.conqat.lib.commons.error.FormatException;
import org.conqat.lib.commons.string.StringUtils;
import org.conqat.lib.commons.system.SystemUtils;
import org.conqat.lib.commons.version.Version;
/**
* Utility class for .NET related stuff. Executes a .NET process and checks for a valid .NET runtime
* installation.
*/
public class DotNetUtils {
private static final Logger LOGGER = LogManager.getLogger(DotNetUtils.class);
/**
* Executable name of mono, used to execute .NET programs on non-windows machines.
*/
public static final String MONO_COMMAND = "mono";
/**
* Executable name of dotnet, used to execute .NET (>4, core) DLLs.
*
* See dotnet command online
* reference for details.
*/
private static final String DOTNET_COMMAND = "dotnet";
/** Regex for extracting the version of installed .NET runtimes. **/
private static final Pattern DOTNET_RUNTIME_VERSION_PATTERN = Pattern
.compile("Microsoft\\.NETCore\\.App\\s+?([0-9.]+)\\s+?\\[.+\\]");
/** The minimum .NET version that we require. **/
private static final Version DOTNET_MIN_VERSION = new Version(6, 0);
/**
* Creates a {@link ProcessBuilder} for executing a .net assembly. If the current OS is not Windows
* mono will be used for executing the assembly.
*/
public static ProcessBuilder createDotNetProcessBuilder(String... arguments) {
ProcessBuilder builder = new ProcessBuilder();
if (!SystemUtils.isWindows()) {
builder.command().add(MONO_COMMAND);
}
builder.command().addAll(Arrays.asList(arguments));
return builder;
}
/**
* Tries to execute the {@code dotnet} tool to determine if a suitable .NET runtime is installed. We
* assume that the newest available runtime version is the one used per default on the current
* system. If this runtime version is equal or newer to the minimal required version
* ({@link #DOTNET_MIN_VERSION}), this check succeeds.
*
* Error messages are generated and returned for the following cases:
*
* - The {@code dotnet} tool returned an error, indicating that something is wrong with the
* installation.
* - No .NET runtime could be detected.
* - The newest detected .NET runtime is too old.
* - The {@code dotnet} tool could not be executed. This can happen due a missing or invalid .NET
* installation or due to OS issues.
*
*
* @return {@code null} if the check succeeds without errors, else an error message string.
*/
public static @Nullable String checkDotnetIsInstalled() {
try {
ProcessUtils.ExecutionResult result = ProcessUtils
.execute(new String[] { DotNetUtils.DOTNET_COMMAND, "--list-runtimes" });
String errorMessage = performDotnetCheck(result.getReturnCode(), result.getStdout(), result.getStderr());
if (errorMessage != null) {
logDotNetInstallationDetails(result);
}
return errorMessage;
} catch (IOException e) {
logDotNetInstallationDetails(null);
return String.format(
"Error during the detection of the .NET runtime. `dotnet` could not be executed. "
+ "The .NET runtime version %s or higher has to be installed.\nFurther details: %s",
DOTNET_MIN_VERSION, e.getMessage());
}
}
private static void logDotNetInstallationDetails(ProcessUtils.ExecutionResult executionResult) {
LOGGER.error(".NET installation could not be detected or did not fulfill requirements. "
+ "This log will contain additional warning messages to simplify debugging.");
String path = System.getenv("PATH");
LOGGER.warn("Value of $PATH: " + path);
Optional fullPath = findUsedDotNetInstallation(path);
if (!fullPath.isPresent()) {
LOGGER.warn("Could not find " + DOTNET_COMMAND + " executable on the path!");
} else {
LOGGER.warn("First installation found on PATH: " + fullPath.get().getAbsolutePath());
LOGGER.warn("Can execute: " + fullPath.get().canExecute());
}
if (executionResult != null) {
LOGGER.warn("Execution returned error code " + executionResult.getReturnCode());
LOGGER.warn("Std out: " + executionResult.getStdout());
LOGGER.warn("Std err: " + executionResult.getStderr());
}
}
private static Optional findUsedDotNetInstallation(String path) {
for (String directory : path.split(File.pathSeparator)) {
File file = new File(directory, DOTNET_COMMAND);
if (file.isFile()) {
return Optional.of(file);
}
file = new File(directory, DOTNET_COMMAND + ".exe");
if (file.isFile()) {
return Optional.of(file);
}
}
return Optional.empty();
}
/**
* Performs the .NET version check given the return code, stdout, and stderr of a
* {@code dotnet --list-runtimes} invocation.
*
* Error messages are generated and returned for the following cases:
*
* - The {@code dotnet} tool returned an error, indicating that something is wrong with the
* installation.
* - No .NET runtime could be detected.
* - The newest detected .NET runtime is too old.
*
*
* @return {@code null} if everything is fine, else an error message string.
*/
/* package */ static @Nullable String performDotnetCheck(int returnCode, @NonNull String stdout,
@NonNull String stderr) {
if (returnCode != 0) {
return String.format("Error executing `dotnet` (%d). %s", returnCode,
generateGenericInstallErrorMessage(stdout, stderr));
}
List installedVersions = extractRuntimeVersions(stdout);
Optional highestVersion = installedVersions.stream().max(Comparator.naturalOrder());
if (!highestVersion.isPresent()) {
return String.format("No .NET runtime detected. %s", generateGenericInstallErrorMessage(stdout, stderr));
}
if (!highestVersion.get().isGreaterOrEqual(DOTNET_MIN_VERSION)) {
return String.format("Unsupported .NET runtime detected (latest: %s). %s", highestVersion.get(),
generateGenericInstallErrorMessage(stdout, stderr));
}
return null;
}
/**
* Generates the generic trailing part for the error messages of the .NET runtime version detection.
*/
private static @NonNull String generateGenericInstallErrorMessage(String stdout, String stderr) {
return String.format(
"Please make sure that version %s or higher of the .NET runtime is installed.\n"
+ "std out: %s\nstd error: %s",
DOTNET_MIN_VERSION, StringUtils.isEmpty(stdout) ? "-empty-" : stdout,
StringUtils.isEmpty(stderr) ? "-empty-" : stderr);
}
/**
* Executes a .NET program in a thread-safe way.
*/
public static ProcessUtils.ExecutionResult executeDotNet(String[] completeArguments) throws IOException {
return ProcessUtils.execute(createDotNetProcessBuilder(completeArguments));
}
/**
* Extracts all .NET runtime versions from the given raw output of a {@code dotnet --list-runtimes}
* invocation.
*/
/* package */static @NonNull List extractRuntimeVersions(@NonNull String rawVersionsList) {
return StringUtils.splitLinesAsList(rawVersionsList).stream().map(DotNetUtils::extractRuntimeVersionFromLine)
.filter(Objects::nonNull).collect(Collectors.toList());
}
/**
* Extracts the .NET runtime version from the given single raw output line of a
* {@code dotnet --list-runtimes} invocation using {@link #DOTNET_RUNTIME_VERSION_PATTERN}.
*/
/* package */ static @Nullable Version extractRuntimeVersionFromLine(@NonNull String line) {
Matcher matcher = DOTNET_RUNTIME_VERSION_PATTERN.matcher(line);
if (!matcher.find() || matcher.groupCount() != 1) {
return null;
}
try {
return Version.parseVersion(matcher.group(1));
} catch (FormatException e) {
return null;
}
}
}