/ RNS / Utilities / rnx.py
rnx.py
  1  #!/usr/bin/env python3
  2  
  3  # Reticulum License
  4  #
  5  # Copyright (c) 2016-2025 Mark Qvist
  6  #
  7  # Permission is hereby granted, free of charge, to any person obtaining a copy
  8  # of this software and associated documentation files (the "Software"), to deal
  9  # in the Software without restriction, including without limitation the rights
 10  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 11  # copies of the Software, and to permit persons to whom the Software is
 12  # furnished to do so, subject to the following conditions:
 13  #
 14  # - The Software shall not be used in any kind of system which includes amongst
 15  #   its functions the ability to purposefully do harm to human beings.
 16  #
 17  # - The Software shall not be used, directly or indirectly, in the creation of
 18  #   an artificial intelligence, machine learning or language model training
 19  #   dataset, including but not limited to any use that contributes to the
 20  #   training or development of such a model or algorithm.
 21  #
 22  # - The above copyright notice and this permission notice shall be included in
 23  #   all copies or substantial portions of the Software.
 24  #
 25  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 26  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 27  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 28  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 29  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 30  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 31  # SOFTWARE.
 32  
 33  import RNS
 34  import subprocess
 35  import argparse
 36  import shlex
 37  import time
 38  import sys
 39  import os
 40  #import tty
 41  
 42  from RNS._version import __version__
 43  
 44  APP_NAME = "rnx"
 45  identity = None
 46  reticulum = None
 47  allow_all = False
 48  allowed_identity_hashes = []
 49  
 50  def prepare_identity(identity_path):
 51      global identity
 52      if identity_path == None:
 53          identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
 54  
 55      if os.path.isfile(identity_path):
 56          identity = RNS.Identity.from_file(identity_path)                
 57  
 58      if identity == None:
 59          RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
 60          identity = RNS.Identity()
 61          identity.to_file(identity_path)
 62  
 63  def listen(configdir, identitypath = None, verbosity = 0, quietness = 0, allowed = [], print_identity = False, disable_auth = None, disable_announce=False):
 64      global identity, allow_all, allowed_identity_hashes, reticulum
 65  
 66      targetloglevel = 3+verbosity-quietness
 67      reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
 68      
 69      prepare_identity(identitypath)
 70      destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "execute")
 71  
 72      if print_identity:
 73          print("Identity     : "+str(identity))
 74          print("Listening on : "+RNS.prettyhexrep(destination.hash))
 75          exit(0)
 76  
 77      if disable_auth:
 78          allow_all = True
 79      else:
 80          if allowed != None:
 81              for a in allowed:
 82                  try:
 83                      dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
 84                      if len(a) != dest_len:
 85                          raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
 86                      try:
 87                          destination_hash = bytes.fromhex(a)
 88                          allowed_identity_hashes.append(destination_hash)
 89                      except Exception as e:
 90                          raise ValueError("Invalid destination entered. Check your input.")
 91                  except Exception as e:
 92                      print(str(e))
 93                      exit(1)
 94          try:
 95              allowed_file_name = "allowed_identities"
 96              allowed_file = None
 97              if os.path.isfile(os.path.expanduser("/etc/rnx/"+allowed_file_name)):
 98                  allowed_file = os.path.expanduser("/etc/rnx/"+allowed_file_name)
 99              elif os.path.isfile(os.path.expanduser("~/.config/rnx/"+allowed_file_name)):
