Last active
June 16, 2023 19:50
-
-
Save AlexTMjugador/7049bdecfe94c893c457d78084e0dfd6 to your computer and use it in GitHub Desktop.
Toy example that shows how to integrate PackSquash in a Java application.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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