Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

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 }