mlshell: Rápido como mula con patines

Enviado por Point_to_null el 31 Julio, 2009 - 11:38.

Scriptmlshell es una interfaz sencilla para programar operaciones en ml-donkey en python e incluye, como ejemplo, un optimizador de descargas.

¡No sobrecargues esa mula!

Como muchos uso la red ed2k (entre otras) para realizar descargas, pero mis descargas no suelen ir a toda velocidad por dos razones:

  • Mi ISP apesta: Limita mi cantidad de conexiones y tiene picos de trafico muy marcados, hay horas del día en que se pone insoportablemente lento.
  • Tengo gustos raros: Mi música favorita no es del tipo que suele aparecer en los rankings ni en las listas de ventas. Pocos la oímos, pocos la tenemos, pocos la compartimos.

En consecuencia me resulta muy difícil conseguir buenos recursos y, más aún, verlos en linea. Por eso me duele mucho ver los logs y enterarme que ese álbum que tanto quiero tardará una semana más en llegar porque las dos horas que estuvo enteramente disponible yo tenía un limite de descargas de 6 KB/s.

Pero tampoco puedo dejar mi mula con un limite de descarga alto porque, aunque no descargará nunca muy rápido, la velocidad de subida está siempre a tope y eso afecta muy notoriamente en la navegación.

La solución: escribir un script que ajuste la velocidad de bajada/subida de acuerdo a la disponibilidad de mis descargas.

El problema: mldonkey_command parece estar roto en las nuevas versiones de mlnet como ejecutable independiente y no parece que vaya a ser reparado pronto, de hecho... algunos script distribuidos con el proyecto lo reemplazaron con una interfaz muy primitiva escrita en perl que obtiene los datos desde la interfaz web.

Entonces, antes de escribir un script que procese la información tuve que escribir uno que la pueda obtener del servidor y, también, modificar las configuraciones sobre la marcha. Para esto escribí un modulo python que usa urllib2 para manejar mldonkey a través de su interfaz web y le agregué algo de abstracción.

