5

I've been browsing SO and google for a while now for an answer to this question, but I can't seem to find one that really works. I'll start from the beginning:

I created a Java class with a method that runs a batch file in the background (the command window does not appear). The program works great, except that it would be a little confusing to the end user, since the batch file takes a while to complete--the user will not know if the program is still running or not. After the batch script finishes executing, a message dialog appears saying it's finished, but for the period of time between when the method is run and the dialog appears, it looks as if the program is doing nothing.

So here's my question: I would very much like to display a new frame with a text area that shows the output of the batch file. However, I understand that this is very difficult to do without creating temporary files, writing to them, reading from them, and so on. I would rather avoid that if possible. Therefore, I have decided it might be better to display an indeterminate JProgressBar while the process is running, and close it when the process is finished. Unfortunately, I don't think Swing can handle this since it would require running multiple processes at once. I have heard of a SwingWorker but am not exactly sure how that would be used in this case. I have the following SSCCE, which works, but does not have the progress bar implemented.

public myClass(){
    public static void main(String[] args){
        String[] commands = {"cmd.exe", "/C", "C:\\users\\....\\myBat.bat"};
        Process p = Runtime.getRuntime().exec(commands);
        p.waitFor()
        JOptionPane.showMessageDialog(null, "Process finished!");
    }
}

While p.waitFor() waits for the process, there is nothing on the screen. I just want something showing the user that a process is still running. Thoughts? Thanks!

mKorbel
  • 109,525
  • 20
  • 134
  • 319
DerStrom8
  • 1,311
  • 2
  • 23
  • 45

1 Answers1

12

You can run a ProcessBuilder in the background of a SwingWorker, as shown below, to get both output and a progress bar.

image

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.swing.*;

/**
 * @se http://stackoverflow.com/a/20603012/230513
 * @see http://stackoverflow.com/a/17763395/230513
 */
public class SwingWorkerExample {

