1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4"""Automate Audacity via mod-script-pipe. 5 6Pipe Client may be used as a command-line script to send commands to 7Audacity via the mod-script-pipe interface, or loaded as a module. 8Requires Python 2.7 or later. Python 3 strongly recommended. 9 10====================== 11Command Line Interface 12====================== 13 14 usage: pipeclient.py [-h] [-t] [-s ] [-d] 15 16Arguments 17--------- 18 -h,--help: optional 19 show short help and exit 20 -t, --timeout: float, optional 21 timeout for reply in seconds (default: 10) 22 -s, --show-time: bool, optional 23 show command execution time (default: True) 24 -d, --docs: optional 25 show this documentation and exit 26 27Example 28------- 29 $ python3 pipeclient.py -t 20 -s False 30 31 Launches command line interface with 20 second time-out for 32 returned message, and don't show the execution time. 33 34 When prompted, enter the command to send (not quoted), or 'Q' to quit. 35 36 $ Enter command or 'Q' to quit: GetInfo: Type=Tracks Format=LISP 37 38============ 39Module Usage 40============ 41 42Note that on a critical error (such as broken pipe), the module just exits. 43If a more graceful shutdown is required, replace the sys.exit()'s with 44exceptions. 45 46Example 47------- 48 49 # Import the module: 50 >>> import pipeclient 51 52 # Create a client instance: 53 >>> client = pipeclient.PipeClient() 54 55 # Send a command: 56 >>> client.write("Command", timer=True) 57 58 # Read the last reply: 59 >>> print(client.read()) 60 61See Also 62-------- 63PipeClient.write : Write a command to _write_pipe. 64PipeClient.read : Read Audacity's reply from pipe. 65 66Copyright Steve Daulton 2018 67Released under terms of the GNU General Public License version 2: 68<http://www.gnu.org/licenses/old-licenses/gpl-2.0.html /> 69 70""" 71 72import os 73import sys 74import threading 75import time 76import errno 77import argparse 78 79 80if sys.version_info[0] < 3 and sys.version_info[1] < 7: 81 sys.exit('PipeClient Error: Python 2.7 or later required') 82 83# Platform specific constants 84if sys.platform == 'win32': 85 WRITE_NAME = '\\\\.\\pipe\\ToSrvPipe' 86 READ_NAME = '\\\\.\\pipe\\FromSrvPipe' 87 EOL = '\r\n\0' 88else: 89 # Linux or Mac 90 PIPE_BASE = '/tmp/audacity_script_pipe.' 91 WRITE_NAME = PIPE_BASE + 'to.' + str(os.getuid()) 92 READ_NAME = PIPE_BASE + 'from.' + str(os.getuid()) 93 EOL = '\n' 94 95 96class PipeClient(): 97 """Write / read client access to Audacity via named pipes. 98 99 Normally there should be just one instance of this class. If 100 more instances are created, they all share the same state. 101 102 __init__ calls _write_thread_start() and _read_thread_start() on 103 first instantiation. 104 105 Parameters 106 ---------- 107 None 108 109 Attributes 110 ---------- 111 reader_pipe_broken : event object 112 Set if pipe reader fails. Audacity may have crashed 113 reply_ready : event object 114 flag cleared when command sent and set when response received 115 timer : bool 116 When true, time the command execution (default False) 117 reply : string 118 message received when Audacity completes the command 119 120 See Also 121 -------- 122 write : Write a command to _write_pipe. 123 read : Read Audacity's reply from pipe. 124 125 """ 126 127 reader_pipe_broken = threading.Event() 128 reply_ready = threading.Event() 129 130 _shared_state = {} 131 132 def __new__(cls, *p, **k): 133 self = object.__new__(cls, *p, **k) 134 self.__dict__ = cls._shared_state 135 return self 136 137 def __init__(self): 138 self.timer = False 139 self._start_time = 0 140 self._write_pipe = None 141 self.reply = '' 142 if not self._write_pipe: 143 self._write_thread_start() 144 self._read_thread_start() 145 146 def _write_thread_start(self): 147 """Start _write_pipe thread""" 148 # Pipe is opened in a new thread so that we don't 149 # freeze if Audacity is not running. 150 write_thread = threading.Thread(target=self._write_pipe_open) 151 write_thread.daemon = True 152 write_thread.start() 153 # Allow a little time for connection to be made. 154 time.sleep(0.1) 155 if not self._write_pipe: 156 sys.exit('PipeClientError: Write pipe cannot be opened.') 157 158 def _write_pipe_open(self): 159 """Open _write_pipe.""" 160 self._write_pipe = open(WRITE_NAME, 'w') 161 162 def _read_thread_start(self): 163 """Start read_pipe thread.""" 164 read_thread = threading.Thread(target=self._reader) 165 read_thread.daemon = True 166 read_thread.start() 167 168 def write(self, command, timer=False): 169 """Write a command to _write_pipe. 170 171 Parameters 172 ---------- 173 command : string 174 The command to send to Audacity 175 timer : bool, optional 176 If true, time the execution of the command 177 178 Example 179 ------- 180 write("GetInfo: Type=Labels", timer=True): 181 182 """ 183 self.timer = timer 184 print('Sending command:', command) 185 self._write_pipe.write(command + EOL) 186 # Check that read pipe is alive 187 if PipeClient.reader_pipe_broken.isSet(): 188 sys.exit('PipeClient: Read-pipe error.') 189 try: 190 self._write_pipe.flush() 191 if self.timer: 192 self._start_time = time.time() 193 self.reply = '' 194 PipeClient.reply_ready.clear() 195 except IOError as err: 196 if err.errno == errno.EPIPE: 197 sys.exit('PipeClient: Write-pipe error.') 198 else: 199 raise 200 201 def _reader(self): 202 """Read FIFO in worker thread.""" 203 # Thread will wait at this read until it connects. 204 # Connection should occur as soon as _write_pipe has connected. 205 read_pipe = open(READ_NAME, 'r') 206 message = '' 207 pipe_ok = True 208 while pipe_ok: 209 line = read_pipe.readline() 210 # Stop timer as soon as we get first line of response. 211 stop_time = time.time() 212 while pipe_ok and line != '\n': 213 message += line 214 line = read_pipe.readline() 215 if line == '': 216 # No data in read_pipe indicates that the pipe is broken 217 # (Audacity may have crashed). 218 PipeClient.reader_pipe_broken.set() 219 pipe_ok = False 220 if self.timer: 221 xtime = (stop_time - self._start_time) * 1000 222 message += 'Execution time: {0:.2f}ms'.format(xtime) 223 self.reply = message 224 PipeClient.reply_ready.set() 225 message = '' 226 read_pipe.close() 227 228 def read(self): 229 """Read Audacity's reply from pipe. 230 231 Returns 232 ------- 233 string 234 The reply from the last command sent to Audacity, or null string 235 if reply not received. Null string usually indicates that Audacity 236 is still processing the last command. 237 238 """ 239 if not PipeClient.reply_ready.isSet(): 240 return '' 241 return self.reply 242 243 244def bool_from_string(strval): 245 """Return boolean value from string""" 246 if strval.lower() in ('true', 't', '1', 'yes', 'y'): 247 return True 248 if strval.lower() in ('false', 'f', '0', 'no', 'n'): 249 return False 250 raise argparse.ArgumentTypeError('Boolean value expected.') 251 252 253def main(): 254 """Interactive command-line for PipeClient""" 255 256 parser = argparse.ArgumentParser() 257 parser.add_argument('-t', '--timeout', type=float, metavar='', default=10, 258 help="timeout for reply in seconds (default: 10") 259 parser.add_argument('-s', '--show-time', metavar='True/False', 260 nargs='?', type=bool_from_string, 261 const='t', default='t', dest='show', 262 help='show command execution time (default: True)') 263 parser.add_argument('-d', '--docs', action='store_true', 264 help='show documentation and exit') 265 args = parser.parse_args() 266 267 if args.docs: 268 print(__doc__) 269 sys.exit(0) 270 271 client = PipeClient() 272 while True: 273 reply = '' 274 if sys.version_info[0] < 3: 275 message = raw_input("\nEnter command or 'Q' to quit: ") 276 else: 277 message = input("\nEnter command or 'Q' to quit: ") 278 start = time.time() 279 if message.upper() == 'Q': 280 sys.exit(0) 281 elif message == '': 282 pass 283 else: 284 client.write(message, timer=args.show) 285 while reply == '': 286 time.sleep(0.1) # allow time for reply 287 if time.time() - start > args.timeout: 288 reply = 'PipeClient: Reply timed-out.' 289 else: 290 reply = client.read() 291 print(reply) 292 293 294if __name__ == '__main__': 295 main() 296