Nota: Depende de /usr/bin/html2text, que se encuentra en el paquete html2text en debian.


    programacion/python/mlshell.py (ver ultima versión)
  1. #!/usr/bin/env python
  2. #-*- coding: UTF-8 -*-
  3. """mlshell: Wrapper to the web interface of ml-donkey that implement minimal
  4. shell capacities and can be used to automatice operacions."""
  5. import urllib2
  6. import base64
  7. import subprocess
  8. import re
  9. import os
  10. import logging
  11. from ConfigParser import SafeConfigParser
  12. from optparse import OptionParser, OptionValueError
  13. logging.root.setLevel(5)
  14. def html2text(html):
  15. """Process a html document to render as simple text"""
  16. proc = subprocess.Popen(["/usr/bin/html2text", "-style", "compact"],
  17. stdin=subprocess.PIPE, stdout=subprocess.PIPE)
  18. proc.stdin.writelines(html)
  19. proc.stdin.close()
  20. text = unicode("".join(proc.stdout.readlines()), 'utf-8', 'replace').strip()
  21. text = re.compile(r""":(\s*)""").sub(": ", text)
  22. text = re.compile(r"""(\s*)wiki$""", re.MULTILINE).sub("", text)
  23. return text
  24. class MLSession:
  25. """Wrapper to the web interface of ml-donkey"""
  26. def __init__(self, user=None, password=None, host="localhost", port=4080):
  27. if user and password:
  28. self.header = "Basic " + base64.encodestring("%s:%s" %
  29. (user, password)).strip()
  30. else:
  31. self.header = None
  32. self.host = host
  33. self.port = port
  34. self.fmt_prompt = u"(d%.1fKB/s|u%.1fKB/s): "
  35. self.locals = {
  36. "s" : self.bw_stats,
  37. "auto_rate" : lambda: self.optimize_rate_limits(
  38. self.variables.get("maxdownload", None),
  39. self.variables.get("maxupload", None),
  40. self.variables.get("minupload", None),
  41. self.variables.get("margin", None))
  42. }
  43. self.variables = {}
  44. def bw_stats(self):
  45. """Return the current bandwidth stats: (down, up)"""
  46. stats = [st.strip() for st in self.execute("bw_stats").split()]
  47. down_rate = float(stats[1])
  48. up_rate = float(stats[4])
  49. return down_rate, up_rate
  50. def optimize_rate_limits(self, maxdownload=None, maxupload=None,
  51. minupload=None, margin=None):
  52. """Optimize the rate limits to the best download rate."""
  53. maxdownload = maxdownload or self.variables.get("maxdownload", "0")
  54. maxupload = maxupload or self.variables.get("maxupload", "10")
  55. minupload = minupload or self.variables.get("minupload", "1")
  56. margin = margin or self.variables.get("margin", "down_rate ** (1/3.)")
  57. table = {36:9, 32:8, 28:7, 24:6, 20:5, 16:4, 9:3, 6:2, 3:1}
  58. down_rate, up_rate = self.bw_stats()[:2]
  59. self.variables.update((("down_rate", down_rate), ("up_rate", up_rate)))
  60. maxdownload = eval(str(maxdownload), self.variables)
  61. maxupload = eval(str(maxupload), self.variables)
  62. if maxupload == 0:
  63. maxupload = 10
  64. minupload = eval(str(minupload), self.variables)
  65. margin = eval(str(margin), self.variables)
  66. down_limit = down_rate + margin
  67. down_limit = min((k for k in table.iterkeys() if k >= down_limit))
  68. if maxdownload:
  69. if down_limit == 0:
  70. down_limit = maxdownload
  71. else:
  72. down_limit = min(down_limit, maxdownload)
  73. up_limit = min(max(table.get(down_limit, 10), minupload), maxupload)
  74. # down_limit = dict((i[::-1] for i in table.iteritems()))[up_limit]
  75. logging.debug(" ".join((" down_rate: %.2f", "up_rate: %.2f",
  76. "margin: %.2f", "down_limit: %d", "up_limit: %d")) %
  77. (down_rate, up_rate, margin, down_limit, up_limit))
  78. logging.debug(self.execute("set max_hard_upload_rate %d" % up_limit))
  79. logging.debug(self.execute("set max_hard_download_rate %d" %
  80. down_limit))
  81. return down_limit, up_limit
  82. def get_prompt(self):
  83. """Generate a prompt text"""
  84. return self.fmt_prompt % self.bw_stats()
  85. def execute(self, order):
  86. """Find the method asosiated or the order on the web interface"""
  87. if order in self.locals:
  88. return self.locals[order]()
  89. else:
  90. order = order.replace(" ", "+")
  91. req = urllib2.Request("http://%s:%d/submit?q=%s" % (self.host,
  92. self.port, order))
  93. req.add_header('Authorization', self.header)
  94. response = urllib2.urlopen(req)
  95. html = "".join(response.readlines())
  96. return html2text(html)
  97. def main():
  98. """Main rutine"""
  99. # == Reading the config file ==
  100. # Define the defaults value
  101. config = SafeConfigParser({
  102. "host":"localhost",
  103. "port":"4080",
  104. "user":None,
  105. "password":None,
  106. "maxdownload":"0",
  107. "maxupload":"10",
  108. "minupload":"1",
  109. "margin":"down_rate ** (1/3.)",
  110. })
  111. # Read the values on the file
  112. config.read(os.path.expanduser('~/.mlshell'))
  113. # == Reading the options of the execution ==
  114. def define_variable(option, opt_str, value, parser):
  115. """Handle the -d/--define option and populate the variables dict"""
  116. logging.debug(option.dest)
  117. logging.debug(value)
  118. variables = getattr(parser.values, option.dest)
  119. try:
  120. variable = re.search(r"".join(("^\s*([a-zA-Z_][a-zA-Z\d_]*)",
  121. "\s*=\s*(.*)\s*$")), value).groups()
  122. except AttributeError:
  123. raise OptionValueError("Declaración incorrecta: %s" % value)
  124. else:
  125. variables.update((variable,))
  126. logging.debug(variables)
  127. # Instance the parser and define the usage message
  128. parser = OptionParser(usage="""
  129. %prog [-vqd]
  130. %prog [-vqd] file
  131. %prog [-vqdc] command""", version="%prog 2")
  132. # Define the options and the actions of each one
  133. parser.add_option("-c", help="Read the comands from" +
  134. " the arg instead of from the standard input", action="store_true",
  135. dest="command")
  136. parser.add_option("-v", "--verbose", action="count", dest="verbose")
  137. parser.add_option("-q", "--quiet", action="count", dest="quiet")
  138. parser.add_option("-d", "--define", metavar="VAR=VALUE", action="callback",
  139. callback=define_variable, type="string", nargs=1, dest="variables",
  140. help="Define a variable VAR to VALUE")
  141. # Define the default options
  142. parser.set_defaults(verbose=2, quiet=0, variables={})
  143. # Process the options
  144. options, args = parser.parse_args()
  145. # == Execution ==
  146. # Crate the wrapper instance
  147. session = MLSession(
  148. config.get("Server", "user"),
  149. config.get("Server", "password")
  150. )
  151. for variable in config.options("Variables"):
  152. session.variables[variable] = config.get("Variables", variable)
  153. for variable in options.variables:
  154. session.variables[variable] = options.variables[variable]
  155. logging.debug(session.variables)
  156. if options.command:
  157. for order in args:
  158. print(session.execute(order))
  159. return 0
  160. elif args:
  161. print("Leyendo comandos en %s" % args[0])
  162. else:
  163. order = None
  164. while order not in ("q", "kill"):
  165. try:
  166. order = raw_input(session.get_prompt()).strip()
  167. except EOFError:
  168. return
  169. else:
  170. print(session.execute(order))
  171. if __name__ == "__main__":
  172. exit(main())

