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}