100                  allowed_file = os.path.expanduser("~/.config/rnx/"+allowed_file_name)
101              elif os.path.isfile(os.path.expanduser("~/.rnx/"+allowed_file_name)):
102                  allowed_file = os.path.expanduser("~/.rnx/"+allowed_file_name)
103              if allowed_file != None:
104                  with open(allowed_file, "r") as af_handle:
105                      allowed_by_file = af_handle.read().replace("\r", "").split("\n")
106                      for allowed_ID in allowed_by_file:
107                          if len(allowed_ID) == (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2:
108                              allowed_identity_hashes.append(bytes.fromhex(allowed_ID))      
109          except Exception as e:
110              print(str(e))
111              exit(1)
112                      
113      if len(allowed_identity_hashes) < 1 and not disable_auth:
114          print("Warning: No allowed identities configured, rncx will not accept any commands!")
115  
116      destination.set_link_established_callback(command_link_established)
117  
118      if not allow_all:
119          destination.register_request_handler(
120              path = "command",
121              response_generator = execute_received_command,
122              allow = RNS.Destination.ALLOW_LIST,
123              allowed_list = allowed_identity_hashes
124          )
125      else:
126          destination.register_request_handler(
127              path = "command",
128              response_generator = execute_received_command,
129              allow = RNS.Destination.ALLOW_ALL,
130          )
131  
132      RNS.log("rnx listening for commands on "+RNS.prettyhexrep(destination.hash))
133  
134      if not disable_announce:
135          destination.announce()
136      
137      while True:
138          time.sleep(1)
139  
140  def command_link_established(link):
141      link.set_remote_identified_callback(initiator_identified)
142      link.set_link_closed_callback(command_link_closed)
143      RNS.log("Command link "+str(link)+" established")
144  
145  def command_link_closed(link):
146      RNS.log("Command link "+str(link)+" closed")
147  
148  def initiator_identified(link, identity):
149      global allow_all
150      RNS.log("Initiator of link "+str(link)+" identified as "+RNS.prettyhexrep(identity.hash))
151      if not allow_all and not identity.hash in allowed_identity_hashes:
152          RNS.log("Identity "+RNS.prettyhexrep(identity.hash)+" not allowed, tearing down link")
153          link.teardown()
154  
155  def execute_received_command(path, data, request_id, remote_identity, requested_at):
156      command = data[0].decode("utf-8")  # Command to execute
157      timeout = data[1]                  # Timeout in seconds
158      o_limit = data[2]                  # Size limit for stdout
159      e_limit = data[3]                  # Size limit for stderr
160      stdin   = data[4]                  # Data passed to stdin
161  
162      if remote_identity != None:
163          RNS.log("Executing command ["+command+"] for "+RNS.prettyhexrep(remote_identity.hash))
164      else:
165          RNS.log("Executing command ["+command+"] for unknown requestor")
166  
167      result    = [
168          False,                         # 0: Command was executed
169          None,                          # 1: Return value
170          None,                          # 2: Stdout
171          None,                          # 3: Stderr
172          None,                          # 4: Total stdout length
173          None,                          # 5: Total stderr length
174          time.time(),                   # 6: Started
175          None,                          # 7: Concluded
176      ]
177  
178      try:
179          process = subprocess.Popen(shlex.split(command), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
180          result[0] = True
181  
182      except Exception as e:
183          result[0] = False
184          return result
185  
186      stdout = b""
187      stderr = b""
188      timed_out = False
189  
190      if stdin != None:
191          process.stdin.write(stdin)
192  
193      while True:
194          try:
195              stdout, stderr = process.communicate(timeout=1)
196              if process.poll() != None:
197                  break
198  
199              if len(stdout) > 0:
200                  print(str(stdout))
201                  sys.stdout.flush()
202  
203          except subprocess.TimeoutExpired:
204              pass
205  
206          if timeout != None and time.time() > result[6]+timeout:
207              RNS.log("Command ["+command+"] timed out and is being killed...")
208              process.terminate()
209              process.wait()
210              if process.poll() != None:
211                  stdout, stderr = process.communicate()
212              else:
213                  stdout = None
214                  stderr = None
215  
216              break
217  
218      if timeout != None and time.time() < result[6]+timeout:
219          result[7] = time.time()
220  
221      # Deliver result
222      result[1] = process.returncode
223  
224      if o_limit != None and len(stdout) > o_limit:
225          if o_limit == 0:
226              result[2] = b""
227          else:
228              result[2] = stdout[0:o_limit]
229      else:
230          result[2] = stdout
231  
232      if e_limit != None and len(stderr) > e_limit:
233          if e_limit == 0:
234              result[3] = b""
235          else:
236              result[3] = stderr[0:e_limit]
237      else:
238          result[3] = stderr
239  
240      result[4] = len(stdout)
241      result[5] = len(stderr)
242  
243      if timed_out:
244          RNS.log("Command timed out")
245          return result
246  
247      if remote_identity != None:
248          RNS.log("Delivering result of command ["+str(command)+"] to "+RNS.prettyhexrep(remote_identity.hash))
249      else:
250          RNS.log("Delivering result of command ["+str(command)+"] to unknown requestor")
251  
252      return result
253  
254  def spin(until=None, msg=None, timeout=None):
255      i = 0
256      syms = "⢄⢂⢁⡁⡈⡐⡠"
257      if timeout != None:
258          timeout = time.time()+timeout
259  
260      print(msg+"  ", end=" ")
261      while (timeout == None or time.time()<timeout) and not until():
262          time.sleep(0.1)
263          print(("\b\b"+syms[i]+" "), end="")
264          sys.stdout.flush()
265          i = (i+1)%len(syms)
266  
267      print("\r"+" "*len(msg)+"  \r", end="")
268  
269      if timeout != None and time.time() > timeout:
270          return False
271      else:
272          return True
273  
274  current_progress = 0.0
275  stats = []
276  speed = 0.0
277  def spin_stat(until=None, timeout=None):
278      global current_progress, response_transfer_size, speed
279      i = 0
280      syms = "⢄⢂⢁⡁⡈⡐⡠"
281      if timeout != None:
282          timeout = time.time()+timeout
283  
284      while (timeout == None or time.time()<timeout) and not until():
285          time.sleep(0.1)
286          prg = current_progress
287          percent = round(prg * 100.0, 1)
288          stat_str = str(percent)+"% - " + size_str(int(prg*response_transfer_size)) + " of " + size_str(response_transfer_size) + " - " +size_str(speed, "b")+"ps"
289          print("\r                                                                                  \rReceiving result "+syms[i]+" "+stat_str, end=" ")
290  
291          sys.stdout.flush()
292          i = (i+1)%len(syms)
293  
294      print("\r                                                                                  \r", end="")
295  
296      if timeout != None and time.time() > timeout:
297          return False
298      else:
299          return True
300  
301  def remote_execution_done(request_receipt):
302      pass
303  
304  def remote_execution_progress(request_receipt):
305      stats_max = 32
306      global current_progress, response_transfer_size, speed
307      current_progress = request_receipt.progress
308      response_transfer_size = request_receipt.response_transfer_size
309      now = time.time()
310      got = current_progress*response_transfer_size
311      entry = [now, got]
312      stats.append(entry)
313      while len(stats) > stats_max:
314          stats.pop(0)
315  
316      span = now - stats[0][0]
317      if span == 0:
318          speed = 0
319      else:
320          diff = got - stats[0][1]
321          speed = diff/span
322  
323  link = None
324  listener_destination = None
325  remote_exec_grace = 2.0
326  def execute(configdir, identitypath = None, verbosity = 0, quietness = 0, detailed = False, mirror = False, noid = False, destination = None, command = None, stdin = None, stdoutl = None, stderrl = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, result_timeout = None, interactive = False):
327      global identity, reticulum, link, listener_destination, remote_exec_grace
328  
329      try:
330          dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
331          if len(destination) != dest_len:
332              raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
333          try:
334              destination_hash = bytes.fromhex(destination)
335          except Exception as e:
336              raise ValueError("Invalid destination entered. Check your input.")
337      except Exception as e:
338          print(str(e))
339          exit(241)
340  
341      if reticulum == None:
342          targetloglevel = 3+verbosity-quietness
343          reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
344  
345      if identity == None:
346          prepare_identity(identitypath)
347  
348      if not RNS.Transport.has_path(destination_hash):
349          RNS.Transport.request_path(destination_hash)
350          if not spin(until=lambda: RNS.Transport.has_path(destination_hash), msg="Path to "+RNS.prettyhexrep(destination_hash)+" requested", timeout=timeout):
351              print("Path not found")
352              exit(242)
353  
354      if listener_destination == None:
355          listener_identity = RNS.Identity.recall(destination_hash)
356          listener_destination = RNS.Destination(
357              listener_identity,
358              RNS.Destination.OUT,
359              RNS.Destination.SINGLE,
360              APP_NAME,
361              "execute"
362          )
363  
364      if link == None or link.status == RNS.Link.CLOSED or link.status == RNS.Link.PENDING:
365          link = RNS.Link(listener_destination)
366          link.did_identify = False
367      
368      if not spin(until=lambda: link.status == RNS.Link.ACTIVE, msg="Establishing link with "+RNS.prettyhexrep(destination_hash), timeout=timeout):
369          print("Could not establish link with "+RNS.prettyhexrep(destination_hash))
370          exit(243)
371  
372      if not noid and not link.did_identify:
373          link.identify(identity)
374          link.did_identify = True
375  
376      if stdin != None:
377          stdin = stdin.encode("utf-8")
378  
379      request_data = [
380          command.encode("utf-8"),  # Command to execute
381          timeout,                  # Timeout in seconds
382          stdoutl,                  # Size limit for stdout
383          stderrl,                  # Size limit for stderr
384          stdin,                    # Data passed to stdin
385      ]
386  
387      # TODO: Tune
388      rexec_timeout = timeout+link.rtt*4+remote_exec_grace
389  
390      request_receipt = link.request(
391          path="command",
392          data=request_data,
393          response_callback=remote_execution_done,
394          failed_callback=remote_execution_done,
395          progress_callback=remote_execution_progress,
396          timeout=rexec_timeout
397      )
398  
399      spin(
400          until=lambda:link.status == RNS.Link.CLOSED or (request_receipt.status != RNS.RequestReceipt.FAILED and request_receipt.status != RNS.RequestReceipt.SENT),
401          msg="Sending execution request",
402          timeout=rexec_timeout+0.5
403      )
404  
405      if link.status == RNS.Link.CLOSED:
406          print("Could not request remote execution, link was closed")
407          exit(244)
408  
409      if request_receipt.status == RNS.RequestReceipt.FAILED:
410          print("Could not request remote execution")
411          if interactive:
412              return
413          else:
414              exit(244)
415  
416      spin(
417          until=lambda:request_receipt.status != RNS.RequestReceipt.DELIVERED,
418          msg="Command delivered, awaiting result",
419          timeout=timeout
420      )
421  
422      if request_receipt.status == RNS.RequestReceipt.FAILED:
423          print("No result was received")
424          if interactive:
425              return
426          else:
427              exit(245)
428  
429      spin_stat(
430          until=lambda:request_receipt.status != RNS.RequestReceipt.RECEIVING,
431          timeout=result_timeout
432      )
433  
434      if request_receipt.status == RNS.RequestReceipt.FAILED:
435          print("Receiving result failed")
436          if interactive:
437              return
438          else:
439              exit(246)
440  
441      if request_receipt.response != None:
442          try:
443              executed = request_receipt.response[0]
444              retval = request_receipt.response[1]
445              stdout = request_receipt.response[2]
446              stderr = request_receipt.response[3]
447              outlen = request_receipt.response[4]
448              errlen = request_receipt.response[5]
449              started = request_receipt.response[6]
450              concluded = request_receipt.response[7]
451  
452          except Exception as e:
453              print("Received invalid result")
454              if interactive:
455                  return
456              else:
457                  exit(247)
458  
459          if executed:
460              if detailed:
461                  if stdout != None and len(stdout) > 0:
462                      print(stdout.decode("utf-8"), end="")
463                  if stderr != None and len(stderr) > 0:
464                      print(stderr.decode("utf-8"), file=sys.stderr, end="")
465  
466                  sys.stdout.flush()
467                  sys.stderr.flush()
468  
469                  print("\n--- End of remote output, rnx done ---")
470                  if started != None and concluded != None:
471                      cmd_duration = round(concluded - started, 3)
472                      print("Remote command execution took "+str(cmd_duration)+" seconds")
473  
474                      total_size = request_receipt.response_size
475                      if request_receipt.request_size != None:
476                          total_size += request_receipt.request_size
477  
478                      transfer_duration = round(request_receipt.response_concluded_at - request_receipt.sent_at - cmd_duration, 3)
479                      if transfer_duration == 1:
480                          tdstr = " in 1 second"
481                      elif transfer_duration < 10:
482                          tdstr = " in "+str(transfer_duration)+" seconds"
483                      else:
484                          tdstr = " in "+pretty_time(transfer_duration)
485  
486                      spdstr = ", effective rate "+size_str(total_size/transfer_duration, "b")+"ps"
487  
488                      print("Transferred "+size_str(total_size)+tdstr+spdstr)
489  
490                  if outlen != None and stdout != None:
491                      if len(stdout) < outlen:
492                          tstr = ", "+str(len(stdout))+" bytes displayed"
493                      else:
494                          tstr = ""
495                      print("Remote wrote "+str(outlen)+" bytes to stdout"+tstr)
496                  
497                  if errlen != None and stderr != None:
498                      if len(stderr) < errlen:
499                          tstr = ", "+str(len(stderr))+" bytes displayed"
500                      else:
501                          tstr = ""
502                      print("Remote wrote "+str(errlen)+" bytes to stderr"+tstr)
503  
504              else:
505                  if stdout != None and len(stdout) > 0:
506                      print(stdout.decode("utf-8"), end="")
507                  if stderr != None and len(stderr) > 0:
508                      print(stderr.decode("utf-8"), file=sys.stderr, end="")
509  
510  
511                  if (stdoutl != 0 and len(stdout) < outlen) or (stderrl != 0 and len(stderr) < errlen):
512                      sys.stdout.flush()
513                      sys.stderr.flush()
514                      print("\nOutput truncated before being returned:")
515                      if len(stdout) != 0 and len(stdout) < outlen:
516                          print("  stdout truncated to "+str(len(stdout))+" bytes")
517                      if len(stderr) != 0 and len(stderr) < errlen:
518                          print("  stderr truncated to "+str(len(stderr))+" bytes")
519          else:
520              print("Remote could not execute command")
521              if interactive:
522                  return
523              else:
524                  exit(248)
525      else:
526          print("No response")
527          if interactive:
528              return
529          else:
530              exit(249)
531  
532      try:
533          if not interactive:
534              link.teardown()
535  
536      except Exception as e:
537          pass
538  
539      if not interactive and mirror:
540          if request_receipt.response[1] != None:
541              exit(request_receipt.response[1])
542          else:
543              exit(240)
544      else:
545          if interactive:
546              if mirror:
547                  return request_receipt.response[1]
548              else:
549                  return None
550          else:
551              exit(0)
552  
553  def main():
554      try:
555          parser = argparse.ArgumentParser(description="Reticulum Remote Execution Utility")
556          parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the listener", type=str)
557          parser.add_argument("command", nargs="?", default=None, help="command to be execute", type=str)
558          parser.add_argument("--config", metavar="path", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
559          parser.add_argument('-v', '--verbose', action='count', default=0, help="increase verbosity")
560          parser.add_argument('-q', '--quiet', action='count', default=0, help="decrease verbosity")
561          parser.add_argument('-p', '--print-identity', action='store_true', default=False, help="print identity and destination info and exit")
562          parser.add_argument("-l", '--listen', action='store_true', default=False, help="listen for incoming commands")
563          parser.add_argument('-i', metavar="identity", action='store', dest="identity", default=None, help="path to identity to use", type=str)
564          parser.add_argument("-x", '--interactive', action='store_true', default=False, help="enter interactive mode")
565          parser.add_argument("-b", '--no-announce', action='store_true', default=False, help="don't announce at program start")
566          parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="accept from this identity", type=str)
567          parser.add_argument('-n', '--noauth', action='store_true', default=False, help="accept commands from anyone")
568          parser.add_argument('-N', '--noid', action='store_true', default=False, help="don't identify to listener")
569          parser.add_argument("-d", '--detailed', action='store_true', default=False, help="show detailed result output")
570          parser.add_argument("-m", action='store_true', dest="mirror", default=False, help="mirror exit code of remote command")
571          parser.add_argument("-w", action="store", metavar="seconds", type=float, help="connect and request timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
572          parser.add_argument("-W", action="store", metavar="seconds", type=float, help="max result download time", default=None)
573          parser.add_argument("--stdin", action='store', default=None, help="pass input to stdin", type=str)
574          parser.add_argument("--stdout", action='store', default=None, help="max size in bytes of returned stdout", type=int)
575          parser.add_argument("--stderr", action='store', default=None, help="max size in bytes of returned stderr", type=int)
576          parser.add_argument("--version", action="version", version="rnx {version}".format(version=__version__))
577          
578          args = parser.parse_args()
579  
580          if args.listen or args.print_identity:
581              listen(
582                  configdir = args.config,
583                  identitypath = args.identity,
584                  verbosity=args.verbose,
585                  quietness=args.quiet,
586                  allowed = args.allowed,
587                  print_identity=args.print_identity,
588                  disable_auth=args.noauth,
589                  disable_announce=args.no_announce,
590              )
591  
592          elif args.destination != None and args.command != None:
593              execute(
594                  configdir = args.config,
595                  identitypath = args.identity,
596                  verbosity = args.verbose,
597                  quietness = args.quiet,
598                  detailed = args.detailed,
599                  mirror = args.mirror,
600                  noid = args.noid,
601                  destination = args.destination,
602                  command = args.command,
603                  stdin = args.stdin,
604                  stdoutl = args.stdout,
605                  stderrl = args.stderr,
606                  timeout = args.w,
607                  result_timeout = args.W,
608                  interactive = args.interactive,
609              )
610  
611          if args.destination != None and args.interactive:
612              # command_history_max = 5000
613              # command_history = []
614              # command_current = ""
615              # history_idx = 0
616              # tty.setcbreak(sys.stdin.fileno())
617  
618              code = None
619              while True:
620                  try:
621                      cstr = str(code) if code and code != 0 else ""
622                      prompt = cstr+"> "
623                      print(prompt,end="")
624  
625                      # cmdbuf = b""
626                      # while True:
627                      #     ch = sys.stdin.read(1)
628                      #     cmdbuf += ch.encode("utf-8")
629                      #     print("\r"+prompt+cmdbuf.decode("utf-8"), end="")    
630                      
631                      command = input()
632                      if command.lower() == "exit" or command.lower() == "quit":
633                          exit(0)
634  
635                  except KeyboardInterrupt:
636                      exit(0)
637                  except EOFError:
638                      exit(0)
639  
640                  if command.lower() == "clear":
641                      print('\033c', end='')
642  
643                  # command_history.append(command)
644                  # while len(command_history) > command_history_max:
645                  #     command_history.pop(0)
646  
647                  else:
648                      code = execute(
649                          configdir = args.config,
650                          identitypath = args.identity,
651                          verbosity = args.verbose,
652                          quietness = args.quiet,
653                          detailed = args.detailed,
654                          mirror = args.mirror,
655                          noid = args.noid,
656                          destination = args.destination,
657                          command = command,
658                          stdin = None,
659                          stdoutl = args.stdout,
660                          stderrl = args.stderr,
661                          timeout = args.w,
662                          result_timeout = args.W,
663                          interactive = True,
664                      )
665  
666          else:
667              print("")
668              parser.print_help()
669              print("")
670  
671      except KeyboardInterrupt:
672          # tty.setnocbreak(sys.stdin.fileno())
673          print("")
674          if link != None:
675              link.teardown()
676          exit()
677  
678  def size_str(num, suffix='B'):
679      units = ['','K','M','G','T','P','E','Z']
680      last_unit = 'Y'
681  
682      if suffix == 'b':
683          num *= 8
684          units = ['','K','M','G','T','P','E','Z']
685          last_unit = 'Y'
686  
687      for unit in units:
688          if abs(num) < 1000.0:
689              if unit == "":
690                  return "%.0f %s%s" % (num, unit, suffix)
691              else:
692                  return "%.2f %s%s" % (num, unit, suffix)
693          num /= 1000.0
694  
695      return "%.2f%s%s" % (num, last_unit, suffix)
696  
697  def pretty_time(time, verbose=False):
698      days = int(time // (24 * 3600))
699      time = time % (24 * 3600)
700      hours = int(time // 3600)
701      time %= 3600
702      minutes = int(time // 60)
703      time %= 60
704      seconds = round(time, 2)
705      
706      ss = "" if seconds == 1 else "s"
707      sm = "" if minutes == 1 else "s"
708      sh = "" if hours == 1 else "s"
709      sd = "" if days == 1 else "s"
710  
711      components = []
712      if days > 0:
713          components.append(str(days)+" day"+sd if verbose else str(days)+"d")
714  
715      if hours > 0:
716          components.append(str(hours)+" hour"+sh if verbose else str(hours)+"h")
717  
718      if minutes > 0:
719          components.append(str(minutes)+" minute"+sm if verbose else str(minutes)+"m")
720  
721      if seconds > 0:
722          components.append(str(seconds)+" second"+ss if verbose else str(seconds)+"s")
723  
724      i = 0
725      tstr = ""
726      for c in components:
727          i += 1
728          if i == 1:
729              pass
730          elif i < len(components):
731              tstr += ", "
732          elif i == len(components):
733              tstr += " and "
734  
735          tstr += c
736  
737      return tstr
738  
739  if __name__ == "__main__":
740      main()