Si se lo ejecuta como programa independiente funciona de modo similar a una shell (de ahí su nombre) leyendo los comandos de la entrada estándar o de un archivo pasado como parámetro y escribiendo en la salida estándar. Las opciones pueden ser guardadas en un archivo de configuración ~/.mlshell


    .mlshell
  1. [Server]
  2. host = localhost
  3. port = 4080
  4. user = admin
  5. password = mipass
  6. [Variables]
  7. maxdownload = 0
  8. maxupload = 0
  9. minupload = 1
  10. margin = down_rate ** (1/3.)

Como ejemplo escribí un par de ordenes sencillas, entre ellas la mejor es auto_rate, que al ejecutarse ajustará las velocidades de bajada y subida para sacar mejor provecho a los recursos actuales sin derrochar ancho de banda mrgreen . Para sacar mejor provecho lo instalé en mi crontab (usando la opción -c de mlshell):


    crontab
  1. # m h dom mon dow command
  2. SHELL=/bin/sh
  3. PATH="/sbin:/usr/sbin:/usr/local/sbin:/bin:/usr/bin:/usr/local/bin:/usr/bin/X11:/usr/games:/home/deimos/bin"
  4. #m h ddm mes dds command
  5. * * * * * mlshell -c auto_rate > /dev/null 2>&1

Opcionalmente podemos poner limites en franjas horarias determinadas usando "-d maxdownload=24" o "-d maxupload=4", ecetera. Otros ejemplos los dejo como ejercicio del lector, que no por nada escribí tantas opciones con sus respectivas ayudas wink .

A modo de ejemplo os pongo unas gráficas, esta la tomé luego de tener la primer versión instalada en crontab, observad el progreso:
Grafico 1

Esta segunda muestra el funcionamiento normal de mis descargas usando auto_rate con un limite de descarga de 36 KB/s:

