-1

I have the following function in my code.

public static void runExecutable(String filePath, String command, boolean commandLine, boolean wait) {
    
    InputStream inputStream = CommonMethods.class.getClassLoader().getResourceAsStream(filePath);
    String tempFileSuffix = "";
    switch (CommonMethods.getOperatingSystem()) {
        case "Windows":
            tempFileSuffix = ".exe";
            break;
        case "Linux":
        case "Mac":
            tempFileSuffix = ".bin";
            break;
    }
    
    
    
    File tempFile;
    try {
        tempFile = File.createTempFile("AutoFPCurator", tempFileSuffix);
    } catch (IOException e) {
        new ErrorDialog(e);
        return;
    }

    
    try (FileOutputStream outputStream = new FileOutputStream(tempFile)) {
        byte[] buffer = new byte[1024];
        int length;
        while ((length = inputStream.read(buffer)) > 0) {
            outputStream.write(buffer, 0, length);
        }
        outputStream.close();
    } catch (IOException e) {
        new ErrorDialog(e);
        return;
    }
    
    
    
    
    String cmdProgram = "";
    String cFlag = "-c";
    switch (CommonMethods.getOperatingSystem()) {
        case "Windows": 
            cmdProgram = "cmd.exe";
            cFlag = "/c";
            break;
        case "Mac":
            cmdProgram = "sh";
            break;
        case "Linux":
            cmdProgram = "bash";
            break;
        default:
            return;
    }
    
    ProcessBuilder processBuilder = !commandLine ? new ProcessBuilder(tempFile.getAbsolutePath(), command)
                            : new ProcessBuilder(cmdProgram, cFlag, tempFile.getAbsolutePath(), command);
    processBuilder.redirectErrorStream(true);
    
    
    Process process;
    try {
        process = processBuilder.start();
    } catch (IOException e) {
        new ErrorDialog(e);
        return;
    }
    if (!wait) {
        return;
    }
    
    
    try {
        process.waitFor();
    } catch (InterruptedException e) {
        new ErrorDialog(e);
    }
    
    
}

This code is designed to run executables from src/main/resources/, as it specifies, however it's especially used to compress folders into 7z files (using standalone 7z executables in its src/main/resources directory. I use executables because 7Z compression algorithms made for Java (such as Apache Commons's), doesn't compress it the way I need it to be compressed for my purposes, unlike the executable. I downloaded the Linux 7z executable, extracted it, and used 7zz in my program). When I call it like so:

switch (CommonMethods.getOperatingSystem()) {
         case "Windows":
             exeString = "programs/7zip/7za.exe";
             break;
         case "Mac":
             exeString = "programs/7zip/7zzmac";
             break;
         case "Linux":
             exeString = "programs/7zip/7zznux";
             break;
         default:
             System.out.println(commonStrs.get("unsupportedOS"));
             return;
    }
    CommonMethods.runExecutable(exeString, args, true, true);

It works on Windows, but I tried it on Linux and it didn't work. I've been trying to pinpoint the problem. The arguments are valid and it does appear to be running, but it just doesn't compress the folder. I ran the raw argument that the function builds myself into Linux Terminal and it said "Permission denied" in response to my attempt to access to the tmp folder (which is where the temporary 7zip executable is stored). I tried storing it in an alternate location that is perfectly accessible to no avail.

TL;DR, how do I get this working in Linux (and Mac by extension maybe?)? Just to be clear, this works on Windows.

