001/*
002 * Gridarta MMORPG map editor for Crossfire, Daimonin and similar games.
003 * Copyright (C) 2000-2010 The Gridarta Developers.
004 *
005 * This program is free software; you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation; either version 2 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License along
016 * with this program; if not, write to the Free Software Foundation, Inc.,
017 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
018 */
019
020package net.sf.gridarta.utils;
021
022import java.awt.BorderLayout;
023import java.awt.Color;
024import java.awt.Component;
025import java.awt.Font;
026import java.awt.Frame;
027import java.awt.Window;
028import java.io.File;
029import java.io.IOException;
030import java.io.InputStream;
031import java.util.Queue;
032import java.util.concurrent.ConcurrentLinkedQueue;
033import javax.swing.Action;
034import javax.swing.JDialog;
035import javax.swing.JPanel;
036import javax.swing.JScrollPane;
037import javax.swing.JTextArea;
038import javax.swing.JToolBar;
039import javax.swing.SwingUtilities;
040import net.sf.japi.swing.action.ActionBuilder;
041import net.sf.japi.swing.action.ActionBuilderFactory;
042import net.sf.japi.swing.action.ActionMethod;
043import org.apache.log4j.Category;
044import org.apache.log4j.Logger;
045import org.jetbrains.annotations.NotNull;
046import org.jetbrains.annotations.Nullable;
047
048/**
049 * Class to run an external process.
050 * @author <a href="mailto:cher@riedquat.de">Christian.Hujer</a>
051 */
052public class ProcessRunner extends JPanel {
053
054    /**
055     * The Logger for printing log messages.
056     */
057    @NotNull
058    private static final Category log = Logger.getLogger(ProcessRunner.class);
059
060    /**
061     * Serial Version.
062     */
063    private static final long serialVersionUID = 1L;
064
065    /**
066     * Action Builder.
067     */
068    @NotNull
069    private static final ActionBuilder ACTION_BUILDER = ActionBuilderFactory.getInstance().getActionBuilder("net.sf.gridarta");
070
071    /**
072     * The Dialog.
073     * @serial include
074     */
075    @Nullable
076    private Window dialog;
077
078    /**
079     * The i18n key.
080     * @serial include
081     */
082    @NotNull
083    private final String key;
084
085    /**
086     * The command and arguments.
087     * @serial include
088     */
089    @NotNull
090    private String[] command;
091
092    /**
093     * The working directory for the command.
094     * @serial include
095     */
096    @Nullable
097    private final File dir;
098
099    /**
100     * The Process.
101     */
102    @Nullable
103    private transient Process process;
104
105    /**
106     * JTextArea with log.
107     * @serial include
108     */
109    @NotNull
110    private final JTextArea stdtxt = new JTextArea(25, 80);
111
112    /**
113     * CopyOutput for stdout.
114     * @serial include
115     */
116    @NotNull
117    private final CopyOutput stdout = new CopyOutput("stdout", stdtxt);
118
119    /**
120     * CopyOutput for stderr.
121     * @serial include
122     */
123    @NotNull
124    private final CopyOutput stderr = new CopyOutput("stderr", stdtxt);
125
126    /**
127     * Action for start.
128     * @serial include
129     */
130    @NotNull
131    private final Action controlStart = ACTION_BUILDER.createAction(false, "controlStart", this);
132
133    /**
134     * Action for stop.
135     * @serial include
136     */
137    @NotNull
138    private final Action controlStop = ACTION_BUILDER.createAction(false, "controlStop", this);
139
140    /**
141     * The lock object for thread synchronization.
142     */
143    @NotNull
144    private final Object lock = new Object();
145
146    /**
147     * Creates a ProcessRunner for running the given command in the given
148     * directory.
149     * @param key i18n key
150     * @param command the command to run and its arguments
151     * @param dir the working directory for command
152     */
153    private ProcessRunner(@NotNull final String key, @NotNull final String[] command, @Nullable final String dir) {
154        this.key = key;
155        this.command = command.clone();
156        this.dir = dir == null ? null : new File(dir);
157        setLayout(new BorderLayout());
158        stdtxt.setFont(new Font("monospaced", Font.PLAIN, stdtxt.getFont().getSize()));
159        stdtxt.setEditable(false);
160        stdtxt.setFocusable(false);
161        stdtxt.setLineWrap(true);
162        stdtxt.setBackground(Color.BLACK);
163        stdtxt.setForeground(Color.WHITE);
164        final Component scrollPane = new JScrollPane(stdtxt);
165        scrollPane.setFocusable(true);
166        add(scrollPane, BorderLayout.CENTER);
167        final JToolBar toolBar = new JToolBar();
168        toolBar.add(controlStart);
169        toolBar.add(controlStop);
170        toolBar.add(ACTION_BUILDER.createAction(false, "controlClear", this));
171        toolBar.add(ActionBuilderUtils.newLabel(ACTION_BUILDER, "controlCloseOkay"));
172        controlStop.setEnabled(false);
173        add(toolBar, BorderLayout.SOUTH);
174    }
175
176    /**
177     * Creates a ProcessRunner for running the given command in its directory.
178     * @param key i18n key
179     * @param command the command to run and its arguments
180     */
181    public ProcessRunner(@NotNull final String key, @NotNull final String[] command) {
182        this(key, command, new File(command[0]).getParent());
183    }
184
185    /**
186     * Show a dialog if not already visible.
187     * @param parent owner frame to display on
188     */
189    public void showDialog(@NotNull final Frame parent) {
190        if (dialog == null || dialog.getOwner() != parent) {
191            createDialog(parent);
192        }
193        assert dialog != null;
194        dialog.setVisible(true);
195        assert dialog != null;
196        dialog.requestFocus();
197    }
198
199    /**
200     * Create the dialog.
201     * @param parent owner frame to display on
202     */
203    private void createDialog(@NotNull final Frame parent) {
204        dialog = new JDialog(parent, ActionBuilderUtils.getString(ACTION_BUILDER, key + ".title"));
205        dialog.add(this);
206        assert dialog != null;
207        dialog.pack();
208        assert dialog != null;
209        dialog.setLocationRelativeTo(parent);
210    }
211
212    /**
213     * Set the command to be executed by this ProcessRunner.
214     * @param command the command to run and its arguments
215     */
216    public void setCommand(@NotNull final String[] command) {
217        this.command = command.clone();
218    }
219
220    /**
221     * Action method for starting.
222     */
223    @ActionMethod
224    public void controlStart() {
225        synchronized (lock) {
226            if (process != null) {
227                try {
228                    try {
229                        process.getInputStream().close();
230                    } catch (final IOException ignored) {
231                        // ignore
232                    }
233                    try {
234                        process.getErrorStream().close();
235                    } catch (final IOException ignored) {
236                        // ignore
237                    }
238                    try {
239                        process.getOutputStream().close();
240                    } catch (final IOException ignored) {
241                        // ignore
242                    }
243                    process.exitValue();
244                } catch (final IllegalThreadStateException ignored) {
245                    log.error("Still running!");
246                    // Process is still running, don't start a new one
247                    return;
248                }
249                process = null;
250            }
251            try {
252                process = new ProcessBuilder(command).directory(dir).redirectErrorStream(true).start();
253                final InputStream out = process.getInputStream();
254                final InputStream err = process.getErrorStream();
255                stdout.start(out);
256                if (out != err) {
257                    stderr.start(err);
258                }
259                controlStop.setEnabled(true);
260                controlStart.setEnabled(false);
261            } catch (final IOException e) {
262                ACTION_BUILDER.showMessageDialog(this, "controlError", e);
263            }
264        }
265    }
266
267    /**
268     * Action method for stopping.
269     */
270    @ActionMethod
271    public void controlStop() {
272        if (process != null) {
273            process.destroy();
274        }
275        controlStop.setEnabled(false);
276        controlStart.setEnabled(true);
277    }
278
279    /**
280     * Action method for clearing the log.
281     */
282    @ActionMethod
283    public void controlClear() {
284        stdtxt.setText("");
285    }
286
287    /**
288     * Class for reading data from a stream and appending it to a JTextArea.
289     */
290    private static class CopyOutput implements Runnable {
291
292        /**
293         * BufferedReader to read from.
294         */
295        @Nullable
296        private InputStream in;
297
298        /**
299         * JTextArea to write to.
300         */
301        @NotNull
302        private final Appender appender;
303
304        /**
305         * Title.
306         */
307        @NotNull
308        private final String title;
309
310        /**
311         * Create a CopyOutput.
312         * @param title Title for this CopyOutput
313         * @param textArea JTextArea to append output to
314         */
315        private CopyOutput(@NotNull final String title, @NotNull final JTextArea textArea) {
316            this.title = title;
317            appender = new Appender(textArea);
318        }
319
320        @Override
321        public void run() {
322            try {
323                try {
324                    final byte[] buf = new byte[4096];
325                    while (true) {
326                        assert in != null;
327                        final int bytesRead = in.read(buf);
328                        if (bytesRead == -1) {
329                            break;
330                        }
331                        appender.append(new String(buf, 0, bytesRead));
332                    }
333                    //for (String line; (line = in.readLine()) != null;) {
334                    //    appender.append(title, line, "\n");
335                    //}
336                } finally {
337                    assert in != null;
338                    in.close();
339                }
340            } catch (final IOException e) {
341                appender.append(title, ": ", e.toString());
342            } finally {
343                in = null;
344            }
345        }
346
347        /**
348         * Start running.
349         * @param stream InputStream to read from
350         */
351        private void start(@NotNull final InputStream stream) {
352            if (in != null) {
353                if (log.isInfoEnabled()) {
354                    log.info("Trying to stop previous stream.");
355                }
356                try {
357                    assert in != null;
358                    in.close();
359                } catch (final IOException ignored) {
360                    // ignore
361                }
362                if (log.isInfoEnabled()) {
363                    log.info("Stopped previous stream.");
364                }
365            }
366            //in = new BufferedInputStream(stream);
367            in = stream;
368            new Thread(this).start();
369        }
370
371        /**
372         * Class for SwingUtilities to append text. Why is this class used?
373         * Quite simple. Swing is not thread-safe. All modifications on realized
374         * Swing components must be done by the AWT Event thread. Concurrent
375         * modifications might crash some or more swing components. The
376         * CopyOutput is a thread of its own. This class makes sure that
377         * appending text to the JTextArea is done by the AWT Event thread.
378         */
379        private static class Appender implements Runnable {
380
381            /**
382             * Strings to append.
383             */
384            @NotNull
385            private final Queue<String> texts = new ConcurrentLinkedQueue<String>();
386
387            /**
388             * JTextArea to append to.
389             */
390            @NotNull
391            private final JTextArea textArea;
392
393            /**
394             * Create an Appender.
395             * @param textArea JTextArea to append to
396             */
397            Appender(@NotNull final JTextArea textArea) {
398                this.textArea = textArea;
399            }
400
401            /**
402             * Append text to the JTextArea.
403             * @param texts texts to append
404             */
405            public void append(@NotNull final String... texts) {
406                for (final String text : texts) {
407                    this.texts.offer(text);
408                }
409                SwingUtilities.invokeLater(this);
410            }
411
412            @Override
413            public void run() {
414                while (!texts.isEmpty()) {
415                    textArea.append(texts.poll());
416                }
417            }
418
419        }
420
421    }
422
423}