(Notad la casi perfecta simetría entre velocidad de descarga y subida)

Espero que a alguien más le sea útil mi ocurrencia bigsmile .

Imagen de warcry
Enviado por warcry el 31 Julio, 2009 - 11:41.

¿Y qué se supone que es lo que hace?

Imagen de Point_to_null
Enviado por Point_to_null el 31 Julio, 2009 - 11:46.
warcry escribió:

¿Y qué se supone que es lo que hace?

Si que eres rápido, lo publiqué para ver como quedaba el resaltado, ya termino de escribir tongue

Imagen de warcry
Enviado por warcry el 31 Julio, 2009 - 11:47.
Point_to_null escribió:
warcry escribió:

¿Y qué se supone que es lo que hace?

Si que eres rápido, lo publiqué para ver como quedaba el resaltado, ya termino de escribir tongue

ok, ok.
Me encanta mlnet =D.

Imagen de warcry
Enviado por warcry el 31 Julio, 2009 - 13:09.
Point_to_null escribió:

Mi ISP apesta

Bienvenido al club de usuarios de ISPs apestosos. ¿El tuyo limita la descarga desde http a velocidades de modém? El mío por lo menos sí.

Muy buena la foto de la mula.

Imagen de warcry
Enviado por warcry el 3 Agosto, 2009 - 12:07.

¿Esperabas que te comentaran más?

Imagen de Point_to_null
Enviado por Point_to_null el 3 Agosto, 2009 - 15:09.
warcry escribió:

¿Esperabas que te comentaran más?

jajajaja, pues... a mi me cambió la vida. Supuse que eramos más los que usamos mldonkey y nos gusta programarlo todo. Pero.. la verdad que no me sorprendí, estoy empezando a comprender los gustos de la audiencia.

Imagen de eliminado010
Enviado por eliminado010 el 3 Agosto, 2009 - 15:14.
Point_to_null escribió:
warcry escribió:

¿Esperabas que te comentaran más?

jajajaja, pues... a mi me cambió la vida. Supuse que eramos más los que usamos ml y nos gusta programarlo todo. Pero.. la verdad que no, estoy empezando a comprender los gustos de la audiencia.

Yo uso mlnet... pero mi conexión es bastante buena para no necesitar esto por suerte... de todas formas es muy util y me lo he guardado para usarlo en el futuro si me es necesario.

Saludos

Imagen de aBuSiViTo
Enviado por aBuSiViTo el 3 Agosto, 2009 - 18:37.
Point_to_null escribió:
warcry escribió:

¿Esperabas que te comentaran más?

jajajaja, pues... a mi me cambió la vida. Supuse que eramos más los que usamos mldonkey y nos gusta programarlo todo. Pero.. la verdad que no me sorprendí, estoy empezando a comprender los gustos de la audiencia.

Si tan solo tuviese internet...

Pero buee... sigue asi puntito, que lo que haces es genial.

Yo descargaba en la mula y alla en mi pais me hacia real falta eso :)

Saludos!

Imagen de jjgomera
Enviado por jjgomera el 3 Agosto, 2009 - 18:50.
Point_to_null escribió:
warcry escribió:

¿Esperabas que te comentaran más?

jajajaja, pues... a mi me cambió la vida. Supuse que eramos más los que usamos mldonkey y nos gusta programarlo todo. Pero.. la verdad que no me sorprendí, estoy empezando a comprender los gustos de la audiencia.

esto, yo es que uso el amule

gracias por el script que me sirve como ejemplo para aprender python

Imagen de Lycant
Enviado por Lycant el 3 Agosto, 2009 - 19:30.

Muy bueno, me lo guardo por si alguna vez lo necesito :P

Point_to_null escribió:

Tengo gustos raros: Mi música favorita no es del tipo que suele aparecer en los rankings ni en las listas de ventas. Pocos la oímos, pocos la tenemos, pocos la compartimos.

Y cual es?