Source code: org/fluidsynth/api/Executive.java
1 /*
2 * Copyright (C) 2003 Ken Ellinwood.
3 *
4 * This file is part of FluidGUI.
5 *
6 * FluidGUI is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 */
20
21 package org.fluidsynth.api;
22
23 import org.fluidsynth.api.settings.*;
24 import org.fluidsynth.api.event.*;
25
26 import java.io.*;
27 import java.net.*;
28 import java.util.*;
29
30 /** This class provides an API for starting, stopping and interacting
31 * with fluidsynth.
32 */
33 public class Executive
34 {
35
36 private static final int DEFAULT_TCP_PORT = 9800;
37 private static final String EOR_ECHO = "--EOR--";
38 private static final String EOR_ECHO_CMD = "echo " + EOR_ECHO;
39 private static final int EOR_ECHO_CMD_LEN = EOR_ECHO_CMD.length();
40
41 private static Executive instance = null;
42 private static StringBuffer consoleOutput;
43 private static String errorMessage = null;
44 private static Set listeners = new HashSet();
45 private static TerminationStatus terminationStatus;
46
47 private Runner runner = null;
48
49 // TCP port number for opening a socket connection to the synth
50 private int port;
51 private Socket socket = null;
52 private BufferedReader socketReader = null;
53 private BufferedWriter socketWriter = null;
54
55 // flag for detecting abnormal termination
56 private boolean abnormalTermination = true;
57
58 /** Use the start(), instance() methods to get an instance of this class. */
59 private Executive( String[] command, int port)
60 throws IllegalStateException
61 {
62 if (instance != null) throw new IllegalStateException("FluidSynth is already running.");
63
64 this.port = port;
65 terminationStatus = TerminationStatus.NONE;
66
67 instance = this;
68 runner = new Runner( command);
69 new Thread( runner, "Fluidsynth runner").start();
70 }
71
72 /** Add a termination listener. */
73 public synchronized static void addListener( ExecutiveListener listener)
74 {
75 listeners.add( listener);
76 }
77
78 /** Remove a termination listener. */
79 public synchronized static void removeListener( ExecutiveListener listener)
80 {
81 listeners.remove( listener);
82 }
83
84 private synchronized static void fireStarted()
85 {
86 for (Iterator i = listeners.iterator(); i.hasNext(); ) {
87 ExecutiveListener listener = (ExecutiveListener)i.next();
88 listener.started();
89 }
90 }
91
92 private synchronized static void fireStopped()
93 {
94 for (Iterator i = listeners.iterator(); i.hasNext(); ) {
95 ExecutiveListener listener = (ExecutiveListener)i.next();
96 listener.stopped();
97 }
98 }
99
100 /** Start fluidsynth with some simple default options. Clients
101 should wait for the {@link ExecutiveListener#start start}
102 event before issuing commands via {@link #invoke}.
103 @throws IllegalStateException if fluidsynth is already running.
104 */
105 public static Executive start()
106 throws IllegalStateException
107 {
108 return start( new String[] { "fluidsynth", "-i", "-s"}, DEFAULT_TCP_PORT);
109 }
110
111 /** Start fluidsynth in a sub-process with a command line
112 determined by the user-modified settings in the given Settings
113 object. Clients should wait for the {@link
114 ExecutiveListener#start start} event before issuing commands
115 via {@link #invoke}.
116 @throws IllegalStateException if fluidsynth is already running.
117 */
118 public static Executive start( Settings settings)
119 throws IllegalStateException
120 {
121 String exename = settings.lookup(".fluidsynth.exe").getValue();
122
123 // Determine the shell port number
124 int portVal = DEFAULT_TCP_PORT;
125 Setting portSetting = settings.lookup("shell.port");
126 if (portSetting != null) portVal = Integer.parseInt( portSetting.getValue());
127
128 settings = settings.getModifiedSettings();
129
130 Setting[] sarray = settings.getSetting();
131
132 List cmdline = new ArrayList();
133
134 cmdline.add( exename);
135 cmdline.add("-i");
136 cmdline.add("-s");
137
138 for (int i = 0; i < sarray.length; i++) {
139 Setting s = sarray[i];
140
141 if (s.getName().startsWith(".")) continue;
142
143 cmdline.add( s.commandLineArg());
144 }
145
146 Setting extra = settings.lookup(".fluidsynth.extra.args");
147 if (extra != null) {
148 StringTokenizer tokenizer = new StringTokenizer( extra.getValue());
149 while (tokenizer.hasMoreTokens()) cmdline.add( tokenizer.nextToken());
150 }
151
152 return start( (String[])cmdline.toArray( new String[ cmdline.size()]), portVal);
153 }
154
155 /** Start fluidsynth with the given command line. Clients should wait for the {@link
156 ExecutiveListener#start start} event before issuing commands
157 via {@link #invoke}.
158 @throws IllegalStateException if fluidsynth is already running.
159 */
160 public synchronized static Executive start( String[] cmdLineArgs, int port)
161 throws IllegalStateException
162 {
163
164 errorMessage = null;
165 new Executive( cmdLineArgs, port);
166 return instance;
167 }
168
169
170 /** Terminate the fluidsynth process. Harmless to call this more than once. */
171 public synchronized static void stop()
172 {
173 if (instance != null) {
174 instance.shutdown();
175 }
176 }
177
178 /** Get the active instance of this class. If fluidsynth is not
179 * running, this method returns null.
180 */
181 public static Executive instance()
182 {
183 return instance;
184 }
185
186 /** Return true if the synth is running. */
187 public static boolean isRunning()
188 {
189 return instance() != null;
190 }
191
192 public static TerminationStatus getTerminationStatus()
193 {
194 return terminationStatus;
195 }
196
197 /** Send a command to fluidsynth and return its output.
198 * @return a list of strings (i.e, output lines) produced by the command.
199 */
200 public synchronized List invoke( String command)
201 throws IOException, IllegalStateException
202 {
203 ensureConnection();
204 if (command != null) {
205 Log.finer( this, "sending: " + command);
206 socketWriter.write( command, 0, command.length());
207 socketWriter.newLine();
208 }
209 socketWriter.write( EOR_ECHO_CMD, 0, EOR_ECHO_CMD_LEN);
210 Log.finer( this, "sending: " + EOR_ECHO_CMD);
211 socketWriter.newLine();
212 socketWriter.flush();
213
214 ArrayList response = new ArrayList();
215 String line = socketReader.readLine();
216 Log.finer( this, "received: " + line);
217 if (line == null) throw new IOException("End of stream reached while reading from socket");
218 while (!line.equals( EOR_ECHO)) {
219 // System.out.println( line);
220 response.add( line);
221 line = socketReader.readLine();
222 Log.finer( this, "received: " + line);
223 if (line == null) throw new IOException("End of stream reached while reading from socket");
224 }
225
226 return response;
227 }
228
229 /** Return console output produced by fluidsynth (stdout+stderr).
230 * This method will return the output from the current invocation
231 * of the synth, or the previous invocation if the synth is not
232 * currently running. Returns null if the synth has never been
233 * started.
234 */
235 public static String getConsoleOutput()
236 {
237 if (consoleOutput != null) return consoleOutput.toString();
238 return null;
239 }
240
241 /** Returns an error message produced by a start failure.
242 * @return error message or null if no error has occured since start()
243 */
244 public static String getErrorMessage()
245 {
246 return errorMessage;
247 }
248
249 /** Do the actual work of terminating the fluidsynth process. */
250 private void shutdown()
251 {
252 abnormalTermination = false;
253 try {
254 if (socket != null) socket.close();
255 }
256 catch (IOException e) {}
257 socket = null;
258 terminationStatus = TerminationStatus.NORMAL;
259 if (runner != null) runner.destroy();
260 instance = null;
261 fireStopped();
262 }
263
264
265 /** This class provides the thread body that does the Runtime.exec()
266 * of fluidsynth and handles abnormal termination.
267 */
268 class Runner implements Runnable
269 {
270 public Runner( String[] command)
271 {
272 this.command = command;
273 }
274
275 /** Thread body that starts and waits for fluidsynth to finish. */
276 public void run()
277 {
278 try {
279 consoleOutput = new StringBuffer();
280 if (true) {
281 String cline = "";
282 for (int i = 0; i < command.length; i++) {
283 cline += command[i];
284 cline += ' ';
285 }
286 Log.info(this, cline);
287 }
288 process = Runtime.getRuntime().exec( command);
289 new Thread( new ConsoleReader( process.getInputStream()), "Fluidsynth stdout reader").start();
290 new Thread( new ConsoleReader( process.getErrorStream()), "Fluidsynth stderr reader").start();
291
292 // Ping fluidsynth with a no-op command. Issue the start
293 // event upon success.
294 new Thread( new Ping(), "PingFluidsynth").start();
295
296 // Wait for termination
297 int exitStatus = process.waitFor();
298 if (abnormalTermination) {
299 terminationStatus = TerminationStatus.ABNORMAL_TERMINATION;
300 instance = null;
301 fireStopped();
302 }
303 }
304 catch (IOException e)
305 {
306 terminationStatus = TerminationStatus.START_FAILED;
307 errorMessage = e.getMessage();
308 instance = null;
309 fireStopped();
310 }
311 catch (Exception e) {
312 e.printStackTrace();
313 }
314 }
315
316 public void destroy()
317 {
318 if (process != null){
319 process.destroy();
320 process = null;
321 }
322 }
323
324 private String[] command;
325 private Process process;
326 }
327
328 class Ping implements Runnable
329 {
330 public void run()
331 {
332 try {
333 // Ping fluidsynth with a no-op command. Issue the start
334 // event upon success.
335 invoke(null);
336 fireStarted();
337 }
338 catch (IllegalStateException e) {} // Ignore
339 catch (Exception e) {
340 e.printStackTrace();
341 }
342 }
343 }
344
345 class ConsoleReader implements Runnable
346 {
347 int lineMark = 0;
348
349 public ConsoleReader( InputStream stream)
350 {
351 this.stream = stream;
352 }
353
354 public void run()
355 {
356 try {
357 int c = stream.read();
358 while (c != -1) {
359 if (((char)c) == '\n') {
360 Log.finer( this, consoleOutput.substring( lineMark).trim());
361 lineMark = consoleOutput.length();
362 }
363 consoleOutput.append( (char)c);
364 c = stream.read();
365 }
366 }
367 catch (IOException e) {
368 e.printStackTrace();
369 }
370 }
371
372 private InputStream stream;
373 }
374
375 /** Connect to the synth via TCP if we haven't done so already. */
376 private void ensureConnection()
377 throws IOException, IllegalStateException
378 {
379 if (socket == null) {
380 int retries = 0;
381 while (instance != null) {
382 try {
383 socket = new Socket( "localhost", port);
384 socketReader = new BufferedReader( new InputStreamReader( socket.getInputStream()));
385 socketWriter = new BufferedWriter( new OutputStreamWriter( socket.getOutputStream()));
386 return;
387 }
388 catch( ConnectException e) {
389 if (retries++ < 10) {
390 try {
391 Thread.sleep( 2000);
392 }
393 catch (InterruptedException x) {
394 x.printStackTrace();
395 }
396 }
397 else throw e;
398 }
399 }
400 throw new IllegalStateException("fluidsynth not started");
401 }
402 }
403
404
405 /** Ensure that we kill the fluidsynth process when the JVM is terminated. */
406 static class ShutdownHook implements Runnable
407 {
408 public void run()
409 {
410 stop();
411 }
412 }
413
414 static {
415 Runtime.getRuntime().addShutdownHook( new Thread( new ShutdownHook()));
416 }
417
418
419
420 /* Test routine. */
421 static class TestListener implements ExecutiveListener
422 {
423 public void started()
424 {
425 System.out.println("fluidsynth started.");
426 startSuccess = true;
427 synchronized (Executive.class) {
428 Executive.class.notify();
429 }
430 }
431
432 public void stopped()
433 {
434 TerminationStatus status = Executive.getTerminationStatus();
435 System.out.println( status);
436 if (status == TerminationStatus.ABNORMAL_TERMINATION) {
437 System.out.println("Console output:");
438 System.out.println( Executive.getConsoleOutput());
439 }
440 else if (status == TerminationStatus.START_FAILED) {
441 System.out.println( Executive.getErrorMessage());
442 }
443 synchronized (Executive.class) {
444 Executive.class.notify();
445 }
446 }
447 }
448
449 static boolean startSuccess = false;
450
451 /* Test routine. */
452 public static void main( String[] args)
453 {
454 try {
455 addListener( new TestListener());
456 Executive exec = start();
457 synchronized (Executive.class) {
458 Executive.class.wait();
459 }
460 if (startSuccess) {
461 for (Iterator i = exec.invoke("settings").iterator(); i.hasNext(); )
462 System.out.println( i.next());
463 stop();
464 }
465 }
466 catch (IllegalStateException e) {} // Abnormal termination handled by termination listner
467 catch (Exception e)
468 {
469 e.printStackTrace();
470 }
471 System.exit( 0);
472 }
473 }