ThePiGuy
  • 11
  • 5
  • *"using standalone 7z executables in its src/main/resources directory"* - This isn't going to work (as you expect), as `src` is not accessible when the app is exported/bundled, you won't be able to "run" those commands, as they will be embedded inside a zip file (ie the jar file). You may need someway to extract them at runtime and then you may hit a wall over execute permissions, some OS's may not allow you to execute the extract executables (depending on where they are extracted to etc) – MadProgrammer May 25 '23 at 23:49
  • @MadProgrammer What the function does is it *copies the file* into a temporary folder so that it can run it. Did you read the function? I said this works on Windows. – ThePiGuy May 25 '23 at 23:51
  • And as I also said, some OS's may not allow you to execute the extracted executables. And just because it "runs on platform X" doesn't guarantee that it will run on some other platform. Are you aware that [Apache Commons Compress](https://commons.apache.org/proper/commons-compress/index.html) supports 7Z - so you wouldn't need to do all this mucking about with executables? – MadProgrammer May 25 '23 at 23:54
  • @MadProgrammer I should've specified this in my question, but I can't use Apache Common's 7Z compression algorithm. My program needs to compress folders in a very specific way, and Apache Common's way doesnt do it like I need it to. I realize that just because it runs on Windows doesnt mean itll work on other platforms. That's why I tested this program on other platforms in the first place. – ThePiGuy May 25 '23 at 23:56
  • In my first test (on MacOS), I'm able to execute the `7z` binary directly, no need for a `sh` pass through - this was executing the binary from it's installed location - will look at the extraction process, but it may be that the execute attribute is not been set – MadProgrammer May 26 '23 at 00:10
  • @MadProgrammer Can you elaborate further? Also, you're doing this programatically, correct? – ThePiGuy May 26 '23 at 00:17
  • First, all I did was downloaded the `7zz` binary for MacOS, I then wrote a quick Java program to compress a directory. On my second test (on MacOS), extracting the `7zz` binary from the Jar context results in a file which is not executable (`File#canExecute` is `false`) – MadProgrammer May 26 '23 at 00:18
  • On my third test, and based on [How do I programmatically change file permissions?](https://stackoverflow.com/questions/664432/how-do-i-programmatically-change-file-permissions), I modified the temporary files execution attribute as was able to successfully execute the extracted file – MadProgrammer May 26 '23 at 00:21

1 Answers1

1

On macOS (Ventura 13.4), I downloaded the executable for 7zz. I then wrote a quick program to verify that I could run the executable directly via ProcessBuilder (no need for a sh). This was successful.

I then added 7zz to my applications resources and proceeded to extract it to a temp file. This failed as the file was not executable (File#canExecute was false).

Then, based on How do I programmatically change file permissions?, I modified the execution attribute of the temp file (after it was extracted) and this was successful

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Main {
    public static void main(String[] args) {
        try {
            new Main();
        } catch (IOException ex) {
            Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
        } catch (InterruptedException ex) {
            Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public Main() throws IOException, InterruptedException {
        File binary = extractBinary();
        System.out.println(binary);
        System.out.println("Executable: " + binary.canExecute());
        // This could be moved to extractBinary
        // But I wanted to demonstrate the difference between
        // the two binary.canExecute() statements
        Files.setPosixFilePermissions(binary.toPath(), PosixFilePermissions.fromString("rwxr-x---"));
        System.out.println("Executable: " + binary.canExecute());

        ProcessBuilder pb = new ProcessBuilder(
                binary.getAbsolutePath(), 
                "a", 
                "/Users/shane.whitehead/Downloads/Test.7z", 
                "/Users/shane.whitehead/Downloads/7z2300-mac.tar/*"
        );
        pb.redirectErrorStream(true);
        Process p = pb.start();
        InputStreamConsumer consumer = new InputStreamConsumer(p.getInputStream());
        consumer.start();

        p.waitFor();

        consumer.join();
        System.out.println(consumer.getOutput());
    }

    protected File extractBinary() throws IOException {
        File tempFile = File.createTempFile("7zBinary", ".bin");
        try (FileOutputStream fos = new FileOutputStream(tempFile); InputStream is = getClass().getResourceAsStream("/bin/7zz")) {
            is.transferTo(fos);
        }
        return tempFile;
    }

    // This is just here to make my life easier
    public class InputStreamConsumer extends Thread {

        private InputStream is;
        private IOException exp;
        private StringBuilder output;

        public InputStreamConsumer(InputStream is) {
            this.is = is;
        }

        @Override
        public void run() {
            int in = -1;
            output = new StringBuilder(64);
            try {
                while ((in = is.read()) != -1) {
                    output.append((char) in);
                }
            } catch (IOException ex) {
                ex.printStackTrace();
                exp = ex;
            }
        }

        public StringBuilder getOutput() {
            return output;
        }

        public IOException getException() {
            return exp;
        }
    }
}

You might find it easier to first find a working solution on each platform and then design your solution around those requirements, maybe having a factory which generated the ProcessBuilder based on the platform, as an idea.

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366