diff --git a/.gitignore b/.gitignore index c4401246..363c07b0 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,7 @@ rdlogin/rdlogin rdlogmanager/rdlogmanager rdmonitor/rdmonitor rdpadd/rdpadd +rdpadengined/rdpadengined rdpanel/rdpanel rdrepld/rdrepld rdrepld-suse diff --git a/ChangeLog b/ChangeLog index 4fb70b28..71c860e4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18190,6 +18190,30 @@ 'PyPAD.Update::shouldBeProcessed()' method. * Updated the 'pypad_udp.py' script to use the 'PyPAD.Update::shouldBeProcessed()' method. -2018-12-14 Patrick Linstruth +2018-12-13 Fred Gleason + * Added a 'PYPAD_INSTANCES' table to the database. + * Incremented the database version to 303. + * Added a 'PyPAD Instances' button to the 'Edit Station' dialog + in rdadmin(1). + * Added a 'ListPyPAD Instances' dialog in rdadmin(1). + * Added an 'Edit PyPAD' Instance' dialog in rdadmin(1). +2018-12-13 Fred Gleason + * Added support in 'PyPAD.Receiver::setConfigFile()' to load + configuration information from a Rivendell database. +2018-12-17 Fred Gleason + * Renamed the RD_RLM2_CLIENT_TCP_PORT define to RD_PAD_CLIENT_TCP_PORT. + * Renamed the RD_RLM_SOURCE_UNIX_ADDRESS define to + RD_PAD_SOURCE_UNIX_ADDRESS. + * Added a RD_PYPAD_SCRIPT_DIR define. + * Added an 'RDNotification::PypadType' value to the + 'RDNotification::Type' enumeration. + * Added 'PypadOwner=' and 'PypadGroup=' directives to the + '[Identity]' section of rd.conf(5). + * Added an rdpadengined(8) service. + * Added a 'PYPAD_INSTANCES.IS_RUNNING' field to the database. + * Added a 'PYPAD_INSTANCES.EXIT_CODE' field to the database. + * Added a 'PYPAD_INSTANCES.ERROR_TEXT' field to the database. + * Incremented the database version to 304. +2018-12-17 Patrick Linstruth * Added a 'pypad_tunein.py' PyPAD script. * Added a 'pypad_icecast2.py' PyPAD script. diff --git a/Makefile.am b/Makefile.am index 52ec5d5c..8d80e429 100644 --- a/Makefile.am +++ b/Makefile.am @@ -51,6 +51,7 @@ SUBDIRS = icons\ rdpanel\ rdrepld\ rdpadd\ + rdpadengined\ rdselect\ rdservice\ rdvairplayd\ diff --git a/apis/PyPAD/Makefile.am b/apis/PyPAD/Makefile.am index 4e5f15c1..5937fa10 100644 --- a/apis/PyPAD/Makefile.am +++ b/apis/PyPAD/Makefile.am @@ -21,7 +21,7 @@ ## Use automake to process this into a Makefile.in SUBDIRS = api\ - examples\ + scripts\ tests CLEANFILES = *~\ diff --git a/apis/PyPAD/api/PyPAD.py b/apis/PyPAD/api/PyPAD.py index 7c735497..d963d0d8 100644 --- a/apis/PyPAD/api/PyPAD.py +++ b/apis/PyPAD/api/PyPAD.py @@ -20,7 +20,10 @@ import configparser import datetime +import MySQLdb +import signal import socket +import sys import json # @@ -715,6 +718,16 @@ class Receiver(object): def __PyPAD_Process(self,pad): self.__callback(pad) + def __getDbCredentials(self): + config=configparser.ConfigParser() + config.readfp(open('/etc/rd.conf')) + return (config.get('mySQL','Loginname'),config.get('mySQL','Password'), + config.get('mySQL','Hostname'),config.get('mySQL','Database')) + + def __openDb(self): + creds=self.__getDbCredentials() + return MySQLdb.connect(creds[2],creds[0],creds[1],creds[3]) + def setCallback(self,cb): """ Set the processing callback. @@ -727,11 +740,28 @@ class Receiver(object): the 'PyPAD.Update::config()' method will return a parserconfig object created from the specified file. The file must be in INI format. + + A special case is if the supplied filename string begins with + the '$' character. If so, the remainder of the string is assumed + to be an unsigned integer ID that is used to retrieve the + configuration from the 'PYPAD_INSTANCES' table in the database + pointed to by '/etc/rd.conf'. """ - fp=open(filename) - self.__config_parser=configparser.ConfigParser(interpolation=None) - self.__config_parser.readfp(fp) - fp.close() + if filename[0]=='$': # Get the config from the DB + db=self.__openDb() + cursor=db.cursor() + cursor.execute('select CONFIG from PYPAD_INSTANCES where ID='+ + filename[1::]) + config=cursor.fetchone() + self.__config_parser=configparser.ConfigParser(interpolation=None) + self.__config_parser.read_string(config[0]) + db.close() + + else: # Get the config from a file + fp=open(filename) + self.__config_parser=configparser.ConfigParser(interpolation=None) + self.__config_parser.readfp(fp) + fp.close() def start(self,hostname,port): """ @@ -746,6 +776,9 @@ class Receiver(object): port - The TCP port to connect to. For most cases, just use 'PyPAD.PAD_TCP_PORT'. """ + # So we exit cleanly when shutdown by rdpadengined(8) + signal.signal(signal.SIGTERM,SigHandler) + sock=socket.socket(socket.AF_INET) conn=sock.connect((hostname,port)) c=bytes() @@ -763,3 +796,5 @@ class Receiver(object): line=bytes() +def SigHandler(signo,stack): + sys.exit(0) diff --git a/apis/PyPAD/examples/Makefile.am b/apis/PyPAD/scripts/Makefile.am similarity index 72% rename from apis/PyPAD/examples/Makefile.am rename to apis/PyPAD/scripts/Makefile.am index cd6aaccb..ad6a5350 100644 --- a/apis/PyPAD/examples/Makefile.am +++ b/apis/PyPAD/scripts/Makefile.am @@ -22,23 +22,32 @@ install-exec-am: mkdir -p $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD - ../../../helpers/install_python.sh now_and_next.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/now_and_next.py ../../../helpers/install_python.sh pypad_filewrite.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_filewrite.py + cp pypad_filewrite.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_filewrite.exemplar ../../../helpers/install_python.sh pypad_icecast2.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_icecast2.py + cp pypad_icecast2.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_icecast2.exemplar ../../../helpers/install_python.sh pypad_tunein.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_tunein.py + cp pypad_tunein.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_tunein.exemplar ../../../helpers/install_python.sh pypad_udp.py $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_udp.py + cp pypad_udp.exemplar $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_udp.exemplar uninstall-local: - rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/now_and_next.py + rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_filewrite.exemplar rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_filewrite.py + rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_icecast2.exemplar rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_icecast2.py + rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_tunein.exemplar rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_tunein.py + rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_udp.exemplar rm -f $(DESTDIR)$(prefix)/@RD_LIB_PATH@/rivendell/PyPAD/pypad_udp.py -EXTRA_DIST = now_and_next.py\ +EXTRA_DIST = pypad_filewrite.exemplar\ pypad_filewrite.py\ + pypad_icecast2.exemplar\ pypad_icecast2.py\ + pypad_tunein.exemplar\ pypad_tunein.py\ + pypad_udp.exemplar\ pypad_udp.py CLEANFILES = *~\ diff --git a/apis/PyPAD/scripts/pypad_filewrite.exemplar b/apis/PyPAD/scripts/pypad_filewrite.exemplar new file mode 100644 index 00000000..5b8ca4bc --- /dev/null +++ b/apis/PyPAD/scripts/pypad_filewrite.exemplar @@ -0,0 +1,101 @@ +; This is the sample configuration for the 'pypad_filewrite.py' PyPAD script +; for Rivendell, which can be used to write one or more files on the local +; system using Now & Next data. +; + +; Section Header +; +; One section per file to be written should be configured, starting with +; 'File1' and working up consecutively +[File1] + +; Filename +; +; The full path to the file to be written. The filename may contain filepath +; wildcards as defined in Appendix C of the Rivendell Operations and +; Administration Guide. The user running RDAirPlay must have write +; permissions for this location. +Filename=/tmp/rlm_filewrite.txt + +; Append Mode +; +; If set to '0', the file will be completely overwritten with the contents +; of each PAD update. If set to '1', each update will be appended to the +; existing contents of the file. +Append=0 + +; Format String. The string to be output each time RDAirPlay changes +; play state, including any wildcards as placeholders for metadata values. +; +; The list of available wildcards can be found in the 'metadata_wildcards.txt' +; file in the Rivendell documentation directory. +; +FormatString=NOW: %d(ddd MMM d hh:mm:ss yyyy): %t - %a\nNEXT: %D(ddd MMM d hh:mm:ss yyyy): %T - %A\n + +; Encoding. Defines the set of escapes to be applied to the PAD fields. +; The following options are available: +; +; 0 - Perform no character escaping. +; 1 - "XML" escaping: Escape reserved characters as per XML-v1.0 +; 2 - "Web" escaping: Escape reserved characters as per RFC 2396 Section 2.4 +Encoding=0 + +; Log Selection +; +; Set the status for each log to 'Yes', 'No' or 'Onair' to indicate whether +; state changes on that log should be output. If set to 'Onair', then +; output will be generated only if RDAirPlays OnAir flag is active. +MasterLog=Yes +Aux1Log=Yes +Aux2Log=Yes +VLog101=No +VLog102=No +VLog103=No +VLog104=No +VLog105=No +VLog106=No +VLog107=No +VLog108=No +VLog109=No +VLog110=No +VLog111=No +VLog112=No +VLog113=No +VLog114=No +VLog115=No +VLog116=No +VLog117=No +VLog118=No +VLog119=No +VLog120=No + + +; Additional files can be written by adding new sections... +; +;[File2] +;Filename=/home/rd/foo2.txt +;Append=1 +;FormatString=%t by %a\r\n +;MasterLog=Yes +;Aux1Log=No +;Aux2Log=Onair +;VLog101=No +;VLog102=No +;VLog103=No +;VLog104=No +;VLog105=No +;VLog106=No +;VLog107=No +;VLog108=No +;VLog109=No +;VLog110=No +;VLog111=No +;VLog112=No +;VLog113=No +;VLog114=No +;VLog115=No +;VLog116=No +;VLog117=No +;VLog118=No +;VLog119=No +;VLog120=No diff --git a/apis/PyPAD/examples/pypad_filewrite.py b/apis/PyPAD/scripts/pypad_filewrite.py similarity index 95% rename from apis/PyPAD/examples/pypad_filewrite.py rename to apis/PyPAD/scripts/pypad_filewrite.py index 54a2271b..0cb3e5d0 100755 --- a/apis/PyPAD/examples/pypad_filewrite.py +++ b/apis/PyPAD/scripts/pypad_filewrite.py @@ -50,9 +50,9 @@ def ProcessPad(update): # rcvr=PyPAD.Receiver() try: - rcvr.setConfigFile(sys.argv[1]) + rcvr.setConfigFile(sys.argv[3]) except IndexError: eprint('pypad_filewrite.py: you must specify a configuration file') sys.exit(1) rcvr.setCallback(ProcessPad) -rcvr.start('localhost',PyPAD.PAD_TCP_PORT) +rcvr.start(sys.argv[1],int(sys.argv[2])) diff --git a/apis/PyPAD/scripts/pypad_icecast2.exemplar b/apis/PyPAD/scripts/pypad_icecast2.exemplar new file mode 100644 index 00000000..55dcddf5 --- /dev/null +++ b/apis/PyPAD/scripts/pypad_icecast2.exemplar @@ -0,0 +1,108 @@ +; pypad_icecast2.conf +; +; This is the sample configuration file for the 'pypad_icecast2.py' module for +; Rivendell, which can be used to update the metadata on an Icecast2 +; mountpoint using Now & Next data. + + +; Section Header +; +; One section per Icecast2 mountpoint is configured, starting with +; 'Icecast1' and working up consecutively +[Icecast1] + +; User Name +; +; The username of the Icecast2 account to which to send updates. +Username=source + +; Password +; +; The password of the Icecast2 account to which to send updates. +Password=hackme + +; Host Name +; +; The fully-qualified domain name or IP address of the Icecast2 server +Hostname=icecast.example.com + +; Host Port +; +; The TCP port number of the Icecast2 server +Tcpport=8000 + +; Mountpoint +; +; The Icecast2 mountpoint +Mountpoint=/audio.mp3 + +; Format String. The metadata to be sent each time RDAirPlay changes +; play state, including any wildcards as placeholders for metadata values. +; +; The list of available wildcards can be found in the 'metadata_wildcards.txt' +; file in the Rivendell documentation directory. +; +FormatString=%t + +; Log Selection +; +; Set the status for each log to 'Yes', 'No' or 'Onair' to indicate whether +; state changes on that log should be output to this account. If set +; to 'Onair', then output will be generated only if RDAirPlays OnAir flag +; is active. +MasterLog=Yes +Aux1Log=Yes +Aux2Log=Yes +VLog101=No +VLog102=No +VLog103=No +VLog104=No +VLog105=No +VLog106=No +VLog107=No +VLog108=No +VLog109=No +VLog110=No +VLog111=No +VLog112=No +VLog113=No +VLog114=No +VLog115=No +VLog116=No +VLog117=No +VLog118=No +VLog119=No +VLog120=No + + +; Additional Icecast2 mountpoints can be configured by adding new sections... +;[Icecast2] +;Username=source +;Password=letmein +;Hostname=anotherone.example.com +;Tcpport=80 +;Mountpoint=moreaudio.mp3 +;FormatString=%t by %a +;MasterLog=Yes +;Aux1Log=No +;Aux2Log=Onair +;VLog101=No +;VLog102=No +;VLog103=No +;VLog104=No +;VLog105=No +;VLog106=No +;VLog107=No +;VLog108=No +;VLog109=No +;VLog110=No +;VLog111=No +;VLog112=No +;VLog113=No +;VLog114=No +;VLog115=No +;VLog116=No +;VLog117=No +;VLog118=No +;VLog119=No +;VLog120=No diff --git a/apis/PyPAD/scripts/pypad_icecast2.py b/apis/PyPAD/scripts/pypad_icecast2.py new file mode 100755 index 00000000..6277cd6e --- /dev/null +++ b/apis/PyPAD/scripts/pypad_icecast2.py @@ -0,0 +1,100 @@ +#!%PYTHON_BANGPATH% + +# pypad_icecast2.py +# +# Send PAD updates to Icecast2 mountpoint +# +# (C) Copyright 2018 Fred Gleason +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program 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 along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# + +from __future__ import print_function + +import os +import sys +import socket +import requests +from requests.auth import HTTPBasicAuth +import xml.etree.ElementTree as ET +import syslog +import PyPAD +import configparser + +def eprint(*args,**kwargs): + print(pypad_name+': ',file=sys.stderr,end='') + print(*args,file=sys.stderr) + syslog.syslog(syslog.LOG_ERR,*args) + +def iprint(*args,**kwargs): + print(pypad_name+': ',file=sys.stdout,end='') + print(*args,file=sys.stdout) + syslog.syslog(syslog.LOG_INFO,*args) + +def isTrue(string): + l=['Yes','On','True','1'] + return string.lower() in map(str.lower,l) + +def ProcessPad(update): + if update.hasPadType(PyPAD.TYPE_NOW): + n=1 + while(True): + section='Icecast'+str(n) + try: + values={} + values['mount']=update.config().get(section,'Mountpoint') + values['song']=update.resolvePadFields(update.config().get(section,'FormatString'),PyPAD.ESCAPE_URL) + values['mode']='updinfo' + iprint('Updating '+update.config().get(section,'Hostname')+': song='+values['song']) + url="http://%s:%s/admin/metadata" % (update.config().get(section,'Hostname'),update.config().get(section,'Tcpport')) + try: + response=requests.get(url,auth=HTTPBasicAuth(update.config().get(section,'Username'),update.config().get(section,'Password')),params=values) + response.raise_for_status() + except requests.exceptions.RequestException as e: + eprint(str(e)) + n=n+1 + except configparser.NoSectionError: + if(n==1): + eprint('No icecast config found') + return + +# +# Program Name +# +pypad_name=os.path.basename(__file__) + +# +# Open Syslog +# +syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_DAEMON) + + +# +# Create Send Socket +# +send_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) + +# +# Start Receiver +# +rcvr=PyPAD.Receiver() +try: + rcvr.setConfigFile(sys.argv[3]) +except IndexError: + eprint('You must specify a configuration file') + sys.exit(1) +rcvr.setCallback(ProcessPad) +iprint('Started') +rcvr.start(sys.argv[1],int(sys.argv[2])) +iprint('Stopped') diff --git a/apis/PyPAD/scripts/pypad_tunein.exemplar b/apis/PyPAD/scripts/pypad_tunein.exemplar new file mode 100644 index 00000000..f607c033 --- /dev/null +++ b/apis/PyPAD/scripts/pypad_tunein.exemplar @@ -0,0 +1,108 @@ +; pypad_tunein.conf +; +; This is the sample configuration file for the 'pypad_tunein' module for +; Rivendell, which can be used to update the metadata at TuneIn +; server using Now & Next data. +; + + +; Section Header +; +; One section per TuneIn station is configured, starting with +; 'Station1' and working up consecutively +[Station1] + + +; StationID +; +; The Station ID of the TuneIn station to which to send updates. +StationID=s123456 + + +; PartnerID +; +; The Partner ID of the TuneIn station to which to send updates. +PartnerID=changeme + + +; PartnerKey +; +; The Partner Key of the TuneIn station to which to send updates. +PartnerKey=changeme + + +; Metadata String. The metadata fields to be sent each time RDAirPlay changes +; play state, including any wildcards as placeholders for metadata values. +; +; The list of available wildcards can be found in the 'metadata_wildcards.txt' +; file in the Rivendell documentation directory. +; +TitleString=%t +ArtistString=%a +AlbumString=%l + + +; Log Selection +; +; Set the status for each log to 'Yes', 'No' or 'Onair' to indicate whether +; state changes on that log should be output to this station. If set +; to 'Onair', then output will be generated only if RDAirPlays OnAir flag +; is active. +MasterLog=Yes +Aux1Log=Yes +Aux2Log=Yes +VLog101=No +VLog102=No +VLog103=No +VLog104=No +VLog105=No +VLog106=No +VLog107=No +VLog108=No +VLog109=No +VLog110=No +VLog111=No +VLog112=No +VLog113=No +VLog114=No +VLog115=No +VLog116=No +VLog117=No +VLog118=No +VLog119=No +VLog120=No + + +; Additional TuneIn stations can be configured by adding new +; sections... +; +;[Station2] +;StationID=s654321 +;PartnerID=changeme +;PartnerKey=changeme +;TitleString=%t +;ArtistString=%a +;AlbumString=%l +;MasterLog=No +;Aux1Log=Yes +;Aux2Log=No +;VLog101=No +;VLog102=No +;VLog103=No +;VLog104=No +;VLog105=No +;VLog106=No +;VLog107=No +;VLog108=No +;VLog109=No +;VLog110=No +;VLog111=No +;VLog112=No +;VLog113=No +;VLog114=No +;VLog115=No +;VLog116=No +;VLog117=No +;VLog118=No +;VLog119=No +;VLog120=No diff --git a/apis/PyPAD/scripts/pypad_tunein.py b/apis/PyPAD/scripts/pypad_tunein.py new file mode 100755 index 00000000..56b2af39 --- /dev/null +++ b/apis/PyPAD/scripts/pypad_tunein.py @@ -0,0 +1,106 @@ +#!%PYTHON_BANGPATH% + +# pypad_tunein.py +# +# Send PAD updates to TuneIn +# +# (C) Copyright 2018 Fred Gleason +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program 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 along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# + +from __future__ import print_function + +import os +import sys +import socket +import requests +import xml.etree.ElementTree as ET +import syslog +import PyPAD +import configparser + +def eprint(*args,**kwargs): + print(pypad_name+': ',file=sys.stderr,end='') + print(*args,file=sys.stderr) + syslog.syslog(syslog.LOG_ERR,*args) + +def iprint(*args,**kwargs): + print(pypad_name+': ',file=sys.stdout,end='') + print(*args,file=sys.stdout) + syslog.syslog(syslog.LOG_INFO,*args) + +def isTrue(string): + l=['Yes','On','True','1'] + return string.lower() in map(str.lower,l) + +def ProcessPad(update): + if update.hasPadType(PyPAD.TYPE_NOW): + n=1 + while(True): + section='Station'+str(n) + try: + values={} + values['id']=update.config().get(section,'StationID') + values['partnerId']=update.config().get(section,'PartnerID') + values['partnerKey']=update.config().get(section,'PartnerKey') + values['title']=update.resolvePadFields(update.config().get(section,'TitleString'),PyPAD.ESCAPE_URL) + values['artist']=update.resolvePadFields(update.config().get(section,'ArtistString'),PyPAD.ESCAPE_URL) + values['album']=update.resolvePadFields(update.config().get(section,'AlbumString'),PyPAD.ESCAPE_URL) + iprint('Updating TuneIn: artist='+values['artist']+' title='+values['title']+' album='+values['album']) + try: + response=requests.get('http://air.radiotime.com/Playing.ashx',params=values) + response.raise_for_status() + except requests.exceptions.RequestException as e: + eprint(str(e)) + else: + xml=ET.fromstring(response.text) + status=xml.find('./head/status') + if(status.text!='200'): + eprint('Update Failed: '+xml.find('./head/fault').text) + n=n+1 + except configparser.NoSectionError: + if(n==1): + eprint('No station config found') + return + +# +# Program Name +# +pypad_name=os.path.basename(__file__) + +# +# Open Syslog +# +syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_DAEMON) + + +# +# Create Send Socket +# +send_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) + +# +# Start Receiver +# +rcvr=PyPAD.Receiver() +try: + rcvr.setConfigFile(sys.argv[3]) +except IndexError: + eprint('You must specify a configuration file') + sys.exit(1) +rcvr.setCallback(ProcessPad) +iprint('Started') +rcvr.start(sys.argv[1],int(sys.argv[2])) +iprint('Stopped') diff --git a/apis/PyPAD/scripts/pypad_udp.exemplar b/apis/PyPAD/scripts/pypad_udp.exemplar new file mode 100644 index 00000000..11c17840 --- /dev/null +++ b/apis/PyPAD/scripts/pypad_udp.exemplar @@ -0,0 +1,105 @@ +; This is the sample configuration for the 'pypad_udp.py' PyPAD script for +; Rivendell, which can be used to output Now & Next data to one or more +; remote UDP ports. + +; Section Header +; +; One section per remote UDP port is configured, starting with 'Udp1' and +; working up consecutively +[Udp1] + +; IP Address +; +; The IP address of the remote UDP port, in dotted-quad notation. +IpAddress=127.0.0.1 + +; UDP Port +; +; The UDP port number of the remote UDP port, in the range 0 - 65,535. +UdpPort=1234 + +; Format String. The string to be output each time RDAirPlay changes +; play state, including any wildcards as placeholders for metadata values. +; +; The list of available wildcards can be found in the 'metadata_wildcards.txt' +; file in the Rivendell documentation directory. +; +FormatString=NOW: %d(ddd MMM d hh:mm:ss yyyy): %t - %a\nNEXT: %D(ddd MMM d hh:mm:ss yyyy): %T - %A\n + +; Encoding. Defines the set of escapes to be applied to the PAD fields. +; The following options are available: +; +; 0 - Perform no character escaping. +; 1 - "XML" escaping: Escape reserved characters as per XML-v1.0 +; 2 - "Web" escaping: Escape reserved characters as per RFC 2396 Section 2.4 +Encoding=0 + +; Null Update Handling. Defines how 'null' updates --i.e. those with a cart +; number of '0' -- should be handled. +; +; 0 - Process all updates regardless of cart values. +; 1 - Process update only if the 'now' cart is not null. +; 2 - Process update only if the 'next' cart is not null. +; 3 - Process update only if both the 'now' and 'next' carts are not null. +ProcessNullUpdates=0 + +; Log Selection +; +; Set the status for each log to 'Yes', 'No' or 'Onair' to indicate whether +; state changes on that log should be output on this udp port. If set +; to 'Onair', then output will be generated only if RDAirPlays OnAir flag +; is active. +MasterLog=Yes +Aux1Log=Yes +Aux2Log=Yes +VLog101=No +VLog102=No +VLog103=No +VLog104=No +VLog105=No +VLog106=No +VLog107=No +VLog108=No +VLog109=No +VLog110=No +VLog111=No +VLog112=No +VLog113=No +VLog114=No +VLog115=No +VLog116=No +VLog117=No +VLog118=No +VLog119=No +VLog120=No + + +; Additional UDP destinations can be configured by adding new sections... +;[Udp2] +;FormatString=Artist: %a%r +;IpAddress=192.168.10.22 +;UdpPort=6789 +;ProcessNullUpdates=0 +;MasterLog=Yes +;Aux1Log=No +;Aux2Log=Onair +;VLog101=No +;VLog102=No +;VLog103=No +;VLog104=No +;VLog105=No +;VLog106=No +;VLog107=No +;VLog108=No +;VLog109=No +;VLog110=No +;VLog111=No +;VLog112=No +;VLog113=No +;VLog114=No +;VLog115=No +;VLog116=No +;VLog117=No +;VLog118=No +;VLog119=No +;VLog120=No diff --git a/apis/PyPAD/examples/pypad_udp.py b/apis/PyPAD/scripts/pypad_udp.py similarity index 95% rename from apis/PyPAD/examples/pypad_udp.py rename to apis/PyPAD/scripts/pypad_udp.py index 5e6deac8..b6eabb1b 100755 --- a/apis/PyPAD/examples/pypad_udp.py +++ b/apis/PyPAD/scripts/pypad_udp.py @@ -50,9 +50,9 @@ send_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) rcvr=PyPAD.Receiver() try: - rcvr.setConfigFile(sys.argv[1]) + rcvr.setConfigFile(sys.argv[3]) except IndexError: eprint('pypad_udp.py: you must specify a configuration file') sys.exit(1) rcvr.setCallback(ProcessPad) -rcvr.start("localhost",PyPAD.PAD_TCP_PORT) +rcvr.start(sys.argv[1],int(sys.argv[2])) diff --git a/apis/PyPAD/tests/Makefile.am b/apis/PyPAD/tests/Makefile.am index cf90181d..71772ad0 100644 --- a/apis/PyPAD/tests/Makefile.am +++ b/apis/PyPAD/tests/Makefile.am @@ -20,7 +20,8 @@ ## ## Use automake to process this into a Makefile.in -EXTRA_DIST = filepath_test.py +EXTRA_DIST = filepath_test.py\ + now_and_next.py\ pad_test.py CLEANFILES = *~\ diff --git a/apis/PyPAD/examples/now_and_next.py b/apis/PyPAD/tests/now_and_next.py similarity index 100% rename from apis/PyPAD/examples/now_and_next.py rename to apis/PyPAD/tests/now_and_next.py diff --git a/conf/rd.conf-sample b/conf/rd.conf-sample index 6d3f1cfb..c5fcdf5a 100644 --- a/conf/rd.conf-sample +++ b/conf/rd.conf-sample @@ -11,6 +11,11 @@ AudioOwner=rivendell AudioGroup=rivendell +; These entries are used to define the system user and group that PyPAD +; scripts will run under. +PypadOwner=pypad +PypadGroup=pypad + ; This password is used by the various Rivendell modules to log into ; Rivendell system services [caed(8), ripcd(8), rdcatchd(8)]. Password=letmein diff --git a/configure.ac b/configure.ac index f77c20d7..cce20c21 100644 --- a/configure.ac +++ b/configure.ac @@ -470,7 +470,7 @@ AC_CONFIG_FILES([rivendell.spec \ apis/Makefile \ apis/PyPAD/Makefile \ apis/PyPAD/api/Makefile \ - apis/PyPAD/examples/Makefile \ + apis/PyPAD/scripts/Makefile \ apis/PyPAD/tests/Makefile \ apis/rivwebcapi/Makefile \ apis/rivwebcapi/rivwebcapi.pc \ @@ -518,6 +518,7 @@ AC_CONFIG_FILES([rivendell.spec \ rdpanel/Makefile \ rdrepld/Makefile \ rdpadd/Makefile \ + rdpadengined/Makefile \ rdselect/Makefile \ rdservice/Makefile \ rdvairplayd/Makefile \ diff --git a/docs/tables/Makefile.am b/docs/tables/Makefile.am index 1d94a0da..42c3154b 100644 --- a/docs/tables/Makefile.am +++ b/docs/tables/Makefile.am @@ -73,6 +73,7 @@ EXTRA_DIST = audio_cards.txt\ panels.txt\ panel_names.txt\ podcasts.txt\ + pypad_instances.txt\ rdairplay_channels.txt\ rd_airplay.txt\ rd_catch.txt\ diff --git a/docs/tables/pypad_instances.txt b/docs/tables/pypad_instances.txt new file mode 100644 index 00000000..a6de650e --- /dev/null +++ b/docs/tables/pypad_instances.txt @@ -0,0 +1,15 @@ + PYPAD_INSTANCES Table Layout for Rivendell + +The PYPAD_INSTANCES table holds information about PyPAD script instances +being managed by rdpadengined(8). + +FIELD NAME TYPE REMARKS +------------------------------------------------------------------------------ +ID int(10) unsigned * Primary key, Auto Increment +STATION_NAME varchar(64) From STATIONS.NAME +SCRIPT_PATH varchar(191) +DESCRIPTION varchar(191) +CONFIG text +IS_RUNNING enum('N','Y') +EXIT_CODE int(11) signed +ERROR_TEXT text diff --git a/lib/Makefile.am b/lib/Makefile.am index 6b8e1f9f..69a5021e 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -191,6 +191,7 @@ dist_librd_la_SOURCES = dbversion.h\ rdplaymeter.cpp rdplaymeter.h\ rdpeaksexport.cpp rdpeaksexport.h\ rdpodcast.cpp rdpodcast.h\ + rdprocess.cpp rdprocess.h\ rdprofile.cpp rdprofile.h\ rdpushbutton.cpp rdpushbutton.h\ rdrecording.cpp rdrecording.h\ @@ -315,6 +316,7 @@ nodist_librd_la_SOURCES = moc_rdadd_cart.cpp\ moc_rdpasswd.cpp\ moc_rdplay_deck.cpp\ moc_rdplaymeter.cpp\ + moc_rdprocess.cpp\ moc_rdpushbutton.cpp\ moc_rdrehash.cpp\ moc_rdrenderer.cpp\ diff --git a/lib/dbversion.h b/lib/dbversion.h index 172e654c..184545d3 100644 --- a/lib/dbversion.h +++ b/lib/dbversion.h @@ -24,7 +24,7 @@ /* * Current Database Version */ -#define RD_VERSION_DATABASE 302 +#define RD_VERSION_DATABASE 304 #endif // DBVERSION_H diff --git a/lib/lib.pro b/lib/lib.pro index 4e8b7366..79102f81 100644 --- a/lib/lib.pro +++ b/lib/lib.pro @@ -128,6 +128,7 @@ SOURCES += rdpanel_button.cpp SOURCES += rdpasswd.cpp SOURCES += rdplay_deck.cpp SOURCES += rdplaymeter.cpp +SOURCES += rdprocess.cpp SOURCES += rdprofile.cpp SOURCES += rdpushbutton.cpp SOURCES += rdrecording.cpp @@ -261,6 +262,7 @@ HEADERS += rdpaths.h HEADERS += rdpasswd.h HEADERS += rdplay_deck.h HEADERS += rdplaymeter.h +HEADERS += rdprocess.h HEADERS += rdprofile.h HEADERS += rdpushbutton.h HEADERS += rdrecording.h diff --git a/lib/librd_cs.ts b/lib/librd_cs.ts index 2069d345..020d825e 100644 --- a/lib/librd_cs.ts +++ b/lib/librd_cs.ts @@ -2395,6 +2395,25 @@ Zkuste to, prosím, znovu! OK + + RDProcess + + no such program + + + + ok + + + + process crashed + + + + process returned exit code + + + RDRecording diff --git a/lib/librd_de.ts b/lib/librd_de.ts index be00fef5..b5cff07b 100644 --- a/lib/librd_de.ts +++ b/lib/librd_de.ts @@ -2386,6 +2386,25 @@ bitte erneut versuchen! OK + + RDProcess + + no such program + + + + ok + + + + process crashed + + + + process returned exit code + + + RDRecording diff --git a/lib/librd_es.ts b/lib/librd_es.ts index 1e2934fc..72ae9d2e 100644 --- a/lib/librd_es.ts +++ b/lib/librd_es.ts @@ -2384,6 +2384,25 @@ please try again! Aceptar + + RDProcess + + no such program + + + + ok + + + + process crashed + + + + process returned exit code + + + RDRecording diff --git a/lib/librd_fr.ts b/lib/librd_fr.ts index 7c4ddb77..08039ed8 100644 --- a/lib/librd_fr.ts +++ b/lib/librd_fr.ts @@ -2048,6 +2048,25 @@ please try again! + + RDProcess + + no such program + + + + ok + + + + process crashed + + + + process returned exit code + + + RDRehash diff --git a/lib/librd_nb.ts b/lib/librd_nb.ts index ec9a9e6f..fd44465a 100644 --- a/lib/librd_nb.ts +++ b/lib/librd_nb.ts @@ -2388,6 +2388,25 @@ prøv ein gong til! OK + + RDProcess + + no such program + + + + ok + + + + process crashed + + + + process returned exit code + + + RDRecording diff --git a/lib/librd_nn.ts b/lib/librd_nn.ts index ec9a9e6f..fd44465a 100644 --- a/lib/librd_nn.ts +++ b/lib/librd_nn.ts @@ -2388,6 +2388,25 @@ prøv ein gong til! OK + + RDProcess + + no such program + + + + ok + + + + process crashed + + + + process returned exit code + + + RDRecording diff --git a/lib/librd_pt_BR.ts b/lib/librd_pt_BR.ts index e6ce6993..5e77a069 100644 --- a/lib/librd_pt_BR.ts +++ b/lib/librd_pt_BR.ts @@ -2391,6 +2391,25 @@ por favor, tente novamente! OK + + RDProcess + + no such program + + + + ok + + + + process crashed + + + + process returned exit code + + + RDRecording diff --git a/lib/rd.h b/lib/rd.h index 536ddfa7..1dfcf573 100644 --- a/lib/rd.h +++ b/lib/rd.h @@ -219,6 +219,8 @@ */ #define RD_DEFAULT_AUDIO_OWNER "user" #define RD_DEFAULT_AUDIO_GROUP "users" +#define RD_DEFAULT_PYPAD_OWNER "nobody" +#define RD_DEFAULT_PYPAD_GROUP "nobody" #define RD_DEFAULT_LABEL "Default Configuration" /* @@ -594,10 +596,10 @@ #define RD_STATUS_BACKGROUND_COLOR "#AAFFFF" /* - * RLM2 Connection Points + * PAD Update Connection Points */ -#define RD_RLM2_CLIENT_TCP_PORT 34289 -#define RD_RLM2_SOURCE_UNIX_ADDRESS "m4w8n8fsfddf-473fdueusurt-8954" +#define RD_PAD_CLIENT_TCP_PORT 34289 +#define RD_PAD_SOURCE_UNIX_ADDRESS "m4w8n8fsfddf-473fdueusurt-8954" #endif // RD_H diff --git a/lib/rdconfig.cpp b/lib/rdconfig.cpp index d20d4293..2bb4c4e0 100644 --- a/lib/rdconfig.cpp +++ b/lib/rdconfig.cpp @@ -380,6 +380,18 @@ QString RDConfig::audioGroup() const } +QString RDConfig::pypadOwner() const +{ + return conf_pypad_owner; +} + + +QString RDConfig::pypadGroup() const +{ + return conf_pypad_group; +} + + QString RDConfig::ripcdLogname() const { return conf_ripcd_logname; @@ -446,6 +458,18 @@ gid_t RDConfig::gid() const } +uid_t RDConfig::pypadUid() const +{ + return conf_pypad_uid; +} + + +gid_t RDConfig::pypadGid() const +{ + return conf_pypad_gid; +} + + QString RDConfig::caeLogfile() const { return conf_cae_logfile; @@ -537,6 +561,10 @@ bool RDConfig::load() profile->stringValue("Identity","AudioOwner",RD_DEFAULT_AUDIO_OWNER); conf_audio_group= profile->stringValue("Identity","AudioGroup",RD_DEFAULT_AUDIO_GROUP); + conf_pypad_owner= + profile->stringValue("Identity","PypadOwner",RD_DEFAULT_PYPAD_OWNER); + conf_pypad_group= + profile->stringValue("Identity","PypadGroup",RD_DEFAULT_PYPAD_GROUP); conf_label=profile->stringValue("Identity","Label",RD_DEFAULT_LABEL); conf_audio_store_mount_source= @@ -626,6 +654,12 @@ bool RDConfig::load() if((groups=getgrnam(profile->stringValue("Identity","AudioGroup")))!=NULL) { conf_gid=groups->gr_gid; } + if((user=getpwnam(profile->stringValue("Identity","PypadOwner")))!=NULL) { + conf_pypad_uid=user->pw_uid; + } + if((groups=getgrnam(profile->stringValue("Identity","PypadGroup")))!=NULL) { + conf_pypad_gid=groups->gr_gid; + } conf_cae_logfile=profile->stringValue("Caed","Logfile",""); conf_enable_mixer_logging=profile->boolValue("Caed","EnableMixerLogging"); conf_use_realtime=profile->boolValue("Tuning","UseRealtime",false); @@ -705,6 +739,8 @@ void RDConfig::clear() conf_password=""; conf_audio_owner=""; conf_audio_group=""; + conf_pypad_owner=""; + conf_pypad_group=""; conf_audio_root=RD_AUDIO_ROOT; conf_audio_extension=RD_AUDIO_EXTENSION; conf_label=RD_DEFAULT_LABEL; @@ -724,6 +760,8 @@ void RDConfig::clear() conf_channels=RD_DEFAULT_CHANNELS; conf_uid=65535; conf_gid=65535; + conf_pypad_uid=65535; + conf_pypad_gid=65535; conf_cae_logfile=""; conf_enable_mixer_logging=false; conf_use_realtime=false; diff --git a/lib/rdconfig.h b/lib/rdconfig.h index a0676ffc..e6e59193 100644 --- a/lib/rdconfig.h +++ b/lib/rdconfig.h @@ -91,6 +91,8 @@ class RDConfig QString password() const; QString audioOwner() const; QString audioGroup() const; + QString pypadOwner() const; + QString pypadGroup() const; QString audioRoot() const; QString audioExtension() const; QString audioFileName (QString cutname); @@ -113,6 +115,8 @@ class RDConfig unsigned channels() const; uid_t uid() const; gid_t gid() const; + uid_t pypadUid() const; + gid_t pypadGid() const; bool useRealtime(); int realtimePriority(); int transcodingDelay() const; @@ -160,6 +164,8 @@ class RDConfig QString conf_password; QString conf_audio_owner; QString conf_audio_group; + QString conf_pypad_owner; + QString conf_pypad_group; QString conf_audio_root; QString conf_audio_extension; QString conf_label; @@ -178,6 +184,8 @@ class RDConfig unsigned conf_channels; uid_t conf_uid; gid_t conf_gid; + uid_t conf_pypad_uid; + gid_t conf_pypad_gid; QString conf_cae_logfile; bool conf_enable_mixer_logging; bool conf_use_realtime; diff --git a/lib/rdlogplay.cpp b/lib/rdlogplay.cpp index 9d1c0804..47f9cf00 100644 --- a/lib/rdlogplay.cpp +++ b/lib/rdlogplay.cpp @@ -81,7 +81,7 @@ RDLogPlay::RDLogPlay(int id,RDEventPlayer *player,Q3SocketDevice *nn_sock, // RLM2 Connection // play_pad_socket=new RDUnixSocket(this); - if(!play_pad_socket->connectToAbstract(RD_RLM2_SOURCE_UNIX_ADDRESS)) { + if(!play_pad_socket->connectToAbstract(RD_PAD_SOURCE_UNIX_ADDRESS)) { fprintf(stderr,"RLMHost: unable to connect to rdrlmd\n"); } diff --git a/lib/rdnotification.cpp b/lib/rdnotification.cpp index 0e19ba2d..055aa275 100644 --- a/lib/rdnotification.cpp +++ b/lib/rdnotification.cpp @@ -103,6 +103,10 @@ bool RDNotification::read(const QString &str) notify_id=QVariant(args[3]); break; + case RDNotification::PypadType: + notify_id=QVariant(args[3].toUInt()); + break; + case RDNotification::NullType: case RDNotification::LastType: break; @@ -142,6 +146,10 @@ QString RDNotification::write() const ret+=notify_id.toString(); break; + case RDNotification::PypadType: + ret+=QString().sprintf("%u",notify_id.toUInt()); + break; + case RDNotification::NullType: case RDNotification::LastType: break; @@ -163,6 +171,10 @@ QString RDNotification::typeString(RDNotification::Type type) ret="LOG"; break; + case RDNotification::PypadType: + ret="PYPAD"; + break; + case RDNotification::NullType: case RDNotification::LastType: break; diff --git a/lib/rdnotification.h b/lib/rdnotification.h index db203c82..4331117d 100644 --- a/lib/rdnotification.h +++ b/lib/rdnotification.h @@ -27,7 +27,7 @@ class RDNotification { public: - enum Type {NullType=0,CartType=1,LogType=2,LastType=3}; + enum Type {NullType=0,CartType=1,LogType=2,PypadType=3,LastType=4}; enum Action {NoAction=0,AddAction=1,DeleteAction=2,ModifyAction=3, LastAction=4}; RDNotification(Type type,Action action,const QVariant &id); diff --git a/lib/rdpaths.h.in b/lib/rdpaths.h.in index 6f32906d..4068b0fe 100644 --- a/lib/rdpaths.h.in +++ b/lib/rdpaths.h.in @@ -44,5 +44,11 @@ extern "C" { */ #define RD_PREFIX "@prefix@" +/* + * PyPAD + */ +#define RD_PYPAD_PYTHON_PATH "@PYTHON@" +#define RD_PYPAD_SCRIPT_DIR "@libdir@/rivendell/PyPAD" + #endif // RDPATHS_H diff --git a/rdservice/process.cpp b/lib/rdprocess.cpp similarity index 74% rename from rdservice/process.cpp rename to lib/rdprocess.cpp index 747ef674..07b29b37 100644 --- a/rdservice/process.cpp +++ b/lib/rdprocess.cpp @@ -1,4 +1,4 @@ -// process.cpp +// rdprocess.cpp // // Process container for the Rivendell Services Manager // @@ -20,44 +20,45 @@ #include -#include "process.h" +#include "rdprocess.h" -Process::Process(int id,QObject *parent) +RDProcess::RDProcess(int id,QObject *parent) : QObject(parent) { p_id=id; p_process=new QProcess(this); + p_private_data=NULL; connect(p_process,SIGNAL(started()),this,SLOT(startedData())); connect(p_process,SIGNAL(finished(int,QProcess::ExitStatus)), this,SLOT(finishedData(int,QProcess::ExitStatus))); } -Process::~Process() +RDProcess::~RDProcess() { delete p_process; } -QProcess *Process::process() const +QProcess *RDProcess::process() const { return p_process; } -QString Process::program() const +QString RDProcess::program() const { return p_program; } -QStringList Process::arguments() const +QStringList RDProcess::arguments() const { return p_arguments; } -void Process::start(const QString &program,const QStringList &args) +void RDProcess::start(const QString &program,const QStringList &args) { p_program=program; p_arguments=args; @@ -71,19 +72,19 @@ void Process::start(const QString &program,const QStringList &args) } -QString Process::errorText() const +QString RDProcess::errorText() const { return p_error_text; } -void Process::startedData() +void RDProcess::startedData() { emit started(p_id); } -void Process::finishedData(int exit_code,QProcess::ExitStatus status) +void RDProcess::finishedData(int exit_code,QProcess::ExitStatus status) { p_error_text=tr("ok"); @@ -100,3 +101,15 @@ void Process::finishedData(int exit_code,QProcess::ExitStatus status) emit finished(p_id); } + + +void *RDProcess::privateData() const +{ + return p_private_data; +} + + +void RDProcess::setPrivateData(void *priv) +{ + p_private_data=priv; +} diff --git a/rdservice/process.h b/lib/rdprocess.h similarity index 82% rename from rdservice/process.h rename to lib/rdprocess.h index c1741c04..6d01a91f 100644 --- a/rdservice/process.h +++ b/lib/rdprocess.h @@ -1,6 +1,6 @@ -// process.h +// rdprocess.h // -// Process container for the Rivendell Services Manager +// Process container // // (C) Copyright 2018 Fred Gleason // @@ -18,23 +18,25 @@ // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. // -#ifndef PROCESS_H -#define PROCESS_H +#ifndef RDPROCESS_H +#define RDPROCESS_H #include #include -class Process : public QObject +class RDProcess : public QObject { Q_OBJECT; public: - Process(int id,QObject *parent=0); - ~Process(); + RDProcess(int id,QObject *parent=0); + ~RDProcess(); QProcess *process() const; QString program() const; QStringList arguments() const; void start(const QString &program,const QStringList &args); QString errorText() const; + void *privateData() const; + void setPrivateData(void *priv); signals: void started(int id); @@ -50,7 +52,8 @@ class Process : public QObject QStringList p_arguments; QProcess *p_process; QString p_error_text; + void *p_private_data; }; -#endif // PROCESS_H +#endif // RDPROCESS_H diff --git a/rdadmin/Makefile.am b/rdadmin/Makefile.am index 1dfdf72e..1e1415b3 100644 --- a/rdadmin/Makefile.am +++ b/rdadmin/Makefile.am @@ -73,6 +73,7 @@ dist_rdadmin_SOURCES = add_feed.cpp add_feed.h\ edit_node.cpp edit_node.h\ edit_now_next.cpp edit_now_next.h\ edit_nownextplugin.cpp edit_nownextplugin.h\ + edit_pypad.cpp edit_pypad.h\ edit_rdairplay.cpp edit_rdairplay.h\ edit_rdlibrary.cpp edit_rdlibrary.h\ edit_rdlogedit.cpp edit_rdlogedit.h\ @@ -103,6 +104,7 @@ dist_rdadmin_SOURCES = add_feed.cpp add_feed.h\ list_livewiregpios.cpp list_livewiregpios.h\ list_matrices.cpp list_matrices.h\ list_nodes.cpp list_nodes.h\ + list_pypads.cpp list_pypads.h\ list_replicator_carts.cpp list_replicator_carts.h\ list_replicators.cpp list_replicators.h\ list_reports.cpp list_reports.h\ @@ -117,7 +119,8 @@ dist_rdadmin_SOURCES = add_feed.cpp add_feed.h\ rename_group.cpp rename_group.h\ test_import.cpp test_import.h\ view_adapters.cpp view_adapters.h\ - view_node_info.cpp view_node_info.h + view_node_info.cpp view_node_info.h\ + view_pypad_errors.cpp view_pypad_errors.h nodist_rdadmin_SOURCES = moc_add_feed.cpp\ moc_add_group.cpp\ @@ -149,6 +152,7 @@ nodist_rdadmin_SOURCES = moc_add_feed.cpp\ moc_edit_node.cpp\ moc_edit_now_next.cpp\ moc_edit_nownextplugin.cpp\ + moc_edit_pypad.cpp\ moc_edit_rdairplay.cpp\ moc_edit_rdlibrary.cpp\ moc_edit_rdlogedit.cpp\ @@ -178,6 +182,7 @@ nodist_rdadmin_SOURCES = moc_add_feed.cpp\ moc_list_livewiregpios.cpp\ moc_list_matrices.cpp\ moc_list_nodes.cpp\ + moc_list_pypads.cpp\ moc_list_replicator_carts.cpp\ moc_list_replicators.cpp\ moc_list_reports.cpp\ @@ -192,7 +197,8 @@ nodist_rdadmin_SOURCES = moc_add_feed.cpp\ moc_rename_group.cpp\ moc_test_import.cpp\ moc_view_adapters.cpp\ - moc_view_node_info.cpp + moc_view_node_info.cpp\ + moc_view_pypad_errors.cpp rdadmin_LDADD = @LIB_RDLIBS@ -lsamplerate @LIBVORBIS@ @QT4_LIBS@ -lQt3Support diff --git a/rdadmin/edit_pypad.cpp b/rdadmin/edit_pypad.cpp new file mode 100644 index 00000000..2a1e5989 --- /dev/null +++ b/rdadmin/edit_pypad.cpp @@ -0,0 +1,145 @@ +// edit_pypad.cpp +// +// Edit a PyPAD Instance Configuration +// +// (C) Copyright 2018 Fred Gleason +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program 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 along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +// + +#include +#include + +#include "edit_pypad.h" + +EditPypad::EditPypad(int id,QWidget *parent) + : QDialog(parent) +{ + edit_id=id; + + setMinimumSize(sizeHint()); + setWindowTitle(tr("Edit PyPAD Instance")+ + " ["+tr("ID")+QString().sprintf(": %u]",id)); + + // + // Fonts + // + QFont label_font(font().family(),font().pointSize(),QFont::Bold); + + // + // Script Path + // + edit_script_path_label=new QLabel(tr("Script Path")+":",this); + edit_script_path_label->setFont(label_font); + edit_script_path_label->setAlignment(Qt::AlignVCenter|Qt::AlignRight); + edit_script_path_edit=new QLineEdit(this); + edit_script_path_edit->setReadOnly(true); + + // + // Description + // + edit_description_label=new QLabel(tr("Description")+":",this); + edit_description_label->setFont(label_font); + edit_description_label->setAlignment(Qt::AlignVCenter|Qt::AlignRight); + edit_description_edit=new QLineEdit(this); + + // + // Configuration + // + edit_config_label=new QLabel(tr("Configuration"),this); + edit_config_label->setFont(label_font); + edit_config_label->setAlignment(Qt::AlignVCenter|Qt::AlignLeft); + edit_config_text=new QTextEdit(this); + edit_config_text->setAcceptRichText(false); + edit_config_text->setWordWrapMode(QTextOption::WrapAnywhere); + + // + // OK Button + // + edit_ok_button=new QPushButton(tr("OK"),this); + edit_ok_button->setFont(label_font); + connect(edit_ok_button,SIGNAL(clicked()),this,SLOT(okData())); + + // + // Cancel Button + // + edit_cancel_button=new QPushButton(tr("Cancel"),this); + edit_cancel_button->setFont(label_font); + connect(edit_cancel_button,SIGNAL(clicked()),this,SLOT(cancelData())); + + // + // Load Values + // + QString sql=QString("select ")+ + "SCRIPT_PATH,"+ // 00 + "DESCRIPTION,"+ // 01 + "CONFIG "+ // 02 + "from PYPAD_INSTANCES where "+ + QString().sprintf("ID=%u",edit_id); + RDSqlQuery *q=new RDSqlQuery(sql); + if(q->first()) { + edit_script_path_edit->setText(q->value(0).toString()); + edit_description_edit->setText(q->value(1).toString()); + edit_config_text->insertPlainText(q->value(2).toString()); + edit_config_text->moveCursor(QTextCursor::Start); + } + delete q; +} + + +QSize EditPypad::sizeHint() const +{ + return QSize(600,800); +} + + +QSizePolicy EditPypad::sizePolicy() const +{ + return QSizePolicy(QSizePolicy::Fixed,QSizePolicy::Fixed); +} + + +void EditPypad::okData() +{ + QString sql=QString("update PYPAD_INSTANCES set ")+ + "DESCRIPTION=\""+RDEscapeString(edit_description_edit->text())+"\","+ + "CONFIG=\""+RDEscapeString(edit_config_text->toPlainText())+"\" where "+ + QString().sprintf("ID=%u",edit_id); + RDSqlQuery::apply(sql); + + done(true); +} + + +void EditPypad::cancelData() +{ + done(false); +} + + +void EditPypad::resizeEvent(QResizeEvent *e) +{ + edit_script_path_label->setGeometry(10,10,100,20); + edit_script_path_edit->setGeometry(115,10,size().width()-135,20); + + edit_description_label->setGeometry(10,32,100,20); + edit_description_edit->setGeometry(115,32,size().width()-135,20); + + edit_config_label->setGeometry(10,54,150,20); + edit_config_text->setGeometry(10,76,size().width()-20,size().height()-146); + + edit_ok_button->setGeometry(size().width()-180,size().height()-60,80,50); + + edit_cancel_button->setGeometry(size().width()-90,size().height()-60,80,50); +} diff --git a/rdadmin/edit_pypad.h b/rdadmin/edit_pypad.h new file mode 100644 index 00000000..104503f7 --- /dev/null +++ b/rdadmin/edit_pypad.h @@ -0,0 +1,58 @@ +// edit_pypad.h +// +// Edit a PyPAD Instance Configuration +// +// (C) Copyright 2018 Fred Gleason +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program 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 along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +// + +#ifndef EDIT_PYPAD_H +#define EDIT_PYPAD_H + +#include +#include +#include +#include +#include + +class EditPypad : public QDialog +{ + Q_OBJECT + public: + EditPypad(int id,QWidget *parent=0); + QSize sizeHint() const; + QSizePolicy sizePolicy() const; + + private slots: + void okData(); + void cancelData(); + + protected: + void resizeEvent(QResizeEvent *e); + + private: + QLabel *edit_script_path_label; + QLineEdit *edit_script_path_edit; + QLabel *edit_description_label; + QLineEdit *edit_description_edit; + QLabel *edit_config_label; + QTextEdit *edit_config_text; + QPushButton *edit_ok_button; + QPushButton *edit_cancel_button; + int edit_id; +}; + + +#endif // EDIT_PYPAD_H diff --git a/rdadmin/edit_station.cpp b/rdadmin/edit_station.cpp index 8e39054c..c3e99764 100644 --- a/rdadmin/edit_station.cpp +++ b/rdadmin/edit_station.cpp @@ -45,6 +45,7 @@ #include "list_hostvars.h" #include "edit_jack.h" #include "list_matrices.h" +#include "list_pypads.h" #include "edit_rdairplay.h" #include "edit_rdlibrary.h" @@ -441,6 +442,15 @@ EditStation::EditStation(QString sname,QWidget *parent) station_jack_button->setText(tr("JACK\nSettings")); connect(station_jack_button,SIGNAL(clicked()),this,SLOT(jackSettingsData())); + // + // PyPAD Instances Button + // + station_pypad_button=new QPushButton(this); + station_pypad_button->setFont(font); + station_pypad_button->setText(tr("PyPAD\nInstances")); + connect(station_pypad_button,SIGNAL(clicked()), + this,SLOT(pypadInstancesData())); + // // Ok Button // @@ -858,6 +868,14 @@ void EditStation::jackSettingsData() } +void EditStation::pypadInstancesData() +{ + ListPypads *d=new ListPypads(station_station,this); + d->exec(); + delete d; +} + + void EditStation::startCartClickedData() { int cartnum=station_start_cart_edit->text().toUInt(); @@ -976,7 +994,9 @@ void EditStation::resizeEvent(QResizeEvent *e) station_adapters_button->setGeometry(290,554,80,50); - station_jack_button->setGeometry(155,614,80,50); + station_jack_button->setGeometry(110,614,80,50); + + station_pypad_button->setGeometry(200,614,80,50); station_ok_button->setGeometry(size().width()-180,size().height()-60,80,50); station_cancel_button-> diff --git a/rdadmin/edit_station.h b/rdadmin/edit_station.h index 546a3dea..6162b775 100644 --- a/rdadmin/edit_station.h +++ b/rdadmin/edit_station.h @@ -68,6 +68,7 @@ class EditStation : public QDialog void editHostvarsData(); void editDropboxesData(); void jackSettingsData(); + void pypadInstancesData(); void startCartClickedData(); void stopCartClickedData(); @@ -143,6 +144,7 @@ class EditStation : public QDialog QPushButton *station_ttys_button; QPushButton *station_adapters_button; QPushButton *station_jack_button; + QPushButton *station_pypad_button; QPushButton *station_ok_button; QPushButton *station_cancel_button; }; diff --git a/rdadmin/list_pypads.cpp b/rdadmin/list_pypads.cpp new file mode 100644 index 00000000..11ed6d49 --- /dev/null +++ b/rdadmin/list_pypads.cpp @@ -0,0 +1,388 @@ +// list_pypads.cpp +// +// List PyPAD Instances +// +// (C) Copyright 2018 Fred Gleason +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program 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 along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "edit_pypad.h" +#include "list_pypads.h" +#include "view_pypad_errors.h" + +// +// Icons +// +#include "../icons/greenball.xpm" +#include "../icons/redball.xpm" + +ListPypads::ListPypads(RDStation *station,QWidget *parent) + : QDialog(parent) +{ + list_station=station; + + setModal(true); + setMinimumSize(sizeHint()); + + setWindowTitle("RDAdmin - "+tr("PyPAD Instances on")+" "+ + rda->station()->name()); + + // + // Fix the Window Size + // + setMinimumWidth(sizeHint().width()); + setMinimumHeight(sizeHint().height()); + + // + // Create Fonts + // + QFont bold_font=QFont("Helvetica",12,QFont::Bold); + bold_font.setPixelSize(12); + QFont font=QFont("Helvetica",12,QFont::Normal); + font.setPixelSize(12); + + // + // Icons + // + list_greenball_pixmap=new QPixmap(greenball_xpm); + list_redball_pixmap=new QPixmap(redball_xpm); + + // + // Instances List Box + // + list_list_view=new RDListView(this); + list_list_view->setAllColumnsShowFocus(true); + list_list_view->setItemMargin(5); + list_list_view->addColumn(" "); + list_list_view->setColumnAlignment(0,Qt::AlignCenter); + list_list_view->addColumn(tr("ID")); + list_list_view->setColumnAlignment(1,Qt::AlignRight); + list_list_view->addColumn(tr("Description")); + list_list_view->setColumnAlignment(2,Qt::AlignLeft); + list_list_view->addColumn(tr("Script Path")); + list_list_view->setColumnAlignment(3,Qt::AlignLeft); + list_list_view->addColumn(tr("Exit Code")); + list_list_view->setColumnAlignment(4,Qt::AlignRight); + connect(list_list_view, + SIGNAL(doubleClicked(Q3ListViewItem *,const QPoint &,int)), + this, + SLOT(doubleClickedData(Q3ListViewItem *,const QPoint &,int))); + + // + // Add Button + // + list_add_button=new QPushButton(this); + list_add_button->setFont(bold_font); + list_add_button->setText(tr("&Add")); + connect(list_add_button,SIGNAL(clicked()),this,SLOT(addData())); + + // + // Edit Button + // + list_edit_button=new QPushButton(this); + list_edit_button->setFont(bold_font); + list_edit_button->setText(tr("&Edit")); + connect(list_edit_button,SIGNAL(clicked()),this,SLOT(editData())); + + // + // Delete Button + // + list_delete_button=new QPushButton(this); + list_delete_button->setFont(bold_font); + list_delete_button->setText(tr("&Delete")); + connect(list_delete_button,SIGNAL(clicked()),this,SLOT(deleteData())); + + // + // Error Button + // + list_error_button=new QPushButton(this); + list_error_button->setFont(bold_font); + list_error_button->setText(tr("&Error\nLog")); + connect(list_error_button,SIGNAL(clicked()),this,SLOT(errorData())); + + // + // Close Button + // + list_close_button=new QPushButton(this); + list_close_button->setDefault(true); + list_close_button->setFont(bold_font); + list_close_button->setText(tr("&Close")); + connect(list_close_button,SIGNAL(clicked()),this,SLOT(closeData())); + + // + // Load Values + // + RefreshList(); + + // + // Update Timer + // + list_update_timer=new QTimer(this); + list_update_timer->setSingleShot(true); + connect(list_update_timer,SIGNAL(timeout()),this,SLOT(updateData())); + list_update_timer->start(3000); +} + + +QSize ListPypads::sizeHint() const +{ + return QSize(600,400); +} + + +QSizePolicy ListPypads::sizePolicy() const +{ + return QSizePolicy(QSizePolicy::Fixed,QSizePolicy::Fixed); +} + + +void ListPypads::addData() +{ + // + // Get Script Name + // + QString script= + QFileDialog::getOpenFileName(this,tr("Select PyPAD Script"), + RD_PYPAD_SCRIPT_DIR, + "Python Scripts (*.py)"); + if(script.isNull()) { + return; + } + + // + // Get Exemplar + // + QString exemplar=""; + QStringList f0=script.split("."); + f0.last()="exemplar"; + QFile *file=new QFile(f0.join(".")); + if(file->open(QIODevice::ReadOnly)) { + exemplar=file->readAll(); + file->close(); + } + delete file; + + QString sql=QString("insert into PYPAD_INSTANCES set ")+ + "STATION_NAME=\""+RDEscapeString(list_station->name())+"\","+ + "SCRIPT_PATH=\""+RDEscapeString(script)+"\","+ + "DESCRIPTION=\""+ + RDEscapeString("new "+script.split("/").last()+" instance")+"\","+ + "CONFIG=\""+RDEscapeString(exemplar)+"\""; + int id=RDSqlQuery::run(sql).toInt(); + EditPypad *d=new EditPypad(id,this); + if(d->exec()) { + RDListViewItem *item=new RDListViewItem(list_list_view); + item->setId(id); + RefreshItem(item); + list_list_view->ensureItemVisible(item); + item->setSelected(true); + RDNotification notify=RDNotification(RDNotification::PypadType, + RDNotification::AddAction,id); + rda->ripc()->sendNotification(notify); + } + else { + sql=QString("delete from PYPAD_INSTANCES where ")+ + QString().sprintf("ID=%u",id); + RDSqlQuery::apply(sql); + } + delete d; +} + + +void ListPypads::editData() +{ + RDListViewItem *item; + + if((item=(RDListViewItem *)list_list_view->selectedItem())==NULL) { + return; + } + EditPypad *d=new EditPypad(item->id(),this); + if(d->exec()) { + RefreshItem(item); + RDNotification notify=RDNotification(RDNotification::PypadType, + RDNotification::ModifyAction, + item->id()); + rda->ripc()->sendNotification(notify); + } + delete d; +} + + +void ListPypads::deleteData() +{ + QString sql; + RDListViewItem *item; + + if((item=(RDListViewItem *)list_list_view->selectedItem())==NULL) { + return; + } + if(QMessageBox::question(this,tr("Delete Instance"), + tr("Are your sure you want to delete this instance?"), + QMessageBox::Yes,QMessageBox::No)== + QMessageBox::No) { + return; + } + sql=QString("delete from PYPAD_INSTANCES where ")+ + QString().sprintf("ID=%d",item->id()); + RDSqlQuery::apply(sql); + RDNotification notify=RDNotification(RDNotification::PypadType, + RDNotification::DeleteAction,item->id()); + rda->ripc()->sendNotification(notify); + delete item; +} + + +void ListPypads::errorData() +{ + RDListViewItem *item; + + if((item=(RDListViewItem *)list_list_view->selectedItem())==NULL) { + return; + } + ViewPypadErrors *d=new ViewPypadErrors(item->id(),this); + d->exec(); + delete d; +} + + +void ListPypads::doubleClickedData(Q3ListViewItem *item,const QPoint &pt, + int col) +{ + editData(); +} + + +void ListPypads::closeData() +{ + done(0); +} + + +void ListPypads::updateData() +{ + QString sql; + RDSqlQuery *q; + + RDListViewItem *item=(RDListViewItem *)list_list_view->firstChild(); + while(item!=NULL) { + sql=QString("select ")+ + "IS_RUNNING,"+ // 00 + "EXIT_CODE "+ // 01 + "from PYPAD_INSTANCES where "+ + QString().sprintf("ID=%d",item->id()); + q=new RDSqlQuery(sql); + if(q->first()) { + if(q->value(0).toString()=="Y") { + item->setPixmap(0,*list_greenball_pixmap); + } + else { + item->setPixmap(0,*list_redball_pixmap); + } + item->setText(4,QString().sprintf("%d",q->value(1).toInt())); + } + delete q; + item=(RDListViewItem *)item->nextSibling(); + } + list_update_timer->start(3000); +} + + +void ListPypads::resizeEvent(QResizeEvent *e) +{ + list_list_view->setGeometry(10,10,size().width()-20,size().height()-80); + + list_add_button->setGeometry(10,size().height()-60,80,50); + list_edit_button->setGeometry(100,size().height()-60,80,50); + list_delete_button->setGeometry(190,size().height()-60,80,50); + + list_error_button->setGeometry(300,size().height()-60,80,50); + + list_close_button->setGeometry(size().width()-90,size().height()-60,80,50); +} + + +void ListPypads::RefreshList() +{ + QString sql; + RDSqlQuery *q; + RDListViewItem *item; + + list_list_view->clear(); + sql=QString("select ")+ + "ID,"+ // 00 + "IS_RUNNING,"+ // 01 + "DESCRIPTION,"+ // 02 + "SCRIPT_PATH,"+ // 03 + "EXIT_CODE "+ // 04 + "from PYPAD_INSTANCES where "+ + "STATION_NAME=\""+RDEscapeString(list_station->name())+"\""; + q=new RDSqlQuery(sql); + while(q->next()) { + item=new RDListViewItem(list_list_view); + item->setId(q->value(0).toInt()); + if(q->value(1).toString()=="Y") { + item->setPixmap(0,*list_greenball_pixmap); + } + else { + item->setPixmap(0,*list_redball_pixmap); + } + item->setText(1,QString().sprintf("%u",q->value(0).toUInt())); + item->setText(2,q->value(2).toString()); + item->setText(3,q->value(3).toString()); + item->setText(4,QString().sprintf("%d",q->value(4).toInt())); + } + delete q; +} + + +void ListPypads::RefreshItem(RDListViewItem *item) +{ + QString sql; + RDSqlQuery *q; + + sql=QString("select ")+ + "IS_RUNNING,"+ // 00 + "DESCRIPTION,"+ // 01 + "SCRIPT_PATH,"+ // 02 + "EXIT_CODE "+ //03 + "from PYPAD_INSTANCES where "+ + QString().sprintf("ID=%u",item->id()); + q=new RDSqlQuery(sql); + if(q->first()) { + if(q->value(0).toString()=="Y") { + item->setPixmap(0,*list_greenball_pixmap); + } + else { + item->setPixmap(0,*list_redball_pixmap); + } + item->setText(1,QString().sprintf("%u",item->id())); + item->setText(2,q->value(1).toString()); + item->setText(3,q->value(2).toString()); + item->setText(4,QString().sprintf("%d",q->value(3).toInt())); + } + delete q; +} diff --git a/rdadmin/list_pypads.h b/rdadmin/list_pypads.h new file mode 100644 index 00000000..f026017d --- /dev/null +++ b/rdadmin/list_pypads.h @@ -0,0 +1,69 @@ +// list_pypads.h +// +// List PyPAD Instances +// +// (C) Copyright 2018 Fred Gleason +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program 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 along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +// + +#ifndef LIST_PYPADS_H +#define LIST_PYPADS_H + +#include +#include +#include +#include + +#include +#include +#include + +class ListPypads : public QDialog +{ + Q_OBJECT + public: + ListPypads(RDStation *station,QWidget *parent=0); + QSize sizeHint() const; + QSizePolicy sizePolicy() const; + + private slots: + void addData(); + void editData(); + void deleteData(); + void errorData(); + void doubleClickedData(Q3ListViewItem *item,const QPoint &pt,int col); + void closeData(); + void updateData(); + + protected: + void resizeEvent(QResizeEvent *e); + + private: + void RefreshList(); + void RefreshItem(RDListViewItem *item); + RDListView *list_list_view; + QPushButton *list_add_button; + QPushButton *list_edit_button; + QPushButton *list_delete_button; + QPushButton *list_error_button; + QPushButton *list_close_button; + QPixmap *list_greenball_pixmap; + QPixmap *list_redball_pixmap; + RDStation *list_station; + QTimer *list_update_timer; +}; + + +#endif // LIST_PYPADS_H diff --git a/rdadmin/rdadmin.pro b/rdadmin/rdadmin.pro index 813691b0..8e4e4fd6 100644 --- a/rdadmin/rdadmin.pro +++ b/rdadmin/rdadmin.pro @@ -47,6 +47,7 @@ x11 { SOURCES += edit_matrix.cpp SOURCES += edit_nownextplugin.cpp SOURCES += edit_now_next.cpp + SOURCES += edit_pypad.cpp SOURCES += edit_rdairplay.cpp SOURCES += edit_rdlibrary.cpp SOURCES += edit_rdlogedit.cpp @@ -70,6 +71,7 @@ x11 { SOURCES += list_hostvars.cpp SOURCES += list_livewiregpios.cpp SOURCES += list_matrices.cpp + SOURCES += list_pypads.cpp SOURCES += list_reports.cpp SOURCES += list_replicator_carts.cpp SOURCES += list_replicators.cpp @@ -81,6 +83,7 @@ x11 { SOURCES += rename_group.cpp SOURCES += test_import.cpp SOURCES += view_adapters.cpp + SOURCES += view_pypad_errors.cpp SOURCES += xpm_info_banner1.cpp SOURCES += xpm_info_banner2.cpp } @@ -113,6 +116,7 @@ x11 { HEADERS += edit_matrix.h HEADERS += edit_nownextplugin.h HEADERS += edit_now_next.h + HEADERS += edit_pypad.h HEADERS += edit_rdairplay.h HEADERS += edit_rdlibrary.h HEADERS += edit_rdlogedit.h @@ -136,6 +140,7 @@ x11 { HEADERS += list_hostvars.h HEADERS += list_livewiregpios.h HEADERS += list_matrices.h + HEADERS += list_pypads.h HEADERS += list_reports.h HEADERS += list_replicator_carts.h HEADERS += list_replicators.h @@ -147,6 +152,7 @@ x11 { HEADERS += rename_group.h HEADERS += test_import.h HEADERS += view_adapters.h + HEADERS += view_pypad_errors.h } TRANSLATIONS += rdadmin_cs.ts diff --git a/rdadmin/rdadmin_cs.ts b/rdadmin/rdadmin_cs.ts index f3c0d177..9162ca58 100644 --- a/rdadmin/rdadmin_cs.ts +++ b/rdadmin/rdadmin_cs.ts @@ -2305,6 +2305,37 @@ GPIOs Vybrat přídavný modul + + EditPypad + + Edit PyPAD Instance + + + + ID + + + + Script Path + + + + Description + + + + Configuration + + + + OK + + + + Cancel + Zrušit + + EditRDAirPlay @@ -3820,6 +3851,11 @@ Ports Report Editor + + PyPAD +Instances + + EditSvc @@ -5184,6 +5220,62 @@ Stále ještě jej chcete smazat? BroadcastTools SRC-16 + + ListPypads + + PyPAD Instances on + + + + ID + + + + Description + + + + Script Path + + + + Exit Code + + + + &Add + &Přidat + + + &Edit + &Upravit + + + &Delete + S&mazat + + + &Error +Log + + + + &Close + &Zavřít + + + Select PyPAD Script + + + + Delete Instance + + + + Are your sure you want to delete this instance? + + + ListReplicatorCarts @@ -6216,4 +6308,19 @@ pro naplnění databáze zdroji zvuku. + + ViewPypadErrors + + Script Error Log + + + + ID + + + + Close + + + diff --git a/rdadmin/rdadmin_de.ts b/rdadmin/rdadmin_de.ts index 9d63d308..fcf2dd6b 100644 --- a/rdadmin/rdadmin_de.ts +++ b/rdadmin/rdadmin_de.ts @@ -2219,6 +2219,37 @@ GPIOs Plugin wählen + + EditPypad + + Edit PyPAD Instance + + + + ID + + + + Script Path + + + + Description + + + + Configuration + + + + OK + + + + Cancel + + + EditRDAirPlay @@ -3705,6 +3736,11 @@ Ports Report Editor + + PyPAD +Instances + + EditSvc @@ -5042,6 +5078,62 @@ Generieren BroadcastTools SRC-16 + + ListPypads + + PyPAD Instances on + + + + ID + + + + Description + + + + Script Path + + + + Exit Code + + + + &Add + &Hinzufügen + + + &Edit + &Editieren + + + &Delete + &Löschen + + + &Error +Log + + + + &Close + &Schliessen + + + Select PyPAD Script + + + + Delete Instance + + + + Are your sure you want to delete this instance? + + + ListReplicatorCarts @@ -5937,4 +6029,19 @@ eingeben um die Audioressourcendatenbank zu füllen. + + ViewPypadErrors + + Script Error Log + + + + ID + + + + Close + + + diff --git a/rdadmin/rdadmin_es.ts b/rdadmin/rdadmin_es.ts index 151df141..32139b57 100644 --- a/rdadmin/rdadmin_es.ts +++ b/rdadmin/rdadmin_es.ts @@ -2318,6 +2318,37 @@ GPIOs Elegir plugin + + EditPypad + + Edit PyPAD Instance + + + + ID + + + + Script Path + + + + Description + + + + Configuration + + + + OK + + + + Cancel + Cancelar + + EditRDAirPlay @@ -3829,6 +3860,11 @@ Ports Report Editor + + PyPAD +Instances + + EditSvc @@ -5111,6 +5147,62 @@ Do you still want to delete it? Adaptador de Audio Local + + ListPypads + + PyPAD Instances on + + + + ID + + + + Description + + + + Script Path + + + + Exit Code + + + + &Add + + + + &Edit + &Editar + + + &Delete + E&liminar + + + &Error +Log + + + + &Close + &Cerrar + + + Select PyPAD Script + + + + Delete Instance + + + + Are your sure you want to delete this instance? + + + ListReplicatorCarts @@ -6179,4 +6271,19 @@ Revise los parámetros e intente de nuevo. + + ViewPypadErrors + + Script Error Log + + + + ID + + + + Close + + + diff --git a/rdadmin/rdadmin_fr.ts b/rdadmin/rdadmin_fr.ts index 4b021dbe..f6d3cb5c 100644 --- a/rdadmin/rdadmin_fr.ts +++ b/rdadmin/rdadmin_fr.ts @@ -1865,6 +1865,37 @@ GPIOs + + EditPypad + + Edit PyPAD Instance + + + + ID + + + + Script Path + + + + Description + + + + Configuration + + + + OK + + + + Cancel + + + EditRDAirPlay @@ -3234,6 +3265,11 @@ Ports Report Editor + + PyPAD +Instances + + EditSvc @@ -4300,6 +4336,62 @@ PARTICULAR PURPOSE. Touch the "View License" button for details. + + ListPypads + + PyPAD Instances on + + + + ID + + + + Description + + + + Script Path + + + + Exit Code + + + + &Add + + + + &Edit + + + + &Delete + + + + &Error +Log + + + + &Close + + + + Select PyPAD Script + + + + Delete Instance + + + + Are your sure you want to delete this instance? + + + ListReplicatorCarts @@ -4929,4 +5021,19 @@ please check your settings and try again. + + ViewPypadErrors + + Script Error Log + + + + ID + + + + Close + + + diff --git a/rdadmin/rdadmin_nb.ts b/rdadmin/rdadmin_nb.ts index bc0dd723..41a17f73 100644 --- a/rdadmin/rdadmin_nb.ts +++ b/rdadmin/rdadmin_nb.ts @@ -2191,6 +2191,37 @@ GPIOs + + EditPypad + + Edit PyPAD Instance + + + + ID + + + + Script Path + + + + Description + + + + Configuration + + + + OK + + + + Cancel + + + EditRDAirPlay @@ -3670,6 +3701,11 @@ Ports Report Editor + + PyPAD +Instances + + EditSvc @@ -4947,6 +4983,62 @@ Klikk på "Lisens"-knappen for fleire opplysningar. BroadcastTools SS4.4 + + ListPypads + + PyPAD Instances on + + + + ID + + + + Description + + + + Script Path + + + + Exit Code + + + + &Add + &Legg til + + + &Edit + R&ediger + + + &Delete + &Slett + + + &Error +Log + + + + &Close + &Lukk + + + Select PyPAD Script + + + + Delete Instance + + + + Are your sure you want to delete this instance? + + + ListReplicatorCarts @@ -5826,4 +5918,19 @@ Sjekk oppsettet ditt og prøv att. + + ViewPypadErrors + + Script Error Log + + + + ID + + + + Close + + + diff --git a/rdadmin/rdadmin_nn.ts b/rdadmin/rdadmin_nn.ts index bc0dd723..41a17f73 100644 --- a/rdadmin/rdadmin_nn.ts +++ b/rdadmin/rdadmin_nn.ts @@ -2191,6 +2191,37 @@ GPIOs + + EditPypad + + Edit PyPAD Instance + + + + ID + + + + Script Path + + + + Description + + + + Configuration + + + + OK + + + + Cancel + + + EditRDAirPlay @@ -3670,6 +3701,11 @@ Ports Report Editor + + PyPAD +Instances + + EditSvc @@ -4947,6 +4983,62 @@ Klikk på "Lisens"-knappen for fleire opplysningar. BroadcastTools SS4.4 + + ListPypads + + PyPAD Instances on + + + + ID + + + + Description + + + + Script Path + + + + Exit Code + + + + &Add + &Legg til + + + &Edit + R&ediger + + + &Delete + &Slett + + + &Error +Log + + + + &Close + &Lukk + + + Select PyPAD Script + + + + Delete Instance + + + + Are your sure you want to delete this instance? + + + ListReplicatorCarts @@ -5826,4 +5918,19 @@ Sjekk oppsettet ditt og prøv att. + + ViewPypadErrors + + Script Error Log + + + + ID + + + + Close + + + diff --git a/rdadmin/rdadmin_pt_BR.ts b/rdadmin/rdadmin_pt_BR.ts index 98505192..2e92e3a8 100644 --- a/rdadmin/rdadmin_pt_BR.ts +++ b/rdadmin/rdadmin_pt_BR.ts @@ -2194,6 +2194,37 @@ GPIOs Selecionar Plugin + + EditPypad + + Edit PyPAD Instance + + + + ID + + + + Script Path + + + + Description + + + + Configuration + + + + OK + + + + Cancel + + + EditRDAirPlay @@ -3693,6 +3724,11 @@ Ports Report Editor + + PyPAD +Instances + + EditSvc @@ -5014,6 +5050,62 @@ Você ainda quer Deletar? BroadcastTools SS4.4 + + ListPypads + + PyPAD Instances on + + + + ID + + + + Description + + + + Script Path + + + + Exit Code + + + + &Add + &Adicionar + + + &Edit + &Editar + + + &Delete + &Deletar + + + &Error +Log + + + + &Close + &Fechar + + + Select PyPAD Script + + + + Delete Instance + + + + Are your sure you want to delete this instance? + + + ListReplicatorCarts @@ -5901,4 +5993,19 @@ por favor, cheque suas configurações e tente novamente + + ViewPypadErrors + + Script Error Log + + + + ID + + + + Close + + + diff --git a/rdadmin/view_pypad_errors.cpp b/rdadmin/view_pypad_errors.cpp new file mode 100644 index 00000000..531aaa58 --- /dev/null +++ b/rdadmin/view_pypad_errors.cpp @@ -0,0 +1,76 @@ +// view_pypad_errors.cpp +// +// View the Edit Log for a PyPAD Instance +// +// (C) Copyright 2018 Fred Gleason +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program 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 along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +// + +#include + +#include "view_pypad_errors.h" + +ViewPypadErrors::ViewPypadErrors(int id,QWidget *parent) + : QDialog(parent) +{ + setMinimumSize(sizeHint()); + setWindowTitle(tr("Script Error Log")+ + " ["+tr("ID")+QString().sprintf(": %d]",id)); + + // + // Fonts + // + QFont label_font(font().family(),font().pointSize(),QFont::Bold); + + // + // Viewer + // + view_text=new QTextEdit(this); + view_text->setReadOnly(true); + QString sql=QString("select ERROR_TEXT from PYPAD_INSTANCES where ")+ + QString().sprintf("ID=%u",id); + RDSqlQuery *q=new RDSqlQuery(sql); + if(q->first()) { + view_text->setPlainText(q->value(0).toString()); + } + delete q; + + // + // Close Button + // + view_close_button=new QPushButton(tr("Close"),this); + view_close_button->setFont(label_font); + connect(view_close_button,SIGNAL(clicked()),this,SLOT(closeData())); +} + + +QSize ViewPypadErrors::sizeHint() const +{ + return QSize(600,400); +} + + +void ViewPypadErrors::closeData() +{ + done(true); +} + + +void ViewPypadErrors::resizeEvent(QResizeEvent *e) +{ + view_text->setGeometry(10,10,size().width()-20,size().height()-80); + + view_close_button->setGeometry(size().width()-90,size().height()-60,80,50); +} diff --git a/rdadmin/view_pypad_errors.h b/rdadmin/view_pypad_errors.h new file mode 100644 index 00000000..c3ed0ff5 --- /dev/null +++ b/rdadmin/view_pypad_errors.h @@ -0,0 +1,48 @@ +// view_pypad_errors.h +// +// View the Edit Log for a PyPAD Instance +// +// (C) Copyright 2018 Fred Gleason +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program 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 along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +// + +#ifndef VIEW_PYPAD_ERRORS_H +#define VIEW_PYPAD_ERRORS_H + +#include +#include +#include + +class ViewPypadErrors : public QDialog +{ + Q_OBJECT + public: + ViewPypadErrors(int id,QWidget *parent=0); + QSize sizeHint() const; + + private slots: + void closeData(); + + protected: + void resizeEvent(QResizeEvent *e); + + private: + QTextEdit *view_text; + QPushButton *view_close_button; +}; + + +#endif // VIEW_PYPAD_ERRORS_H + diff --git a/rdairplay/rdairplay.cpp b/rdairplay/rdairplay.cpp index c05a0a62..d8cd34eb 100644 --- a/rdairplay/rdairplay.cpp +++ b/rdairplay/rdairplay.cpp @@ -315,16 +315,6 @@ MainWidget::MainWidget(QWidget *parent) // air_nownext_socket=new Q3SocketDevice(Q3SocketDevice::Datagram); - /* - // - // RLM2 Connection - // - air_pad_socket=new RDUnixSocket(this); - if(!air_pad_socket->connectToAbstract(RD_RLM2_SOURCE_UNIX_ADDRESS)) { - fprintf(stderr,"RLMHost: unable to connect to rdrlmd\n"); - } - */ - // // Log Machines // diff --git a/rdpadd/rdpadd.cpp b/rdpadd/rdpadd.cpp index d6010b9b..aeeaceed 100644 --- a/rdpadd/rdpadd.cpp +++ b/rdpadd/rdpadd.cpp @@ -85,9 +85,9 @@ MainObject::MainObject(QObject *parent) pad_client_server=new QTcpServer(this); connect(pad_client_server,SIGNAL(newConnection()), this,SLOT(newClientConnectionData())); - if(!pad_client_server->listen(QHostAddress::Any,RD_RLM2_CLIENT_TCP_PORT)) { + if(!pad_client_server->listen(QHostAddress::Any,RD_PAD_CLIENT_TCP_PORT)) { fprintf(stderr,"rdpadd: unable to bind client port %d\n", - RD_RLM2_CLIENT_TCP_PORT); + RD_PAD_CLIENT_TCP_PORT); exit(1); } @@ -105,7 +105,7 @@ MainObject::MainObject(QObject *parent) pad_source_server=new RDUnixServer(this); connect(pad_source_server,SIGNAL(newConnection()), this,SLOT(newSourceConnectionData())); - if(!pad_source_server->listenToAbstract(RD_RLM2_SOURCE_UNIX_ADDRESS)) { + if(!pad_source_server->listenToAbstract(RD_PAD_SOURCE_UNIX_ADDRESS)) { fprintf(stderr,"rdpadd: unable to bind source socket [%s]\n", (const char *)pad_source_server->errorString().toUtf8()); exit(1); diff --git a/rdpadengined/Makefile.am b/rdpadengined/Makefile.am new file mode 100644 index 00000000..0e9c8756 --- /dev/null +++ b/rdpadengined/Makefile.am @@ -0,0 +1,48 @@ +## Makefile.am +## +## Rivendell PyPAD Script Host +## +## (C) Copyright 2018 Fred Gleason +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License version 2 as +## published by the Free Software Foundation. +## +## This program 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 along with this program; if not, write to the Free Software +## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +## +## +## Use automake to process this into a Makefile.in + +AM_CPPFLAGS = -Wall -DPREFIX=\"$(prefix)\" -I$(top_srcdir)/lib @QT4_CFLAGS@ -I/usr/include/Qt3Support +LIBS = -L$(top_srcdir)/lib -L$(top_srcdir)/rdhpi +MOC = @QT_MOC@ + +# The dependency for qt's Meta Object Compiler (moc) +moc_%.cpp: %.h + $(MOC) $< -o $@ + + +sbin_PROGRAMS = rdpadengined + +dist_rdpadengined_SOURCES = rdpadengined.cpp rdpadengined.h + +nodist_rdpadengined_SOURCES = moc_rdpadengined.cpp + +rdpadengined_LDADD = @LIB_RDLIBS@ @LIBVORBIS@ @QT4_LIBS@ -lQt3Support + +CLEANFILES = *~\ + *.idb\ + *ilk\ + *.obj\ + *.pdb\ + moc_* + +MAINTAINERCLEANFILES = *~\ + Makefile.in diff --git a/rdpadengined/rdpadengined.cpp b/rdpadengined/rdpadengined.cpp new file mode 100644 index 00000000..48d03686 --- /dev/null +++ b/rdpadengined/rdpadengined.cpp @@ -0,0 +1,285 @@ +// rdpadengined.cpp +// +// Rivendell PAD Consolidation Server +// +// (C) Copyright 2018 Fred Gleason +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program 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 along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +// + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "rdpadengined.h" + +bool global_pad_exiting=false; + +void SigHandler(int signo) +{ + switch(signo) { + case SIGINT: + case SIGTERM: + global_pad_exiting=true; + break; + } +} + + +MainObject::MainObject(QObject *parent) + : QObject(parent) +{ + QString err_msg; + + // + // Open the Database + // + rda=new RDApplication("rdpadengined","rdpadengined",RDPADENGINED_USAGE,this); + if(!rda->open(&err_msg)) { + fprintf(stderr,"rdpadengined: %s\n",(const char *)err_msg); + exit(1); + } + + // + // Shed root permissions + // + if(getuid()==0) { + if(setgid(rda->config()->pypadGid())!=0) { + fprintf(stderr,"rdpadengined: unable to set GID to %d [%s]\n", + rda->config()->pypadGid(),strerror(errno)); + exit(1); + } + if(setuid(rda->config()->pypadUid())!=0) { + fprintf(stderr,"rdpadengined: unable to set UID to %d [%s]\n", + rda->config()->pypadUid(),strerror(errno)); + exit(1); + } + } + + // + // Read Command Options + // + for(unsigned i=0;icmdSwitch()->keys();i++) { + if(!rda->cmdSwitch()->processed(i)) { + fprintf(stderr,"rdpadengined: unknown command option \"%s\"\n", + (const char *)rda->cmdSwitch()->key(i)); + exit(2); + } + } + + // + // RIPCD Connection + // + connect(rda->ripc(),SIGNAL(connected(bool)), + this,SLOT(ripcConnectedData(bool))); + connect(rda->ripc(),SIGNAL(notificationReceived(RDNotification *)), + this,SLOT(notificationReceivedData(RDNotification *))); + rda-> + ripc()->connectHost("localhost",RIPCD_TCP_PORT,rda->config()->password()); + + // + // Exit Timer + // + pad_exit_timer=new QTimer(this); + connect(pad_exit_timer,SIGNAL(timeout()),this,SLOT(exitData())); + pad_exit_timer->start(100); + + ::signal(SIGINT,SigHandler); + ::signal(SIGTERM,SigHandler); +} + + +void MainObject::ripcConnectedData(bool state) +{ + QStringList args; + QString sql; + RDSqlQuery *q; + + // + // Clear DB Records + // + sql=QString("update PYPAD_INSTANCES set ")+ + "IS_RUNNING=\"N\","+ + "EXIT_CODE=0,"+ + "ERROR_TEXT=null "+ + "where STATION_NAME=\""+RDEscapeString(rda->station()->name())+"\""; + RDSqlQuery::apply(sql); + + // + // Start Scripts + // + sql=QString("select ")+ + "ID," // 00 + "SCRIPT_PATH " // 01 + "from PYPAD_INSTANCES where "+ + "STATION_NAME=\""+RDEscapeString(rda->station()->name())+"\""; + q=new RDSqlQuery(sql); + while(q->next()) { + StartScript(q->value(0).toUInt(),q->value(1).toString()); + } + delete q; +} + + +void MainObject::notificationReceivedData(RDNotification *notify) +{ + QString sql; + RDSqlQuery *q; + + if(notify->type()==RDNotification::PypadType) { + int id=notify->id().toUInt(); + sql=QString("select SCRIPT_PATH from PYPAD_INSTANCES where ")+ + QString().sprintf("ID=%u && ",id)+ + "STATION_NAME=\""+RDEscapeString(rda->station()->name())+"\""; + q=new RDSqlQuery(sql); + while(q->next()) { + switch(notify->action()) { + case RDNotification::AddAction: + StartScript(id,q->value(0).toString()); + break; + + case RDNotification::DeleteAction: + pad_instances.value(id)->setPrivateData((void *)true); // No Restart + KillScript(id); + break; + + case RDNotification::ModifyAction: + KillScript(id); + break; + + case RDNotification::NoAction: + case RDNotification::LastAction: + break; + } + } + delete q; + } +} + + +void MainObject::instanceStartedData(int id) +{ + SetRunStatus(id,true); +} + + +void MainObject::instanceFinishedData(int id) +{ + RDProcess *proc=pad_instances.value(id); + + if(proc->process()->exitStatus()!=QProcess::NormalExit) { + fprintf(stderr,"rdpadengined: process %d crashed\n",id); + SetRunStatus(id,false,-1,proc->process()->readAllStandardError()); + proc->deleteLater(); + pad_instances.remove(id); + return; + } + if(proc->process()->exitCode()==0) { + SetRunStatus(id,false); + bool no_restart=(bool)proc->privateData(); + QString script_path=proc->arguments().at(0); + proc->deleteLater(); + pad_instances.remove(id); + if(!no_restart) { + StartScript(id,script_path); + } + } + else { + if(!global_pad_exiting) { + SetRunStatus(id,false,proc->process()->exitCode(), + proc->process()->readAllStandardError()); + } + } +} + + +void MainObject::exitData() +{ + if(global_pad_exiting) { + // + // Shut down all instances + // + for(QMap::const_iterator it=pad_instances.begin(); + it!=pad_instances.end();it++) { + it.value()->setPrivateData((void *)true); // No Restart + it.value()->process()->terminate(); + } + + // + // Update Database + // + QString sql=QString("update PYPAD_INSTANCES set ")+ + "IS_RUNNING=\"N\" where "+ + "STATION_NAME=\""+RDEscapeString(rda->station()->name())+"\""; + RDSqlQuery::apply(sql); + exit(0); + } +} + + +void MainObject::StartScript(unsigned id,const QString &script_path) +{ + RDProcess *proc=new RDProcess(id,this); + pad_instances[id]=proc; + connect(proc,SIGNAL(started(int)),this,SLOT(instanceStartedData(int))); + connect(proc,SIGNAL(finished(int)),this,SLOT(instanceFinishedData(int))); + QStringList args; + args.push_back(script_path); + args.push_back("localhost"); + args.push_back(QString().sprintf("%u",RD_PAD_CLIENT_TCP_PORT)); + args.push_back(QString().sprintf("$%u",id)); + pad_instances.value(id)->start(RD_PYPAD_PYTHON_PATH,args); + syslog(LOG_NOTICE,"starting: %s %s",(const char *)proc->program().toUtf8(), + (const char *)proc->arguments().join(" ").toUtf8()); +} + + +void MainObject::KillScript(unsigned id) +{ + pad_instances.value(id)->process()->terminate(); +} + + +void MainObject::SetRunStatus(unsigned id,bool state,int exit_code, + const QString &err_text) const +{ + QString sql=QString("update PYPAD_INSTANCES set ")+ + "IS_RUNNING=\""+RDYesNo(state)+"\","+ + QString().sprintf("EXIT_CODE=%u,",exit_code); + if(err_text.isNull()) { + sql+="ERROR_TEXT=null "; + } + else { + sql+="ERROR_TEXT=\""+RDEscapeString(err_text)+"\" "; + } + sql+=QString().sprintf("where ID=%u",id); + RDSqlQuery::apply(sql); +} + + +int main(int argc,char *argv[]) +{ + QCoreApplication a(argc,argv); + + new MainObject(); + return a.exec(); +} diff --git a/rdpadengined/rdpadengined.h b/rdpadengined/rdpadengined.h new file mode 100644 index 00000000..5d6bf076 --- /dev/null +++ b/rdpadengined/rdpadengined.h @@ -0,0 +1,58 @@ +// rdpadengined.h +// +// Rivendell PAD Consolidation Server +// +// (C) Copyright 2018 Fred Gleason +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program 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 along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +// + +#ifndef RDPADENGINED_H +#define RDPADENGINED_H + +#include +#include +#include +#include + +#include +#include + +#define RDPADENGINED_USAGE "\n\n" + + +class MainObject : public QObject +{ + Q_OBJECT + public: + MainObject(QObject *parent=0); + + private slots: + void ripcConnectedData(bool state); + void notificationReceivedData(RDNotification *notify); + void instanceStartedData(int id); + void instanceFinishedData(int id); + void exitData(); + + private: + void StartScript(unsigned id,const QString &script_path); + void KillScript(unsigned id); + void SetRunStatus(unsigned id,bool state,int exit_code=0, + const QString &err_text=QString()) const; + QMap pad_instances; + QTimer *pad_exit_timer; +}; + + +#endif // RDPADENGINED_H diff --git a/rdservice/Makefile.am b/rdservice/Makefile.am index 26cd7fbb..7491a8a2 100644 --- a/rdservice/Makefile.am +++ b/rdservice/Makefile.am @@ -32,13 +32,11 @@ moc_%.cpp: %.h sbin_PROGRAMS = rdservice dist_rdservice_SOURCES = maint_routines.cpp\ - process.cpp process.h\ rdservice.cpp rdservice.h\ shutdown.cpp\ startup.cpp -nodist_rdservice_SOURCES = moc_process.cpp\ - moc_rdservice.cpp +nodist_rdservice_SOURCES = moc_rdservice.cpp rdservice_LDADD = @LIB_RDLIBS@ @LIBVORBIS@ @QT4_LIBS@ -lQt3Support diff --git a/rdservice/maint_routines.cpp b/rdservice/maint_routines.cpp index 8f5c3cd3..a9529367 100644 --- a/rdservice/maint_routines.cpp +++ b/rdservice/maint_routines.cpp @@ -111,7 +111,7 @@ int MainObject::GetMaintInterval() const void MainObject::RunEphemeralProcess(int id,const QString &program, const QStringList &args) { - svc_processes[id]=new Process(id,this); + svc_processes[id]=new RDProcess(id,this); connect(svc_processes[id],SIGNAL(finished(int)), this,SLOT(processFinishedData(int))); svc_processes[id]->start(program,args); diff --git a/rdservice/rdservice.h b/rdservice/rdservice.h index 5359dfb3..87bb8fa3 100644 --- a/rdservice/rdservice.h +++ b/rdservice/rdservice.h @@ -23,21 +23,21 @@ #include #include -#include #include -#include "process.h" +#include #define RDSERVICE_CAED_ID 0 #define RDSERVICE_RIPCD_ID 1 #define RDSERVICE_RDCATCHD_ID 2 #define RDSERVICE_RDPADD_ID 3 -#define RDSERVICE_RDVAIRPLAYD_ID 4 -#define RDSERVICE_RDREPLD_ID 5 -#define RDSERVICE_LOCALMAINT_ID 6 -#define RDSERVICE_SYSTEMMAINT_ID 7 -#define RDSERVICE_PURGECASTS_ID 8 -#define RDSERVICE_LAST_ID 9 +#define RDSERVICE_RDPADENGINED_ID 4 +#define RDSERVICE_RDVAIRPLAYD_ID 5 +#define RDSERVICE_RDREPLD_ID 6 +#define RDSERVICE_LOCALMAINT_ID 7 +#define RDSERVICE_SYSTEMMAINT_ID 8 +#define RDSERVICE_PURGECASTS_ID 9 +#define RDSERVICE_LAST_ID 10 #define RDSERVICE_FIRST_DROPBOX_ID 100 class MainObject : public QObject @@ -62,7 +62,7 @@ class MainObject : public QObject int GetMaintInterval() const; void RunEphemeralProcess(int id,const QString &program, const QStringList &args); - QMap svc_processes; + QMap svc_processes; QTimer *svc_maint_timer; QTimer *svc_exit_timer; }; diff --git a/rdservice/shutdown.cpp b/rdservice/shutdown.cpp index 96b46bc4..76b96bbd 100644 --- a/rdservice/shutdown.cpp +++ b/rdservice/shutdown.cpp @@ -42,7 +42,7 @@ void MainObject::Shutdown() void MainObject::ShutdownDropboxes() { - for(QMap::iterator it=svc_processes.begin(); + for(QMap::iterator it=svc_processes.begin(); it!=svc_processes.end();it++) { if(it.key()>=RDSERVICE_FIRST_DROPBOX_ID) { it.value()->process()->kill(); diff --git a/rdservice/startup.cpp b/rdservice/startup.cpp index f56bedf3..eee9d2ae 100644 --- a/rdservice/startup.cpp +++ b/rdservice/startup.cpp @@ -41,6 +41,7 @@ bool MainObject::Startup(QString *err_msg) // KillProgram("rdrepld"); KillProgram("rdvairplayd"); + KillProgram("rdpadengined"); KillProgram("rdpadd"); KillProgram("rdcatchd"); KillProgram("ripcd"); @@ -49,7 +50,7 @@ bool MainObject::Startup(QString *err_msg) // // caed(8) // - svc_processes[RDSERVICE_CAED_ID]=new Process(RDSERVICE_CAED_ID,this); + svc_processes[RDSERVICE_CAED_ID]=new RDProcess(RDSERVICE_CAED_ID,this); args.clear(); svc_processes[RDSERVICE_CAED_ID]->start(QString(RD_PREFIX)+"/sbin/caed",args); if(!svc_processes[RDSERVICE_CAED_ID]->process()->waitForStarted(-1)) { @@ -61,7 +62,7 @@ bool MainObject::Startup(QString *err_msg) // // ripcd(8) // - svc_processes[RDSERVICE_RIPCD_ID]=new Process(RDSERVICE_RIPCD_ID,this); + svc_processes[RDSERVICE_RIPCD_ID]=new RDProcess(RDSERVICE_RIPCD_ID,this); args.clear(); svc_processes[RDSERVICE_RIPCD_ID]-> start(QString(RD_PREFIX)+"/sbin/ripcd",args); @@ -74,7 +75,7 @@ bool MainObject::Startup(QString *err_msg) // // rdcatchd(8) // - svc_processes[RDSERVICE_RDCATCHD_ID]=new Process(RDSERVICE_RDCATCHD_ID,this); + svc_processes[RDSERVICE_RDCATCHD_ID]=new RDProcess(RDSERVICE_RDCATCHD_ID,this); args.clear(); svc_processes[RDSERVICE_RDCATCHD_ID]-> start(QString(RD_PREFIX)+"/sbin/rdcatchd",args); @@ -87,7 +88,7 @@ bool MainObject::Startup(QString *err_msg) // // rdpadd(8) // - svc_processes[RDSERVICE_RDPADD_ID]=new Process(RDSERVICE_RDPADD_ID,this); + svc_processes[RDSERVICE_RDPADD_ID]=new RDProcess(RDSERVICE_RDPADD_ID,this); args.clear(); svc_processes[RDSERVICE_RDPADD_ID]-> start(QString(RD_PREFIX)+"/sbin/rdpadd",args); @@ -97,10 +98,31 @@ bool MainObject::Startup(QString *err_msg) return false; } + // + // *** BAND-AID * BAND_AID * YEECH! *** + // This Makes It Work, but I think we're going to need to implement + // socket activation on all of these services. + // + sleep(1); + + // + // rdpadengined(8) + // + svc_processes[RDSERVICE_RDPADENGINED_ID]= + new RDProcess(RDSERVICE_RDPADENGINED_ID,this); + args.clear(); + svc_processes[RDSERVICE_RDPADENGINED_ID]-> + start(QString(RD_PREFIX)+"/sbin/rdpadengined",args); + if(!svc_processes[RDSERVICE_RDPADENGINED_ID]->process()->waitForStarted(-1)) { + *err_msg=tr("unable to start rdpadengined(8)")+": "+ + svc_processes[RDSERVICE_RDPADENGINED_ID]->errorText(); + return false; + } + // // rdvairplayd(8) // - svc_processes[RDSERVICE_RDVAIRPLAYD_ID]=new Process(RDSERVICE_RDVAIRPLAYD_ID,this); + svc_processes[RDSERVICE_RDVAIRPLAYD_ID]=new RDProcess(RDSERVICE_RDVAIRPLAYD_ID,this); args.clear(); svc_processes[RDSERVICE_RDVAIRPLAYD_ID]-> start(QString(RD_PREFIX)+"/sbin/rdvairplayd",args); @@ -117,7 +139,7 @@ bool MainObject::Startup(QString *err_msg) "STATION_NAME=\""+RDEscapeString(rda->station()->name())+"\""; q=new RDSqlQuery(sql); if(q->first()) { - svc_processes[RDSERVICE_RDREPLD_ID]=new Process(RDSERVICE_RDREPLD_ID,this); + svc_processes[RDSERVICE_RDREPLD_ID]=new RDProcess(RDSERVICE_RDREPLD_ID,this); args.clear(); svc_processes[RDSERVICE_RDREPLD_ID]-> start(QString(RD_PREFIX)+"/sbin/rdrepld",args); @@ -246,7 +268,7 @@ bool MainObject::StartDropboxes(QString *err_msg) args.push_back(q->value(1).toString()); args.push_back(q->value(2).toString()); - svc_processes[id]=new Process(id,this); + svc_processes[id]=new RDProcess(id,this); svc_processes[id]->start(QString(RD_PREFIX)+"/bin/rdimport",args); if(!svc_processes[id]->process()->waitForStarted(-1)) { *err_msg=tr("unable to start dropbox")+": "+ diff --git a/systemd/rivendell.service.in b/systemd/rivendell.service.in index ff26a735..275f3c06 100644 --- a/systemd/rivendell.service.in +++ b/systemd/rivendell.service.in @@ -5,7 +5,7 @@ After=network.target remote-fs.target nss-lookup.target [Service] Type=simple ExecStart=@prefix@/sbin/rdservice -PrivateTmp=true +PrivateTmp=false Restart=always RestartSec=2 StartLimitInterval=120 diff --git a/utils/rddbmgr/revertschema.cpp b/utils/rddbmgr/revertschema.cpp index 6b9eedc6..d259375a 100644 --- a/utils/rddbmgr/revertschema.cpp +++ b/utils/rddbmgr/revertschema.cpp @@ -40,6 +40,26 @@ bool MainObject::RevertSchema(int cur_schema,int set_schema,QString *err_msg) // NEW SCHEMA REVERSIONS GO HERE... + // + // Revert 304 + // + if((cur_schema==304)&&(set_schemacur_schema)) { + sql=QString("create table if not exists PYPAD_INSTANCES (")+ + "ID int auto_increment not null primary key,"+ + "STATION_NAME varchar(64) not null,"+ + "SCRIPT_PATH varchar(191) not null,"+ + "DESCRIPTION varchar(191) default '[new]',"+ + "CONFIG text not null,"+ + "index STATION_NAME_IDX(STATION_NAME))"+ + " charset utf8mb4 collate utf8mb4_general_ci"+ + db_table_create_postfix; + if(!RDSqlQuery::apply(sql,err_msg)) { + return false; + } + + WriteSchemaVersion(++cur_schema); + } + + if((cur_schema<304)&&(set_schema>cur_schema)) { + sql=QString("alter table PYPAD_INSTANCES add column ")+ + "IS_RUNNING enum('N','Y') not null default 'N' after CONFIG"; + if(!RDSqlQuery::apply(sql,err_msg)) { + return false; + } + sql=QString("alter table PYPAD_INSTANCES add column ")+ + "EXIT_CODE int not null default 0 after IS_RUNNING"; + if(!RDSqlQuery::apply(sql,err_msg)) { + return false; + } + sql=QString("alter table PYPAD_INSTANCES add column ")+ + "ERROR_TEXT text after EXIT_CODE"; + if(!RDSqlQuery::apply(sql,err_msg)) { + return false; + } + + WriteSchemaVersion(++cur_schema); + } + // NEW SCHEMA UPDATES GO HERE...