/*
* Server.java
* JCollider
*
* Copyright (c) 2004-2010 Hanns Holger Rutz. All rights reserved.
*
* This software is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either
* version 2, june 1991 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License (gpl.txt) along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de , or visit http://www.sciss.de/jcollider
*
*
* JCollider is closely modelled after SuperCollider Language,
* often exhibiting a direct translation from Smalltalk to Java.
* SCLang is a software originally developed by James McCartney,
* which has become an Open Source project.
* See http://supercollider.sourceforge.net/ for details.
*
*
* Changelog:
* 04-Aug-05 created
* 11-Aug-05 getClientAddr() added
* 28-Aug-05 correctly determines isLocal
* 04-Sep-05 uses OSCTransmitter
* 24-Sep-05 boot-completion actions are executed before
* any server event notification is fired
* 07-Oct-05 sendMsgSync recognized /fail replies ; added sendBundleSync()
* 24-Jul-06 uses updated server options with variable block allocator class ; alive thread robust against unresponsive server
* ; added extended sendMsgSync and sendBundleSync methods ; responders recreated after quit
* 01-Oct-06 uses new NetUtil and allows for TCP mode
* 25-Aug-08 OSC buffer size increased to 64K
* 11-Jan-10 works with stupidly changed scsynth OSC replies
*/
package de.sciss.jcollider;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.Timer;
import de.sciss.app.BasicEvent;
import de.sciss.app.EventManager;
import de.sciss.net.OSCBundle;
import de.sciss.net.OSCChannel;
import de.sciss.net.OSCClient;
import de.sciss.net.OSCMessage;
/**
* Closely following SClang's server class,
* this is the client side <strong>representation</strong>
* of a supercollider server.
* <P>
* <B>As of v0.29</B>, the server must be started explicitly by calling <code>start()</code>
* unless the server is booted. The call to <code>start()</code> tells the <code>OSCClient</code> to
* start listening for incoming messages. For TCP connections, this will try to connect to the server.
*
* @todo the behaviour should be changed to the server
* representation automatically sending dumpOSC and
* notification status and initializing the tree,
* whenever the running status becomes true!
*
* @warning don't rely on the default group's node ID. it is
* planned to use a client specific ID in the next
* version, so JCollider and SClang can peacefully
* coexist on the same server without killing each
* other's groups (which happens at the moment if
* you press cmd+period in sclang)
*
* @synchronization unless specified, all methods should be
* regarded thread safe
*
* @author Hanns Holger Rutz
* @version 0.34, 11-Jan-10
*/
public class Server
implements Constants, EventManager.Processor
{
/**
* We just use the same default scsynth
* server port (57110) here as assumed by sclang
*/
public static final int DEFAULT_PORT = 57110;
private static final Set setServers = Collections.synchronizedSet( new HashSet() );
private static final Map mapServerNames = Collections.synchronizedMap( new HashMap() );
private final String name;
private final InetSocketAddress addr;
private final ServerOptions options;
private final int clientID;
private final boolean isLocal;
private volatile boolean serverRunning = false;
protected volatile boolean serverBooting = false;
private boolean notified = true;
protected Buffer[] bufferArray;
protected OSCResponderNode bufInfoResponder;
protected boolean waitingForBufInfo;
protected int waitingBufs;
private NodeIDAllocator nodeAllocator;
private BlockAllocator controlBusAllocator, audioBusAllocator, bufferAllocator;
private static String program = "scsynth";
private static boolean inform = true;
protected static volatile PrintStream printStream = System.err;
// protected static final Timer appClock = new Timer(); // nice work-around, eh?
private StatusWatcher aliveThread = null;
// OSC communication
protected final OSCClient c;
private final OSCMultiResponder multi;
// private final OSCTransmitter trns;
// private final DatagramChannel dch;
private int dumpMode = kDumpOff;
private final Group defaultGroup;
// status watcher
protected final Status status = new Status();
// messaging
private final EventManager em = new EventManager( this );
private final List collBootCompletion = new ArrayList();
protected BootThread bootThread = null;
private static final OSCMessage statusMsg = new OSCMessage( "/status" );
protected final Server enc_this = this;
protected final Object syncBootThread = new Object();
/**
* Creates a new <code>Server</code> representation
* object. Note that this will neither "create"
* a server, nor boot one, nor contact one. It simply
* establishes means of communication to a server
* at the given address. The caller is responsible for
* contacting the server and, before starting to create
* nodes and such, to initialize the default tree
* using <code>initTree</code>, and to ping the server
* using <code>startAliveThread</code> if one wishes
* to be informed about server boots and quits.
*
* @param name the name of the server. this arbitrary but
* must be unique since a set of created servers
* is internally maintained. currently no check
* on this uniqueness is performed. this is also
* the name of the OSC thread and the one shown
* in a server panel
* @param addr the address at which scsynth responds. as
* of this writing, the communication will use
* the UDP protocol
* @param options an instance of <code>ServerOptions</code>
* which needn't reflect the actual server settings
* when contacting a remote server, but which are
* used when booting a local server.
* @param clientID this is a concept taken from sclang, something about
* which the server knows nothing. it merely conditions
* the node ID allocator to occupy different value
* ranges for up to 31 different clients, so they won't
* conflict. beware, that as of this writing, there is
* no mechanism to coordinate the use of buffers and
* busses between different concurrent clients.
* the ID can be something between <code>0</code> (default) and
* <code>NodeIDAllocator.getUserMax()</code>
*
* @throws IOException if a networking error occurs
*
* @see #startAliveThread()
* @see #boot()
* @see #initTree()
*/
public Server( String name, InetSocketAddress addr, ServerOptions options, int clientID )
throws IOException
{
this.name = name;
this.addr = addr;
this.options = options;
this.clientID = clientID;
// this doesn't work XXX
// isLocal = addr.getAddress().isAnyLocalAddress();
// isLocal = true;
final InetAddress host = addr.getAddress();
if( host == null ) throw new IOException( "Server.new : unresolved network address " + addr );
isLocal = host.isLoopbackAddress() || host.equals( InetAddress.getLocalHost() );
Server.mapServerNames.put( name, this );
Server.setServers.add( this );
// defaultGroup = Group.basicNew( this, 1 ); // XXX should be changed
defaultGroup = Group.basicNew( this, 0 );
try {
// its crucial to create a multi responder with our server's
// address here, because we need to use it's channel for
// sendMsg/sendBundle in order for responders to get the replies!!
c = OSCClient.newUsing( options.getProtocol(), 0, host.isLoopbackAddress() );
// c.start();
c.setBufferSize( 0x10000 );
multi = new OSCMultiResponder( c );
// dch = (DatagramChannel) multi.getChannel(); // XXX
// trns = new OSCTransmitter( dch, addr );
// trns = OSCTransmitter.newUsing( dch );
c.setTarget( addr );
createNewAllocators();
resetBufferAutoInfo();
}
catch( IOException e1 ) {
Server.mapServerNames.remove( name );
Server.setServers.remove( this );
throw e1;
}
// ---- listeners and processors ----
// XXX there is no class level listener registration
// at the moment, also it's not too useful anyway
// Server.changed(\serverAdded, this);
}
// // when SC quits, the DatagramChannel is closed
// // ; if we continue to use it, we might end up with
// // java.nio.channels.ClosedChannelException s.
// // hence, we re-create the responders
// // ; NO WE DON'T XXX
// private void initCommunication()
// throws IOException
// {
////System.err.println( "initCommunication" );
// if( multi != null ) {
////System.err.println( " dipose "+multi.hashCode() );
// multi.dispose();
//// multi = null;
// }
////System.err.println( " recreate multi" );
// multi = new OSCMultiResponder( addr );
// dch = multi.getChannel();
////System.err.println( " recreate trns; multi = "+multi.hashCode() );
// trns = new OSCTransmitter( dch, addr );
////System.err.println( " recreate done" );
//
// createNewAllocators();
// resetBufferAutoInfo();
// }
/**
* Creates a server representation
* for the default client (<code>0</code>).
*/
public Server( String name, InetSocketAddress addr, ServerOptions options )
throws IOException
{
this( name, addr, options, 0 );
}
/**
* Creates a server representation
* for the default client (<code>0</code>),
* using default options.
*
* @see ServerOptions#ServerOptions()
*/
public Server( String name, InetSocketAddress addr )
throws IOException
{
this( name, addr, new ServerOptions() );
}
/**
* Creates a representation for a
* server which listens at the loopback
* address (<code>127.0.0.1:57110</code>).
*/
public Server( String name )
throws IOException
{
this( name, new InetSocketAddress( "127.0.0.1", DEFAULT_PORT ));
}
/**
* Returns the address which is
* assumed to be the one at which
* the server listens
*
* @return the address (IP plus port)
* which was used to instantiate
* the server representation
*/
public InetSocketAddress getAddr()
{
return addr;
}
protected OSCMultiResponder getMultiResponder()
{
return multi;
}
public void start()
throws IOException
{
c.start();
}
/**
* Returns the socket address that the client
* (that's us) is using to send messages to the server
*
* @return the socket address which is used
* to send messages to the server
* (i.e. the sender of <code>sendMsg</code> and <code>sendBundle</code>)
*
* @see AbstractOSCCommunicator#getChannel()
*/
// public InetSocketAddress getClientAddr()
// {
// final DatagramSocket ds = ((DatagramChannel) multi.getChannel()).socket(); // XXX
//
// return new InetSocketAddress( ds.getLocalAddress(), ds.getLocalPort() );
// }
/**
* Queries the server representation's name
*
* @return the name which was used to
* instantiate the <code>Server</code> class
*/
public String getName()
{
return name;
}
/**
* Queries the options for booting the server.
* The returned object is not a copy, and it's
* mutable, so any changes made to the returned
* objects will become effective when calling
* the <code>boot</code> method.
*
* @return the server options describing the
* options to pass to scsynth when booting it
*/
public ServerOptions getOptions()
{
return options;
}
/**
* Queries the client ID used
* for allocating node IDs.
*
* @return the ID used in the <code>Server</code>
* constructor
*/
public int getClientID()
{
return clientID;
}
/**
* Queries the application path
* to scsynth.
*
* @return the pathname stored for
* the location of scsynth. this is the
* path which will be used when booting
* the server. the default value being
* the cwd will probably not work for your
* application, therefore it's advised to set
* the path using <code>setProgram</code> before
* trying to boot
*/
public static String getProgram()
{
return program;
}
/**
* Changes the path at which scsynth is supposed
* to located on the local harddisk. This is the
* path used to boot a local server. Note that this
* path is stored globally (for all server instances).
* For simplicity, this is a string and not a <code>File</code>
* object. To convert from a file, use
* <code>File.getAbsolutePath()</code>.
*
* @param program full pathname to scsynth(.exe)
*
* @see File#getAbsolutePath()
*/
public static void setProgram( String program )
{
Server.program = program;
}
/**
* Turns on or off verbose messaging
* of the server representation. This has
* little effect at the moment, but when turned
* on, a few more messages will be printed to the
* console.
*
* @param onOff <code>true</code> means, be
* a bit more verbose
*/
public static void setInform( boolean onOff )
{
Server.inform = onOff;
}
/**
* Changes the stream to which messages are
* printed. By default, <code>Server</code> will
* print on <code>System.err</code>. This is
* a global method and affects all server instances.
* This method is particularly important because
* it's value will be read by the <code>boot</code>
* method to determine the print stream to which
* a locally booted server will print. Changing
* the stream after the server was booted, will not
* affect it's print out at all, so that needs to
* be done in advance.
*
* @param printStream the new print stream to use
*/
public static void setPrintStream( PrintStream printStream )
{
Server.printStream = printStream;
}
/**
* Queries the currently used print stream for
* outputting messages
*
* @return the print stream used for message printout
* and for the locally booted server
*/
public static PrintStream getPrintStream()
{
return printStream;
}
/**
* Queries the booting state
*
* @return <code>true</code> if the local server is being booted
* at the moment, <code>false</code> otherwise.
*
* @see #isRunning()
*/
public boolean isBooting()
{
return serverBooting;
}
protected void setBooting( boolean serverBooting )
{
this.serverBooting = serverBooting;
}
/**
* Queries the running state. To become automatically
* informed about changes of this state, register a
* listener using the <code>addListener</code> method.
*
* @return <code>true</code> if a connection is established
* to a supercollider server. <code>false</code> if the
* server is not running or communication broke down.
* note that this value will only be valid, when the
* ping-thread is running, which is either started
* automatically when booting the server (<code>boot</code>)
* or manually by invoking <code>startAliveThread()</code>.
*
* @todo a future version may incorporate automatic service
* discovery.
*
* @see #addListener( ServerListener )
* @see #boot()
* @see #startAliveThread()
*/
public boolean isRunning()
{
return serverRunning;
}
protected void setRunning( boolean serverRunning )
{
//System.err.println( "ici : "+serverRunning );
synchronized( syncBootThread ) {
if( this.serverRunning != serverRunning ) {
this.serverRunning = serverRunning;
if( !serverRunning ) {
// recordNode = nil;
changed( ServerEvent.STOPPED );
if( bootThread != null ) {
try {
bootThread.keepScRunning = false;
syncBootThread.wait( 4000 );
}
catch( InterruptedException e1 ) { /* empty */ }
}
} else {
//System.err.println( "ici" );
while( !collBootCompletion.isEmpty() ) {
((CompletionAction) collBootCompletion.remove( 0 )).completion( this );
}
changed( ServerEvent.RUNNING );
}
}
}
}
private void createNewAllocators()
{
nodeAllocator = new NodeIDAllocator( getClientID() );
controlBusAllocator = options.getBlockAllocFactory().create( options.getNumControlBusChannels() );
audioBusAllocator = options.getBlockAllocFactory().create( options.getNumAudioBusChannels(), options.getFirstPrivateBus() );
bufferAllocator = options.getBlockAllocFactory().create( options.getNumBuffers() );
}
/**
* Automatic buffer ID allocator
* for package internal use only.
*/
protected BlockAllocator getBufferAllocator()
{
return bufferAllocator;
}
/**
* Automatic audio bus allocator
* for package internal use only.
*/
protected BlockAllocator getAudioBusAllocator()
{
return audioBusAllocator;
}
/**
* Automatic control bus allocator
* for package internal use only.
*/
protected BlockAllocator getControlBusAllocator()
{
return controlBusAllocator;
}
/**
* Queries the server's (audio) sampling rate.
* This is the <strong>nominal</strong> rate
* as returned by the <code>/status.reply</code>
* message from the server. This is only valid
* when the server is running and the ping-thread
* was started. To be automatically informed about
* status changes, you can register a listener
* using <code>addListener</code>.
*
* @return the nominal (i.e. constant) sampling rate
* of the server
*
* @see #addListener( ServerListener )
* @see #startAliveThread()
* @see #getStatus()
*/
public double getSampleRate()
{
return status.sampleRate;
}
/**
* Queries the latest reported server status.
* The returned object is a snapshot of what
* the server last send using a <code>/status.reply</code>
* message. It's only valid when the server is
* running and the ping-thread is running.
*
* @return the <code>Status</code> with fields
* for number of UGens, Nodes etc.
*/
public Status getStatus()
{
return Status.copyFrom( status );
}
/**
* Queries the current OSC dumping mode.
* Note that the value reflects just what
* <strong>we</strong> know about the status.
* When the server is booted, a dump message
* is automatically send, so the reported
* value should be considered correct, until
* a different client sends a dumpOSC message
* or the server is terminated and restarted.
*
* @return the mode at which the server
* dumps OSC messages it receives. this
* can be either of <code>kDumpOff</code>,
* <code>kDumpText</code>, <code>kDumpHex</code>
* or <code>kDumpBoth</code>.
*
* @see Constants#kDumpText
*/
public int getDumpMode()
{
return dumpMode;
}
/**
* Returns the "default" group
* of the server. This is the one used as
* target when no explicit target is specified
* for node creation, such as in
* <code>new Synth( defName, argNames, argValues, null )</code>.
*
* @return the default group. the value is
* only valid (i.e. representing a group
* that really exists), when the server representation
* has initialized it's node tree, which is done
* automatically after booting, or by calling
* <code>initTree</code> explicitly.
*
* @see #initTree()
*
* @warning do not rely on a particular
* group returned here, since it is
* likely that it will change in a future version
*/
public Group getDefaultGroup()
{
return defaultGroup;
}
/**
* Returns a group representation of the
* server, which at the moment is a synonym for
* <code>getDefaultGroup</code>. This will change in
* one of the next version to return the root node
* (ID 0) however!!
*/
public Group asTarget()
{
return defaultGroup;
}
protected static void inform( String txt )
{
if( inform ) printStream.println( txt );
}
/**
* Starts booting a local server
* and establishes the ping-thread when done
*
* @synchronization must be called in the event thread
*/
public void boot()
throws IOException
{
boot( true );
}
/**
* Starts booting a local server. A local
* server can only be booted, when the server's address
* is local. Be sure to set the appropriate application
* path using <code>setProgram</code> before calling this method.
* <p>
* This method does nothing, when
* the booting process is assumed to be ongoing already,
* or when the server has already been successfully booted.
* <p>
* When booting is finished, the ping-thread is established
* depending on the <code>startAliveThread</code> flag,
* the node tree is initialized (<code>initTree</code>),
* and all actions registered through <code>addDoWhenBooted</code>
* are executed. Independantly, when the ping-thread receives
* the first reply, the running status is updated, hence
* informing all listeners registered using <code>addListener</code>.
*
* @param startAliveThread <code>true</code> to start a ping-thread
* once the server was booted. this is required
* if you wish to read the running status using
* <code>isRunning()</code> or using listeners
*
* @throws IOException if an error occurs when starting server,
* particularly if the path to the server application
* is wrong.
* @throws IllegalStateException if you try to boot a remote server
*
* @see #setProgram( String )
* @see #isLocal()
* @see #isBooting()
* @see #isRunning()
* @see Server#addDoWhenBooted( Server.CompletionAction )
* @see #startAliveThread()
*
* @synchronization must be called in the event thread
*/
public void boot( boolean startAliveThread )
throws IOException
{
if( isRunning() ) {
printStream.println( "server already running" );
return;
}
if( isBooting() ) {
printStream.println( "server already booting" );
return;
}
if( !isLocal ) throw new IllegalStateException( "Server.boot() : only allowed for local servers!" );
final CompletionAction whenBooted = new CompletionAction() {
public void completion( Server s )
{
try {
s.setBooting( false );
if( s.getDumpMode() != kDumpOff ) {
s.dumpOSC( s.getDumpMode() );
}
if( s.isNotified() ) {
Server.inform( "notification is on" );
s.notify( true );
} else {
Server.inform( "notification is off" );
}
s.initTree(); // XXX inefficient since it re-created the node allocator
}
catch( IOException e1 ) {
printError( "Server.boot", e1 );
}
}
};
setBooting( true );
try {
// if( startAliveThread ) startAliveThread();
// XXX inefficient since it was created already in constructor, should use reset instead
// (serverOptions is immutable here)
createNewAllocators();
resetBufferAutoInfo();
addDoWhenBooted( whenBooted );
bootServerApp( startAliveThread );
}
catch( IOException e1 ) {
removeDoWhenBooted( whenBooted );
try {
stopAliveThread();
}
catch( IOException e2 ) {
printError( "Server.boot", e2 );
}
setBooting( false );
throw e1;
}
}
/**
* Sends a message to the server
* requesting OSC dump (text format).
*
* @throws IOException if the message failed to be sent
*/
public void dumpOSC()
throws IOException
{
dumpOSC( kDumpText );
}
/**
* Sends a message to the server
* requesting OSC dumping being turned on or off
*
* @param dumpMode either of <code>kDumpOff</code> (do not dump),
* <code>kDumpText</code> (dump text format),
* <code>kDumpHex</code> (hexadecimal printout),
* <code>kDumpBoth</code> (both text + hex)
*
* @see Constants#kDumpText
*
* @throws IOException if the message failed to be sent
*/
public void dumpOSC( int dumpMode )
throws IOException
{
sendMsg( dumpOSCMsg( dumpMode ));
}
/**
* Creates the OSC message which will ask the
* server to turn on or off OSC dumping
*
* @param dumpMode see <code>dumpOSC( int )</code> for details
*
* @see #dumpOSC( int )
*/
public OSCMessage dumpOSCMsg( int dumpMode )
{
this.dumpMode = dumpMode;
return new OSCMessage( "/dumpOSC", new Object[] { new Integer( dumpMode )});
}
/**
* Changes the way incoming messages are dumped
* to the console. By default incoming messages are not
* dumped. Incoming messages are those received
* by the client from the server, before they
* get delivered to registered <code>OSCResponderNode</code>s.
*
* @param dumpMode see <code>dumpOSC( int )</code> for details
*
* @see #dumpOSC( int )
*/
public void dumpIncomingOSC( int dumpMode )
{
// multi.dumpOSC( dumpMode, printStream );
c.dumpIncomingOSC( dumpMode, printStream );
}
/**
* Changes the way outgoing messages are dumped
* to the console. By default outgoing messages are not
* dumped. Outgoing messages are those send via
* <code>sendMsg</code> or <code>sendBundle</code>.
*
* @param dumpMode see <code>dumpOSC( int )</code> for details
*
* @see #dumpOSC( int )
*/
public void dumpOutgoingOSC( int dumpMode )
{
// trns.dumpOSC( dumpMode, printStream );
c.dumpOutgoingOSC( dumpMode, printStream );
}
/**
* Sends a message to the server
* requesting to be notified about server actions
* such as nodes being added and deleted. This
* is also required to receive <code>/tr</code>
* messages from a <code>sendTrig</code> UGen.
* The status of the notification flag is saved
* internally, so when a local server is booted
* and notification was turned on, a new
* <code>/notify</code> message is sent.
* <p>
* By default (when creating a new instance of
* <code>Server</code>), the notification flag
* is <code>true</code>.
*
* @param notified <code>true</code> to turn notification on,
* <code>false</code> to turn it off
*
* @warning if you boot a local server, the flag
* is sent to the server. However,
* if you wish to contact a server manually, you
* will have to call this method explicitly. In
* a future version this behaviour will change,
* so the server representation automatically
* sends dumpOSC and notification flag as well
* as tree initialization, whenever the server
* becomes available.
*
* @throws IOException if the message failed to be sent
*/
public void notify( boolean notified )
throws IOException
{
this.notified = notified;
sendMsg( new OSCMessage( "/notify", new Object[] { new Integer( notified ? 1 : 0 )}));
}
/**
* Queries the server notification status.
* By default, notification is turned on.
* However, the flag represents the true
* notification state only, when it was send
* to the server, which happens after a local
* boot but not when simply starting the ping-thread!
* this will change in a future version.
*
* @return <code>true</code> if notification is turned on.
*/
public boolean isNotified()
{
return notified;
}
/**
* After the server has been contacted,
* calling this method will create the default group.
* This is automatically called after booting,
* but not automatically when starting the ping-thread
* manually.
* this behaviour will change in a future version.
*
* @synchronization must be called in the event thread
*/
public void initTree()
throws IOException
{
nodeAllocator = new NodeIDAllocator( getClientID() );
// sendMsg( new OSCMessage( "/g_new", new Object[] { new Integer( 1 )}));
}
/**
* Registers an action to be executed
* after the boot process is complete.
* Often it may be more convenient to simply
* add a server listener.
*
* @param action action to be executed,
* when the server running status
* becomes true after the boot process
*/
public void addDoWhenBooted( CompletionAction action )
{
collBootCompletion.add( action );
}
/**
* Unregisters an action from being executed
* after the boot process. Note that the
* action is automatically removed after it has
* been executed, so you have to call this method
* only if you wish to <strong>cancel</strong>
* the action.
*
* @param action action to be removed
*/
public void removeDoWhenBooted( CompletionAction action )
{
collBootCompletion.remove( action );
}
/**
* Registers a listener to be informed about
* server status changes. These changes include
* the server starting to run (or more precisely
* being successfully contact), the server being
* stopped (or more precisely having lost contact),
* and status changes, which are updated regularly
* when the ping-thread was started.
*
* @param l listener to be added
*/
public void addListener( ServerListener l )
{
em.addListener( l );
}
/**
* Unregisters a listener from being informed about
* server status changes.
*
* @param l listener to be removed
*/
public void removeListener( ServerListener l )
{
em.removeListener( l );
}
protected void changed( int id )
{
em.dispatchEvent( new ServerEvent( this, id, System.currentTimeMillis(), this ));
}
private void bootServerApp( boolean startAliveThread )
{
final int port = getAddr().getPort();
final List cmdList = getOptions().toOptionList( port );
cmdList.add( 0, Server.program );
final String[] cmdArray = ServerOptions.optionListToStringArray( cmdList );
Server.inform( "Booting SuperCollider server at " + getOptions().getProtocol().toUpperCase() + " port " + port + " ..." );
// bootThread = new BootThread( this, cmdArray, getOptions().getEnvMap(), startAliveThread );
synchronized( syncBootThread ) {
bootThread = new BootThread( this, cmdArray, startAliveThread );
}
}
/**
* Returns <code>true</code> if the
* server has an address on the same
* machine as the client.
*/
public boolean isLocal()
{
return isLocal;
}
/**
* Returns <code>true</code> if the
* server was locally booted by this client.
*
* @return <code>true</code> if we booted
* the server ourselves. this suggests
* that we are also responsible for shutting
* it down when quitting the client
*/
public boolean didWeBootTheServer()
{
return( bootThread != null );
}
/**
* Begins to ping the server in regular intervals
* to detect a newly established connection, the
* loss of the connection and the current server status.
* By default, this thread is started after booting
* the local server. In other cases, for the running
* status to become valid and for the listeners to
* be informed about server starts and stops, this
* method needs to be called manually.
* <p>
* If the ping-thread is already running, this method does nothing.
*
* @throws IOException if a networking error occurs
*
* @synchronization must be called in the event thread
*/
public void startAliveThread()
throws IOException
{
startAliveThread( 2.0f, 0.7f, 4 );
}
/**
* Starts the ping-thread with specified
* initial delay and ping period.
* <p>
* If the ping-thread is already running, this method does nothing.
*
* @param delay delay in seconds after which
* the first ping is send (defaults to 2 seconds)
* @param period period in seconds at which pings
* are send (defaults to 0.7 seconds).
* note that the behaviour of lost pings might
* change in a future version. As of now, a lost
* ping will result in server running status becoming
* false and firing a corresponding server event to
* registered listeners. The can be inappropriate
* if the listener starts to dispose internals while
* actually the server was just too slow and keeps
* playing orphaned synths.
*
* @throws IOException if a networking error occurs
*
* @synchronization must be called in the event thread
*/
public void startAliveThread( float delay, float period, int deathBounces )
throws IOException
{
synchronized( syncBootThread ) {
if( aliveThread == null ) {
aliveThread = new StatusWatcher( delay, period, deathBounces );
aliveThread.start();
}
}
}
/**
* Stops the ping-thread.
* Note that the thread is a demon. Therefore
* it will not block quitting the java VM and will
* automatically terminate <code>System.exit</code> is called.
*
* @throws IOException if a networking error occurs
*
* @synchronization must be called in the event thread
*/
public void stopAliveThread()
throws IOException
{
synchronized( syncBootThread ) {
if( aliveThread != null ) {
aliveThread.stop();
aliveThread = null;
}
}
}
// private static void resumeThreads()
// throws IOException
// {
// Server server;
//
// for( Iterator iter = setServers.iterator(); iter.hasNext(); ) {
// server = (Server) iter.next();
// server.stopAliveThread();
// server.startAliveThread( 0.7f, 0.7f);
// }
// }
protected void status()
throws IOException
{
sendMsg( statusMsg );
}
/**
* Sends an OSC message to the server
*
* @param msg the message to send
*
* @throws IOException if sending the message fails.
* this can happen because of a network error,
* because of a malformed message or because of
* a buffer overflow (message exceeding 8K)
*/
public void sendMsg( OSCMessage msg )
throws IOException
{
// trns.send( msg );
c.send( msg );
}
/**
* Sends an OSC bundle for scheduling to the server
*
* @param bndl the bundle to send
*
* @throws IOException if sending the bundle fails.
* this can happen because of a network error,
* because of a malformed bundle or its contained messages
* or because of a buffer overflow (bundle exceeding 8K)
*/
public void sendBundle( OSCBundle bndl )
throws IOException
{
// trns.send( bndl );
c.send( bndl );
}
/**
* Sends a message and waits for a corresponding <code>/done</code>
* reply from the server.
*
* @param msg the message to send
* @param timeout the maximum amount of time in seconds to wait
* @return <code>true</code> if the successfull reply was
* receivied within the timeout; <code>false</code> if
* no reply was received in time or a <code>/fail</code>
* message was received.
*
* @throws IOException if sending the message or receiving the reply fails
*/
public boolean sendMsgSync( OSCMessage msg, float timeout )
throws IOException
{
final OSCMessage result = sendMsgSync( msg, "/done", "/fail", 0, msg.getName(), timeout );
return( (result != null) && result.getName().equals( "/done" ));
}
/**
* Sends a message and waits for a corresponding reply or failure message
* from the server.
*
* @param msg the message to send
* @param doneCmd the OSC command with which the server replies upon success
* @param failCmd the OSC command with which the server replies upon failure (can be <code>null</code>)
* @param doneArgIdx the OSC reply message argument index to match
* @param doneArgMatch the OSC reply message argument value to match
* @param timeout the maximum amount of time in seconds to wait
* @return the reply message or <code>null</code> if
* no reply was received in time
*
* @throws IOException if sending the message or receiving the reply fails
*/
public OSCMessage sendMsgSync( OSCMessage msg, String doneCmd, String failCmd, int doneArgIdx, Object doneArgMatch, float timeout )
throws IOException
{
return sendMsgSync( msg, doneCmd, failCmd,
new int[] { doneArgIdx }, new Object[] { doneArgMatch },
new int[] { 0 }, new Object[] { msg.getName() },
timeout );
}
/**
* Sends a message and waits for a corresponding reply or failure message
* from the server.
*
* @param msg the message to send
* @param doneCmd the OSC command with which the server replies upon success
* @param failCmd the OSC command with which the server replies upon failure (can be <code>null</code>)
* @param doneArgIndices the OSC reply message argument indices to match for success
* @param doneArgMatches the OSC reply message argument values to match for success
* @param failArgIndices the OSC reply message argument indices to match for failure
* @param failArgMatches the OSC reply message argument values to match for failure
* @param timeout the maximum amount of time in seconds to wait
* @return the reply message or <code>null</code> if
* no reply was received in time
*
* @throws IOException if sending the message or receiving the reply fails
*/
public OSCMessage sendMsgSync( OSCMessage msg, String doneCmd, String failCmd,
int[] doneArgIndices, Object[] doneArgMatches,
int[] failArgIndices, Object[] failArgMatches,
float timeout )
throws IOException
{
final SyncResponder resp = new SyncResponder( doneCmd, failCmd, doneArgIndices, doneArgMatches, failArgIndices, failArgMatches );
try {
synchronized( resp ) {
resp.add();
sendMsg( msg );
resp.wait( (long) (timeout * 1000) );
}
}
catch( InterruptedException e1 ) { /* ignored */ }
finally {
resp.remove();
}
return resp.replyMsg;
}
/**
* Sends a bundle and waits for a <code>/done</code>
* reply for a given command name from the server.
*
* @param bndl the bundle to send
* @param cmdName to name of the message command to be replied to
* @param timeout the maximum amount of time in seconds to wait
* @return <code>true</code> if the successfull reply was
* receivied within the timeout; <code>false</code> if
* no reply was received in time or a <code>/fail</code>
* message was received.
*
* @throws IOException if sending the bundle or receiving the reply fails
*/
public boolean sendBundleSync( OSCBundle bndl, String cmdName, float timeout )
throws IOException
{
final OSCMessage result = sendBundleSync( bndl, "/done", "/fail", 0, cmdName, timeout );
return( (result != null) && result.getName().equals( "/done" ) );
}
/**
* Sends a bundle and waits for a corresponding reply or failure message
* from the server.
*
* @param bndl the bundle to send
* @param doneCmd the OSC command with which the server replies upon success
* @param failCmd the OSC command with which the server replies upon failure (can be <code>null</code>)
* @param argIdx the OSC reply message argument index to match
* @param argMatch the OSC reply message argument value to match
* @param timeout the maximum amount of time in seconds to wait
* @return the reply message or <code>null</code> if
* no reply was received in time
*
* @throws IOException if sending the bundle or receiving the reply fails
*/
public OSCMessage sendBundleSync( OSCBundle bndl, String doneCmd, String failCmd, int argIdx, Object argMatch, float timeout )
throws IOException
{
return sendBundleSync( bndl, doneCmd, failCmd, new int[] { argIdx }, new Object[] { argMatch },
new int[ 0 ], new Object[ 0 ], timeout );
}
/**
* Sends a bundle and waits for a corresponding reply or failure message
* from the server.
*
* @param bndl the bundle to send
* @param doneCmd the OSC command with which the server replies upon success
* @param failCmd the OSC command with which the server replies upon failure (can be <code>null</code>)
* @param doneArgIndices the OSC reply message argument indices to match
* @param doneArgMatches the OSC reply message argument values to match
* @param timeout the maximum amount of time in seconds to wait
* @return the reply message or <code>null</code> if
* no reply was received in time
*
* @throws IOException if sending the bundle or receiving the reply fails
*/
public OSCMessage sendBundleSync( OSCBundle bndl, String doneCmd, String failCmd,
int[] doneArgIndices, Object[] doneArgMatches,
int[] failArgIndices, Object[] failArgMatches,
float timeout )
throws IOException
{
final SyncResponder resp = new SyncResponder( doneCmd, failCmd, doneArgIndices, doneArgMatches, failArgIndices, failArgMatches );
try {
synchronized( resp ) {
resp.add();
sendBundle( bndl );
resp.wait( (long) (timeout * 1000) );
}
}
catch( InterruptedException e1 ) { /* ignored */ }
finally {
resp.remove();
}
return resp.replyMsg;
}
/**
* Sends a <code>/sync</code> message to the server and waits for a
* corresponding <code>/synced</code> reply.
*
* @param timeout the maximum amount of time in seconds to wait
* @return <code>true</code> if the successfull reply was
* receivied within the timeout.
*
* @throws IOException if sending the message or receiving the reply fails
*/
public boolean sync( float timeout )
throws IOException
{
return sync( null, timeout );
}
/**
* Attaches a <code>/sync</code> message to the list of messages in a
* bundle and sends the bundle to server, waiting for a
* corresponding <code>/synced</code> reply.
*
* @param bndl the bundle to send. a <code>/sync</code> message
* is appended to this bundle. <code>bndl</code> may
* be <code>null</code>, in this case the <code>/sync</code>
* is send alone.
* @param timeout the maximum amount of time in seconds to wait
* @return <code>true</code> if the successfull reply was
* receivied within the timeout.
*
* @throws IOException if sending the message or receiving the reply fails
*/
public boolean sync( OSCBundle bndl, float timeout )
throws IOException
{
final Integer id = new Integer( UniqueID.next() );
if( bndl == null ) bndl = new OSCBundle();
bndl.addPacket( new OSCMessage( "/sync", new Object[] { id }));
return( sendBundleSync( bndl, "/synced", null, 0, id, timeout ) != null );
}
/**
* Allocates a new free node ID for a group or synth.
*
* @return the new node ID.
*
* @todo should be accompanied by a nextPermanentNodeID method
* @todo should throw a RuntimeException if the allocator
* reaches its limit?
*/
public int nextNodeID()
{
return nodeAllocator.alloc();
}
/**
* For internal use by <code>Buffer</code> objects.
* Do not use yourself.
*/
protected void addBuf( Buffer buf )
{
// Buffer objects are cached in an Array for easy
// auto buffer info updating
bufferArray[ buf.getBufNum() ] = buf;
}
/**
* For internal use by <code>Buffer</code> objects.
* Do not use yourself.
*/
protected void freeBuf( int idx )
{
bufferArray[ idx ] = null;
}
/**
* For internal use by <code>Buffer</code> objects.
* Do not use yourself.
*/
protected void waitForBufInfo()
throws IOException
{
// /b_info on the way
// keeps a reference count of waiting Buffers so that only one responder is needed
if( !waitingForBufInfo ) {
bufInfoResponder = new OSCResponderNode( this, "/b_info", new OSCResponderNode.Action() {
public void respond( OSCResponderNode r, OSCMessage msg, long time )
{
if( msg.getArgCount() < 4 ) return;
try {
final Buffer buf = bufferArray[ ((Number) msg.getArg( 0 )).intValue() ];
if( buf != null ) {
buf.setNumFrames( ((Number) msg.getArg( 1 )).intValue() );
buf.setNumChannels( ((Number) msg.getArg( 2 )).intValue() );
buf.setSampleRate( ((Number) msg.getArg( 3 )).doubleValue() );
buf.queryDone();
if( --waitingBufs == 0 ) {
waitingForBufInfo = false;
r.remove();
}
}
}
catch( ClassCastException e2 ) {
printError( "Server.waitForBufInfo", e2 );
}
}
}).add();
waitingForBufInfo = true;
}
waitingBufs++;
}
private void resetBufferAutoInfo()
throws IOException
{
bufferArray = new Buffer[ options.getNumBuffers() ];
waitingBufs = 0;
waitingForBufInfo = false;
if( bufInfoResponder != null ) {
bufInfoResponder.remove();
}
}
/**
* Prints a textual representation of this
* object onto the given stream
*
* @param stream the stream to print on
*/
public void printOn( PrintStream stream )
{
stream.print( "Server(" + getName() + "," + getAddr() + "," + getOptions() + "," + getClientID() + ")" );
}
/**
* Sends an asynchronous <code>/quit</code>
* to the server and cleans up the client's resources
*
* @throws IOException if the message failed to be sent
* or if cleanup failed
*
* @warning unlike in SClang, this stops the alive thread,
* so in case you want to recognize remote server starts,
* call <code>startAliveThread</code> afterwards.
*
* @synchronization must be called in the event thread
*/
public void quit()
throws IOException
{
sendMsg( quitMsg() );
Server.inform( "/quit sent" );
cleanUpAfterQuit();
}
/**
* Constructs a quit message for the server
*
* @return OSCMessage requesting the server to quit
*/
public OSCMessage quitMsg()
{
return new OSCMessage( "/quit", OSCMessage.NO_ARGS );
}
private void cleanUpAfterQuit()
{
try {
stopAliveThread();
// alive = false;
dumpMode = 0;
setBooting( false );
setRunning( false );
// if(scopeWindow.notNil) { scopeWindow.quit };
// new RootNode( this ).freeAll();
//
// sendMsg( new OSCMessage( "/g_freeAll", new Object[] { new Integer( 0 )}));
//try {
// Thread.sleep( 1000 );
//}
//catch( InterruptedException e1 ) {}
// initCommunication();
createNewAllocators();
resetBufferAutoInfo();
}
catch( IOException e1 ) {
printError( "Server.cleanUpAfterQuit", e1 );
}
}
/**
* Disposes any resources
* allocated by this representation.
* This shuts down OSC communication
* and server event dispatching.
* Do not use this object any more
* after calling this method.
*
* @synchronization must be called in the event thread
*/
public void dispose()
{
multi.dispose();
setServers.remove( this );
mapServerNames.remove( getName() );
em.dispose();
}
/**
* Synchronously quit the server. If the server is not
* booting and not running according to the running status,
* this method returns immediately. Otherwise sends a <code>/quit</code>
* message and waits for the reply. If no reply is received,
* and the server was locally booted, terminates the server process.
*
* @return <code>true</code> if the server has successfully quit
*
* @throws IOException if the message failed to be sent
* or if cleanup failed
*
* @synchronization must be called in the event thread
*/
public boolean quitAndWait()
throws IOException
{
try {
if( !isBooting() && !isRunning() ) return true;
final OSCMessage msg = quitMsg();
for( int i = 0; i < 16; i++ ) {
if( sendMsgSync( msg, 0.5f )) {
cleanUpAfterQuit();
return true;
}
}
// ok, now last chance : if local, kill the process
if( isLocal ) {
try {
synchronized( syncBootThread ) {
if( bootThread != null ) {
bootThread.keepScRunning = false;
syncBootThread.wait( 4000 );
}
}
}
catch( InterruptedException e1 ) { /* ignored */ }
}
if( !isBooting() && !isRunning() ) return true;
printOn( printStream );
printStream.println( " : failed to quit!" );
return false;
}
finally {
cleanUpAfterQuit();
}
}
/**
* Synchronously quits all known servers.
* That is all servers for which a <code>Server</code>
* representation instance has been created.
* Cathes all thrown exceptions.
*
* @synchronization must be called in the event thread
*/
public static void quitAll()
{
Server s;
for( Iterator iter = setServers.iterator(); iter.hasNext(); ) {
s = (Server) iter.next();
if( s.isLocal ) {
try {
s.quitAndWait();
}
catch( IOException e1 ) {
printError( "Server.quitAll", e1 );
}
}
s.dispose();
}
}
protected static void printError( String name, Throwable t )
{
// printStream.println( name + " : " + t.getClass().getName() + " : " + t.getLocalizedMessage() );
printStream.print( name + " : " );
t.printStackTrace( printStream );
}
// ----------- EventManager.Processor interface -----------
/**
* This is used to dispatch
* server events. Do not call this method.
*/
public void processEvent( BasicEvent e )
{
ServerListener listener;
final ServerEvent sce = (ServerEvent) e;
for( int i = 0; i < em.countListeners(); i++ ) {
listener = (ServerListener) em.getListener( i );
listener.serverAction( sce );
}
}
// ----------- internal clases and interfaces -----------
/**
* This interface is used to describe an action
* to be executed after some process such as
* booting the server has completed.
*
* @see Server#addDoWhenBooted( Server.CompletionAction )
*/
public static interface CompletionAction
{
/**
* Requests the implementing class to
* perform any action it wishes. This is
* called after the corresponding process
* to which this action was attached, has completed.
*
* @param server the server representation
* for which the process completed
*/
public void completion( Server server );
}
private class BootThread
extends Thread
{
private final String[] cmdArray;
// private final String[] envArray;
protected volatile boolean keepScRunning = true;
protected final Server server;
private final boolean startAliveThread;
// private BootThread( Server server, String[] cmdArray, Map envMap, boolean startAliveThread )
protected BootThread( Server server, String[] cmdArray, boolean startAliveThread )
{
super( server.getName() );
this.cmdArray = cmdArray;
this.server = server;
this.startAliveThread = startAliveThread;
// if( envMap != null ) {
// Map.Entry me;
// final Iterator iter = envMap.entrySet().iterator();
// envArray = new String[ envMap.size() ];
// for( int i = 0; iter.hasNext(); i++ ) {
// me = (Map.Entry) iter.next();
// envArray[ i ] = me.getKey().toString() + "=" + me.getValue().toString();
// }
// } else {
// envArray = null;
// }
setDaemon( true );
start();
}
public void run()
{
Process p = null;
int resultCode = -1;
boolean pRunning = true;
boolean cStarted = false;
InputStream inStream, errStream;
final byte[] inBuf = new byte[128];
final byte[] errBuf = new byte[128];
final File cwd = new File( cmdArray[0] ).getParentFile();
try {
// NOTE: using envArray will make some scsynth versions (e.g. PPC G3) fail to boot
// because they seem to rely on some environment variables that get lost this way
// (providing a null argument preserves the current environment variables)
// p = Runtime.getRuntime().exec( cmdArray, envArray, cwd );
p = Runtime.getRuntime().exec( cmdArray, null, cwd );
// "Implementation note: It is a good idea for the input stream to be buffered."
inStream = new BufferedInputStream( p.getInputStream() );
errStream = new BufferedInputStream( p.getErrorStream() );
while( keepScRunning && pRunning ) {
if( !cStarted ) {
try {
//System.err.println( "...try" );
server.start();
//System.err.println( "...succeeded" );
cStarted = true;
if( startAliveThread ) server.startAliveThread( 2.0f, 0.7f, 8 ); // allow really long unresponsiveness as a real server quit is recognized instantly
}
// thrown when in TCP mode and socket not yet available
catch( ConnectException e1 ) {
//e1.printStackTrace();
}
}
try {
Thread.sleep( 500 ); // a kind of cheesy way to wait for the program to end
}
catch( InterruptedException e5 ) { /* ignored */ }
handleConsole( inStream, inBuf );
handleConsole( errStream, errBuf );
try {
resultCode = p.exitValue();
pRunning = false;
p = null;
printStream.println( "scsynth terminated (" + resultCode +")" );
}
// gets thrown if we call exitValue() while sc still running
catch( IllegalThreadStateException e1 ) { /* ignored */ }
} // while( keepScRunning && pRunning )
}
catch( IOException e3 ) {
printError( "BootThread.run", e3 );
}
finally {
if( p != null ) {
// printStream.println( "scsynth didn't quit. we're killing it!" );
p.destroy();
}
synchronized( syncBootThread ) {
try {
server.stopAliveThread();
}
catch( IOException e1 ) {
printError( "Server.stopAliveThread", e1 );
}
server.bootThread = null; // ! must be before setRunning !
server.setBooting( false );
server.setRunning( false );
syncBootThread.notifyAll();
}
}
}
// redirect console
private void handleConsole( InputStream stream, byte[] buf )
{
int i;
try {
while( stream.available() > 0 ) {
i = Math.min( buf.length, stream.available() );
stream.read( buf, 0, i );
printStream.write( buf, 0, i );
}
}
catch( IOException e1 ) { /* ignored XXX */ }
}
}
/**
* A static field only class
* describing the snapshot of the server status
* as delivered by <code>getStatus</code>
*
* @see #getStatus()
*/
public static class Status
{
public int numUGens;
public int numSynths;
public int numGroups;
public int numSynthDefs;
public float avgCPU;
public float peakCPU;
public volatile double sampleRate;
public volatile double actualSampleRate;
protected static Status copyFrom( Status s )
{
final Status result = new Status();
synchronized( s ) {
result.numUGens = s.numUGens;
result.numSynths = s.numSynths;
result.numGroups = s.numGroups;
result.numSynthDefs = s.numSynthDefs;
result.avgCPU = s.avgCPU;
result.peakCPU = s.peakCPU;
result.sampleRate = s.sampleRate;
result.actualSampleRate = s.actualSampleRate;
}
return result;
}
}
private class StatusWatcher
// extends TimerTask
implements OSCResponderNode.Action, ActionListener
{
private int alive = 0;
private final int delayMillis;
private final int periodMillis;
private final OSCResponderNode resp;
private final int deathBounces;
private final Timer timer;
// private StatusWatcher( float delay, float period )
// {
// this( delay, period, 4 );
// }
//
protected StatusWatcher( float delay, float period, int deathBounces )
{
delayMillis = (int) (delay * 1000);
periodMillis = (int) (period * 1000);
resp = new OSCResponderNode( enc_this, "/status.reply", this );
this.deathBounces = deathBounces;
timer = new Timer( periodMillis, this );
timer.setInitialDelay( delayMillis );
}
protected void start()
throws IOException
{
//System.err.println( "start" );
//new Throwable().printStackTrace();
resp.add();
timer.restart();
// appClock.schedule( this, delayMillis, periodMillis );
}
protected void stop()
throws IOException
{
//System.err.println( "stop" );
//new Throwable().printStackTrace();
// this.cancel();
timer.stop();
resp.remove();
}
public void actionPerformed( ActionEvent e )
{
//System.err.println( "setRunning( "+alive+" )" );
if( alive > 0 ) {
setRunning( true );
alive--;
} else {
setRunning( false );
}
if( serverBooting && getOptions().getProtocol().equals( OSCChannel.TCP ) && !c.isConnected() ) {
try {
// c.connect();
c.start();
}
catch( IOException e1 ) {
printError( "Server.status", e1 );
}
} else {
try {
status();
}
catch( IOException e1 ) {
printError( "Server.status", e1 );
}
}
}
// XXX create specific osc message decoder
public void respond( OSCResponderNode r, OSCMessage msg, long time )
{
if( msg.getArgCount() < 9 ) return;
alive = deathBounces;
try {
// msg.at( 0 ) == 1
synchronized( status ) {
status.numUGens = ((Number) msg.getArg( 1 )).intValue();
status.numSynths = ((Number) msg.getArg( 2 )).intValue();
status.numGroups = ((Number) msg.getArg( 3 )).intValue();
status.numSynthDefs = ((Number) msg.getArg( 4 )).intValue();
status.avgCPU = ((Number) msg.getArg( 5 )).floatValue();
status.peakCPU = ((Number) msg.getArg( 6 )).floatValue();
status.sampleRate = ((Number) msg.getArg( 7 )).doubleValue();
status.actualSampleRate = ((Number) msg.getArg( 8 )).doubleValue();
}
// setRunning( true ); // should be thread safe ?
changed( ServerEvent.COUNTS );
}
catch( ClassCastException e1 ) {
printError( "StatusWatcher.messageReceived", e1 );
}
}
}
/*
* A helper OSC responder
* for asynchronous communication.
*/
private class SyncResponder
implements OSCResponderNode.Action
{
protected volatile OSCMessage replyMsg = null;
private final OSCResponderNode doneResp;
private final OSCResponderNode failResp;
private final String doneCmdName;
private final int[] doneArgIndices;
private final Object[] doneArgMatches;
private final int doneMinArgNum;
private final String failCmdName;
private final int[] failArgIndices;
private final Object[] failArgMatches;
private final int failMinArgNum;
// protected SyncResponder( String doneCmdName, String failCmdName, int argIdx, Object argMatch )
// throws IOException
// {
// this( doneCmdName, failCmdName, new int[] { argIdx }, new Object[] { argMatch });
// }
protected SyncResponder( String doneCmdName, String failCmdName,
int[] doneArgIndices, Object[] doneArgMatches,
int[] failArgIndices, Object[] failArgMatches )
throws IOException
{
this.doneCmdName = doneCmdName;
this.doneArgIndices = doneArgIndices;
this.doneArgMatches = doneArgMatches;
this.failCmdName = failCmdName;
this.failArgIndices = failArgIndices;
this.failArgMatches = failArgMatches;
int i = 0;
for( int j = 0; j < doneArgIndices.length; j++ ) i = Math.max( i, doneArgIndices[ j ]);
doneMinArgNum = i;
doneResp = new OSCResponderNode( enc_this, doneCmdName, this );
if( failCmdName != null ) {
i = 0;
for( int j = 0; j < failArgIndices.length; j++ ) i = Math.max( i, failArgIndices[ j ]);
failMinArgNum = i;
failResp = new OSCResponderNode( enc_this, failCmdName, this );
} else {
failMinArgNum = 0;
failResp = null;
}
}
protected void add()
throws IOException
{
doneResp.add();
if( failResp != null ) failResp.add();
}
protected void remove()
{
doneResp.remove();
if( failResp != null ) failResp.remove();
}
public void respond( OSCResponderNode r, OSCMessage msg, long time )
{
if( msg.getName().equals( doneCmdName )) {
doneMessageReceived( msg );
} else if( msg.getName().equals( failCmdName )) {
failMessageReceived( msg );
} else {
assert false : msg.getName();
}
}
private void doneMessageReceived( OSCMessage msg )
{
if( msg.getArgCount() < doneMinArgNum ) return;
for( int i = 0; i < doneArgIndices.length; i++ ) {
if( !msg.getArg( doneArgIndices[ i ]).equals( doneArgMatches[ i ])) return;
}
replyMsg = msg;
remove();
synchronized( this ) {
this.notifyAll();
}
}
private void failMessageReceived( OSCMessage msg )
{
if( msg.getArgCount() < failMinArgNum ) return;
for( int i = 0; i < failArgIndices.length; i++ ) {
if( !msg.getArg( failArgIndices[ i ]).equals( failArgMatches[ i ])) return;
}
replyMsg = msg;
remove();
synchronized( this ) {
this.notifyAll();
}
}
}
}