python

python

  • Capturas da webcam atualizado pra 2024

    Esse é um update to artigo usando python pra capturar a webcam. E com o uso do obamawatched 2021. Eu atualizei recentemente o obamawatcher pra rodar com PySide6 e está aqui funcionando no meu laptop atual de trabalho. No meu pessoal também. Ao menos acho que está funcionando.

    O resultado final já está no vídeo acima. Como são várias imagens, os comandos que listei antes já não funcionam muito bem. Eu precisei primeiro redimensionar as imagens pra 640x360 para ficar em widescreen (16:9). As fotos mais antigas saíram em formato quadrado porque era o que a Webcam suportava na época. Então precisei cortar pra ficarem no aspecto correto. E pra isso eu usei python com pillow.

      
        #! /usr/bin/env python3
    
    import os
    import re
    import argparse
    try:
        from PIL import Image
    except ModuleNotFoundError as e:
        raise Exception("missing pillow - run: pip install Pillow") from e
    
    
    golden_rate = 1280/720
    default_size_x = 640
    default_size_y = 360
    
    parse = argparse.ArgumentParser(description="Script to resize pictures from a specific directory to the same size")
    parse.add_argument("--directory", required=True, help="directory with images jpg")
    args = parse.parse_args()
    
    for filename in sorted(os.listdir(args.directory)):
        if not re.search("jpg", filename):
            continue
        with Image.open(args.directory + "/" + filename) as image:
            width, height = image.size
            rate = float(width)/float(height)
            is_golden = rate == golden_rate
    
            is_correted = False
            rate_from_default = width / default_size_x
            if rate_from_default == 1:
                pass 
            elif rate_from_default > 1:
                image = image.resize((default_size_x, int(height/rate_from_default)))
                is_correted = True
            else:
                image = image.resize((default_size_x, int(height/rate_from_default)), Image.Resampling.LANCZOS)
                is_correted = True
    
            if not is_golden:
                image = image.crop((0, 0, default_size_x, default_size_y))
    
            image.save(filename)
            print(f"{filename}: {width}x{height}, golden: {is_golden}, corrected: {is_correted}")
    
    
    print(f"Golden rate: {golden_rate}")
     
    

    Tendo as imagens no mesmo formato, basta ordernar e usar ffmpeg pra montar o vídeo.

      
    #! /usr/bin/env bash
    
    die() {
      echo "$1" >&2
      exit 1
    }
    
    counter=0
    for img in [0-9]*.jpg
    do
      serial=$(printf "%06d" $counter)
      new_name="G${serial}.JPG"
      counter=$(expr $counter + 1)
      if [ -f "$new_name" ]; then
        echo "$new_name already exists"
        continue
      fi
      echo "$img => $new_name"
      mv $img $new_name
    done
    
    case $(uname -s) in
      Linux)
        echo "Merging images into single video file: output.mp4"
        ffmpeg -r 8 -i G%06d.JPG -c:v h264 -b:v 5M output.mp4 || \
          die "Failed to render output.mp4"
        ;;
      Darwin) 
        echo "Merging images into single video file: output.mp4"
        ffmpeg -hwaccel auto -r 8 -i G%06d.JPG -c:v h264_videotoolbox -b:v 5M output.mp4 || \
          die "Failed to render output.mp4"
    esac    
     
    

    Eu não testei o código de macOS, então pode ser que não funcione. Meu PC atual, um Lenovo Thinkpad, é processador e GPU Intel. Eu tentei usar -hwaccel vaapi mas as cores saiam erradas, no estilo negativo de filme. Então deixei rodar no processador mesmo.

    E pra comparar o resultado em mpeg comparado com o mesmo em gif:

      
    ░▒▓ …/tmp/imagens-webcam/Webcam2 via v3.12.3 (venv) 15:33 
    ❯ gm convert -delay 20 *JPG output.gif
    ░▒▓ …/tmp/imagens-webcam/Webcam2 via v3.12.3 (venv) 15:38 
    ❯ ls -l output.*
    .rw-rw-r-- 209M helio 27 Nov 15:38  output.gif
    .rw-rw-r-- 103M helio 27 Nov 13:54  output.mp4
     
    

    A geração do gif demorou uma eternidade, 5 minutos. O ffmpeg foram só alguns segundos. O tamanho foi o dobro no gif. Pra ver a imagem, teria de carregar tudo e só depois ver o resultando. Como mpeg, vai enviando o vídeo aos poucos.

    Eu finalizei o vídeo no kdenlive, fazendo o merge com o vídeo anterior e adicionando a música. Ficou uma nostalgia gostosa.

    E com certeza atualizarei daqui alguns anos.

  • Melhorando a previsão do ano do Linux no desktop

    Eu resolvi melhorar a previsão e o modelo pra tal sobre o Linux no desktop, uma realidade inegável.  Então escrevi algo em python pra fazer isso pra mim o trabalho e usar de algum modelo já pronto.

    
    (venv) helio@goosfraba ~/D/linux-desktop-dominance-forecast (main)> ./forecasting.py
                Windows  OS X  Unknown  Linux  Chrome OS  iOS  Android  \
    Date                                                                 
    2009-01-01    95.42  3.68     0.17   0.64        0.0  0.0      0.0   
    2009-02-01    95.39  3.76     0.14   0.62        0.0  0.0      0.0   
    2009-03-01    95.22  3.87     0.16   0.65        0.0  0.0      0.0   
    2009-04-01    95.13  3.92     0.17   0.66        0.0  0.0      0.0   
    2009-05-01    95.25  3.75     0.24   0.65        0.0  0.0      0.0   
    
                Playstation  Other  
    Date                            
    2009-01-01         0.08   0.02  
    2009-02-01         0.07   0.02  
    2009-03-01         0.08   0.02  
    2009-04-01         0.10   0.02  
    2009-05-01         0.09   0.02  
                Windows   OS X  Unknown  Linux  Chrome OS  iOS  Android  \
    Date                                                                  
    2024-01-01    73.00  16.11     5.33   3.77       1.78  0.0      0.0   
    2024-02-01    72.17  15.42     6.10   4.03       2.27  0.0      0.0   
    2024-03-01    72.47  14.68     6.52   4.05       2.27  0.0      0.0   
    2024-04-01    73.50  14.70     5.34   3.88       2.56  0.0      0.0   
    2024-05-01    73.91  14.90     4.87   3.77       2.54  0.0      0.0   
    2024-06-01    72.81  14.97     6.23   4.05       1.93  0.0      0.0   
    2024-07-01    72.10  14.92     7.13   4.44       1.41  0.0      0.0   
    
                Playstation  Other  
    Date                            
    2024-01-01          0.0   0.01  
    2024-02-01          0.0   0.01  
    2024-03-01          0.0   0.01  
    2024-04-01          0.0   0.01  
    2024-05-01          0.0   0.01  
    2024-06-01          0.0   0.01  
    2024-07-01          0.0   0.01  
    /home/helio/DEVEL/linux-desktop-dominance-forecast/venv/lib/python3.12/site-packages/statsmodels/tsa/base/tsa_model.py:473: ValueWarning: No frequency information was provided, so inferred frequency MS will be used.
      self._init_dates(dates, freq)
    /home/helio/DEVEL/linux-desktop-dominance-forecast/venv/lib/python3.12/site-packages/statsmodels/tsa/base/tsa_model.py:473: ValueWarning: No frequency information was provided, so inferred frequency MS will be used.
      self._init_dates(dates, freq)
    /home/helio/DEVEL/linux-desktop-dominance-forecast/venv/lib/python3.12/site-packages/statsmodels/tsa/statespace/sarimax.py:978: UserWarning: Non-invertible starting MA parameters found. Using zeros as starting parameters.
      warn('Non-invertible starting MA parameters found.'
    RUNNING THE L-BFGS-B CODE
    
               * * *
    
    Machine precision = 2.220D-16
     N =           11     M =           10
     This problem is unconstrained.
    
    At X0         0 variables are exactly at the bounds
    
    At iterate    0    f=  2.00575D-01    |proj g|=  1.24633D+00
    
    At iterate    5    f= -1.49635D-01    |proj g|=  1.77236D-01
    
    At iterate   10    f= -3.64537D-01    |proj g|=  1.24837D-01
    
    At iterate   15    f= -3.84843D-01    |proj g|=  1.22570D-01
    
    At iterate   20    f= -4.20877D-01    |proj g|=  9.71167D-02
    
    At iterate   25    f= -4.29351D-01    |proj g|=  1.13565D-01
    
    At iterate   30    f= -4.33425D-01    |proj g|=  9.06436D-02
    
    At iterate   35    f= -4.34142D-01    |proj g|=  3.98871D-02
    
    At iterate   40    f= -4.36192D-01    |proj g|=  4.26757D-01
    
    At iterate   45    f= -4.37801D-01    |proj g|=  6.82859D-02
    
    At iterate   50    f= -4.37938D-01    |proj g|=  1.67285D-02
    
               * * *
    
    Tit   = total number of iterations
    Tnf   = total number of function evaluations
    Tnint = total number of segments explored during Cauchy searches
    Skip  = number of BFGS updates skipped
    Nact  = number of active bounds at final generalized Cauchy point
    Projg = norm of the final projected gradient
    F     = final function value
    
               * * *
    
       N    Tit     Tnf  Tnint  Skip  Nact     Projg        F
       11     50     55      1     0     0   1.673D-02  -4.379D-01
      F = -0.43793754789434586     
    
    STOP: TOTAL NO. of ITERATIONS REACHED LIMIT                 
    /home/helio/DEVEL/linux-desktop-dominance-forecast/venv/lib/python3.12/site-packages/statsmodels/base/model.py:607:
      ConvergenceWarning: Maximum Likelihood optimization failed to converge. Check mle_retvals
      warnings.warn("Maximum Likelihood optimization failed to "
    The year of Linux on the Desktop: 2036-11-30 00:00:00
    

    Se ignorarmos os pequenos erros e avisos que aparecem, coisa pouca e irrelevante como valor divergir demais, podemos ver claramente quando o ano do Linux no desktop acontece: 2036-11-30

    Então a previsão revisada é que logo estaremos em todos os lugares. Aguardem-nos!

    E claro que publiquei isso tudo no GitHub:

    E usei o seguinte artigo como referência (e código, diga-se de passagem):

    Bom ano do Linux no desktop pra todos vocês.

  • Monitorando interfaces no Linux via netlink usando Python

    Esses dias estava analisando um código que era parecido com shell script.  Sua função era monitorar as interfaces de rede para, no caso de alteração (nova interface surgindo ou desaparecendo), registrar a mudança de objetos no sistema de dados do opensaf (objetos no IMM pra quem conhece).

    Mas o código fazia algo parecido com um shell que a todo healthcheck do AMF (framework do opensaf que é muito semelhante ao systemd e roda determinado programa ou script de tempos em tempos) fazia uma busca com o comando "ip link addr list" e comparava com o que estava  armazenado no IMM.  Algo como:

    def healthcheckCallback(self, invocation, compName, healthcheckKey):
    		        saAmfResponse(self.check_macs,
    				      invocation, eSaAisErrorT.SA_AIS_OK)

    def check_macs(self):
    macs = []
    for line in self.get_link_list().split("\n"):
    if not re.search("link/ether", line): continue
    # [ 'link/ether', '52:54:00:c3:5d:88', 'brd', 'ff:ff:ff:ff:ff:ff']
    mac.append(line.split()[1])
    imm_obj = self.get_from_imm()
    if imm_obj != macs:
    self.update_imm(mac)

    def get_link_list(self):
    linux_command = "ip link list"
    return self.run_shell(linux_command)

    Essa é uma forma bastante simplificada pra tentar visualizar como tudo funciona.  Eu propositalmente tirei comentários extras e deixei mais limpo apenas para poder comentar aqui. 

    • Agora explicando o que cada método faz ali:
    • healthcheckCallback(): esse é  o método que eu registrei junto ao AMF pra que seja chamado de tempos e tempos e rode a função check_macs().  Não vou entrar em detalhes dos outros parâmetros pois são inerentes em como o AMF funciona junto ao OpenSAF.  Deixei o link pra o exemplo de uma implementação completa ao final do artigo.
    • check_macs(): é uma função que pega uma listagem em formato array do comando "ip link list" e armazena os MAC, endereço de camada 2 da placa de rede.
    • get_link_list(): apenas pra deixar mais legível a parte que busca e monta o array de informações de rede de um sistema Linux.  O método run_shell() é apenas um subprocess.check_output() de forma mais legível (e por isso omiti essa parte do código).

    Como a chamada pra buscar os dados junto ao IMM no OpenSAF tem muitas linhas, eu só deixei um get_from_imm() que retornará um array de mac registrados anteriormente.  Se esse valor for diferente do coletado, então é chamado o método update_imm() com os macs que devem estar lá.

    Funciona?  Sim, funciona.  Mas... se não houve nenhuma mudança nas interfaces de rede (como a subida de uma interface de VIP ou mesmo um container em docker), por quê eu preciso rodar o get_link_list()? 

    Entendeu qual foi meu ponto?

    O código em si consiste em rodar o monitoramente separado numa thread.  Toda vez que o código detecta uma mudança (na verdade o kernel sinaliza isso), ele altera uma variável que o programa lê durante o healthcheck.  Algo como:

    def check_macs(self):
    if self.network_changed is False: return

    Assim bem simples.  Teve mudança? network_changed vira um True.

    Linux tem mecanismos pra detectar uma mudanças na interfaces de rede.  Por quê não usar?  E foi o que fiz.

    Criei um método chamado monitor_link() que é iniciado junto com programa no método initialize(), que é parte de como o AMF faz as chamadas de callback:

    self.thread = threading.Thread(target=self.monitor_link, args=())
    self.thread.start()

    E como funciona o monitor_link()?  Aqui tenho de pedir desculpas antecipadamente que enquanto o código utiliza menos CPU e memória que chamar um shell script, o tamanho e complexidade é bastante grande.  No fim troquei 2 linhas de código por umas 35 linhas.  Na verdade eu praticamente escrevi o código por trás do "ip link".  Mas o resultado ficou independente desse comando e mesmo de utilizar um shell externo pra buscar o resultado.

    A primeira coisa é criar um socket do tipo AF_NETLINK.  Em seguida fazer um bind() num ID aleatório e monitorar com RTMGRP_LINK.

    def monitor_link(self):
        # Create the netlink socket and bind to RTMGRP_LINK,
        s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
        s.bind((os.getpid(), RTMGRP_LINK))
    

    pra gerar o código aleatório que é um inteiro, usei os.getpid() pra usar o PID do próprio programa.

    Em seguida é iniciado um loop com select() em cima do descritor do socket pra leitura.  Quando aparecer algum dado, daí sim a informação é lida.

    rlist, wlist, xlist = select.select([s.fileno()], [], [], 1)

    O que vem a seguir são quebras da sequência de bits até chegar no ponto é possível ver o tipo de mensagem que chegou do select().  Se o tipo de mensagem for NOOP de algo nulo, apenas continue monitorando no select().  Se vier algum ERROR, pare o programa.  Se vier mensagem e não for do tipo NEWLINK pra um link novo ou mudança de MAC, também continue aguardando no select().

    if msg_type == NLMSG_NOOP: continue
    elif msg_type == NLMSG_ERROR: break
    elif msg_type != RTM_NEWLINK: continue

    Por fim uma iteração nos dados pra buscar o tipo.  Se o dado for do tipo IFLA_IFNAME, que é uma nova interface ou mudança de nome, ou IFLA_ADDRESS, que é MAC e endereço IP, muda a flag de network_changed pra True. 

    rta_type == IFLA_IFNAME or rta_type == IFLA_ADDRESS:

    E é isso.  O código completo segue abaixo.

    def monitor_link(self):
        # Create the netlink socket and bind to RTMGRP_LINK,
        s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
        s.bind((os.getpid(), RTMGRP_LINK))
    
        while self.terminating is False:
            rlist, wlist, xlist = select.select([s.fileno()], [], [], 1)
    if self.network_changed is True: continue if self.terminating is True: return
    try: data = os.read(rlist[0], 65535) except: continue msg_len, msg_type, flags, seq, pid = struct.unpack("=LHHLL", data[:16]) if msg_type == NLMSG_NOOP: continue elif msg_type == NLMSG_ERROR: break elif msg_type != RTM_NEWLINK: continue data = data[16:] family, _, if_type, index, flags, change = struct.unpack("=BBHiII", data[:16]) remaining = msg_len - 32 data = data[16:] while remaining: rta_len, rta_type = struct.unpack("=HH", data[:4]) if rta_len < 4: break rta_data = data[4:rta_len] increment = (rta_len + 4 - 1) & ~(4 - 1) data = data[increment:] remaining -= increment if rta_type == IFLA_IFNAME or rta_type == IFLA_ADDRESS: self.network_changed = True

    Encontrei essa implementação no Stack Overflow buscando informação do código em C.  Foi uma grande ajuda e deixou meu programa muito mais coerente com o que eu realmente queria.

    Ficou muito maior?  Ficou.  Mas também ficou muito mais 1337 :)

    Mais:

    [1] Exemplo de uso de python com AMF no OpenSAF: https://sourceforge.net/p/opensaf/staging/ci/default/tree/python/samples/amf_demo

    [2] Projeto OpenSAF: https://sourceforge.net/projects/opensaf/

    [3] Implementação original desse código: https://stackoverflow.com/questions/44299342/netlink-interface-listener-in-python

  • pycurl no MacOS

    Tive de trabalhar nessa semana com um caso que me exigiu usar o pycurl no Python.  O problema foi que escrevi um script que rodava baixando artefatos de build no Jenkins usando o módulo requests, e o mesmo não funcionava mais no Gitlab.

    Depois de gastar um pouco de tempo no request, e usando o curl do exemplo do site do Gitlab, eu acabei desistindo e indo pra usar o pycurl no script.  De cara descobri que não tinha pycurl instalado.  E no MacOS não foi tão simples como poderia ter sido.  A receita de bolo pra instalar o pycurl foi a seguinte sequência:

    helio@MacOS> arch -arm64 brew install openssl curl
    helio@MacOS> export PATH=/opt/homebrew/opt/curl/bin:$PATH
    helio@MacOS> export LDFLAGS="-L/opt/homebrew/opt/curl/lib":$LDFLAGS
    helio@MacOS> export CPPFLAGS="-I/opt/homebrew/opt/curl/include":$CPPFLAGS
    helio@MacOS> arch -arm64 pip install --no-cache-dir --compile --ignore-installed --install-option="--with-openssl" --install-option="--openssl-dir=/opt/homebrew/Cellar/openssl@3/3.0.7" pycurl

    Quando algo funciona em curl é fácil escrever o código em python.  Basta rodar com o parâmetro --libcurl foo.c que ele joga o código em funcionou dentro do arquivo.c no formato pra linguagem C, mas é bem próximo do uso em python.

       hnd = curl_easy_init();
      curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);
      curl_easy_setopt(hnd, CURLOPT_URL, "https://gitlab.[redacted]/api/v4/projects/[redacted]/jobs/[redacted]/artifacts");
      curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L);
      curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1);
      curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/7.86.0");
      curl_easy_setopt(hnd, CURLOPT_FOLLOWLOCATION, 1L);
      curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
      curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS);
      curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
      curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);
    

    Em python:

                 url = "https://gitlab.[redacted]/api/v4/projects/[redacted]/jobs/[redacted]/artifacts"
                 buffer = BytesIO()
                c = pycurl.Curl()
                c.setopt(c.URL, url)
                c.setopt(c.BUFFERSIZE, 102400)
                c.setopt(c.NOPROGRESS, 1)
                if GITLAB_PRIVATE_TOKEN:
                    c.setopt(c.HTTPHEADER, [ "PRIVATE-TOKEN:" + GITLAB_PRIVATE_TOKEN ])
                else:
                    c.setopt(c.HTTPHEADER, [ USERNAME + ":" + PASSWORD])
                c.setopt(c.USERAGENT, "curl/7.84.0")
                c.setopt(c.FOLLOWLOCATION, 1)
                c.setopt(c.HTTP_VERSION, c.CURL_HTTP_VERSION_2TLS)
                c.setopt(c.TCP_KEEPALIVE, 1)
                c.setopt(c.WRITEDATA, buffer)
                c.perform()
                c.close()

    E assim o código saiu funcionando.

  • Rodando um webserver localmente em Python

    Toda vez que gravamos o Unix Load On (Canal Unix Load On), eu acabo fazendo uma seleção tosca do que vamos falar.  Isso sempre me deu uma coceira de resolver.  E nada melhor que o bom e velho Python.

    Hoje eu coloquei as mangas de fora e fiz funcionar.  No bom e velho modo script: rodo num shell, e pego o resultado.  Mas cheguei no ponto em que gostaria de que isso estivesse disponível em modo web, pra eu poder mostrar durante o programa diretamente no browser.

    O Python fornece o módulo SimpleHTTPServer, que aliás parece que no Python3 virou uma classe de http.serve.  Mas tem.  Ele mostra o filesystem via interface http a partir de onde você chama o módulo.  Não deveria ser complicado fazer o output do meu script ir pra uma interface http.  E realmente foi o que fiz.

    
    def start():
    
        class Handler(BaseHTTPRequestHandler):
            def do_GET(self):
                self.send_response(200)
                self.send_header("Content-type", "text/html")
                self.end_headers()
                client_ip, client_port = self.client_address
                reqpath = self.path.rstrip()
                print(f"request from {client_ip}:{client_port} for {reqpath}")
                article = get_final_article()
                title = get_title(article)
                link = get_link(article)
                response = f"""
    <h1>Title: <a href="/{link}">{title}
    <h2>Link: <a href="/{link}">{link}</a></h2>
    """ 
                content = bytes(response.encode("utf-8"))
                self.wfile.write(content)
    

    Se leu com atenção vai ver que a linha do artigo eu pego em "article = get_final_article()".  Isso retorna algo como:

    * [Linux developers get ready to wield the secateurs against elderly microprocessors • The Register](https://www.theregister.com/2021/01/11/linux_olld_cpus/)

    Dai o restante é sanitizar cada um pra mostrar corretamente.

    O script todo pode ser visto aqui: https://github.com/helioloureiro/homemadescripts/blob/master/random_article.py

    O resultado no console é algo parecido com isso aqui:

    
    ./random_article.py 
    127.0.0.1 - - [26/Mar/2021 21:31:27] "GET /newarticle? HTTP/1.1" 200 -
    request from 127.0.0.1:46060 for /newarticle?
     = Articles =
     * /helioloureiro/canalunixloadon/blob/master/pautas/2016030.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2016040.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2016041.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2016050.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2016051.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2016090.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2016100.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2016110.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2016120.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2017010.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2017020.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2017030.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2017040.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2017060.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2017061.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2017070.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2017080.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2017110.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2018030.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2018050.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2018060.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2018080.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2018100.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2019020.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2019040.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2019070.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/2019110.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20200717.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20200807.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20201001.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20201015.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20201029.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20201115.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20201204.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20210121.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20210205.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20210215.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20210312.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20210325.md
     * /helioloureiro/canalunixloadon/blob/master/pautas/20210410.md
    Latest: /helioloureiro/canalunixloadon/blob/master/pautas/20210410.md
    https://raw.githubusercontent.com/helioloureiro/canalunixloadon/blob/master/pautas/20210410.md
    Article selected: * [Linux developers get ready to wield the secateurs against elderly microprocessors • The Register](https://www.theregister.com/2021/01/11/linux_olld_cpus/)
    title: Linux developers get ready to wield the secateurs against elderly microprocessors • The Register
    article: * [Linux developers get ready to wield the secateurs against elderly microprocessors • The Register](https://www.theregister.com/2021/01/11/linux_olld_cpus/)
    
We use cookies

We use cookies on our website. Some of them are essential for the operation of the site, while others help us to improve this site and the user experience (tracking cookies). You can decide for yourself whether you want to allow cookies or not. Please note that if you reject them, you may not be able to use all the functionalities of the site.