|
27 | 27 | # 2016-01-03:
|
28 | 28 | # - Added more options to parser.
|
29 | 29 | #
|
| 30 | +# Changes |
| 31 | +# 2023-05-22: |
| 32 | +# - Replaced the deprecated optparse module with argparse. |
| 33 | +# - Adjusted the code style to conform to PEP 8 guidelines. |
| 34 | +# - Used with statement for file handling to ensure proper resource cleanup. |
| 35 | +# - Incorporated exception handling to catch and handle potential errors. |
| 36 | +# - Made variable names more descriptive for better readability. |
| 37 | +# - Introduced constants for better code maintainability. |
30 | 38 |
|
31 | 39 | from __future__ import print_function
|
32 | 40 | import socket
|
33 | 41 | import sys
|
34 | 42 | import os
|
35 |
| -import optparse |
| 43 | +import argparse |
36 | 44 | import logging
|
37 | 45 | import hashlib
|
38 | 46 | import random
|
|
41 | 49 | FLASH = 0
|
42 | 50 | SPIFFS = 100
|
43 | 51 | AUTH = 200
|
44 |
| -PROGRESS = False |
45 |
| -# update_progress() : Displays or updates a console progress bar |
46 |
| -## Accepts a float between 0 and 1. Any int will be converted to a float. |
47 |
| -## A value under 0 represents a 'halt'. |
48 |
| -## A value at 1 or bigger represents 100% |
| 52 | + |
| 53 | +# Constants |
| 54 | +PROGRESS_BAR_LENGTH = 60 |
| 55 | + |
| 56 | +# update_progress(): Displays or updates a console progress bar |
49 | 57 | def update_progress(progress):
|
50 |
| - if (PROGRESS): |
51 |
| - barLength = 60 # Modify this to change the length of the progress bar |
52 |
| - status = "" |
53 |
| - if isinstance(progress, int): |
54 |
| - progress = float(progress) |
55 |
| - if not isinstance(progress, float): |
56 |
| - progress = 0 |
57 |
| - status = "error: progress var must be float\r\n" |
58 |
| - if progress < 0: |
59 |
| - progress = 0 |
60 |
| - status = "Halt...\r\n" |
61 |
| - if progress >= 1: |
62 |
| - progress = 1 |
63 |
| - status = "Done...\r\n" |
64 |
| - block = int(round(barLength*progress)) |
65 |
| - text = "\rUploading: [{0}] {1}% {2}".format( "="*block + " "*(barLength-block), int(progress*100), status) |
66 |
| - sys.stderr.write(text) |
| 58 | + if PROGRESS: |
| 59 | + status = "" |
| 60 | + if isinstance(progress, int): |
| 61 | + progress = float(progress) |
| 62 | + if not isinstance(progress, float): |
| 63 | + progress = 0 |
| 64 | + status = "Error: progress var must be float\r\n" |
| 65 | + if progress < 0: |
| 66 | + progress = 0 |
| 67 | + status = "Halt...\r\n" |
| 68 | + if progress >= 1: |
| 69 | + progress = 1 |
| 70 | + status = "Done...\r\n" |
| 71 | + block = int(round(PROGRESS_BAR_LENGTH * progress)) |
| 72 | + text = "\rUploading: [{0}] {1}% {2}".format("=" * block + " " * (PROGRESS_BAR_LENGTH - block), int(progress * 100), status) |
| 73 | + sys.stderr.write(text) |
| 74 | + sys.stderr.flush() |
| 75 | + else: |
| 76 | + sys.stderr.write(".") |
| 77 | + sys.stderr.flush() |
| 78 | + |
| 79 | +def serve(remote_addr, local_addr, remote_port, local_port, password, filename, command=FLASH): |
| 80 | + # Create a TCP/IP socket |
| 81 | + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 82 | + server_address = (local_addr, local_port) |
| 83 | + logging.info('Starting on %s:%s', str(server_address[0]), str(server_address[1])) |
| 84 | + try: |
| 85 | + sock.bind(server_address) |
| 86 | + sock.listen(1) |
| 87 | + except Exception as e: |
| 88 | + logging.error("Listen Failed: %s", str(e)) |
| 89 | + return 1 |
| 90 | + |
| 91 | + content_size = os.path.getsize(filename) |
| 92 | + file_md5 = hashlib.md5(open(filename, 'rb').read()).hexdigest() |
| 93 | + logging.info('Upload size: %d', content_size) |
| 94 | + message = '%d %d %d %s\n' % (command, local_port, content_size, file_md5) |
| 95 | + |
| 96 | + # Wait for a connection |
| 97 | + inv_tries = 0 |
| 98 | + data = '' |
| 99 | + msg = 'Sending invitation to %s ' % remote_addr |
| 100 | + sys.stderr.write(msg) |
67 | 101 | sys.stderr.flush()
|
68 |
| - else: |
69 |
| - sys.stderr.write('.') |
| 102 | + while inv_tries < 10: |
| 103 | + inv_tries += 1 |
| 104 | + sock2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| 105 | + remote_address = (remote_addr, int(remote_port)) |
| 106 | + try: |
| 107 | + sent = sock2.sendto(message.encode(), remote_address) |
| 108 | + except: |
| 109 | + sys.stderr.write('failed\n') |
| 110 | + sys.stderr.flush() |
| 111 | + sock2.close() |
| 112 | + logging.error('Host %s Not Found', remote_addr) |
| 113 | + return 1 |
| 114 | + sock2.settimeout(TIMEOUT) |
| 115 | + try: |
| 116 | + data = sock2.recv(37).decode() |
| 117 | + break |
| 118 | + except: |
| 119 | + sys.stderr.write('.') |
| 120 | + sys.stderr.flush() |
| 121 | + sock2.close() |
| 122 | + sys.stderr.write('\n') |
70 | 123 | sys.stderr.flush()
|
| 124 | + if inv_tries == 10: |
| 125 | + logging.error('No response from the ESP') |
| 126 | + return 1 |
| 127 | + if data != "OK": |
| 128 | + if data.startswith('AUTH'): |
| 129 | + nonce = data.split()[1] |
| 130 | + cnonce_text = '%s%u%s%s' % (filename, content_size, file_md5, remote_addr) |
| 131 | + cnonce = hashlib.md5(cnonce_text.encode()).hexdigest() |
| 132 | + passmd5 = hashlib.md5(password.encode()).hexdigest() |
| 133 | + result_text = '%s:%s:%s' % (passmd5, nonce, cnonce) |
| 134 | + result = hashlib.md5(result_text.encode()).hexdigest() |
| 135 | + sys.stderr.write('Authenticating...') |
| 136 | + sys.stderr.flush() |
| 137 | + message = '%d %s %s\n' % (AUTH, cnonce, result) |
| 138 | + sock2.sendto(message.encode(), remote_address) |
| 139 | + sock2.settimeout(10) |
| 140 | + try: |
| 141 | + data = sock2.recv(32).decode() |
| 142 | + except: |
| 143 | + sys.stderr.write('FAIL\n') |
| 144 | + logging.error('No Answer to our Authentication') |
| 145 | + sock2.close() |
| 146 | + return 1 |
| 147 | + if data != "OK": |
| 148 | + sys.stderr.write('FAIL\n') |
| 149 | + logging.error('%s', data) |
| 150 | + sock2.close() |
| 151 | + sys.exit(1) |
| 152 | + return 1 |
| 153 | + sys.stderr.write('OK\n') |
| 154 | + else: |
| 155 | + logging.error('Bad Answer: %s', data) |
| 156 | + sock2.close() |
| 157 | + return 1 |
| 158 | + sock2.close() |
71 | 159 |
|
72 |
| -def serve(remoteAddr, localAddr, remotePort, localPort, password, filename, command = FLASH): |
73 |
| - # Create a TCP/IP socket |
74 |
| - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
75 |
| - server_address = (localAddr, localPort) |
76 |
| - logging.info('Starting on %s:%s', str(server_address[0]), str(server_address[1])) |
77 |
| - try: |
78 |
| - sock.bind(server_address) |
79 |
| - sock.listen(1) |
80 |
| - except: |
81 |
| - logging.error("Listen Failed") |
82 |
| - return 1 |
83 |
| - |
84 |
| - content_size = os.path.getsize(filename) |
85 |
| - f = open(filename,'rb') |
86 |
| - file_md5 = hashlib.md5(f.read()).hexdigest() |
87 |
| - f.close() |
88 |
| - logging.info('Upload size: %d', content_size) |
89 |
| - message = '%d %d %d %s\n' % (command, localPort, content_size, file_md5) |
90 |
| - |
91 |
| - # Wait for a connection |
92 |
| - inv_trys = 0 |
93 |
| - data = '' |
94 |
| - msg = 'Sending invitation to %s ' % (remoteAddr) |
95 |
| - sys.stderr.write(msg) |
96 |
| - sys.stderr.flush() |
97 |
| - while (inv_trys < 10): |
98 |
| - inv_trys += 1 |
99 |
| - sock2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
100 |
| - remote_address = (remoteAddr, int(remotePort)) |
101 |
| - try: |
102 |
| - sent = sock2.sendto(message.encode(), remote_address) |
103 |
| - except: |
104 |
| - sys.stderr.write('failed\n') |
105 |
| - sys.stderr.flush() |
106 |
| - sock2.close() |
107 |
| - logging.error('Host %s Not Found', remoteAddr) |
108 |
| - return 1 |
109 |
| - sock2.settimeout(TIMEOUT) |
| 160 | + logging.info('Waiting for device...') |
110 | 161 | try:
|
111 |
| - data = sock2.recv(37).decode() |
112 |
| - break; |
| 162 | + sock.settimeout(10) |
| 163 | + connection, client_address = sock.accept() |
| 164 | + sock.settimeout(None) |
| 165 | + connection.settimeout(None) |
113 | 166 | except:
|
114 |
| - sys.stderr.write('.') |
115 |
| - sys.stderr.flush() |
116 |
| - sock2.close() |
117 |
| - sys.stderr.write('\n') |
118 |
| - sys.stderr.flush() |
119 |
| - if (inv_trys == 10): |
120 |
| - logging.error('No response from the ESP') |
121 |
| - return 1 |
122 |
| - if (data != "OK"): |
123 |
| - if(data.startswith('AUTH')): |
124 |
| - nonce = data.split()[1] |
125 |
| - cnonce_text = '%s%u%s%s' % (filename, content_size, file_md5, remoteAddr) |
126 |
| - cnonce = hashlib.md5(cnonce_text.encode()).hexdigest() |
127 |
| - passmd5 = hashlib.md5(password.encode()).hexdigest() |
128 |
| - result_text = '%s:%s:%s' % (passmd5 ,nonce, cnonce) |
129 |
| - result = hashlib.md5(result_text.encode()).hexdigest() |
130 |
| - sys.stderr.write('Authenticating...') |
131 |
| - sys.stderr.flush() |
132 |
| - message = '%d %s %s\n' % (AUTH, cnonce, result) |
133 |
| - sock2.sendto(message.encode(), remote_address) |
134 |
| - sock2.settimeout(10) |
135 |
| - try: |
136 |
| - data = sock2.recv(32).decode() |
137 |
| - except: |
138 |
| - sys.stderr.write('FAIL\n') |
139 |
| - logging.error('No Answer to our Authentication') |
140 |
| - sock2.close() |
141 |
| - return 1 |
142 |
| - if (data != "OK"): |
143 |
| - sys.stderr.write('FAIL\n') |
144 |
| - logging.error('%s', data) |
145 |
| - sock2.close() |
146 |
| - sys.exit(1); |
| 167 | + logging.error('No response from device') |
| 168 | + sock.close() |
147 | 169 | return 1
|
148 |
| - sys.stderr.write('OK\n') |
149 |
| - else: |
150 |
| - logging.error('Bad Answer: %s', data) |
151 |
| - sock2.close() |
152 |
| - return 1 |
153 |
| - sock2.close() |
154 |
| - |
155 |
| - logging.info('Waiting for device...') |
156 |
| - try: |
157 |
| - sock.settimeout(10) |
158 |
| - connection, client_address = sock.accept() |
159 |
| - sock.settimeout(None) |
160 |
| - connection.settimeout(None) |
161 |
| - except: |
162 |
| - logging.error('No response from device') |
| 170 | + try: |
| 171 | + with open(filename, "rb") as f: |
| 172 | + if PROGRESS: |
| 173 | + update_progress(0) |
| 174 | + else: |
| 175 | + sys.stderr.write('Uploading') |
| 176 | + sys.stderr.flush() |
| 177 | + offset = 0 |
| 178 | + while True: |
| 179 | + chunk = f.read(1024) |
| 180 | + if not chunk: |
| 181 | + break |
| 182 | + offset += len(chunk) |
| 183 | + update_progress(offset / float(content_size)) |
| 184 | + connection.settimeout(10) |
| 185 | + try: |
| 186 | + connection.sendall(chunk) |
| 187 | + res = connection.recv(10) |
| 188 | + last_response_contained_ok = 'OK' in res.decode() |
| 189 | + except Exception as e: |
| 190 | + sys.stderr.write('\n') |
| 191 | + logging.error('Error Uploading: %s', str(e)) |
| 192 | + connection.close() |
| 193 | + return 1 |
| 194 | + |
| 195 | + if last_response_contained_ok: |
| 196 | + logging.info('Success') |
| 197 | + connection.close() |
| 198 | + return 0 |
| 199 | + |
| 200 | + sys.stderr.write('\n') |
| 201 | + logging.info('Waiting for result...') |
| 202 | + count = 0 |
| 203 | + while count < 5: |
| 204 | + count += 1 |
| 205 | + connection.settimeout(60) |
| 206 | + try: |
| 207 | + data = connection.recv(32).decode() |
| 208 | + logging.info('Result: %s', data) |
| 209 | + |
| 210 | + if "OK" in data: |
| 211 | + logging.info('Success') |
| 212 | + connection.close() |
| 213 | + return 0 |
| 214 | + |
| 215 | + except Exception as e: |
| 216 | + logging.error('Error receiving result: %s', str(e)) |
| 217 | + connection.close() |
| 218 | + return 1 |
| 219 | + |
| 220 | + logging.error('Error response from device') |
| 221 | + connection.close() |
| 222 | + return 1 |
| 223 | + |
| 224 | + finally: |
| 225 | + connection.close() |
| 226 | + |
163 | 227 | sock.close()
|
164 | 228 | return 1
|
165 |
| - try: |
166 |
| - f = open(filename, "rb") |
167 |
| - if (PROGRESS): |
168 |
| - update_progress(0) |
169 |
| - else: |
170 |
| - sys.stderr.write('Uploading') |
171 |
| - sys.stderr.flush() |
172 |
| - offset = 0 |
173 |
| - while True: |
174 |
| - chunk = f.read(1024) |
175 |
| - if not chunk: break |
176 |
| - offset += len(chunk) |
177 |
| - update_progress(offset/float(content_size)) |
178 |
| - connection.settimeout(10) |
179 |
| - try: |
180 |
| - connection.sendall(chunk) |
181 |
| - res = connection.recv(10) |
182 |
| - lastResponseContainedOK = 'OK' in res.decode() |
183 |
| - except: |
184 |
| - sys.stderr.write('\n') |
185 |
| - logging.error('Error Uploading') |
186 |
| - connection.close() |
187 |
| - f.close() |
188 |
| - sock.close() |
189 |
| - return 1 |
190 | 229 |
|
191 |
| - if lastResponseContainedOK: |
192 |
| - logging.info('Success') |
193 |
| - connection.close() |
194 |
| - f.close() |
195 |
| - sock.close() |
196 |
| - return 0 |
197 | 230 |
|
198 |
| - sys.stderr.write('\n') |
199 |
| - logging.info('Waiting for result...') |
200 |
| - try: |
201 |
| - count = 0 |
202 |
| - while True: |
203 |
| - count=count+1 |
204 |
| - connection.settimeout(60) |
205 |
| - data = connection.recv(32).decode() |
206 |
| - logging.info('Result: %s' ,data) |
207 |
| - |
208 |
| - if "OK" in data: |
209 |
| - logging.info('Success') |
210 |
| - connection.close() |
211 |
| - f.close() |
212 |
| - sock.close() |
213 |
| - return 0; |
214 |
| - if count == 5: |
215 |
| - logging.error('Error response from device') |
216 |
| - connection.close() |
217 |
| - f.close() |
218 |
| - sock.close() |
219 |
| - return 1 |
220 |
| - except e: |
221 |
| - logging.error('No Result!') |
222 |
| - connection.close() |
223 |
| - f.close() |
224 |
| - sock.close() |
225 |
| - return 1 |
226 |
| - |
227 |
| - finally: |
228 |
| - connection.close() |
229 |
| - f.close() |
230 |
| - |
231 |
| - sock.close() |
232 |
| - return 1 |
233 |
| -# end serve |
234 |
| - |
235 |
| - |
236 |
| -def parser(unparsed_args): |
237 |
| - parser = optparse.OptionParser( |
238 |
| - usage = "%prog [options]", |
239 |
| - description = "Transmit image over the air to the esp32 module with OTA support." |
240 |
| - ) |
241 |
| - |
242 |
| - # destination ip and port |
243 |
| - group = optparse.OptionGroup(parser, "Destination") |
244 |
| - group.add_option("-i", "--ip", |
245 |
| - dest = "esp_ip", |
246 |
| - action = "store", |
247 |
| - help = "ESP32 IP Address.", |
248 |
| - default = False |
249 |
| - ) |
250 |
| - group.add_option("-I", "--host_ip", |
251 |
| - dest = "host_ip", |
252 |
| - action = "store", |
253 |
| - help = "Host IP Address.", |
254 |
| - default = "0.0.0.0" |
255 |
| - ) |
256 |
| - group.add_option("-p", "--port", |
257 |
| - dest = "esp_port", |
258 |
| - type = "int", |
259 |
| - help = "ESP32 ota Port. Default 3232", |
260 |
| - default = 3232 |
261 |
| - ) |
262 |
| - group.add_option("-P", "--host_port", |
263 |
| - dest = "host_port", |
264 |
| - type = "int", |
265 |
| - help = "Host server ota Port. Default random 10000-60000", |
266 |
| - default = random.randint(10000,60000) |
267 |
| - ) |
268 |
| - parser.add_option_group(group) |
269 |
| - |
270 |
| - # auth |
271 |
| - group = optparse.OptionGroup(parser, "Authentication") |
272 |
| - group.add_option("-a", "--auth", |
273 |
| - dest = "auth", |
274 |
| - help = "Set authentication password.", |
275 |
| - action = "store", |
276 |
| - default = "" |
277 |
| - ) |
278 |
| - parser.add_option_group(group) |
279 |
| - |
280 |
| - # image |
281 |
| - group = optparse.OptionGroup(parser, "Image") |
282 |
| - group.add_option("-f", "--file", |
283 |
| - dest = "image", |
284 |
| - help = "Image file.", |
285 |
| - metavar="FILE", |
286 |
| - default = None |
287 |
| - ) |
288 |
| - group.add_option("-s", "--spiffs", |
289 |
| - dest = "spiffs", |
290 |
| - action = "store_true", |
291 |
| - help = "Use this option to transmit a SPIFFS image and do not flash the module.", |
292 |
| - default = False |
293 |
| - ) |
294 |
| - parser.add_option_group(group) |
295 |
| - |
296 |
| - # output group |
297 |
| - group = optparse.OptionGroup(parser, "Output") |
298 |
| - group.add_option("-d", "--debug", |
299 |
| - dest = "debug", |
300 |
| - help = "Show debug output. And override loglevel with debug.", |
301 |
| - action = "store_true", |
302 |
| - default = False |
303 |
| - ) |
304 |
| - group.add_option("-r", "--progress", |
305 |
| - dest = "progress", |
306 |
| - help = "Show progress output. Does not work for ArduinoIDE", |
307 |
| - action = "store_true", |
308 |
| - default = False |
309 |
| - ) |
310 |
| - group.add_option("-t", "--timeout", |
311 |
| - dest = "timeout", |
312 |
| - type = "int", |
313 |
| - help = "Timeout to wait for the ESP32 to accept invitation", |
314 |
| - default = 10 |
315 |
| - ) |
316 |
| - parser.add_option_group(group) |
317 |
| - |
318 |
| - (options, args) = parser.parse_args(unparsed_args) |
319 |
| - |
320 |
| - return options |
321 |
| -# end parser |
| 231 | +def parse_args(unparsed_args): |
| 232 | + parser = argparse.ArgumentParser( |
| 233 | + description="Transmit image over the air to the ESP32 module with OTA support." |
| 234 | + ) |
| 235 | + |
| 236 | + # destination ip and port |
| 237 | + parser.add_argument("-i", "--ip", dest="esp_ip", action="store", help="ESP32 IP Address.", default=False) |
| 238 | + parser.add_argument("-I", "--host_ip", dest="host_ip", action="store", help="Host IP Address.", default="0.0.0.0") |
| 239 | + parser.add_argument("-p", "--port", dest="esp_port", type=int, help="ESP32 OTA Port. Default: 3232", default=3232) |
| 240 | + parser.add_argument( |
| 241 | + "-P", "--host_port", dest="host_port", type=int, help="Host server OTA Port. Default: random 10000-60000", default=random.randint(10000, 60000) |
| 242 | + ) |
| 243 | + |
| 244 | + # authentication |
| 245 | + parser.add_argument("-a", "--auth", dest="auth", help="Set authentication password.", action="store", default="") |
| 246 | + |
| 247 | + # image |
| 248 | + parser.add_argument("-f", "--file", dest="image", help="Image file.", metavar="FILE", default=None) |
| 249 | + parser.add_argument("-s", "--spiffs", dest="spiffs", action="store_true", help="Transmit a SPIFFS image and do not flash the module.", default=False) |
| 250 | + |
| 251 | + # output |
| 252 | + parser.add_argument("-d", "--debug", dest="debug", action="store_true", help="Show debug output. Overrides loglevel with debug.", default=False) |
| 253 | + parser.add_argument("-r", "--progress", dest="progress", action="store_true", help="Show progress output. Does not work for Arduino IDE.", default=False) |
| 254 | + parser.add_argument("-t", "--timeout", dest="timeout", type=int, help="Timeout to wait for the ESP32 to accept invitation.", default=10) |
| 255 | + |
| 256 | + return parser.parse_args(unparsed_args) |
322 | 257 |
|
323 | 258 |
|
324 | 259 | def main(args):
|
325 |
| - options = parser(args) |
326 |
| - loglevel = logging.WARNING |
327 |
| - if (options.debug): |
328 |
| - loglevel = logging.DEBUG |
329 |
| - |
330 |
| - logging.basicConfig(level = loglevel, format = '%(asctime)-8s [%(levelname)s]: %(message)s', datefmt = '%H:%M:%S') |
331 |
| - logging.debug("Options: %s", str(options)) |
332 |
| - |
333 |
| - # check options |
334 |
| - global PROGRESS |
335 |
| - PROGRESS = options.progress |
336 |
| - |
337 |
| - global TIMEOUT |
338 |
| - TIMEOUT = options.timeout |
339 |
| - |
340 |
| - if (not options.esp_ip or not options.image): |
341 |
| - logging.critical("Not enough arguments.") |
342 |
| - return 1 |
| 260 | + options = parse_args(args) |
| 261 | + log_level = logging.WARNING |
| 262 | + if options.debug: |
| 263 | + log_level = logging.DEBUG |
| 264 | + |
| 265 | + logging.basicConfig(level=log_level, format="%(asctime)-8s [%(levelname)s]: %(message)s", datefmt="%H:%M:%S") |
| 266 | + logging.debug("Options: %s", str(options)) |
| 267 | + |
| 268 | + # check options |
| 269 | + global PROGRESS |
| 270 | + PROGRESS = options.progress |
| 271 | + |
| 272 | + global TIMEOUT |
| 273 | + TIMEOUT = options.timeout |
| 274 | + |
| 275 | + if not options.esp_ip or not options.image: |
| 276 | + logging.critical("Not enough arguments.") |
| 277 | + return 1 |
343 | 278 |
|
344 |
| - command = FLASH |
345 |
| - if (options.spiffs): |
346 |
| - command = SPIFFS |
| 279 | + command = FLASH |
| 280 | + if options.spiffs: |
| 281 | + command = SPIFFS |
347 | 282 |
|
348 |
| - return serve(options.esp_ip, options.host_ip, options.esp_port, options.host_port, options.auth, options.image, command) |
349 |
| -# end main |
| 283 | + return serve( |
| 284 | + options.esp_ip, options.host_ip, options.esp_port, options.host_port, options.auth, options.image, command |
| 285 | + ) |
350 | 286 |
|
351 | 287 |
|
352 |
| -if __name__ == '__main__': |
353 |
| - sys.exit(main(sys.argv)) |
| 288 | +if __name__ == "__main__": |
| 289 | + sys.exit(main(sys.argv[1:])) |
0 commit comments