    private final JLabel statusLabel = new JLabel("Status: ", JLabel.CENTER);
    private final JTextArea textArea = new JTextArea(20, 20);
    private JButton startButton = new JButton("Start");
    private JButton stopButton = new JButton("Stop");
    private JProgressBar bar = new JProgressBar();
    private BackgroundTask backgroundTask;
    private final ActionListener buttonActions = new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent ae) {
            JButton source = (JButton) ae.getSource();
            if (source == startButton) {
                textArea.setText(null);
                startButton.setEnabled(false);
                stopButton.setEnabled(true);
                backgroundTask = new BackgroundTask();
                backgroundTask.execute();
                bar.setIndeterminate(true);
            } else if (source == stopButton) {
                backgroundTask.cancel(true);
                backgroundTask.done();
            }
        }
    };

    private void displayGUI() {
        JFrame frame = new JFrame("Swing Worker Example");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        JPanel panel = new JPanel();
        panel.setBorder(
            BorderFactory.createEmptyBorder(5, 5, 5, 5));
        panel.setLayout(new BorderLayout(5, 5));

        JScrollPane sp = new JScrollPane();
        sp.setBorder(BorderFactory.createTitledBorder("Output: "));
        sp.setViewportView(textArea);

        startButton.addActionListener(buttonActions);
        stopButton.setEnabled(false);
        stopButton.addActionListener(buttonActions);
        JPanel buttonPanel = new JPanel();
        buttonPanel.add(startButton);
        buttonPanel.add(stopButton);
        buttonPanel.add(bar);

        panel.add(statusLabel, BorderLayout.PAGE_START);
        panel.add(sp, BorderLayout.CENTER);
        panel.add(buttonPanel, BorderLayout.PAGE_END);

        frame.setContentPane(panel);
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
    }

    private class BackgroundTask extends SwingWorker<Integer, String> {

        private int status;

        public BackgroundTask() {
            statusLabel.setText((this.getState()).toString());
        }

        @Override
        protected Integer doInBackground() {
            try {
                ProcessBuilder pb = new ProcessBuilder("ls", "-lR", "/");
                pb.redirectErrorStream(true);
                Process p = pb.start();
                String s;
                BufferedReader stdout = new BufferedReader(
                    new InputStreamReader(p.getInputStream()));
                while ((s = stdout.readLine()) != null && !isCancelled()) {
                    publish(s);
                }
                if (!isCancelled()) {
                    status = p.waitFor();
                }
                p.getInputStream().close();
                p.getOutputStream().close();
                p.getErrorStream().close();
                p.destroy();
            } catch (IOException | InterruptedException ex) {
                ex.printStackTrace(System.err);
            }
            return status;
        }

        @Override
        protected void process(java.util.List<String> messages) {
            statusLabel.setText((this.getState()).toString());
            for (String message : messages) {
                textArea.append(message + "\n");
            }
        }

        @Override
        protected void done() {
            statusLabel.setText((this.getState()).toString() + " " + status);
            stopButton.setEnabled(false);
            startButton.setEnabled(true);
            bar.setIndeterminate(false);
        }

    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                new SwingWorkerExample().displayGUI();
            }
        });
    }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • This is fantastic! I'll have to work with it a bit to make it fit my needs. It'll have to be a separate class, and pass my process commands in to it. I currently have a class variable and a mutator method to set the value. For some reason though it does not seem to run the batch file. I'll do a little more debugging and will get back to you. – DerStrom8 Dec 16 '13 at 16:02
  • It's working as expected, thank you very much! Unfortunately it's running very slowly. I don't suppose there's a way it could be sped up? – DerStrom8 Dec 16 '13 at 16:38
  • I'd guess it's the batch file; I had to slow it down to test; [profile](http://stackoverflow.com/q/2064427/230513) to be sure. – trashgod Dec 16 '13 at 18:44
  • Before adding this class the batch file would start and finish within about 30 seconds. Now it's been going for over an hour (maybe two?) and still isn't finished. – DerStrom8 Dec 16 '13 at 19:18
  • Hard to say: batch waiting for input? too much text? no line breaks? does it work with `dir /s /b`? – trashgod Dec 16 '13 at 19:31
  • I'm sorry, I should have mentioned that I'm using my own batch file that I wrote to copy files from one drive to another. The process builder uses a string array called "commands", which involves all the arguments and commands that are used to call/run the bat. I'm still unsure why it takes so long though, and I wouldn't even know where to begin looking. – DerStrom8 Dec 16 '13 at 19:52
  • Now, I'm not completely familiar with the SwingWorker class, but I have read that running an external process from the EDT will cause it to be very sluggish, or even become unresponsive. This seems to describe my problem perfectly. I don't suppose that's what is happening here? – DerStrom8 Dec 17 '13 at 23:53
  • 1
    Right; the idea is to run the slow process in `doInBackground`, the worker's background thread, and `process` results on the EDT. Does your batch finish, albeit slowly, or does it remain blocked indefinitely? – trashgod Dec 18 '13 at 00:26
  • It does finish eventually, but a process that takes 30 seconds normally took over two hours when run using the SwingWorker class. I did make a few edits to it, but that was only to change the UI layout. It shouldn't have caused any major bugs like this – DerStrom8 Dec 18 '13 at 00:30
  • Does your batch produce enough output to choke the text area or cause swapping? What did profiling or task manager show? I can't reproduce the effect you describe, but you might try copying the files using one of the approaches shown [here](http://stackoverflow.com/q/106770/230513). – trashgod Dec 18 '13 at 00:40
  • My batch file consists primarily of a loop containing the xcopy command. The directories I'm copying are passed in using the "commands" array that is in my code I posted in the original post. The output from the batch file is simply a line displaying the directory being copied. I'm not sure if this is choking up the text area or not. The task manager only shows a java process and a cmd process (related to this project). I'm not quite sure how to do the profiling you suggest, to be honest that's a new term for me. I'll check your link though. Many thanks for all your help, by the way! – DerStrom8 Dec 18 '13 at 00:45
  • You're seeing the output from each `xcopy`? While your app is running, start `jvisualvm` and attach to it to see what's going on. See also this [answer](http://stackoverflow.com/a/8621099/230513) on `xcopy` or try `Files.copy`. – trashgod Dec 18 '13 at 00:56
  • I do see the output from xcopy, yes. I do not run my programs in the command line, I generally use Eclipse. Is there a tool I can use in Eclipse instead? If I can't get this working I might have to rethink the flow of my entire program and use Files.copy instead of windows batch files – DerStrom8 Dec 18 '13 at 01:02
  • I hate to extend this comment thread further, I just wanted to mention that it currently takes 2-3 minutes to copy 2MB of files. Running straight from the batch file it takes about 3-5 seconds. – DerStrom8 Dec 18 '13 at 01:54
  • I have noticed since I last commented that when the stop button is pressed, it does not close the command process. It remains open in the task manager. I assumed that's what p.destroy() was supposed to do--close it--but it does not seem to be doing that. Any ideas? – DerStrom8 Dec 21 '13 at 03:44
  • That's expected; the host OS owns the thread, which should be in the parked/waiting state; task manager is a rather coarse view; [profile](http://stackoverflow.com/q/2064427/230513) to see more detail. – trashgod Dec 21 '13 at 12:49
  • So it's supposed to leave processes open after completing? That seems like a bit of a resource hog, and my computer has almost crashed a couple of times due to windows explorer freezing. – DerStrom8 Dec 21 '13 at 14:20
  • I'm seeing 48 bytes per defunct worker, which disappear when the JVM exits. – trashgod Dec 21 '13 at 18:06
  • Strange. Mine remained running even after the JVM was exited, and at one point it even continued to copy files even when the "stop" button was pressed. I suppose I'll keep mucking around with it and see what I can figure out. Thank a bunch, you've been a huge help – DerStrom8 Dec 21 '13 at 18:24