@ -12,24 +12,24 @@ MIT License
Copyright 2014 - 2020 Nils Hilbricht https : / / www . laborejo . org
Permission is hereby granted , free of charge , to any person obtaining a copy of this software and
associated documentation files ( the " Software " ) , to deal in the Software without restriction ,
including without limitation the rights to use , copy , modify , merge , publish , distribute ,
sublicense , and / or sell copies of the Software , and to permit persons to whom the Software is
Permission is hereby granted , free of charge , to any person obtaining a copy of this software and
associated documentation files ( the " Software " ) , to deal in the Software without restriction ,
including without limitation the rights to use , copy , modify , merge , publish , distribute ,
sublicense , and / or sell copies of the Software , and to permit persons to whom the Software is
furnished to do so , subject to the following conditions :
The above copyright notice and this permission notice shall be included in all copies or
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software .
THE SOFTWARE IS PROVIDED " AS IS " , WITHOUT WARRANTY OF ANY KIND , EXPRESS OR IMPLIED , INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY , FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT . IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM ,
DAMAGES OR OTHER LIABILITY , WHETHER IN AN ACTION OF CONTRACT , TORT OR OTHERWISE , ARISING FROM , OUT
THE SOFTWARE IS PROVIDED " AS IS " , WITHOUT WARRANTY OF ANY KIND , EXPRESS OR IMPLIED , INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY , FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT . IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM ,
DAMAGES OR OTHER LIABILITY , WHETHER IN AN ACTION OF CONTRACT , TORT OR OTHERWISE , ARISING FROM , OUT
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE .
"""
import logging ;
logger = None #filled by init with prettyName
logger = None #filled by init with prettyName
import struct
import socket
@ -51,9 +51,9 @@ class _IncomingMessage(object):
def __init__ ( self , dgram ) :
#NSM Broadcasts are bundles, but very simple ones. We only need to care about the single message it contains.
#Therefore we can strip the bundle prefix and handle it as normal message.
if b " #bundle " in dgram :
bundlePrefix , singleMessage = dgram . split ( b " / " , maxsplit = 1 )
#Therefore we can strip the bundle prefix and handle it as normal message.
if b " #bundle " in dgram :
bundlePrefix , singleMessage = dgram . split ( b " / " , maxsplit = 1 )
dgram = b " / " + singleMessage # / eaten by split
self . isBroadcast = True
else :
@ -108,13 +108,13 @@ class _IncomingMessage(object):
ValueError if the datagram could not be parsed .
"""
#First test for empty string, which is nothing, followed by a terminating \x00 padded by three additional \x00.
if dgram [ start_index : ] . startswith ( b " \x00 \x00 \x00 \x00 " ) :
if dgram [ start_index : ] . startswith ( b " \x00 \x00 \x00 \x00 " ) :
return " " , start_index + 4
#Otherwise we have a non-empty string that must follow the rules of the docstring.
offset = 0
try :
offset = 0
try :
while dgram [ start_index + offset ] != 0 :
offset + = 1
if offset == 0 :
@ -128,7 +128,7 @@ class _IncomingMessage(object):
# do it ourselves.
if offset > len ( dgram [ start_index : ] ) :
raise ValueError ( ' Datagram is too short ' )
data_str = dgram [ start_index : start_index + offset ]
data_str = dgram [ start_index : start_index + offset ]
return data_str . replace ( b ' \x00 ' , b ' ' ) . decode ( ' utf-8 ' ) , start_index + offset
except IndexError as ie :
raise ValueError ( ' Could not parse datagram %s ' % ie )
@ -155,7 +155,7 @@ class _IncomingMessage(object):
def parse_datagram ( self ) :
try :
self . _address_regexp , index = self . get_string ( self . _dgram , 0 )
self . _address_regexp , index = self . get_string ( self . _dgram , 0 )
if not self . _dgram [ index : ] :
# No params is legit, just return now.
return
@ -276,8 +276,8 @@ class NSMClient(object):
elif loggingLevel == " error " or loggingLevel == 40 :
logging . basicConfig ( level = logging . ERROR ) #production
else :
raise ValueError ( " Unknown logging level: {} . Choose ' info ' or ' error ' " . format ( loggingLevel ) )
logging . warning ( " Unknown logging level: {} . Choose ' info ' or ' error ' " . format ( loggingLevel ) )
logging . basicConfig ( level = logging . INFO ) #development
#given parameters,
self . prettyName = prettyName #keep this consistent! Settle for one name.
@ -298,16 +298,15 @@ class NSMClient(object):
" /nsm/client/hide_optional_gui " : lambda msg : self . hideGUICallback ( ) ,
" /nsm/client/session_is_loaded " : self . _sessionIsLoadedCallback ,
#Hello source-code reader. You can add your own reactions here by nsmClient.reactions[oscpath]=func, where func gets the raw _IncomingMessage OSC object as argument.
#broadcast is handled directly by the function because it has more parameters
#broadcast is handled directly by the function because it has more parameters
}
#self.discardReactions = set(["/nsm/client/session_is_loaded"])
self . discardReactions = set ( )
#Networking and Init
self . sock = socket . socket ( socket . AF_INET , socket . SOCK_DGRAM ) #internet, udp
self . sock . bind ( ( ' ' , 0 ) ) #pick a free port on localhost.
ip , port = self . sock . getsockname ( )
ip , port = self . sock . getsockname ( )
self . ourOscUrl = f " osc.udp:// { ip } : { port } / "
self . executableName = self . getExecutableName ( )
@ -324,7 +323,9 @@ class NSMClient(object):
self . ourClientId = None # the "file extension" of ourClientNameUnderNSM
self . isVisible = None #set in announceGuiVisibility
self . saveStatus = True # true is clean. false means we need saving.
self . announceOurselves ( )
assert self . serverFeatures , self . serverFeatures
assert self . sessionName , self . sessionName
assert self . ourPath , self . ourPath
@ -333,18 +334,20 @@ class NSMClient(object):
self . sock . setblocking ( False ) #We have waited for tha handshake. Now switch blocking off because we expect sock.recvfrom to be empty in 99.99...% of the time so we shouldn't wait for the answer.
#After this point the host must include self.reactToMessage in its event loop
#We assume we are save at startup.
self . announceSaveStatus ( isClean = True )
#We assume we are save at startup.
self . announceSaveStatus ( isClean = True )
logger . info ( " NSMClient client init complete. Going into listening mode. " )
def reactToMessage ( self ) :
""" This is the main loop message. It is added to the clients event loop. """
try :
data , addr = self . sock . recvfrom ( 4096 ) #4096 is quite big. We don't expect nsm messages this big. Better safe than sorry. However, messages will crash the program if they are bigger than 4096.
data , addr = self . sock . recvfrom ( 4096 ) #4096 is quite big. We don't expect nsm messages this big. Better safe than sorry. However, messages will crash the program if they are bigger than 4096.
except BlockingIOError : #happens while no data is received. Has nothing to do with blocking or not.
return None
msg = _IncomingMessage ( data )
msg = _IncomingMessage ( data )
if msg . oscpath in self . reactions :
self . reactions [ msg . oscpath ] ( msg )
elif msg . oscpath in self . discardReactions :
@ -355,13 +358,13 @@ class NSMClient(object):
logger . info ( " Got /reply Saved from NSM Server " )
elif msg . isBroadcast :
if self . broadcastCallback :
logger . info ( f " Got broadcast with messagePath { msg . oscpath } and listOfArguments { msg . params } " )
logger . info ( f " Got broadcast with messagePath { msg . oscpath } and listOfArguments { msg . params } " )
self . broadcastCallback ( self . ourPath , self . sessionName , self . ourClientNameUnderNSM , msg . oscpath , msg . params )
else :
logger . info ( f " No callback for broadcast! Got messagePath { msg . oscpath } and listOfArguments { msg . params } " )
logger . info ( f " No callback for broadcast! Got messagePath { msg . oscpath } and listOfArguments { msg . params } " )
elif msg . oscpath == " /error " :
logger . warning ( " Got /error from NSM Server. Path: {} , Parameter: {} " . format ( msg . oscpath , msg . params ) )
else :
else :
logger . warning ( " Reaction not implemented:. Path: {} , Parameter: {} " . format ( msg . oscpath , msg . params ) )
@ -371,11 +374,11 @@ class NSMClient(object):
if host and port :
url = ( host , port )
else :
url = self . nsmOSCUrl
url = self . nsmOSCUrl
msg = _OutgoingMessage ( path )
for arg in listOfParameters :
msg . add_arg ( arg ) #type is auto-determined by outgoing message
self . sock . sendto ( msg . build ( ) , url )
self . sock . sendto ( msg . build ( ) , url )
def getNsmOSCUrl ( self ) :
""" Return and save the nsm osc url or raise an error """
@ -418,28 +421,30 @@ class NSMClient(object):
else :
return " "
logger . info ( " Sending our NSM-announce message " )
announce = _OutgoingMessage ( " /nsm/server/announce " )
announce . add_arg ( self . prettyName ) #s:application_name
announce . add_arg ( buildClientFeaturesString ( ) ) #s:capabilities
announce . add_arg ( self . executableName ) #s:executable_name
announce . add_arg ( 1 ) #i:api_version_major
announce . add_arg ( 2 ) #i:api_version_minor
announce . add_arg ( int ( getpid ( ) ) ) #i:pid
announce . add_arg ( int ( getpid ( ) ) ) #i:pid
hostname , port = self . nsmOSCUrl
assert hostname , self . nsmOSCUrl
assert port , self . nsmOSCUrl
self . sock . sendto ( announce . build ( ) , self . nsmOSCUrl )
#Wait for /reply (aka 'Howdy, what took you so long?)
data , addr = self . sock . recvfrom ( 1024 )
data , addr = self . sock . recvfrom ( 1024 )
msg = _IncomingMessage ( data )
if msg . oscpath == " /error " :
originalMessage , errorCode , reason = msg . params
originalMessage , errorCode , reason = msg . params
logger . error ( " Code {} : {} " . format ( errorCode , reason ) )
quit ( )
elif msg . oscpath == " /reply " :
elif msg . oscpath == " /reply " :
nsmAnnouncePath , welcomeMessage , managerName , self . serverFeatures = msg . params
assert nsmAnnouncePath == " /nsm/server/announce " , nsmAnnouncePath
logger . info ( " Got /reply " + welcomeMessage )
@ -458,7 +463,7 @@ class NSMClient(object):
replyToOpen . add_arg ( " {} is opened or created " . format ( self . prettyName ) )
self . sock . sendto ( replyToOpen . build ( ) , self . nsmOSCUrl )
else :
raise ValueError ( " Unexpected message path after announce " . format ( ( msg . oscpath , msg . params ) ) )
raise ValueError ( " Unexpected message path after announce: {} " . format ( ( msg . oscpath , msg . params ) ) )
def announceGuiVisibility ( self , isVisible ) :
message = " /nsm/client/gui_is_shown " if isVisible else " /nsm/client/gui_is_hidden "
@ -479,7 +484,7 @@ class NSMClient(object):
logger . info ( " Telling NSM that our clients save state is now: {} " . format ( message ) )
self . sock . sendto ( saveStatus . build ( ) , self . nsmOSCUrl )
def _saveCallback ( self , msg ) :
def _saveCallback ( self , msg ) :
logger . info ( " Telling our client to save as {} " . format ( self . ourPath ) )
self . saveCallback ( self . ourPath , self . sessionName , self . ourClientNameUnderNSM )
replyToSave = _OutgoingMessage ( " /reply " )
@ -492,8 +497,10 @@ class NSMClient(object):
def _sessionIsLoadedCallback ( self , msg ) :
if self . sessionIsLoadedCallback :
logger . info ( " Telling our client that the session has finished loading " )
logger . info ( " Received ' Session is Loaded ' . Our client supports it. Forwarding message... " )
self . sessionIsLoadedCallback ( )
else :
logger . info ( " Received ' Session is Loaded ' . Our client does not support it, which is the default. Discarding message... " )
def sigtermHandler ( self , signal , frame ) :
""" Wait for the user to quit the program
@ -560,15 +567,15 @@ class NSMClient(object):
We offer a clean solution in calling this function which will trigger a round trip over the
NSM server so our client thinks it received a Save instruction . This leads to a clean
state with a good saveStatus and no required extra functionality in the client . """
logger . info ( " instructing the NSM-Server to send Save to ourselves. " )
if " server-control " in self . serverFeatures :
#message = _OutgoingMessage("/nsm/server/save") # "Save All" Command.
message = _OutgoingMessage ( " /nsm/gui/client/save " )
message . add_arg ( " {} " . format ( self . ourClientId ) )
message = _OutgoingMessage ( " /nsm/gui/client/save " )
message . add_arg ( " {} " . format ( self . ourClientId ) )
self . sock . sendto ( message . build ( ) , self . nsmOSCUrl )
else :
logger . warning ( " ...but the NSM-Server does not support server control. Server only supports: {} " . format ( self . serverFeatures ) )
logger . warning ( " ...but the NSM-Server does not support server control. Server only supports: {} " . format ( self . serverFeatures ) )
def changeLabel ( self , label : str ) :
""" This function is implemented because it is provided by NSM. However, it does not much.
@ -578,9 +585,9 @@ class NSMClient(object):
This is fine for us as clients , but you need to provide a GUI field to enter that label . """
logger . info ( " Telling the NSM-Server that our label is now " + label )
message = _OutgoingMessage ( " /nsm/client/label " )
message . add_arg ( label ) #s:label
self . sock . sendto ( message . build ( ) , self . nsmOSCUrl )
message = _OutgoingMessage ( " /nsm/client/label " )
message . add_arg ( label ) #s:label
self . sock . sendto ( message . build ( ) , self . nsmOSCUrl )
def broadcast ( self , path : str , arguments : list ) :
""" /nsm/server/broadcast s:path [arguments...]
@ -590,27 +597,27 @@ class NSMClient(object):
"""
if path . startswith ( " /nsm " ) :
logger . warning ( " Attempted broadbast starting with /nsm. Not allwoed " )
else :
else :
logger . info ( " Sending broadcast " + path + repr ( arguments ) )
message = _OutgoingMessage ( " /nsm/server/broadcast " )
message = _OutgoingMessage ( " /nsm/server/broadcast " )
message . add_arg ( path )
for arg in arguments :
message . add_arg ( arg ) #type autodetect
self . sock . sendto ( message . build ( ) , self . nsmOSCUrl )
self . sock . sendto ( message . build ( ) , self . nsmOSCUrl )
def importResource ( self , filePath ) :
""" aka. import into session
ATTENTION ! You will still receive an absolute path from this function . You need to make
sure yourself that this path will not be saved in your save file , but rather use a place -
holder that gets replaced by the actual session path each time . A good point is after
holder that gets replaced by the actual session path each time . A good point is after
serialisation . search & replace for the session prefix ( " ourPath " ) and replace it with a tag
e . g . < sessionDirectory > . The opposite during load .
Only such a behaviour will make your session portable .
Do not use the following pattern : An alternative that comes to mind is to only work with
relative paths and force your programs workdir to the session directory . Better work with
absolute paths internally .
Do not use the following pattern : An alternative that comes to mind is to only work with
relative paths and force your programs workdir to the session directory . Better work with
absolute paths internally .
Symlinks given path into session dir and returns the linked path relative to the ourPath .
It can handles single files as well as whole directories .
@ -633,11 +640,11 @@ class NSMClient(object):
or of our client program . We do not provide any means to unlink or delete files from the
session directory .
"""
#Even if the project was not saved yet now it is time to make our directory in the NSM dir.
if not os . path . exists ( self . ourPath ) :
os . makedirs ( self . ourPath )
filePath = os . path . abspath ( filePath ) #includes normalisation
if not os . path . exists ( self . ourPath ) : raise FileNotFoundError ( self . ourPath )
if not os . path . isdir ( self . ourPath ) : raise NotADirectoryError ( self . ourPath )