Marcos Dione: remotely-upgrading-centos6-centos7

At $WORK we're in the middle of a massive OS upgrade in all of our client's appliances. Unfortunately, the OS is CentOS, which has no official method to do it except for 'reinstall'. This of course is unacceptable for us, we need an automatic system that can do it remotely. We have developed such a thing, mostly based on RedHat's official tool, redhat-upgrade-tool (rut). There are plenty of tutorials on how to do it on the web.

All was fine until we hit clients using UEFI instead of BIOS[1]. Unluckily rut does not seem to handle the grub to grub2 transition wery well in this case. I had to create a dummy RPM package that, if it detects that the machine boots via EFI, generates a grub2 config file in /boot/efi/EFI/centos/grub.cfg and runs efibootmgr to tell the EFI system to use the EFI partition and file to boot. Here's the whole %posttrans section:

(
    set -x

    efi_parition=$(mount | awk '$3 == "/boot/efi" { print $1 }' | cut -d / -f 3)

    if [ -n "${efi_parition}" ]; then
        # create grub2 config file for the EFI module
        mkdir -pv /boot/efi/EFI/centos
        grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg

        # it's a shame we have to this here and then efibootmgr it's going to do the inverse
        # convert /boot/efi -> /dev/fooX -> foo + X
        efi_disk=$(find -H /sys/block/* -name ${efi_parition} | cut -d / -f 4)
        # we can't just cut the last char (part number could be beyond 9)
        # and we can't assume it's just after the disk name (disk: nvme0n1, partition: nvme0n1p1, part_num: 1)
        efi_parition_number=$(echo ${efi_parition} | egrep -o '[0-9]+$')

        # create an entry in the EFI boot manager and make it the only one to boot
        efibootmgr --create --bootnum 0123 --label grub2 --disk "/dev/${efi_disk}" \
            --part ${efi_parition_number} --loader \\EFI\\centos\\grubx64.efi --bootorder 0123
    fi
) &> /tmp/upgrade-centos-6.9-7.2.log

Another part of the automatic upgrade script detects EFI, installs grub2-efi and that other RPM, so the code is executed during the actual process of upgrading to CentOS7.

But the gist of this post is about how did I manage to test such a thing. Luckily we had 3 Lenovo servers laying around, but I could only reach them via ssh and IPMI. If you don't know, BMC/IPMI is a 'second' computer in your computer with which, among many, many other things, you can remotely turn on/off your computer, access the console remotely and even upload an ISO image and mount it in the 'main' machine as if it was a USB CD-ROM disk. This last one will come handy later. Notice that this is the system in the middle of Bloomberg's article about China infiltrating into companies, so you better get acquainted to it.

These Lenovo machines already had CentOS7 installed, so the first step was to install CentOS6. This should be possible by copying a CentOS6's installer and booting it via grub2. But before that I also had to enable UEFI boot. This broke booting, because the existing system had grbu2 installed in a disk's MBR instead on a EFI partition, and the disk had DOS partitions instead of GPT ones.

So the steps I took to fix this were:

On The client machine:

  • Install the icedtea-plugin (the remote console is a java applet).
  • Download Super Grub2 Disk.
  • Connect via IPMI to the server, request the remote console.

On the remote server (all this through the IMPI remote console):

  • Login, download CentOS6 installer iso.
  • Tell the virtual device manager to add an ISO, using the SG2D ISO.
  • Reboot the machine.
  • Get into the Setup (another EFI module), enable EFI boot mode, reboot.
  • Start the Boot Selection Menu, boot from the virtual CD.

On SG2D/Grub2's interface:

  • Enable GRUB2's RAID and LVM support (the CentOS6 install ISO was in a LVM part).
  • Selected Print the devices/partitions to find my LVM part.
  • Loosely followed these instructions to boot from the ISO image:
    • Open the Grub2 prompt (Press c).
    • loopback loop (lvm/vg0-root)/root/CentOS-6.9-x86_64-minimal.iso
    • linux (loop)/images/pxeboot/vmlinuz
    • linux (loop)/images/pxeboot/initrd.img
    • boot

This boots the installer, but it's not able to find the source media, so install from a URL. After setting up TCP/IP (fixed in my case), I used the URL http://mirrors.sonic.net/centos/6/os/x86_64/.

Freebie: More than I ever wanted to know about UEFI.


[1] In UEFI systems, the BIOS/Legacy mode is just an EFI module that provides the legacy boot method; that is, boots from a disk's MBR.


sysadmin centos uefi grub2

Facundo Batista: Reponiendo la patente

El año pasado nos fuimos unos días con la familia a Ostende, entre Navidad y Año Nuevo. En general tuvimos bueno tiempo (aunque mucho viento, no pudimos hacer mar), pero hubo un par de lluvias complicadas.

Uno de los chaparrones, de especialmente alta intensidad en poco tiempo, nos encontró pegando una vuelta por Valeria del Mar. Cuando empezaron a caer las gotas (cada una como un baldazo de agua) apuntamos para volver al hotel.

En la vuelta pasamos por tres zonas inundadas. Las primeras dos era sólo cuestión de tomársela con calma. Pero la tercera y última estaba tan complicada que dudé mucho en pasar: para el caso aunque camionetas grandes pasaban bien, muchos autos pegaban la vuelta.

Pero le calculé bien y me mandé. Pasamos, aunque el agua llegó al parabrisas al bajarse la trompa del auto en un badén en la mitad de lo inundado. Llegamos al hotel, y cuando me bajé del auto, me di cuenta que habíamos perdido la patente de adelante!

Hice algunas averiguaciones, y no había mucha opción: hay que hacer un duplicado de las chapas y ponérselas al auto (lo cual tarda unas semanas, pero mientras tanto se lleva pegado al vidrio un papelito de trámite provisorio). Pero para eso hay que ir al Registro Automotor, y nosotros habíamos perdido la patente un sábado al mediodía, y el domingo volvíamos a casa, así que no había chance.

No todos los días llovió, eh, hicimos playa también

Fui a la policía, para hacer una declaración de que había perdido la patente. Luego de renegar un poco me la hicieron, pero la verdad es que a la hora de circular esa declaración no me servía para nada: no se puede circular sin patente. Pero tampoco iba a mandar el auto en grúa y a la familia en colectivo hasta casa. Así que me arriesgué, puse adelante la patente de atrás, y volvimos a casa.

Por suerte la caminera nunca me paró, y un par de días después ya hice el trámite en el registro número 3 de Olivos (un placer hacer trámites ahí, siempre te ofrecen más soluciones que trabas o inconvenientes).

A los quince días llamé para ver si estaban las chapas, y no. Tres días después volví a intentar, tampoco. Al otro día me llamaron del registro y me dijeron que les habían avisado que las chapas están demoradas, así que antes de que se venza el mes tenía que pasar a que me "renueven" el papelito provisorio que te permite circular mientras tanto.

El jueves pasado finalmente me llamaron para avisarme que las placas estaban listas, así que al otro día las fui a buscar (y obvio las puse al toque, porque como uno entrega los papelitos provisorios, sino no se puede circular).

Pero al ponerlas me di cuenta que los tornillos de adelante no agarraban bien. Les puse unos más gruesos que tenía por ahí, pero igual no quedaron bien, ¡es que las patentes en la Renault Stepway van agarradas al paragolpes, y el mismo es de plástico! Mejor prevenir que curar, pensé, entonces le puse poxipol a los agujeritos y puse los tornillos, para que queden mejor agarrados.

Pero soy porfiado, y quiero evitar tener el mismo quilombo la próxima vez que cruce un charco (o evitar en lo posible que me las roben, que también es una posibilidad), así que le puse algunos remaches (dos atrás, abajo, para que la chapa no se levante, y cuatro adelante, dos abajo y dos arriba).

La próxima que pierda la patente será porque perdí el paragolpes :p

Le puse varios remaches, no se sale más

Marcos Dione: customizing-the-python-language

Programming languages can be viewed as three things: their syntax and data model, their standard library and the third party libraries you can use. All these define the expressiveness of the language, and determine what can you write (which problems you can solve) and how easily or not. This post/talk is about how expressive I think Python is, and how easy it is or not to change it.

I said that we solve problems by writing (programs), but in fact, Python can solve several problems without really writing a program. You can use the interpreter as a calculator, or use some of the modules a programs:

$ python3 -m http.server 8000

With that you can serve the current directory via HTTP. Or do this:

$ python3 -m timeit '"-".join(str(n) for n in range(100))'
10000 loops, best of 3: 30.2 usec per loop
$ python3 -m timeit '"-".join([str(n) for n in range(100)])'
10000 loops, best of 3: 27.5 usec per loop
$ python3 -m timeit '"-".join(map(str, range(100)))'
10000 loops, best of 3: 23.2 usec per loop

to check which method is faster. Notice that these are modules in the standard library, so you get this functionality out of the box. Of course, you could also install some third party module that has this kind of capability. I find this way of using modules as programs very useful, and I would like to encourage module writers to consider providing such interfaces with your modules if you think it makes sense.

Similarly, there are even programs written in Python that can also be used as modules, which I think should also be considered by all program writers. For instance, I would really like that ssh was also a library; of course, we have paramiko, but I think it's a waste of precious developer time to reimplement the wheel.

The next approach I want to show is glue code. The idea is that you take modules, functions and classes, use them as building blocks, and write a few lines of code that combine them to provide something that didn't exist before:

import centerlines, psycopg2, json, sys, shapely.geometry, shapely.wkt, shapely.wkb

tolerance = 0.00001

s = sys.stdin.read()
data = json.loads(s)
conn = psycopg2.connect(dbname='gis')

ans = dict(type='FeatureCollection', features=[])

for feature in data['features']:
    shape = shapely.geometry.shape(feature['geometry'])

    shape = shape.simplify(tolerance, False)
    skel, medials = centerlines.skeleton_medials_from_postgis(conn, shape)
    medials = centerlines.extend_medials(shape, skel, medials)
    medials = shapely.geometry.MultiLineString([ medial.simplify(tolerance, False)
                                                 for medial in medials ])

    ans['features'].append(dict(type='Feature',
                                geometry=shapely.geometry.mapping(medials)))

s = json.dumps(ans)
print(s)

This example does something quite complex: it takes a JSON representation of a polygon from stdin, calculates the centerline of that polygon, convert is back to a JSON representation and outputs that to stdout. You could say that I'm cheating; most of the complexity is hidden in the shapely and centerlines modules, and I'm using PostgreSQL to do the actual calculation, but this is what we developers do, right?

Once the building blocks are not enough, it's time to write our own. We can write new functions or classes that solve or model part of the problem and we keep adding glue until we're finished. In fact, in the previous example, centerlines.skeleton_medials_from_postgis() and centerlines.extend_medials() are functions that were written for solving this problem in particular.

But the expressiveness of the language does not stop at function or method call and parameter passing; there are also operators and other protocols. For instance, instead of the pure OO call 2.add(3), we can simply write 2 + 3, which makes a lot of sense given our background from 1st grade. Another example which I love is this:

file = open(...)
line = file.readline()
while line:
    # [...]
    line = file.readline()
file.close()

versus

file = open(...)
for line in file:
    # [...]
file.close()

The second version is not only shorter, it's less error prone, as we can easily forget to do the second line = file.readline() and iterate forever on the same line. All this is possible thanks to Python's special methods, which is a section of the Python reference that I definitely recommend reading. This technique allowed me to implement things like this:

command1(args) | command2(args)

which makes a lot of sense if you have a shell scripting background; or this:

with cd(path):
    # this is executed in path

# this is executed back on the original directory

which also will ring a bell for those of you who are used to bash (but for those of you who don't, it's written as ( cd path; ... )). I can now even write this:

with remote(hostname):
    # this body excecutes remotely in hostname via ssh

Following this same pattern with the file example above, we can even simplify it further like so:

with open(...) as file:
    for line in file:
        # [...]

This has the advantage that not only relieves us from closing the file, that would happen even if an unhandled exception is raised within the with block.

Special methods is one of my favorite features of Python. One could argue that this is the ultimate language customization, that not much more can be done. But I'm here to tell you that there is more, that you can still go further. But first let me tell you that I lied to you: the pipe and remote() examples I just gave you are not (only) implemented with special methods. In fact, I'm using a more extreme resource: AST meddling.

As any other programming language, Python execution goes through the steps of a compiler: tokenizing, parsing, proper compilation and execution. Luckily Python gives us access to the intermediate representation between the parsing and compilation steps, know as Abstract Syntax Tree, using the ast.parse() function. Then we can modify this tree at our will and use other functions and classes in the ast module to make sure this modifications are still a valid AST, and finally use compile() and exec() to execute the modified tree.

For instance, this is how I implemented |:

class CrazyASTTransformer(ast.NodeTransformer):
    def visit_BinOp(self, node):
        if type (node.op) == BitOr:
            # BinOp( left=Call1(...), op=BitOr(), right=Call2(...) )
            update_keyword(node.left,
                           keyword(arg='_out', value=Name(id='Pipe', ctx=Load())))
            update_keyword(node.left,
                           keyword (arg='_bg', value=Name(id='True', ctx=Load())))
            ast.fix_missing_locations(node.left)
            update_keyword(node.right, keyword(arg='_in', value=node.left))
            node = node.right
            # Call2(_in=Call1(...), _out=Pipe, _bg=True)

        return node

I used Call1 and Call2 to show which is which; they're really ast.Call objects, which represent a function call. Of course, once I rewrote the tree, most of the code for how the commands are called and how the pipe is set up is in the class that implements commands, which is quite more complex.

For remote() I did something even more extreme: I took the AST of the body of the context manager, I pickle()'d it, added it as an extra parameter to remote(), and replaced it with pass as the body of the context manager, so the AST becomes the equivalent of:

with remote(hostname, ast_of_body_pickled):
    pass

When the context manager really executes, I send the AST over the ssh connection together with the locals() and globals() (its execution context), unpickle in the other side, restore the context, continue with the compile()/exec() dance, and finally repickle the context and send it back. This way the body can see its scope, and its modifications to it are seen in the original machine.

And that should be it. We reached the final frontier of language customization, while maintaining compatibility, through the AST, with the original interpreter...

Or did we? What else could we do? We certainly can't[1] modify the compiler or the execution Virtual Machine, and we already modify the AST, can we do something with Python's tokenizer or parser? Well, like the compiler and the VM, they're written in C, and modifying them would force us to fork the interpreter, with all the drawbacks of maintaining it. But can we make another parser?

On one hand, the Python standard library provides a couple of modules to implement your own parsers: tokenize and parser. If we're inventing a new language, this is one way to go, but if we just want a few minor changes to the original Python language, we must implement the whole tokenizer/parser pair. Do we have other options?

There is another, but not a simple one. pypy is, among other things, a Python implementation written entirely in (r)Python. This implementation runs under Python legacy (2.x), but it can parse and run current Python (3.x) syntax[4]. This implementation includes the tokenizer, the parser, its own AST implementation[2], and, of course, a compiler and the VM. This is all free software, so we can[3] take the tokenizer/parser combination, modify it at will, and as long as we produce a valid (c)Python AST, we can still execute it in the cPython compiler/VM combination.

There are three main reasons to modify this code. First, to make it produce a valid cPython AST, we will need to modify it a lot; cPython's compile() function accepts only ASTs built with instances of the classes from the ast module (or str or bytes[5]), it does not indulge into duck-typing. pypy produces ASTs with instances of its own implementation of the ast module; rewriting the code is tiresome but not difficult.

Second, on the receiving side, if we're trying to parse and execute a particular version of Python, we must run it at least under the oldest Python version that handles that syntax. For instance, when I wanted to support f-strings in my language, I had no option but to run the language on top of Python-3.6, because that's when they were introduced. This meant that a big part of the modifications we have to do is to convert it to Py3.

Finally, we must modify it so it accepts the syntax we want; otherwise, why bother? :)

So what do we get with all this fooling around? Now we can modify the syntax so, for instance, we can accept expressions as keyword argument names, or remove the restriction that keyword and positional arguments must be in a particular order:

grep(--quiet=True, 'mdione', '/etc/passwd')

After we modify the parser, it's able to generate an AST, but this AST is invalid because the compiler will reject it. So we still have to recourse to more AST meddling before passing it to the compiler. What I did for the parameter meddling was to create a o() function which accepts a key and a value, so --quiet=True becomes the AST equivalent of o('--quiet', True). Once we've finished this meddling, the original, official, unmodified interpreter will happily execute our monster.

All of these techniques are used in ayrton in some way or another, even the first one: I use python3 -m unittest discover ayrton to run the unit tests!


[1] Well, technically we can, it's free software, remember!

[2] The cPython AST, while being part of the standard library, is not guaranteed to be stable from versions to version, so we can't really consider it as part of the API. I think this is the reason why other implementations took the liberty to do it their own way.

[3] ... as long as we respect the license.

[4] In fact some of the work is implemented in the py3.5 branch, not yet merged into default. I'm using the code from this branch.

[5] This would also be another avenue: feed compile() the definite bytecode, but that looks like doing a lot of effort, way more than what I explain here.


python ayrton

Mariano Guerra: Download frontend generated data to a file with clojurescript

I had to write this because I couldn't find it with a quick search, so here it his for future people like me (or me).

This is how you allow users to download a file from data generated in the browser with the save file dialog in cljs:

(defn to-json [v] (.stringify js/JSON v))

(defn download-object-as-json [value export-name]
        (let [data-blob (js/Blob. #js [(to-json value)] #js {:type "application/json"})
                  link (.createElement js/document "a")]
          (set! (.-href link) (.createObjectURL js/URL data-blob))
          (.setAttribute link "download" export-name)
          (.appendChild (.-body js/document) link)
          (.click link)
          (.removeChild (.-body js/document) link)))

You call it like this:

(download-object-as-json (clj->js {:hello "world"}) "myfile.json")

As an extra, it shows many idioms to interoperate with js and js objects.

Marcos Dione: third-party-apps-not-working-in-fairphone-os-18.09.2

More than a year ago I bought a FairPhone2 because that's what a geek with social responsible inclinations (and some hardware hacking) does. It came with Android Marshmallow (aka 6), but last December I bit the bullet and upgraded to Nougat (7.1). Also, as any megacorporation paranoid geek would do, I don't have a Google account (even when 90%+ of my mails ends up in their humongous belly, but who uses mail nowadays anyways...), so I have been using it with F-Droid and Yalp Store.

The upgrade went smoothly, and almost right after it I was poking aroung the Yalp Store when I saw several system updates, including the Android System WebView. This component is the one responsible of showing web content in your apps, and, believe me, you use it more than you think. The new Android came with version 67.0.3396.87, and Yalp Store was offering v71.0.3578.99, so I didn't think about it and installed the further upgrade, along with most of the apps that I knew were not installed through F-Droid[1]. There's also the fact that since Nougat, ASWB is deprecated in favor of embracing Chrome, but I have it disabled in my phone, just like most of the Google Apps (including Google Play Services).

The issue came when I tried to launch the official Selfoss reader. The list of articles worked fine, but trying to read one made the app crash. Even worse were the two homebanking apps I have: they didn't even show their main screen.

Thanks to a small troubleshooting session with jochensp in the #fairphone ICR channel, we found out that in fact it was a ASWB problem (hint: use adb logcat). Once more I had to use one of those don't-know-how-shady-it-is APK mirror sites (I used APKMirror, if you're curious, but don't blame me if the soft you install from there comes with all kinds of troyans).

The first thing I tried was to downgrade to the original version, so I downloaded the closest one I found (they didn't have the exact version, which makes me wonder how ofthen do they scan apps for upgrades), but downgrades don't work, even with adb install -r -d. For some reason, the same site offered a newer version than Yalp Store (72.0.3626.53, which I just found out it's a beta version!), so I upgraded (manual download + install) and that fixed it!


[1] There's an issue where Yalp Store tries to manage the apps installed via F-Droid by offering the versions available in Google Play, but most if the time it doesn't work because F-Droid recompiles everything and I think the keys are different. I hadn't compared if the versions offered by YS are really newer than those in FD).


android fairphone

Damián Avila: Abandoning the oquanta domain name

This is a very short but important post!

As you probably know, this blog can be found as a subdomain of oquanta.info.

Starting Jan 23rd, 2019, I will be abandoning that domain name because GoDaddy is asking me unreasonable prices for renewal... and, actually, I am not using that domain except for the blog itself.

As you probably know as well, for several years, this blog has been hosted in gh-pages, so I will just use the default and expected URL provided by Github: http://damianavila.github.io/blog

Please, make sure to bookmark/save/link the correct URL if you want to keep reading about some of my stuff ;-)

Have a great start of the week!

Damián Avila: Abandoning the oquanta domain name

This is a very short but important post!

As you probably know, this blog can be found as a subdomain of oquanta.info.

Starting Jan 23rd, 2019, I will be abandoning that domain name because GoDaddy is asking me unreasonable prices for renewal... and, actually, I am not using that domain except for the blog itself.

As you probably know as well, for several years, this blog has been hosted in gh-pages, so I will just use the default and expected URL provided by Github: http://damianavila.github.io/blog

Please, make sure to bookmark/save/link the correct URL if you want to keep reading about some of my stuff ;-)

Have a great start of the week!

Facundo Batista: Encarando el nuevo año

No soy de hacer esas "promesas de fin de año" (¿o son de año nuevo?) en las que la gente dice "este año adelgazo", "este año aprendo surf", "este año salgo del closet", etc. Pero me agarraron ganas de planificar un poco los siguientes doce meses y ponerme como grandes objetivos en los que me gustaría invertir principalmente esfuerzo, como una forma de arrancar más ordenado.

Hay una frase que leí hace poco en un texto de Forn, de Primo Levi, que indica que "Los objetivos de la vida son la mejor defensa contra la muerte". No es que a mí me falte cosas para hacer: La idea es solamente ordenar un poco el futuro a corto plazo.

A nivel de desarrollo de software me gustaría empujar fades como proyecto que lidero con alguien más, seguir puliendo mi blog, y más allá de quizás agarrar algún otro con baja prioridad, quiero lanzar este año una especie de mezcla entre grupo de tareas y grupo de aprendizaje (para newbies y diverso) alrededor de CDPedia.

No digo que no trabajaré para nada en otra cosa de software, pero seguro que mi construcción de la agenda día a día y planificación normal de semanas subsiguientes va a estar fuertemente condicionados por estos proyectos que me "anoté".

Do I really look like a guy with a plan?

A nivel Python Argentina lo tengo separado como dos partes. Una es el grupo en sí mismo, y lo que tengo ganas de empujar este año es por un lado la migración conceptual a Discourse (conceptual, digo, porque no vamos a "migrar" realmente, es una nueva herramienta, que apunto a que termine reemplazando la vieja lista de mailman), la creación de un nuevo logo (cumplimos 15 años, es hora, ¿no?) y empujar a que Eventol gane características para ser usado como plataforma de juntadas recurrentes (como meetup.com, pero sin lock-in ni límite de asistentes) para la coordinación de juntadas en todo el país.

La otra es el laburo en la Asociación Civil. Estoy metiéndole detalles y detalles a medida que lo uso al sistema de Socies, y esperando que vuelva Gilga de vacaciones para ponerlo online: mi idea a este respecto es seguir agregándole features y pequeñas mejoras pero muy puntuales en función de que vaya surgiendo las distintas necesidades. Por otro lado, necesitamos un sistema para Soporte de Eventos (que básicamente maneje todo lo que es entrada y salida de dinero alrededor de un evento nuestro o de terceros), pero la idea ahí es armar una definición y que lo haga alguien más.

A nivel de evento propiamente dicho, por otro lado, tengo en vista dos o tres. Dos seguro, que es el PyCamp 2019, el cual ya pagué y compré pasajes (lo organiza Matu Varela, con el soporte de la Asociación Civil, claro), y la PyCon Argentina 2019, que todavía tenemos que disparar la organización propiamente dicha. Digo "dos o tres" porque tenía ganas de ir a la PyCon USA, pero justo cae durante mi cumpleaños, entonces me frenó un poco eso... para colmo me di cuenta de eso luego que propuse algunas charlas, entonces tomé la decisión de que si me aprueban alguna charla voy, pero si no aprovecho y me quedo con mi familia :)

Mariano Guerra: Creemos en la Web: Datos por favor, es promesa

En el capítulo anterior vimos como cargar datos simulando la espera que surge de cargar datos de servicios remotos, ahora vamos a ver como cargar datos remotos de verdad, pero primero tenemos que aprender sobre algo llamado promesas.

En una aplicación web todas las partes del código tienen que colaborar haciendo su trabajo lo mas rápido posible y dejando que otros puedan hacer su trabajo, si un pedazo de código se toma mucho tiempo, otras partes importantes no se ejecutan y se produce algo que te puede haber pasado, que es que la aplicación "se congela".

Es por eso que muchas funcionalidades en js se descomponen en pedazos mas chicos para evitar este problema.

Una de ellas es cargar datos de otros servicios, no podemos darnos el lujo de esperar hasta que respondan, ya que si se toma un par de segundos la aplicación se congela.

La solución es hacer la solicitud y obtener como resultado una promesa.

Una promesa es un objeto que nos permite registrar funciones para cuando la promesa sea cumplida. La promesa puede ser cumplida exitosamente o puede haber un error. También podemos registrar funciones para que corran cuando la promesa sea cumplida, no importa si con éxito o con error.

El tipo de dato promesa (Promise en ingles), no es nada mágico, si no que esta disponible para que la usemos si la necesitamos, vamos a probarla con ejemplos simples, no te preocupes si no entendés la parte de crear promesas, requiere un poco de "pensar de adentro para afuera", normalmente al principio solo "consumimos" promesas, pero es necesario que las creemos así podemos probar todos los casos.

Proba los fragmentos de código en glitch o en la consola en las herramientas de desarrollo.

Una promesa nunca cumplida:

let p1 = new Promise(function (resolve, reject) {});

Esta es la forma mas simple de crear una promesa, el tipo de dato Promise permite crear nuevos objetos de ese tipo con el operador new, el "constructor" de las promesas recibe como argumento una función, la función va a ser llamada en el momento en el que la promesa es construida y dicha función recibe dos argumentos:

resolve
una función a ser llamada si la promesa se cumple con éxito.
reject
una función a ser llamada si la promesa se cumple con un error.

Tanto resolve como reject son funciones que reciben un solo argumento, que va a ser pasado a las funciones que registremos para ambos casos.

Una promesa resuelta instantaneamente con éxito:

let p2 = new Promise(function (resolve, reject) {
    resolve('éxito!');
});

Una promesa resuelta instantaneamente con error:

let p3 = new Promise(function (resolve, reject) {
    reject('error!');
});

Todo muy lindo, pero para que sirve que la resolvamos con éxito o error si no cambia nada?

La utilidad de las promesas es poder registrar una o mas funciones que van a ser llamada en los tres casos que ya mencionamos: éxito, error o cuando se resuelva no importa el caso.

Empecemos registrando una función con la promesa que nunca se resuelve, vamos a usar el método then (entonces en ingles) de los objetos de tipo Promise para registrar una función que va a ser llamada cuando la promesa se cumpla exitosamente, cuando sea llamada va a recibir un argumento que es el valor con el que la promesa se cumplió (el valor prometido ;).

p1.then(function (resultado) {
    alert('p1 resuelta: ' + resultado);
});

A mi en firefox me muestra este resultado, que quiere decir que llamar al método then del objeto promesa devuelve la promesa misma, esto nos va a ser útil después para "encadenar" llamadas a métodos en la promesa.

< Promise { <state>: "pending" }

Fuera de eso, all llamar al método then del objeto promesa p1 no paso nada, es natural ya que esa promesa esta "pendiente" (pending en ingles) y nunca se va a cumplir.

La promesa p2 se cumplió apenas la creamos, que pasa si registramos una función a ser llamada cuando se resuelva con éxito? Probemos:

p2.then(function (resultado) {
    alert('p2 resuelta: ' + resultado);
});

Una aclaración, las funciones que pasamos por parámetro para que sean llamadas en ingles se llaman "callbacks", que traducido es "llamame de vuelta", probablemente use esa palabra de ahora en mas porque es mas corta y para que te acostumbres ya que se usa mucho en la documentación.

Volviendo al código, si lo probaste habrás notado que aparece un cuadro de dialogo mostrando el mensaje "p2 resuelta: éxito!", es decir que si registramos un callback aun después de que la promesa sea cumplida la función va a ser llamada.

Corre el código de nuevo y vas a ver que el dialogo aparece de nuevo, esto es útil ya que no tenemos que preocuparnos si registramos el callback antes o después de que la promesa se resuelva, una cosa a tener en cuenta es que cada callback va a ser llamada una sola vez, ya que cada objeto de promesa puede ser resuelta una vez.

Ahora probemos lo mismo con la promesa que se resuelve con error:

p3.then(function (resultado) {
    alert('p3 resuelta: ' + resultado);
});

El dialogo no aparece... porque then registra callbacks para el caso de éxito, si queremos registrar callbacks para el caso de error, tenemos que usar el método llamado catch (capturar en ingles).

p3.catch(function (resultado) {
    alert('p3 error: ' + resultado);
});

Ahora el dialogo aparece.

Que pasa si queremos hacer algo en ambos casos? hay otro método llamado finally (finalmente en ingles).

p2.finally(function () {
    alert('p2 finally');
});

p3.finally(function () {
    alert('p3 finally');
});

Ambos muestran el dialogo, pero como veras no reciben el valor de resolución porque no sabemos cual sucedió.

Que pasa si queremos hacer un poco de todo, algo si salio bien, por ejemplo actualizar datos, algo si salio mal, por ejemplo mostrar un error y algo siempre, por ejemplo esconder un mensaje de "Cargando".

Obviamente podemos escribir las tres llamadas separadas, pero como mencione las llamadas a los métodos then, catch, finally devuelven la promesa, por lo que podemos hacer algo que se llama "encadenar" llamadas, veamos como es:

p2.then(function (resultado) {
    alert('then: ' + resultado);
})
.catch(function (resultado) {
    alert('catch: ' + resultado);
})
.finally(function () {
    alert('finally');
});

No cambia nada con hacerlo junto o por separado, pero suele hacerse según preferencia así que lo aclaro.

Bueno, basta de promesas (cuac!), veamos como usar esto para cargar datos, para eso vamos a usar una función llamada fetch que hace una solicitud HTTP (como tu navegador para cargar esta pagina, imágenes y datos) y devuelve una promesa, vamos a cargar datos de ejemplo que puse en una pagina:

let url = "https://marianoguerra.github.io/creemos-en-la-web/paginas/promesas/datos.json";
fetch(url);

Ahora con todo lo que sabemos sobre promesas, veamos que nos da la promesa:

fetch(url).then(function (response) {
    console.log(response);
});

Lo que hago es mostrar el valor de response en la consola usando el método log del objeto console (consola en ingles).

A mi en firefox me muestra esto, a vos te puede mostrar algo un poco distinto:

Response { type: "cors", url: "https://marianoguerra.github.io/creemos-en-la-web/paginas/promesas/datos.json", redirected: false, status: 200, ok: true, statusText: "OK", headers: Headers, body: ReadableStream, bodyUsed: false }

Es un objeto de tipo Response (respuesta en ingles) que tiene información variada sobre la solicitud que hicimos, pero lo que nosotros lo que queremos son los datos cuando la solicitud termine, para eso tenemos que pedirle al objeto response que lea el contenido de la respuesta. El objeto response tiene muchos métodos, uno de ellos es el método text, que nos devuelve... otra promesa..., la cual al resolverse nos da el contenido de la solicitud.

fetch(url).then(function (response) {
    response.text().then(function (text) {
        console.log('Texto!', text);
    });
});

Al correrlo debería mostrar lo siguiente en la consola:

Texto! {
    "numero": 42,
    "lista": [1, 2, 3]
}

Como veras el contenido es texto, pero notaras que son datos javascript, el subset de javascript que sirve para describir datos y enviarlos entre computadoras se llama JSON (pronunciado yeison, acrónimo en ingles de JavaScript Object Notation, que significa Notación de Objetos JavaScript).

Hay un objeto llamado JSON que tiene dos métodos útiles, uno llamado parse que recibe como argumento un valor de tipo texto (string) y nos devuelva los datos representados en ese texto, este es el que necesitamos, probemoslo:

fetch(url).then(function (response) {
    response.text().then(function (text) {
        let datos = JSON.parse(text);
        console.log('Datos!', datos);
    });
});

Por suerte como esta es una actividad común, el objeto response tiene un método llamado json que hace la tarea por nosotros:

fetch(url).then(function (response) {
    response.json().then(function (datos) {
        console.log('Datos!', datos);
    });
});

Ya que estamos hablando de JSON veamos el otro método stringify (algo así como "hacer texto" en ingles), que es el inverso de parse, es decir, recibe datos y nos devuelve la representación JSON de esos datos en un valor de tipo texto (string):

JSON.stringify({numero: 42, lista: [1, 2, 3]});

El resultado es:

< "{\"numero\":42,\"lista\":[1,2,3]}"

Para estar seguros de que funciona, probemos el inverso:

JSON.parse("{\"numero\":42,\"lista\":[1,2,3]}");

Notar que para poder insertar comillas dobles en un string, que ya tiene comillas dobles para indicar comienzo y fin, necesitamos poner una barra invertida antes de la comilla, para indicarle que no es el fin del string, sino que queremos poner esa comilla "dentro" del string. Esto normalmente se llama "escapar" caracteres.

Para finalizar, solo recordar que como fetch devuelve una promesa, podemos "encadenar" llamadas a then, catch y finally para hacer distintas operaciones según cual fue el resultado de la solicitud.

La forma general es:

fetch(url).then(function (response) {
    // resultado de la solicitud
})
.catch(function (error) {
    // si hubo error
})
.finally(function (error) {
    // cuando la solicitud termino
});

Resumiendo, aprendimos sobre promesas, llamadas encadenadas, la función fetch para hacer solicitudes a otros servicios, el formato JSON y su objeto con sus métodos parse y stringify.

Facundo Batista: A veces no es tan fácil jugar

Intro 1

En este post les había comentado de este HW que yo usado para montar una nube en casa, no sólo para mi servicio de sincronización de archivos, sino también para generar CDPedias.

Bueno, hace un año o más o menos, se rompió. Empecé a notar que a veces la máquina estaba colgada, y la reiniciaba. Luego me pasó que a veces no levantaba del reset, cada tanto. Y más seguido. Y llegó un punto en que no booteó más :(.

A la hora de intentar arreglarlo me enfrenté con la triste realidad que los componentes no eran estándar para nada. Entonces, ¿cómo sabía qué se había roto? Seguro era la CPU, la mother, o la memoria... pero para ver qué era lo roto tenía que empezar a comprar componentes, quizás en falso. No podía probar componentes de otra computadora que sí sabía que andaban.

Entre una cosa y la otra, un amigo me recomendó poner una "mini PC", que no es más que una computadora PC normal pero pensada para que ocupe poco espacio, y que consuma poco, incluso sacrificando rendimiento, para no tener un ventilador en la CPU y generar menos ruido. Pero todo con componentes "normales" (especialmente la memoria, que es algo que se jode a veces y es trivial encontrar otra y probar). Me terminé comprando esto:

La Biostar A68N-5100

Intro 2

En otro orden de cosas, con Felu hace rato que estamos jugando aventuras gráficas. Él se enganchó con Thimbleweed Park y desde ahí no paramos: las tres Monkey Island, Indiana Jones, Gabriel Knight, Sam y Max, Día del Tentáculo...

...y se nos empezaron a acabar los que corrían más o menos fácil en ScummVM o Dosbox. Se me ocurrió jugar al Myst.

El problema es que el Myst es un juego "de Windows", y conseguir software viejo en Windows es siempre un quilombo. Yo había comprado el juego, unos ¿20? años atrás, pero claro, andá a saber donde están esos discos, posiblemente en el CEAMSE.

¿Entonces?

El siguiente es el relato de cómo armé un setup para jugar al Myst desde la compu que uso normalmente (la de escritorio, que corre KDE Neon sobre Ubuntu Bionic).

Vamos con la aventura de jugar al Myst

Primera imagen (súper representativa) del Myst

Localmente

Lo primero era conseguir un Windows corriendo. Así que bajé el último Virtualbox y lo instalé, luego bajé un Windows legal pensado para armar VMs, y armé una VM con eso (Virtualbox te permite "importar" las definiciones que eso mismo que bajás trae, es casi demasiado fácil).

¡Paréntesis! En todo este artículo uso mucho la sigla "VM": significa Virtual Machine, "máquina virtual" en castellano, simplemente la posibilidad de simular una computadora completa por software (miren Wikipedia para más info). Cierro paréntesis.

Levanté la VM, instalé Steam, donde compré Myst a $78 (Masterpiece edition, que es el original con algunos detalles, no confundir con uno super remasterizado 3D, que no me interesaba, porque quería el original)... sí, 78 pesos.

Lo ejecuté, y aunque funcionaba, se escuchaba muy mal. No con ruido, sino como entrecortado. Estuve revisando y buceando la internechi hasta que encontré que parece que es un problema con la versión de Virtualbox que estoy usando, que es la 5.2. Lo comprobé metiendo un mp3 en la VM y reproduciéndolo ahí: se escuchaba igual de entrecortado, con lo cual descarté que el problema fuese de Steam o Myst.

Por lo que leí parece que es un problema de buffer circular en la simulación de la placa de sonido, donde el guest escribe y el host lee, y hay un mismatch de velocidad de lectura/escritura y todo bien mientras los punteros están en lugares distintos, pero cuando se cruzan, se entrecorta.

En fin, era una porquería.

También encontré que todos decían que la última versión de Virtualbox donde había funcionado bien era la 5.0.40. Fui a bajar esa, pero sólo la sacaron hasta Ubuntu Xenial. O sea, no hay Virtualbox 5.0.40 para la versión de mi sistema operativo, que es más nuevo.

Ahí se me prendió la lamparita: yo tengo un server con Xenial. ¿Se podrá instalar Virtualbox en un servidor, correr un Windows adentro, con Steam, y usarlo desde mi escritorio?

Descubriendo un mundo de posibilidades

Remotamente

Bajé el Virtualbox 5.0.40 y su paquete de extensión y los llevé al server, e instalé:

sudo dpkg -i virtualbox-5.0_5.0.40-115130~Ubuntu~xenial_amd64.deb
sudo VBoxManage extpack install Oracle_VM_VirtualBox_Extension_Pack-5.0.40-115130.vbox-extpack

Luego armé la VM con Windows, y la levanté:

VBoxManage import IE11\ -\ Win7.ovf
VBoxHeadless --startvm "IE11 - Win7"

Para configurarle cosas en general tiene que estar apagada. Pueden simular "apretar el botón de apagado" o directamente "desenchufarla a lo bruto", con los siguientes comandos:

VBoxManage controlvm "IE11 - Win7" acpipowerbutton
VBoxManage controlvm "IE11 - Win7" poweroff

Para usar visualmente la VM, la idea era conectarme remotamente desde mi computadora de escritorio. Para eso hay que habilitar el "escritorio virtual remoto", y agregarle una autenticación por usuario/clave:

VBoxManage modifyvm "IE11 - Win7" --vrde on
VBoxManage setproperty vrdeauthlibrary "VBoxAuthSimple"
VBoxManage modifyvm "IE11 - Win7" --vrdeauthtype external
VBoxManage internalcommands passwordhash LA-CLAVE-QUE-QUIERAN
VBoxManage setextradata "IE11 - Win7" "VBoxAuthSimple/users/EL-USUARIO-QUE-QUIERAN" EL-HASH-QUE-DIO-EL-COMANDO-ANTERIOR

Para que me ande el audio tuve que anularle la deshabilitación por default:

VBoxManage modifyvm "IE11 - Win7" --vrdeproperty Client/DisableAudio=

La comunicación del portapapeles no la pude hacer andar, pero sí el que monte un directorio compartido (instalando primero los agregados en el guest):

VBoxManage storageattach "IE11 - Win7" --storagectl "IDE Controller" --port 1 --device 0 --type dvddrive --medium /usr/share/virtualbox/VBoxGuestAdditions.iso
VBoxManage sharedfolder add "IE11 - Win7" --name shared --hostpath /home/facundo/vbox/shared --automount

No voy a contar el detalle de todos lo que probé y no me anduvo, pero les dejo un par de tips:

  • en algunos casos estos comandos de "cambiar cosas de la VM" dan errores super raros: probá apagando la VM y mandando el mismo comando con la VM apagada (tiene sentido, pero el error original no tiene nada que ver y confunde)

  • estos comandos son piolas para ver las VMs todas, las que tenés corriendo, e info puntual de alguna:

    VBoxManage listvm
    VBoxManage list runningvms
    VBoxManage showvminfo "IE11 - Win7"
    
  • toda la info sobre virtualbox remoto acá.

Fantástico. Ahora puedo levantar el krdc en mi máquina de escritorio, me conecto al Virtualbox de mi server, y tengo una ventana/pantalla con Windows andando. Llevé un mp3, lo reproduje, y escuchaba el audio perfectamente, sin problema alguno.

¡Buenísimo! Instalé Steam, perfecto. Instalé Myst (al loguearme con mi misma cuenta, ya lo tenía comprado).

Ejecuté Myst. Crasheó. Estuve probando algunas cosas (como agregarle aceleración de video 2D, seguía crasheando, o agregarle aceleración 3D, que no me dejaba porque no tenía sistema gráfico asociado).

Tristeza não tem fim.

Todo mal, loco

¿Y ahora? ¿Qué alternativas tenía?

Ahí me cayó la ficha que "mi server en la nube" está realmente a un metro de distancia, y que si le enchufaba un teclado y un mouse podría probar esto mismo pero pseudolocalmente.

Local, pero en la nube (?)

Busqué un cable HDMI (que conecté al mismo monitor que uso con la compu de escritorio, que está conectada por DVI), un mouse viejo, y le robé el teclado a la raspi que tengo para jugar. Enchufé todo, entré, pero tenía todas terminales como sólo texto, no tenía una interfaz gráfica. ¡Claro! Nunca había instalado un escritorio, ya que era una máquina servidora pura. Ergo:

sudo apt install ubuntu-desktop
sudo systemctl enable lightdm
sudo reboot

Ahora sí: inicié sesión en el Ubuntu, abrí Virtualbox, todo de forma gráfica. Entré en Steam. Corrí Myst. Crasheó. :(

Ví que podía hacer en la configuración. Probé lo más fácil: subirle la memoria a la placa de video, y agregarle aceleración 3D (ahora sí estaba en un entorno gráfico, je).

Levanté todo. Probé Myst. ¡Anduvo! Claro, sin sonido porque la máquina server no tiene parlantes enchufados.

Pero volví a la computadora de escritorio (que es decirle al monitor que use la otra entrada), levanté el krdc de nuevo, me conecté al server, donde obviamente ya tenía levantado todo, y finalmente pudimos jugar al Myst.

Funciona un poco lento, porque la computadorita server no es gran cosa (está más pensada para que consuma poco que para que uno corra juegos), y encima tiene al Virtualbox simulando una máquina para que corra Windows, para poder ejecutar Steam donde adentro corre el juego en sí.

Adentro de adentro de adentro de adentro de adentro de

Pero podemos jugar al Myst :D

Feliz año.