Skip to content

Instantly share code, notes, and snippets.

@AlexTMjugador
Last active June 16, 2023 19:50
Show Gist options
  • Save AlexTMjugador/7049bdecfe94c893c457d78084e0dfd6 to your computer and use it in GitHub Desktop.
Save AlexTMjugador/7049bdecfe94c893c457d78084e0dfd6 to your computer and use it in GitHub Desktop.
Toy example that shows how to integrate PackSquash in a Java application.
package io.github.alextmjugador;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ProcessBuilder.Redirect;
import java.nio.charset.StandardCharsets;
/**
* Toy example that shows how to integrate PackSquash in a Java application.
*
* @author AlexTMjugador
*/
public final class PackSquashIntegrationExample {
public static void main(String[] args) {
// Constants that point to interesting paths. These are example absolute Unix
// paths that you should change
final String packSquashExecutablePath = "/opt/PackSquash/packsquash";
final String packFolderPath = "/tmp/pack";
final String partialPackSquashSettingsPath = "/packsquash.toml";
final ProcessBuilder processBuilder = new ProcessBuilder(packSquashExecutablePath);
// Set the working directory of the to-be-started process to the path of the
// pack directory. This allows PackSquash to interpret relative paths in its
// settings to this directory, so you can always append this to the settings
// file and it will work, without bothering to escape strange characters that
// there may be in a path:
// pack_directory = "."
processBuilder.directory(new File(packFolderPath));
// This is not needed, because the initial input of a process builder is a pipe,
// but make sure it really is!
processBuilder.redirectInput(Redirect.PIPE);
// You may customize where the output and error streams write to in order to
// handle the status messages printed by PackSquash as you need. For this
// proof of concept, I will just print back the output stream to System.out
// in a kind of roundabout way (it could be done more easily with Redirect.INHERIT).
// You can print these messages to a nice GUI text box or something like that
processBuilder.redirectOutput(Redirect.PIPE); // Not needed, the initial value is this
// This redirects stderr to stdout (i.e. merges the streams in one).
// You may or may not want to do this, depending on how you want to handle errors,
// and whether you want, for instance, to show them in a message box when the process finishes
processBuilder.redirectErrorStream(true);
// Open the settings file now to be ready to read it later, after starting the process.
// If this fails we bail out before starting the process, so we don't have to kill it
// later and don't waste OS resources.
// Also note that we don't validate anything about the settings file. We just treat it
// as a blob and then let PackSquash complain if it is malformed, which is normally a
// good thing, because PackSquash is the "information expert" here
// (https://en.wikipedia.org/wiki/GRASP_(object-oriented_design)#Information_expert)
final InputStream partialSettingsStream;
try {
partialSettingsStream = new FileInputStream(partialPackSquashSettingsPath);
} catch (final Exception exc) {
System.err.println("- An error occurred while opening the settings file for reading:");
exc.printStackTrace();
// We bail out of this main method. A real application may do another thing
return;
}
try {
// Now that we can read the partial settings file (i.e. without the pack directory) we
// can start the process. It inherits the environment variables available to the parent
// process. PackSquash does not use them currently, but if it were to use them, they would
// take priority, which is a good thing if the end user wants to enforce some thing no matter
// what
final Process packSquashProcess;
try {
packSquashProcess = processBuilder.start();
} catch (final Exception exc) {
System.err.println("- An error occurred while starting the PackSquash process:");
exc.printStackTrace();
// We bail out of this main method. A real application may do another thing
return;
}
// Get a output stream that writes to the process input stream.
// We don't need to buffer it, because we will ever only do two write calls for it, and I think that
// buffering here introduces more overhead than it avoids
final OutputStream packSquashInputStream = packSquashProcess.getOutputStream();
try {
// Because the working directory is the pack directory, we can write a constant
// pack_directory setting here that points to the current directory.
// Note that we do this first because, if we append this at the end, this key may end up
// inside a table or something
packSquashInputStream.write(
("pack_directory = \".\"" + System.lineSeparator()).getBytes(StandardCharsets.UTF_8)
);
// IMPORTANT: you may also want to customize output_file_path so that PackSquash writes the
// result ZIP file somewhere else
// This method is available from Java 9 onwards. I don't know if other JVM languages provide
// a similar method. It can be implemented easily in Java 8 and those other languages if
// needed. Default OpenJDK implementation:
// https://hg.openjdk.java.net/jdk/jdk/file/ee1d592a9f53/src/java.base/share/classes/java/io/InputStream.java#l771
partialSettingsStream.transferTo(packSquashInputStream);
} catch (final IOException exc) {
// This shouldn't happen unless the process gets killed or something is going wrong
// with the OS environment
System.err.println("- An error occurred while communicating with the PackSquash process:");
exc.printStackTrace();
} finally {
// Close the standard input stream of the process when we are done writing the settings.
// This flushes the stream (so every byte is written) and signals EOF to the process,
// so it knows that it has received all the settings and will start doing the work
try {
packSquashInputStream.close();
} catch (final IOException ignored) {
// Best effort done
}
}
// After closing its standard input stream, PackSquash will start doing its thing.
// Let's copy its output to System.out as an example. In a real application you could
// copy that output to GUI component, using another thread if needed to not block the
// user input.
// Usually, when the process finishes, its output streams are closed, so EOF happens
// in the input stream. The docs don't guarantee that, however!
System.out.println("- PackSquash process output:");
final InputStream packSquashOutputStream = new BufferedInputStream(packSquashProcess.getInputStream());
try {
int outputByte;
while ((outputByte = packSquashOutputStream.read()) != -1) {
System.out.write(outputByte);
}
} catch (final IOException exc) {
// The pipe is broken. This may happen if the process gets killed or finishes without
// signaling EOF in its output stream.
// Even if that happens, it could be treated as a normal EOF, but here we print
// the exception anyway
System.err.println("- An error occurred while reading the PackSquash process output:");
exc.printStackTrace();
} finally {
// It is always good practice to cleanup I/O resources you no longer use anyway.
// PackSquash will not try to write any more output when we get here
try {
packSquashOutputStream.close();
} catch (final IOException ignored) {
// Best effort done
}
}
// When we get here, the PackSquash process should be dead, as we either reached EOF
// or the pipe was broken. However, if the second thing occurred, it may happen in an
// insane OS environment that the process is still running.
// As we assume that PackSquash will end its execution someday, just use waitFor()
// to wait for it to finish even in the unlikely case that it didn't, and save us
// from handling the exceptions thrown by the exitValue method.
// If you're not interested in handling PackSquash output, just use waitFor like this
try {
System.out.println("- PackSquash finished with code " + packSquashProcess.waitFor());
} catch (final InterruptedException exc) {
System.out.println("- Thread interrupted while waiting for PackSquash to finish!");
exc.printStackTrace();
}
} finally {
// Always try to close the TOML settings file!
try {
partialSettingsStream.close();
} catch (final IOException ignored) {
// Best effort done
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment