| package com.keenwrite.events; | ||
| -import com.keenwrite.AppCommands; | ||
| - | ||
| import java.util.List; | ||
| */ | ||
| public final class StatusEvent implements AppEvent { | ||
| - /** | ||
| - * Reference a class in the top-level package that doesn't depend on any | ||
| - * JavaFX APIs. | ||
| - */ | ||
| - private static final String PACKAGE_NAME = AppCommands.class.getPackageName(); | ||
| - | ||
| private static final String ENGLISHIFY = | ||
| "(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])"; | ||
| } | ||
| + /** | ||
| + * Constructs a new event that contains information about an unexpected issue. | ||
| + * | ||
| + * @param problem The issue encountered by the software, never {@code null}. | ||
| + */ | ||
| public StatusEvent( final Throwable problem ) { | ||
| - this( "", problem ); | ||
| + this( problem.getMessage(), problem ); | ||
| } | ||
| /** | ||
| * @param message The human-readable message text. | ||
| * @param problem May be {@code null} if no exception was thrown. | ||
| */ | ||
| public StatusEvent( final String message, final Throwable problem ) { | ||
| - assert message != null; | ||
| - mMessage = message; | ||
| + mMessage = message == null ? "" : message; | ||
| mProblem = problem; | ||
| } | ||
| stream( trace.getStackTrace() ) | ||
| .takeWhile( StatusEvent::filter ) | ||
| - .limit( 10 ) | ||
| + .limit( 15 ) | ||
| .toList() | ||
| .forEach( e -> sb.append( e.toString() ).append( NEWLINE ) ); | ||
| private static boolean filter( final StackTraceElement e ) { | ||
| final var clazz = e.getClassName(); | ||
| - return !(clazz.contains( PACKAGE_NAME ) || | ||
| - clazz.contains( "org.renjin." ) || | ||
| + return !(clazz.contains( "org.renjin." ) || | ||
| clazz.contains( "sun." ) || | ||
| clazz.contains( "flexmark." ) || | ||
| - clazz.contains( "java." )); | ||
| + clazz.contains( "java." ) | ||
| + ); | ||
| } | ||
| package com.keenwrite.io; | ||
| -import java.io.File; | ||
| import java.io.FileNotFoundException; | ||
| public CommandNotFoundException( final String command ) { | ||
| super( command ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Creates a new exception indicating that the given command could not be | ||
| - * found (or executed). | ||
| - * | ||
| - * @param file The binary file's command name that could not be run. | ||
| - */ | ||
| - public CommandNotFoundException( final File file ) { | ||
| - this( file.getAbsolutePath() ); | ||
| } | ||
| } | ||
| package com.keenwrite.io; | ||
| +import org.jetbrains.annotations.NotNull; | ||
| + | ||
| import java.io.File; | ||
| import java.io.FileInputStream; | ||
| import java.io.IOException; | ||
| import java.nio.file.Path; | ||
| import java.security.MessageDigest; | ||
| import java.security.NoSuchAlgorithmException; | ||
| +import java.util.ArrayList; | ||
| import java.util.Optional; | ||
| import java.util.function.Function; | ||
| -import java.util.regex.Pattern; | ||
| +import java.util.function.Predicate; | ||
| import static com.keenwrite.constants.Constants.USER_DIRECTORY; | ||
| +import static com.keenwrite.events.StatusEvent.clue; | ||
| +import static com.keenwrite.io.WindowsRegistry.pathsWindows; | ||
| import static com.keenwrite.util.DataTypeConverter.toHex; | ||
| import static java.lang.System.getenv; | ||
| import static java.nio.file.Files.isExecutable; | ||
| -import static java.util.regex.Pattern.compile; | ||
| import static java.util.regex.Pattern.quote; | ||
| import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS; | ||
| private static final String[] EXTENSIONS = new String[] | ||
| {"", ".exe", ".bat", ".cmd", ".msi", ".com"}; | ||
| + | ||
| + private static final String WHERE_COMMAND = | ||
| + IS_OS_WINDOWS ? "where" : "which"; | ||
| /** | ||
| * Number of bytes to read at a time when computing this file's checksum. | ||
| */ | ||
| private static final int BUFFER_SIZE = 16384; | ||
| - | ||
| - //@formatter:off | ||
| - private static final String SYS_KEY = | ||
| - "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; | ||
| - private static final String USR_KEY = | ||
| - "HKEY_CURRENT_USER\\Environment"; | ||
| - //@formatter:on | ||
| - | ||
| - /** | ||
| - * Regular expression pattern for matching %VARIABLE% names. | ||
| - */ | ||
| - private static final String VAR_REGEX = "%.*?%"; | ||
| - private static final Pattern VAR_PATTERN = compile( VAR_REGEX ); | ||
| - | ||
| - private static final String REG_REGEX = "\\s*path\\s+REG_EXPAND_SZ\\s+(.*)"; | ||
| - private static final Pattern REG_PATTERN = compile( REG_REGEX ); | ||
| /** | ||
| /** | ||
| - * Answers whether the path returned from {@link #locate()} is an executable | ||
| - * that can be run using a {@link ProcessBuilder}. | ||
| + * Answers whether an executable can be found that can be run using a | ||
| + * {@link ProcessBuilder}. | ||
| + * | ||
| + * @return {@code true} if the executable is runnable. | ||
| */ | ||
| public boolean canRun() { | ||
| * the fully qualified path, otherwise an empty result. | ||
| * | ||
| - * @param map The mapping function of registry variable names to values. | ||
| - * @return The fully qualified {@link Path} to the executable filename | ||
| - * provided at construction time. | ||
| + * @return Fully qualified path to the executable, if found. | ||
| */ | ||
| - public Optional<Path> locate( final Function<String, String> map ) { | ||
| + public Optional<Path> locate() { | ||
| + final var dirList = new ArrayList<String>(); | ||
| + final var paths = pathsSane(); | ||
| + int began = 0; | ||
| + int ended; | ||
| + | ||
| + while( (ended = paths.indexOf( pathSeparatorChar, began )) != -1 ) { | ||
| + final var dir = paths.substring( began, ended ); | ||
| + began = ended + 1; | ||
| + | ||
| + dirList.add( dir ); | ||
| + } | ||
| + | ||
| + final var dirs = dirList.toArray( new String[]{} ); | ||
| + var path = locate( dirs, "Wizard.container.executable.path" ); | ||
| + | ||
| + if( path.isEmpty() ) { | ||
| + clue(); | ||
| + | ||
| + try { | ||
| + path = where(); | ||
| + } catch( final IOException ex ) { | ||
| + clue( "Wizard.container.executable.which", ex ); | ||
| + } | ||
| + } | ||
| + | ||
| + return path.isPresent() | ||
| + ? path | ||
| + : locate( System::getenv, | ||
| + IS_OS_WINDOWS | ||
| + ? "Wizard.container.executable.registry" | ||
| + : "Wizard.container.executable.path" ); | ||
| + } | ||
| + | ||
| + private Optional<Path> locate( final String[] dirs, final String msg ) { | ||
| final var exe = getName(); | ||
| - final var paths = paths( map ).split( quote( pathSeparator ) ); | ||
| - for( final var path : paths ) { | ||
| - final var p = Path.of( path ).resolve( exe ); | ||
| + for( final var dir : dirs ) { | ||
| + final var p = Path.of( dir ).resolve( exe ); | ||
| for( final var extension : EXTENSIONS ) { | ||
| } | ||
| + clue( msg ); | ||
| return Optional.empty(); | ||
| } | ||
| - /** | ||
| - * Convenience method that locates a binary executable file in the path | ||
| - * by using {@link System#getenv(String)} to retrieve environment variables | ||
| - * that are expanded when parsing the PATH. | ||
| - * | ||
| - * @see #locate(Function) | ||
| - */ | ||
| - public Optional<Path> locate() { | ||
| - return locate( System::getenv ); | ||
| - } | ||
| + private Optional<Path> locate( | ||
| + final Function<String, String> map, final String msg ) { | ||
| + final var paths = paths( map ).split( quote( pathSeparator ) ); | ||
| - /** | ||
| - * Provides {@code null}-safe machinery to get a file name. | ||
| - * | ||
| - * @param p The path to the file name to retrieve (may be {@code null}). | ||
| - * @return The file name or the empty string if the path is not found. | ||
| - */ | ||
| - public static String getFileName( final Path p ) { | ||
| - return p == null ? "" : getPathFileName( p ); | ||
| + return locate( paths, msg ); | ||
| } | ||
| /** | ||
| - * If the path doesn't exist right before typesetting, switch the path | ||
| - * to the user's home directory to increase the odds of the typesetter | ||
| - * succeeding. This could help, for example, if the images directory was | ||
| - * deleted or moved. | ||
| + * Runs {@code where} or {@code which} to determine the fully qualified path | ||
| + * to an executable. | ||
| * | ||
| - * @param path The path to verify existence. | ||
| - * @return The given path, if it exists, otherwise the user's home directory. | ||
| + * @return The path to the executable for this file, if found. | ||
| + * @throws IOException Could not determine the location of the command. | ||
| */ | ||
| - public static Path normalize( final Path path ) { | ||
| - assert path != null; | ||
| - | ||
| - return path.toFile().exists() | ||
| - ? path | ||
| - : USER_DIRECTORY.toPath(); | ||
| - } | ||
| - | ||
| - private static String getPathFileName( final Path p ) { | ||
| - assert p != null; | ||
| - | ||
| - final var f = p.getFileName(); | ||
| + public Optional<Path> where() throws IOException { | ||
| + // The "where" command on Windows will automatically add the extension. | ||
| + final var args = new String[]{WHERE_COMMAND, getName()}; | ||
| + final var output = run( text -> true, args ); | ||
| + final var result = output.lines().findFirst(); | ||
| - return f == null ? "" : f.toString(); | ||
| + return result.map( Path::of ); | ||
| } | ||
| * @return The revised PATH variables as stored in the registry. | ||
| */ | ||
| - private String paths( final Function<String, String> map ) { | ||
| + private static String paths( final Function<String, String> map ) { | ||
| return IS_OS_WINDOWS ? pathsWindows( map ) : pathsSane(); | ||
| } | ||
| - private String pathsSane() { | ||
| - return getenv( "PATH" ); | ||
| - } | ||
| + /** | ||
| + * Answers whether this file's SHA-256 checksum equals the given | ||
| + * hexadecimal-encoded checksum string. | ||
| + * | ||
| + * @param hex The string to compare against the checksum for this file. | ||
| + * @return {@code true} if the checksums match; {@code false} on any | ||
| + * error or checksums don't match. | ||
| + */ | ||
| + public boolean isChecksum( final String hex ) { | ||
| + assert hex != null; | ||
| - @SuppressWarnings( "SpellCheckingInspection" ) | ||
| - private String pathsWindows( final Function<String, String> map ) { | ||
| try { | ||
| - final var hklm = query( SYS_KEY ); | ||
| - final var hkcu = query( USR_KEY ); | ||
| + return checksum( "SHA-256" ).equalsIgnoreCase( hex ); | ||
| + } catch( final Exception ex ) { | ||
| + return false; | ||
| + } | ||
| + } | ||
| - return expand( hklm, map ) + pathSeparator + expand( hkcu, map ); | ||
| - } catch( final IOException ex ) { | ||
| - // Return the PATH environment variable if the registry query fails. | ||
| - return pathsSane(); | ||
| + /** | ||
| + * Returns the hash code for this file. | ||
| + * | ||
| + * @return The hex-encoded hash code for the file contents. | ||
| + */ | ||
| + @SuppressWarnings( "SameParameterValue" ) | ||
| + private String checksum( final String algorithm ) | ||
| + throws NoSuchAlgorithmException, IOException { | ||
| + final var digest = MessageDigest.getInstance( algorithm ); | ||
| + | ||
| + try( final var in = new FileInputStream( this ) ) { | ||
| + final var bytes = new byte[ BUFFER_SIZE ]; | ||
| + int count; | ||
| + | ||
| + while( (count = in.read( bytes )) != -1 ) { | ||
| + digest.update( bytes, 0, count ); | ||
| + } | ||
| + | ||
| + return toHex( digest.digest() ); | ||
| } | ||
| } | ||
| /** | ||
| - * Queries a registry key PATH value. | ||
| + * Runs a command and collects standard output into a buffer. | ||
| * | ||
| - * @param key The registry key name to look up. | ||
| - * @return The value for the registry key. | ||
| + * @param filter Provides an injected test to determine whether the line | ||
| + * read from the command's standard output is to be added to | ||
| + * the result buffer. | ||
| + * @param args The command and its arguments to run. | ||
| + * @return The standard output from the command, filtered. | ||
| + * @throws IOException Could not run the command. | ||
| */ | ||
| - private String query( final String key ) throws IOException { | ||
| - final var regVarName = "path"; | ||
| - final var args = new String[]{"reg", "query", key, "/v", regVarName}; | ||
| + @NotNull | ||
| + public static String run( final Predicate<String> filter, | ||
| + final String[] args ) throws IOException { | ||
| final var process = Runtime.getRuntime().exec( args ); | ||
| final var stream = process.getInputStream(); | ||
| - final var regValue = new StringBuffer( 1024 ); | ||
| + final var stdout = new StringBuffer( 2048 ); | ||
| StreamGobbler.gobble( stream, text -> { | ||
| - if( text.contains( regVarName ) ) { | ||
| - regValue.append( parseRegEntry( text ) ); | ||
| + if( filter.test( text ) ) { | ||
| + stdout.append( WindowsRegistry.parseRegEntry( text ) ); | ||
| } | ||
| } ); | ||
| process.destroy(); | ||
| } | ||
| - | ||
| - return regValue.toString(); | ||
| - } | ||
| - | ||
| - String parseRegEntry( final String text ) { | ||
| - assert text != null; | ||
| - final var matcher = REG_PATTERN.matcher( text ); | ||
| - return matcher.find() ? matcher.group( 1 ) : text.trim(); | ||
| + return stdout.toString(); | ||
| } | ||
| /** | ||
| - * PATH environment variables returned from the registry have unexpanded | ||
| - * variables of the form %VARIABLE%. This method will expand those values, | ||
| - * if possible, from the environment. This will only perform a single | ||
| - * expansion, which should be adequate for most needs. | ||
| + * Provides {@code null}-safe machinery to get a file name. | ||
| * | ||
| - * @param s The %VARIABLE%-encoded value to expand. | ||
| - * @return The given value with all encoded values expanded. | ||
| + * @param p The path to the file name to retrieve (may be {@code null}). | ||
| + * @return The file name or the empty string if the path is not found. | ||
| */ | ||
| - String expand( final String s, final Function<String, String> map ) { | ||
| - // Assigned to the unexpanded string, initially. | ||
| - String expanded = s; | ||
| - | ||
| - final var matcher = VAR_PATTERN.matcher( expanded ); | ||
| - | ||
| - while( matcher.find() ) { | ||
| - final var match = matcher.group( 0 ); | ||
| - String value = map.apply( match ); | ||
| - | ||
| - if( value == null ) { | ||
| - value = ""; | ||
| - } | ||
| - else { | ||
| - value = value.replace( "\\", "\\\\" ); | ||
| - } | ||
| - | ||
| - final var subexpression = compile( quote( match ) ); | ||
| - expanded = subexpression.matcher( expanded ).replaceAll( value ); | ||
| - } | ||
| - | ||
| - return expanded; | ||
| + public static String getFileName( final Path p ) { | ||
| + return p == null ? "" : getPathFileName( p ); | ||
| } | ||
| /** | ||
| - * Answers whether this file's SHA-256 checksum equals the given | ||
| - * hexadecimal-encoded checksum string. | ||
| + * If the path doesn't exist right before typesetting, switch the path | ||
| + * to the user's home directory to increase the odds of the typesetter | ||
| + * succeeding. This could help, for example, if the images directory was | ||
| + * deleted or moved. | ||
| * | ||
| - * @param hex The string to compare against the checksum for this file. | ||
| - * @return {@code true} if the checksums match; {@code false} on any | ||
| - * error or checksums don't match. | ||
| + * @param path The path to verify existence. | ||
| + * @return The given path, if it exists, otherwise the user's home directory. | ||
| */ | ||
| - public boolean isChecksum( final String hex ) { | ||
| - assert hex != null; | ||
| + public static Path normalize( final Path path ) { | ||
| + assert path != null; | ||
| - try { | ||
| - return checksum( "SHA-256" ).equalsIgnoreCase( hex ); | ||
| - } catch( final Exception ex ) { | ||
| - return false; | ||
| - } | ||
| + return path.toFile().exists() ? path : USER_DIRECTORY.toPath(); | ||
| } | ||
| - /** | ||
| - * Returns the hash code for this file. | ||
| - * | ||
| - * @return The hex-encoded hash code for the file contents. | ||
| - */ | ||
| - @SuppressWarnings( "SameParameterValue" ) | ||
| - private String checksum( final String algorithm ) | ||
| - throws NoSuchAlgorithmException, IOException { | ||
| - final var digest = MessageDigest.getInstance( algorithm ); | ||
| + private static String pathsSane() { | ||
| + return getenv( "PATH" ); | ||
| + } | ||
| - try( final var in = new FileInputStream( this ) ) { | ||
| - final var bytes = new byte[ BUFFER_SIZE ]; | ||
| - int count; | ||
| + private static String getPathFileName( final Path p ) { | ||
| + assert p != null; | ||
| - while( (count = in.read( bytes )) != -1 ) { | ||
| - digest.update( bytes, 0, count ); | ||
| - } | ||
| + final var f = p.getFileName(); | ||
| - return toHex( digest.digest() ); | ||
| - } | ||
| + return f == null ? "" : f.toString(); | ||
| } | ||
| } | ||
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.io; | ||
| + | ||
| +import java.io.IOException; | ||
| +import java.util.function.Function; | ||
| +import java.util.regex.Pattern; | ||
| + | ||
| +import static java.io.File.pathSeparator; | ||
| +import static java.lang.System.getenv; | ||
| +import static java.util.regex.Pattern.compile; | ||
| +import static java.util.regex.Pattern.quote; | ||
| + | ||
| +/** | ||
| + * Responsible for obtaining Windows registry key values. | ||
| + */ | ||
| +public class WindowsRegistry { | ||
| + //@formatter:off | ||
| + private static final String SYS_KEY = | ||
| + "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; | ||
| + private static final String USR_KEY = | ||
| + "HKEY_CURRENT_USER\\Environment"; | ||
| + //@formatter:on | ||
| + | ||
| + /** | ||
| + * Regular expression pattern for matching %VARIABLE% names. | ||
| + */ | ||
| + private static final String VAR_REGEX = "%.*?%"; | ||
| + private static final Pattern VAR_PATTERN = compile( VAR_REGEX ); | ||
| + | ||
| + private static final String REG_REGEX = "\\s*path\\s+REG_EXPAND_SZ\\s+(.*)"; | ||
| + private static final Pattern REG_PATTERN = compile( REG_REGEX ); | ||
| + | ||
| + /** | ||
| + * Returns the value of the Windows PATH registry key. | ||
| + * | ||
| + * @return The PATH environment variable if the registry query fails. | ||
| + */ | ||
| + @SuppressWarnings( "SpellCheckingInspection" ) | ||
| + public static String pathsWindows( final Function<String, String> map ) { | ||
| + try { | ||
| + final var hklm = query( SYS_KEY ); | ||
| + final var hkcu = query( USR_KEY ); | ||
| + | ||
| + return expand( hklm, map ) + pathSeparator + expand( hkcu, map ); | ||
| + } catch( final IOException ex ) { | ||
| + return getenv( "PATH" ); | ||
| + } | ||
| + } | ||
| + | ||
| + /** | ||
| + * Queries a registry key PATH value. | ||
| + * | ||
| + * @param key The registry key name to look up. | ||
| + * @return The value for the registry key. | ||
| + */ | ||
| + private static String query( final String key ) throws IOException { | ||
| + final var registryVarName = "path"; | ||
| + final var args = new String[]{"reg", "query", key, "/v", registryVarName}; | ||
| + | ||
| + return SysFile.run( text -> text.contains( registryVarName ), args ); | ||
| + } | ||
| + | ||
| + static String parseRegEntry( final String text ) { | ||
| + assert text != null; | ||
| + | ||
| + final var matcher = REG_PATTERN.matcher( text ); | ||
| + return matcher.find() ? matcher.group( 1 ) : text.trim(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * PATH environment variables returned from the registry have unexpanded | ||
| + * variables of the form %VARIABLE%. This method will expand those values, | ||
| + * if possible, from the environment. This will only perform a single | ||
| + * expansion, which should be adequate for most needs. | ||
| + * | ||
| + * @param s The %VARIABLE%-encoded value to expand. | ||
| + * @return The given value with all encoded values expanded. | ||
| + */ | ||
| + static String expand( final String s, final Function<String, String> map ) { | ||
| + // Assigned to the unexpanded string, initially. | ||
| + String expanded = s; | ||
| + | ||
| + final var matcher = VAR_PATTERN.matcher( expanded ); | ||
| + | ||
| + while( matcher.find() ) { | ||
| + final var match = matcher.group( 0 ); | ||
| + String value = map.apply( match ); | ||
| + | ||
| + if( value == null ) { | ||
| + value = ""; | ||
| + } | ||
| + else { | ||
| + value = value.replace( "\\", "\\\\" ); | ||
| + } | ||
| + | ||
| + final var subexpression = compile( quote( match ) ); | ||
| + expanded = subexpression.matcher( expanded ).replaceAll( value ); | ||
| + } | ||
| + | ||
| + return expanded; | ||
| + } | ||
| +} | ||
| import java.util.concurrent.Callable; | ||
| +import static com.keenwrite.events.StatusEvent.clue; | ||
| import static com.keenwrite.io.StreamGobbler.gobble; | ||
| import static com.keenwrite.io.SysFile.normalize; | ||
| -import static com.keenwrite.typesetting.containerization.Podman.MANAGER; | ||
| import static java.lang.String.format; | ||
| */ | ||
| static boolean isReady() { | ||
| - if( MANAGER.canRun() ) { | ||
| + if( Podman.canRun() ) { | ||
| final var exitCode = new StringBuilder(); | ||
| final var manager = new Podman(); | ||
| // If the typesetter ran with an exit code of 0, it is available. | ||
| return exitCode.indexOf( "0" ) == 0; | ||
| - } catch( final CommandNotFoundException ignored ) { } | ||
| + } catch( final CommandNotFoundException ex ) { | ||
| + clue( ex ); | ||
| + } | ||
| } | ||
| import java.io.File; | ||
| -import java.io.IOException; | ||
| +import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.util.LinkedList; | ||
| import java.util.List; | ||
| -import java.util.NoSuchElementException; | ||
| import static com.keenwrite.Bootstrap.CONTAINER_VERSION; | ||
| import static com.keenwrite.events.StatusEvent.clue; | ||
| import static java.lang.String.format; | ||
| import static java.lang.String.join; | ||
| import static java.lang.System.arraycopy; | ||
| import static java.util.Arrays.copyOf; | ||
| +import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS; | ||
| /** | ||
| * Provides facilities for interacting with a container environment. | ||
| */ | ||
| public final class Podman implements ContainerManager { | ||
| - public static final SysFile MANAGER = new SysFile( "podman" ); | ||
| + private static final String BINARY = "podman"; | ||
| + private static final Path BINARY_PATH = | ||
| + Path.of( | ||
| + format( IS_OS_WINDOWS | ||
| + ? "C:\\Program Files\\RedHat\\Podman\\%s.exe" | ||
| + : "/usr/bin/%s", | ||
| + BINARY | ||
| + ) | ||
| + ); | ||
| + private static final SysFile MANAGER = new SysFile( BINARY ); | ||
| + | ||
| public static final String CONTAINER_SHORTNAME = "typesetter"; | ||
| public static final String CONTAINER_NAME = | ||
| format( "%s:%s", CONTAINER_SHORTNAME, CONTAINER_VERSION ); | ||
| private final List<String> mMountPoints = new LinkedList<>(); | ||
| public Podman() { } | ||
| + | ||
| + /** | ||
| + * Answers whether the container is installed and runnable on the host. | ||
| + * | ||
| + * @return {@code true} if the container is available. | ||
| + */ | ||
| + public static boolean canRun() { | ||
| + try { | ||
| + return getExecutable().toFile().isFile(); | ||
| + } catch( final Exception ex ) { | ||
| + clue( "Wizard.container.executable.run.error", ex ); | ||
| + | ||
| + // If the binary couldn't be found, then indicate that it cannot run. | ||
| + return false; | ||
| + } | ||
| + } | ||
| + | ||
| + private static Path getExecutable() { | ||
| + final var executable = Files.isExecutable( BINARY_PATH ); | ||
| + | ||
| + clue( "Wizard.container.executable.run.scan", BINARY_PATH, executable ); | ||
| + | ||
| + return executable | ||
| + ? BINARY_PATH | ||
| + : MANAGER.locate().orElseThrow(); | ||
| + } | ||
| @Override | ||
| throws CommandNotFoundException { | ||
| try { | ||
| - final var exe = MANAGER.locate(); | ||
| - final var path = exe.orElseThrow(); | ||
| - final var builder = processBuilder( path, args ); | ||
| - final var process = builder.start(); | ||
| + final var path = getExecutable(); | ||
| final var joined = join( ",", args ); | ||
| clue( "Wizard.container.process.enter", path, joined ); | ||
| + | ||
| + final var builder = processBuilder( path, args ); | ||
| + final var process = builder.start(); | ||
| processor.start( process.getInputStream() ); | ||
| return wait( process ); | ||
| - } catch( final NoSuchElementException | | ||
| - IOException | | ||
| - InterruptedException ex ) { | ||
| + } catch( final Exception ex ) { | ||
| + clue( ex ); | ||
| throw new CommandNotFoundException( MANAGER.toString() ); | ||
| } | ||
| -/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.typesetting.installer; | ||
| - | ||
| -/** | ||
| - * Provides common constants across all panes. | ||
| - */ | ||
| -public class WizardConstants { | ||
| - | ||
| - | ||
| - private WizardConstants() { } | ||
| -} | ||
| */ | ||
| private void file_export_pdf( final boolean dir ) { | ||
| - final var workspace = getWorkspace(); | ||
| - final var themes = workspace.getFile( | ||
| - KEY_TYPESET_CONTEXT_THEMES_PATH | ||
| - ); | ||
| - final var theme = workspace.stringProperty( | ||
| - KEY_TYPESET_CONTEXT_THEME_SELECTION | ||
| - ); | ||
| - final var chapters = workspace.stringProperty( | ||
| - KEY_TYPESET_CONTEXT_CHAPTERS | ||
| - ); | ||
| - final var settings = ExportSettings | ||
| - .builder() | ||
| - .with( ExportSettings.Mutator::setTheme, theme ) | ||
| - .with( ExportSettings.Mutator::setChapters, chapters ) | ||
| - .build(); | ||
| - | ||
| // Don't re-validate the typesetter installation each time. If the | ||
| // user mucks up the typesetter installation, it'll get caught the | ||
| // next time the application is started. Don't use |= because it | ||
| // won't short-circuit. | ||
| mCanTypeset = mCanTypeset || Typesetter.canRun(); | ||
| if( mCanTypeset ) { | ||
| + final var workspace = getWorkspace(); | ||
| + final var theme = workspace.stringProperty( | ||
| + KEY_TYPESET_CONTEXT_THEME_SELECTION | ||
| + ); | ||
| + final var chapters = workspace.stringProperty( | ||
| + KEY_TYPESET_CONTEXT_CHAPTERS | ||
| + ); | ||
| + | ||
| + final var settings = ExportSettings | ||
| + .builder() | ||
| + .with( ExportSettings.Mutator::setTheme, theme ) | ||
| + .with( ExportSettings.Mutator::setChapters, chapters ) | ||
| + .build(); | ||
| + | ||
| + final var themes = workspace.getFile( | ||
| + KEY_TYPESET_CONTEXT_THEMES_PATH | ||
| + ); | ||
| + | ||
| // If the typesetter is installed, allow the user to select a theme. If | ||
| // the themes aren't installed, a status message will appear. |
| Wizard.typesetter.themes.checksum=983b8d3c4051c6c002b8111ea786ebc83d5496ab5f8d734e96413c59304b2a75 | ||
| -Wizard.container.install.command=Running ''{0}'' | ||
| +Wizard.container.install.command=Installing container using: ''{0}'' | ||
| Wizard.container.install.await=Waiting for installer to finish | ||
| Wizard.container.install.download.started=Download ''{0}'' started | ||
| Wizard.container.install.download.running=Download in progress, please wait | ||
| -Wizard.container.process.enter=Running ''{0}'' ''{1]'' | ||
| -Wizard.container.process.exit=Exit code: {0} | ||
| +Wizard.container.process.enter=Running ''{0}'' ''{1}'' | ||
| +Wizard.container.process.exit=Process exit code (zero means success): {0} | ||
| +Wizard.container.executable.run.scan=''{0}'' is executable: {1} | ||
| +Wizard.container.executable.run.error=Cannot run container | ||
| +Wizard.container.executable.which=Cannot find container using search command | ||
| +Wizard.container.executable.path=Cannot find container using PATH variable | ||
| +Wizard.container.executable.registry=Cannot find container using registry | ||
| # STEP 1: Introduction panel (all) |
| import org.junit.jupiter.api.Test; | ||
| -import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| -import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| +import java.io.IOException; | ||
| +import java.nio.file.Path; | ||
| +import java.util.Optional; | ||
| +import java.util.concurrent.atomic.AtomicBoolean; | ||
| +import java.util.function.Function; | ||
| + | ||
| +import static org.junit.jupiter.api.Assertions.*; | ||
| class SysFileTest { | ||
| - private static final String REG_PATH_PREFIX = | ||
| - "%USERPROFILE%"; | ||
| - private static final String REG_PATH_SUFFIX = | ||
| - "\\AppData\\Local\\Microsoft\\WindowsApps;"; | ||
| - private static final String REG_PATH = REG_PATH_PREFIX + REG_PATH_SUFFIX; | ||
| @Test | ||
| void test_Locate_ExistingExecutable_PathFound() { | ||
| - final var command = "ls"; | ||
| - final var file = new SysFile( command ); | ||
| - assertTrue( file.canRun() ); | ||
| - | ||
| - final var located = file.locate(); | ||
| - assertTrue( located.isPresent() ); | ||
| - | ||
| - final var path = located.get(); | ||
| - final var actual = path.toAbsolutePath().toString(); | ||
| - final var expected = "/usr/bin/" + command; | ||
| - | ||
| - assertEquals( expected, actual ); | ||
| + testFunction( SysFile::locate, "ls", "/usr/bin/ls" ); | ||
| } | ||
| @Test | ||
| - void test_Parse_RegistryEntry_ValueObtained() { | ||
| - final var file = new SysFile( "unused" ); | ||
| - final var expected = REG_PATH; | ||
| - final var actual = | ||
| - file.parseRegEntry( " path REG_EXPAND_SZ " + expected ); | ||
| - | ||
| - assertEquals( expected, actual ); | ||
| + void test_Where_ExistingExecutable_PathFound() { | ||
| + testFunction( sysFile -> { | ||
| + try { | ||
| + return sysFile.where(); | ||
| + } catch( final IOException e ) { | ||
| + throw new RuntimeException( e ); | ||
| + } | ||
| + }, "which", "/usr/bin/which" ); | ||
| } | ||
| - @Test | ||
| - void test_Expand_RegistryEntry_VariablesExpanded() { | ||
| - final var value = "UserProfile"; | ||
| - final var file = new SysFile( "unused" ); | ||
| - final var expected = value + REG_PATH_SUFFIX; | ||
| - final var actual = file.expand( REG_PATH, s -> value ); | ||
| + void testFunction( final Function<SysFile, Optional<Path>> consumer, | ||
| + final String command, | ||
| + final String expected ) { | ||
| + final var file = new SysFile( command ); | ||
| + final var path = consumer.apply( file ); | ||
| + final var failed = new AtomicBoolean( false ); | ||
| - assertEquals( expected, actual ); | ||
| + assertTrue( file.canRun() ); | ||
| + | ||
| + path.ifPresentOrElse( | ||
| + location -> { | ||
| + final var actual = location.toAbsolutePath().toString(); | ||
| + | ||
| + assertEquals( expected, actual ); | ||
| + }, | ||
| + () -> failed.set( true ) | ||
| + ); | ||
| + | ||
| + assertFalse( failed.get() ); | ||
| } | ||
| } |
| +package com.keenwrite.io; | ||
| + | ||
| +import org.junit.jupiter.api.Test; | ||
| + | ||
| +import static com.keenwrite.io.WindowsRegistry.*; | ||
| +import static org.junit.jupiter.api.Assertions.*; | ||
| + | ||
| +class WindowsRegistryTest { | ||
| + private static final String REG_PATH_PREFIX = | ||
| + "%USERPROFILE%"; | ||
| + private static final String REG_PATH_SUFFIX = | ||
| + "\\AppData\\Local\\Microsoft\\WindowsApps;"; | ||
| + private static final String REG_PATH = REG_PATH_PREFIX + REG_PATH_SUFFIX; | ||
| + | ||
| + @Test | ||
| + void test_Parse_RegistryEntry_ValueObtained() { | ||
| + final var expected = REG_PATH; | ||
| + final var actual = parseRegEntry( | ||
| + " path REG_EXPAND_SZ " + expected | ||
| + ); | ||
| + | ||
| + assertEquals( expected, actual ); | ||
| + } | ||
| + | ||
| + @Test | ||
| + void test_Expand_RegistryEntry_VariablesExpanded() { | ||
| + final var value = "UserProfile"; | ||
| + final var expected = value + REG_PATH_SUFFIX; | ||
| + final var actual = expand( REG_PATH, s -> value ); | ||
| + | ||
| + assertEquals( expected, actual ); | ||
| + } | ||
| +} | ||
| Author | DaveJarvis <email> |
|---|---|
| Date | 2023-07-19 22:16:30 GMT-0700 |
| Commit | 60426e928c131004eeee2998e4b5b27f390efc57 |
| Parent | 64cb1c8 |
| Delta | 392 lines added, 252 lines removed, 140-line increase |