Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Merge pull request #162 from DaveJarvis/1_typeset_using_podman Typeset using a container

AuthorDave Jarvis <email>
Date2023-01-03 16:48:34 GMT-0800
Commitfd5d7030d37502bbc35d5303df1a10f9b610e288
Parentbb4c75e
Delta573 lines added, 1 line removed, 572-line increase
src/test/java/com/keenwrite/processors/markdown/ImageLinkExtensionTest.java
return ProcessorContext
.builder()
- .with( ProcessorContext.Mutator::setInputPath, inputPath )
+ .with( ProcessorContext.Mutator::setSourcePath, inputPath )
.with( ProcessorContext.Mutator::setExportFormat, XHTML_TEX )
.with( ProcessorContext.Mutator::setCaret, () -> Caret.builder().build() )
src/test/java/com/keenwrite/io/UserDataDirTest.java
+/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.io;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.FileNotFoundException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class UserDataDirTest {
+ @Test
+ void test_Unix_GetAppDirectory_DirectoryExists()
+ throws FileNotFoundException {
+ final var path = UserDataDir.getAppPath( "test" );
+ final var file = path.toFile();
+
+ assertTrue( file.exists() );
+ assertTrue( file.delete() );
+ assertFalse( file.exists() );
+ }
+}
src/main/java/com/keenwrite/util/Time.java
+/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.util;
+
+import java.time.Duration;
+
+import static java.lang.String.format;
+import static java.util.concurrent.TimeUnit.*;
+
+/**
+ * Responsible for time-related functionality.
+ */
+public final class Time {
+ /**
+ * Converts an elapsed time to a human-readable format (hours, minutes,
+ * seconds, and milliseconds).
+ *
+ * @param duration An elapsed time.
+ * @return Human-readable elapsed time.
+ */
+ public static String toElapsedTime( final Duration duration ) {
+ final var elapsed = duration.toMillis();
+ final var hours = MILLISECONDS.toHours( elapsed );
+ final var eHours = elapsed - HOURS.toMillis( hours );
+ final var minutes = MILLISECONDS.toMinutes( eHours );
+ final var eMinutes = eHours - MINUTES.toMillis( minutes );
+ final var seconds = MILLISECONDS.toSeconds( eMinutes );
+ final var eSeconds = eMinutes - SECONDS.toMillis( seconds );
+ final var milliseconds = MILLISECONDS.toMillis( eSeconds );
+
+ return format( "%02d:%02d:%02d.%03d",
+ hours, minutes, seconds, milliseconds );
+ }
+}
src/main/java/com/keenwrite/typesetting/installer/panes/ManagerInitializationPane.java
+/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.typesetting.installer.panes;
+
+import com.keenwrite.typesetting.containerization.ContainerManager;
+
+/**
+ * Responsible for initializing the container manager on all platforms except
+ * for Linux.
+ */
+public final class ManagerInitializationPane extends ManagerOutputPane {
+
+ private static final String PREFIX =
+ "Wizard.typesetter.all.3.install.container";
+
+ public ManagerInitializationPane() {
+ super(
+ PREFIX + ".correct",
+ PREFIX + ".missing",
+ ContainerManager::start,
+ 35
+ );
+ }
+
+ @Override
+ public String getHeaderKey() {
+ return PREFIX + ".header";
+ }
+}
src/main/java/com/keenwrite/typesetting/installer/panes/ManagerOutputPane.java
+package com.keenwrite.typesetting.installer.panes;
+
+import com.keenwrite.io.CommandNotFoundException;
+import com.keenwrite.typesetting.containerization.ContainerManager;
+import com.keenwrite.typesetting.containerization.StreamProcessor;
+import javafx.concurrent.Task;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.BorderPane;
+import org.apache.commons.lang3.function.FailableBiConsumer;
+import org.controlsfx.dialog.Wizard;
+
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.io.StreamGobbler.gobble;
+
+/**
+ * Responsible for showing the output from running commands against a container
+ * manager. There are a few installation steps that run different commands
+ * against the installer, which are platform-specific and cannot be merged.
+ * Common functionality between them is codified in this class.
+ */
+public abstract class ManagerOutputPane extends InstallerPane {
+ private final String PROP_EXECUTOR = getClass().getCanonicalName();
+
+ private final String mCorrectKey;
+ private final String mMissingKey;
+ private final FailableBiConsumer
+ <ContainerManager, StreamProcessor, CommandNotFoundException> mFc;
+ private final ContainerManager mContainer;
+ private final TextArea mTextArea;
+
+ public ManagerOutputPane(
+ final String correctKey,
+ final String missingKey,
+ final FailableBiConsumer
+ <ContainerManager, StreamProcessor, CommandNotFoundException> fc,
+ final int cols
+ ) {
+ mFc = fc;
+ mCorrectKey = correctKey;
+ mMissingKey = missingKey;
+ mTextArea = textArea( 5, cols );
+ mContainer = createContainer();
+
+ final var borderPane = new BorderPane();
+ final var titledPane = titledPane( "Output", mTextArea );
+
+ borderPane.setBottom( titledPane );
+ setContent( borderPane );
+ }
+
+ @Override
+ public void onEnteringPage( final Wizard wizard ) {
+ disableNext( true );
+
+ try {
+ final var properties = wizard.getProperties();
+ final var thread = properties.get( PROP_EXECUTOR );
+
+ if( thread instanceof Thread executor && executor.isAlive() ) {
+ return;
+ }
+
+ final Task<Void> task = createTask( () -> {
+ mFc.accept(
+ mContainer,
+ input -> gobble( input, line -> append( mTextArea, line ) )
+ );
+ properties.remove( thread );
+ return null;
+ } );
+
+ task.setOnSucceeded( event -> {
+ append( mTextArea, get( mCorrectKey ) );
+ properties.remove( thread );
+ disableNext( false );
+ } );
+ task.setOnFailed( event -> append( mTextArea, get( mMissingKey ) ) );
+ task.setOnCancelled( event -> append( mTextArea, get( mMissingKey ) ) );
+
+ final var executor = createThread( task );
+ properties.put( PROP_EXECUTOR, executor );
+ executor.start();
+ } catch( final Exception e ) {
+ throw new RuntimeException( e );
+ }
+ }
+}
src/main/java/com/keenwrite/typesetting/installer/panes/TypesetterImageDownloadPane.java
+/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.typesetting.installer.panes;
+
+import static com.keenwrite.typesetting.containerization.Podman.CONTAINER_NAME;
+
+/**
+ * Responsible for installing the typesetter's image via the container manager.
+ */
+public final class TypesetterImageDownloadPane extends ManagerOutputPane {
+ private static final String PREFIX =
+ "Wizard.typesetter.all.4.download.image";
+
+ public TypesetterImageDownloadPane() {
+ super(
+ PREFIX + ".correct",
+ PREFIX + ".missing",
+ (container, processor) -> container.pull( processor, CONTAINER_NAME ),
+ 45
+ );
+ }
+
+ @Override
+ public String getHeaderKey() {
+ return PREFIX + ".header";
+ }
+}
src/main/java/com/keenwrite/typesetting/installer/panes/TypesetterThemesDownloadPane.java
+/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.typesetting.installer.panes;
+
+import com.keenwrite.io.UserDataDir;
+import com.keenwrite.io.Zip;
+import com.keenwrite.preferences.Workspace;
+import javafx.collections.ObservableMap;
+import org.controlsfx.dialog.Wizard;
+
+import java.io.File;
+import java.io.IOException;
+
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.events.StatusEvent.clue;
+import static com.keenwrite.preferences.AppKeys.KEY_TYPESET_CONTEXT_THEMES_PATH;
+
+/**
+ * Responsible for downloading themes into the application's data directory.
+ * The data directory differs between platforms, which is handled
+ * transparently by the {@link UserDataDir} class.
+ */
+public class TypesetterThemesDownloadPane extends AbstractDownloadPane {
+ private static final String PREFIX =
+ "Wizard.typesetter.all.5.download.themes";
+
+ private final Workspace mWorkspace;
+
+ public TypesetterThemesDownloadPane( final Workspace workspace ) {
+ assert workspace != null;
+ mWorkspace = workspace;
+ }
+
+ @Override
+ public void onEnteringPage( final Wizard wizard ) {
+ // Delete the target themes file to force re-download so that unzipping
+ // the file takes place. This side-steps checksum validation, which would
+ // be best implemented after downloading.
+ deleteTarget();
+ super.onEnteringPage( wizard );
+ }
+
+ @Override
+ protected void onDownloadSucceeded(
+ final String threadName, final ObservableMap<Object, Object> properties ) {
+ super.onDownloadSucceeded( threadName, properties );
+
+ try {
+ process( getTarget() );
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ private void process( final File target ) throws IOException {
+ Zip.extract( target.toPath() );
+
+ // Replace the default themes directory with the downloaded version.
+ final var root = Zip.root( target.toPath() ).toFile();
+
+ // Make sure the typesetter will know where to find the themes.
+ mWorkspace.fileProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ).set( root );
+ mWorkspace.save();
+
+ // The themes pack is no longer needed.
+ deleteTarget();
+ }
+
+ @Override
+ protected String getPrefix() {
+ return PREFIX;
+ }
+
+ @Override
+ protected String getChecksum() {
+ return get( "Wizard.typesetter.themes.checksum" );
+ }
+}
src/main/java/com/keenwrite/typesetting/installer/panes/UniversalManagerInstallPane.java
+/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.typesetting.installer.panes;
+
+/**
+ * Responsible for installing the container manager for any operating system
+ * that was not explicitly detected.
+ */
+public final class UniversalManagerInstallPane extends InstallerPane {
+ private static final String PREFIX =
+ "Wizard.typesetter.all.2.install.container";
+
+ public UniversalManagerInstallPane() { }
+
+ @Override
+ protected String getHeaderKey() {
+ return PREFIX + ".header";
+ }
+}
src/main/java/com/keenwrite/typesetting/installer/panes/UnixManagerInstallPane.java
+/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.typesetting.installer.panes;
+
+import com.keenwrite.ui.clipboard.Clipboard;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.ButtonBar;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import org.jetbrains.annotations.NotNull;
+
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.Messages.getInt;
+import static java.lang.String.format;
+import static org.apache.commons.lang3.SystemUtils.IS_OS_MAC;
+
+public final class UnixManagerInstallPane extends InstallerPane {
+ private static final String PREFIX =
+ "Wizard.typesetter.unix.2.install.container";
+
+ private final TextArea mCommands = textArea( 2, 40 );
+
+ public UnixManagerInstallPane() {
+ final var titledPane = titledPane( "Run", mCommands );
+ final var comboBox = createUnixOsCommandMap();
+ final var selection = comboBox.getSelectionModel();
+ selection
+ .selectedItemProperty()
+ .addListener( ( c, o, n ) -> mCommands.setText( n.command() ) );
+
+ // Auto-select if running on macOS.
+ if( IS_OS_MAC ) {
+ final var items = comboBox.getItems();
+
+ for( final var item : items ) {
+ if( "macOS".equalsIgnoreCase( item.name ) ) {
+ selection.select( item );
+ break;
+ }
+ }
+ }
+ else {
+ selection.select( 0 );
+ }
+
+ final var distro = label( PREFIX + ".os" );
+ distro.setText( distro.getText() + ":" );
+ distro.setPadding( new Insets( PAD / 2.0, PAD, 0, 0 ) );
+
+ final var hbox = new HBox();
+ hbox.getChildren().add( distro );
+ hbox.getChildren().add( comboBox );
+ hbox.setPadding( new Insets( 0, 0, PAD, 0 ) );
+
+ final var stepsPane = new VBox();
+ final var steps = stepsPane.getChildren();
+ steps.add( label( PREFIX + ".step.0" ) );
+ steps.add( spacer() );
+ steps.add( label( PREFIX + ".step.1" ) );
+ steps.add( label( PREFIX + ".step.2" ) );
+ steps.add( label( PREFIX + ".step.3" ) );
+ steps.add( label( PREFIX + ".step.4" ) );
+ steps.add( spacer() );
+
+ steps.add( flowPane(
+ label( PREFIX + ".details.prefix" ),
+ hyperlink( PREFIX + ".details.link" ),
+ label( PREFIX + ".details.suffix" )
+ ) );
+ steps.add( spacer() );
+
+ final var border = new BorderPane();
+ border.setTop( stepsPane );
+ border.setCenter( hbox );
+ border.setBottom( titledPane );
+
+ setContent( border );
+ }
+
+ @Override
+ public Node createButtonBar() {
+ final var node = super.createButtonBar();
+ final var layout = new BorderPane();
+ final var copyButton = button( PREFIX + ".copy.began" );
+
+ // Change the label to indicate clipboard is updated.
+ copyButton.setOnAction( event -> {
+ Clipboard.write( mCommands.getText() );
+ copyButton.setText( get( PREFIX + ".copy.ended" ) );
+ } );
+
+ if( node instanceof ButtonBar buttonBar ) {
+ copyButton.setMinWidth( buttonBar.getButtonMinWidth() );
+ }
+
+ layout.setPadding( new Insets( PAD, PAD, PAD, PAD ) );
+ layout.setLeft( copyButton );
+ layout.setRight( node );
+
+ return layout;
+ }
+
+ @Override
+ protected String getHeaderKey() {
+ return PREFIX + ".header";
+ }
+
+ private record UnixOsCommand( String name, String command )
+ implements Comparable<UnixOsCommand> {
+ @Override
+ public int compareTo(
+ final @NotNull UnixOsCommand other ) {
+ return toString().compareToIgnoreCase( other.toString() );
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ /**
+ * Creates a collection of *nix distributions mapped to instructions for users
+ * to run in a terminal.
+ *
+ * @return A map of *nix to instructions.
+ */
+ private static ComboBox<UnixOsCommand> createUnixOsCommandMap() {
+ new ComboBox<UnixOsCommand>();
+ final var comboBox = new ComboBox<UnixOsCommand>();
+ final var items = comboBox.getItems();
+ final var prefix = PREFIX + ".command";
+ final var distros = getInt( prefix + ".distros", 14 );
+
+ for( int i = 1; i <= distros; i++ ) {
+ final var suffix = format( ".%02d", i );
+ final var name = get( prefix + ".os.name" + suffix );
+ final var command = get( prefix + ".os.text" + suffix );
+
+ items.add( new UnixOsCommand( name, command ) );
+ }
+
+ items.sort( UnixOsCommand::compareTo );
+
+ return comboBox;
+ }
+}
src/main/java/com/keenwrite/typesetting/installer/panes/WindowsManagerDownloadPane.java
+/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.typesetting.installer.panes;
+
+import javafx.collections.ObservableMap;
+
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.typesetting.installer.panes.WindowsManagerInstallPane.WIN_BIN;
+
+/**
+ * Responsible for downloading the container manager software on Windows.
+ */
+public final class WindowsManagerDownloadPane extends AbstractDownloadPane {
+ private static final String PREFIX =
+ "Wizard.typesetter.win.2.download.container";
+
+ @Override
+ protected void updateProperties(
+ final ObservableMap<Object, Object> properties ) {
+ properties.put( WIN_BIN, getTarget() );
+ }
+
+ @Override
+ protected String getPrefix() {
+ return PREFIX;
+ }
+
+ @Override
+ protected String getChecksum() {
+ return get( "Wizard.typesetter.container.checksum" );
+ }
+}
src/main/java/com/keenwrite/typesetting/installer/panes/WindowsManagerInstallPane.java
+/* Copyright 2022 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.typesetting.installer.panes;
+
+import com.keenwrite.typesetting.containerization.ContainerManager;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import org.controlsfx.dialog.Wizard;
+
+import java.io.File;
+
+import static com.keenwrite.Messages.get;
+
+/**
+ * Responsible for installing the container manager on Windows.
+ */
+public final class WindowsManagerInstallPane extends InstallerPane {
+ /**
+ * Property for the installation thread to help with reentrancy.
+ */
+ private static final String WIN_INSTALLER = "windows.container.installer";
+
+ /**
+ * Shared property to track name of container manager binary file.
+ */
+ static final String WIN_BIN = "windows.container.binary";
+
+ private static final String PREFIX =
+ "Wizard.typesetter.win.2.install.container";
+
+ private final ContainerManager mContainer;
+ private final TextArea mCommands;
+
+ public WindowsManagerInstallPane() {
+ mCommands = textArea( 2, 55 );
+
+ final var titledPane = titledPane( "Output", mCommands );
+ append( mCommands, get( PREFIX + ".status.running" ) );
+
+ final var stepsPane = new VBox();
+ final var steps = stepsPane.getChildren();
+ steps.add( label( PREFIX + ".step.0" ) );
+ steps.add( spacer() );
+ steps.add( label( PREFIX + ".step.1" ) );
+ steps.add( label( PREFIX + ".step.2" ) );
+ steps.add( label( PREFIX + ".step.3" ) );
+ steps.add( spacer() );
+ steps.add( titledPane );
+
+ final var border = new BorderPane();
+ border.setTop( stepsPane );
+
+ mContainer = createContainer();
+ }
+
+ @Override
+ public void onEnteringPage( final Wizard wizard ) {
+ disableNext( true );
+
+ // Pull the fully qualified installer path from the properties.
+ final var properties = wizard.getProperties();
+ final var thread = properties.get( WIN_INSTALLER );
+
+ if( thread instanceof Thread installer && installer.isAlive() ) {
+ return;
+ }
+
+ final var binary = properties.get( WIN_BIN );
+ final var key = PREFIX + ".status";
+
+ if( binary instanceof File exe ) {
+ final var task = createTask( () -> {
+ final var exit = mContainer.install( exe );
+
+ // Remove the installer after installation is finished.
+ properties.remove( thread );
+
+ final var msg = exit == 0
+ ? get( key + ".success" )
+ : get( key + ".failure", exit );
+
+ append( mCommands, msg );
+ disableNext( exit != 0 );
+
+ return null;
+ } );
+
+ final var installer = createThread( task );
+ properties.put( WIN_INSTALLER, installer );
+ installer.start();
+ }
+ else {
+ append( mCommands, get( PREFIX + ".unknown", binary ) );
+ }
+ }
+
+ @Override
+ public String getHeaderKey() {
+ return PREFIX + ".header";
+ }
+}