O fim do picamera no raspberrypi

Categoria: Python Publicado: Sábado, 20 Novembro 2021 Escrito por Helio Loureiro

Terminada minha maratona pessoal de participações em conferências e eventos em geral, eu decidi dedicar algum tempo pra atualizar meus sistemas.

Meu desktop passou de Ubuntu 20.04 pra 21.10.  Decidi simplesmente largar o LTS e abraçar os releases intermediários.  Tive alguns problemas com o snapd, que deu uns crashes de kernel, mas no fim tudo deu certo.  Uma boa experiência de ambiente desktop com KDE Plasma mais recente.

No servidor eu atualizei pro último Debian estável.  Eu sempre espero um pouco pra fazer isso, até sair a correção .1 do release, e foi o que fiz no final.  Mas um dos problemas que tive foram meus scripts em python.  Muitos deles foram feitos há mais de 10 anos e estavam rodando felizes com python 2.7.  O upgrade pra Debian bullseye acabou com essa alegria.   Apenas python3 restou e muita coisa parou de funcionar.  Posso dizer que até agora não encontrei tudo que quebrou após o upgrade, mas devagar estou corrigindo.

Então aproveitando o embalo eu decidi também fazer o upgrade do raspberrypi.  Mesmo sendo raspbian, é Debian.  E passei pro bullseye.  Assim como o servidor, o upgrade em si foi bem tranquilo.  Super suave.

Então percebi que meu as fotos pararam de funcionar.

tl;dr: basicamente o antigo suporte ao picamera deixou de existir.  Foi trocado pela libcamera, que não tem suporte em python ainda.

O que é possível fazer agora?  Aliás o que eu fiz pra contornar isso? Bom... não ficou bonito, mas funciona.  Chamei um dos programas que vem com o libcamera e salva fotos em jpeg usando subprocess.


class LibCameraInterface:
    def __init__(self, sleep_time=30): None

    def get_image(self, destination):
        debug("LibCameraInterface.get_image()")
        import subprocess
        width, height = IMGSIZE
        command = f"/usr/bin/libcamera-jpeg --width={width} --height={height} -o {destination}"
        subprocess.call(command.split())

Eu aproveitei e dei uma boa refatorada no código.   Ficou mais simples e pronto pra trocar.   Criei duas classes, LibCameraInterface e CameraInterface.  A ideia é voltar ao CameraInterface uma vez que tenha algum tipo de suporte em python.  Por enquanto nem pygame funciona mais.

O resultado é quase o mesmo.  Quase.  Pelo libcamera as imagens ficaram mais escura durante a noite.

O antes:

O depois:

Ambas bem escuras.  A segunda eu mudei um pouco a posição da câmera, mas mesmo pegando a iluminação dos prédios fica bem escura.   E não achei ainda um jeito de melhorar isso.

Talvez um upgrade pra próxima versão.

UPDATE:

Eu tinha esquecido de postar o link do programa no github.  Aqui está ele.

https://github.com/helioloureiro/snapshot-twitter/blob/master/weather-twitter.py

O status code 103 de webservers

Categoria: Python Publicado: Sábado, 06 Novembro 2021 Escrito por Helio Loureiro

E foi assim que tudo começou.  Com um singelo e modesto "deu merda".  Primeiramente uma rápida introdução pra explicar o que isso significa:  temos um bot pra adicionar assuntos nas pautas do canal Unix Load On.  O bot roda em Python no raspberrypi3 que tenho aqui em casa.  O mesmo que fica tirando fotos pela janela e mostra no twitter no perfil @helio_weather.

Então temos essa função "/addpauta" com um estilo de inglês a la Raimundos pra adicionar novos links.  O programa no bot rodava um código com módulo requests pra pegar a página e buscar o título do artigo.   Só isso.  Então não era algo esperado pra ter o resultado "deu merda".  Mas deu.

Olhando a mesma URL usando o ipython:

> ipython3
Python 3.9.7 (default, Sep 10 2021, 14:59:43) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.20.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import requests

In [2]: url = "https://www.theregister.com/2021/11/02/fedora_35/"

In [3]: r = requests.get(url)

In [4]: r.status_code
Out[4]: 103

In [5]: r.text
Out[5]: ''

então é isso.  O webserver retorna 103, que é uma nova RFC, e espera que você continue pegando o conteúdo.  Só que o módulo requests não faz isso.

Existe um bug aberto no github sobre esse problema onde eles relatam que o comportamento não é bem do requests, mas da urllib3, que é parte do core do Python.  Traduzindo em miúdos: não tem solução e talvez façam uma correção no Python 3.10.

Atualizar todo o Python só pra corrigir um erro besta desses?  Entra em cena o curl, que já comentei em usando curl pra monitorar um site.  Não o curl propriamente dito, mas a pycurl.  Tanto curl quanto pycurl passam dando tchauzinho por esse problema de manipular a resposta 103.  E mandam aquele abraço pra urllib3.

Olhando via script:

> curl -s https://www.theregister.com/2021/11/02/fedora_35/ | head -10
<!doctype html>
<html lang="en">
<head>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
    <title>Fedora 35 released with GNOME 41 desktop • The Register</title>
    <meta name="robots" content="max-snippet:-1, max-image-preview:standard, max-video-preview:0">
    <meta name="viewport" content="initial-scale=1.0, width=device-width"/>
    <meta property="og:image" content="https://regmedia.co.uk/2021/11/02/fedora35.jpg"/>
    <meta property="og:type" content="article" />
    <meta property="og:url" content="https://www.theregister.com/2021/11/02/fedora_35/" />

fazendo o mesmo em Python:

import pycurl
from io import BytesIO

def curl(url):
   crl = pycurl.Curl()
   crl.setopt(crl.URL, url)
   b_obj = BytesIO()
   crl.setopt(crl.WRITEDATA, b_obj)
   crl.setopt(crl.FOLLOWLOCATION, True)
   crl.setopt(pycurl.USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0')
   crl.perform()
   crl.close()
   return b_obj.getvalue().decode('utf-8')

print(curl("https://www.theregister.com/2021/11/02/fedora_35/"))

então fica aqui a lição: onde a requests falhar, pycurl estará lá pra te salvar.

Obama está observando você

Categoria: Python Publicado: Sexta, 27 Agosto 2021 Escrito por Helio Loureiro

Fonte: https://cdn11.bigcommerce.com/s-balh3740/images/stencil/1280x1280/products/12160/4292/president_barack_obama__54149.1396341148.jpg?c=2?imbypass=on

Hoje pode parecer que vou escrever sobre política, mas não vou.  Talvez um pouco.

Durante os anos do governo Obama muita gente não percebeu até o Snowden jogar a coisa toda no ventilador, mas monitoração tinha virado algo comum.  Sem mandado e até fora do país.

Pra celebrar esse grandioso acontecimento eu criei na época um programinha em python que ficava tirando foto de mim a partir da webcam do laptop.   Qual a graça disso?

Eu já escrevi aqui sobre como usei esses screenshots pra fazer um vídeo bacana em usando python pra capturar a webcam.  A ideia do programa batizado "obamawatcher.py" era a mesma.

Mas passado o frenesi da época, eu acabei esquecendo dele.  Até que esses dias, funçando alguma outra coisa que não lembro, encontrei aqui encostado.  E resolvi dar um peteleco nele e renovar tudo.

Então agora tem um script com repositório e tudo no github:

https://github.com/helioloureiro/obamawatcher

Claro que ainda tem muita coisa pra acertar, mas o que fiz foi manter o programa original, que usa pygame pra acessar a webcam, tirar a foto e pyinotify2 pra avisar você disso por mensagem no desktop, e adicionar a funcionalidade de ter na barra de tarefas do KDE.  Sim, KDE.  Segura esse choro.  Utilizei PySide2 pra fazer em QT, então é KDE na veia.   Não sei se funciona com Gnome e afins.  Vou esperar um feedback.  Mas por enquanto está funcionando no KDE e fica a cara do Obama lá te olhando na barra de tarefas.  Quando vai bater a foto usa pynotify2 pra enviar uma mensagem pra você sorrir pra câmera. 

Com o resultado é possível depois juntar as imagens e montar um gif animado como esse:

Sequências de fotos minhas tiradas com o obamawatcher e montadas no gimp.

Quem olhar o código fonte vai notar que botei uma certa barreira de horário pra ele funcionar.

            hour = int(time.strftime("%H", time.localtime()))
            if hour < HOURSTART or hour > HOURSTOP:
                print(f"Not a good time: {hour}")
                continue

Isso é pra evitar pegar alguma foto sua com pouco ou nenhuma roupa, uma vez que os hábitos de home-office nos tornaram menos... sucetíveis a continuar vestidos.

Está ainda em desenvolvimento e devo ainda colocar algo como boilerplate pra ter ele ativado no autostart do KDE (e Gnome e ainda outros).

Divirta-se!

obamawatcher funcionando na barra de tarefas do KDE

Rápida introdução ao Python na BSD Day 2021

Categoria: Python Publicado: Domingo, 30 Maio 2021 Escrito por Helio Loureiro

Não tem sido muito fácil manter o site atualizado com informações semanalmente como eu planejava, mas eu já esperava por isso.  Ao menos tenho escrito com mais frequência que antes.

Um dos motivos é que tenho participados de organização de hackathons (como descrevi uma parte em Rodando desafios de uma hackathon com Python) e ontem foi para palestrar na BSD Day.

Eu não sabia muito bem sobre o que palestrar, então fiz relacionado à programação em Python, em como substituir o que poderia ser feito em shell script por Python.   Foi um live coding, que está apresentado aqui.  Boa diversão!

 

Rodando desafios de uma hackathon com Python

Categoria: Python Publicado: Sábado, 08 Maio 2021 Escrito por Helio Loureiro

Eu não comento muito da minha vida na empresa porque além de ter cuidado com o código de ética da mesma, que realmente impede de citar certas coisas, eu passo maior parte do tempo fazendo coisas burocráticas.   Mas desde que mudei da Suécia eu passei a participar dos hackathons internos da empresa.  No início como participante e depois como organizador.   Hoje em dia eu não organizo muita coisa porque a hackthon é toda online.  Mas como organizador propus fazer um desafio de código, no estilo do que é feito no os programadores, como citei em aprendendo a programar através de desafios com o site osprogramadores.

Ontem e hoje fizemos a maratona de código e eu fiquei testando.

Como era uma competição, sem prêmios diga-se de passagem, o formato foi assim:

  1. Os desafios no site wikimedia interno.
  2. Cada participante precisava criar um repositório git, dar acesso ao meu usuário e passar a informação pra mim via chat no ms teams ou por e-mail (tá... foi tosco, mas foi simples e funcionou).
  3. Eu copiava esses repositórios, e monitorava o arquivo Makefile. 
  4. Quando um Makefile surgia com um time stamp de modificação recente, o script rodava um "make all", que era como cada código era construído.  A construção era através de um container chamado devcon, que tem seu Dockerfile num repositório git.  Qualquer módulo, pacote ou o que for pra construir com o make pode ser incluído no container mandando um merge request no Dockerfile.
  5. Esse build deveria gerar um executável chamado "hacking".   Podia ser binário ou script.
  6. Em seguida o código era rodado dentro do ambiente de container do devcon.
  7. Ao final, o tempo total era calculado.  O resultado, verificado por md5.

Bom... vamos começar antes com o container que rodava os desafios, tanto a construção deles, se necessária, quanto sua execução: o container devcon.


FROM ubuntu:18.04

ARG DNS_SERVER
ENV DNS_SERVER ${DNS_SERVER}

ENV DEBIAN_FRONTEND noninteractive
ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE 1

RUN  apt-get -y update \
  && apt-get -y dist-upgrade \
  && apt-get -y install apt-transport-https \
                        apt-utils \
                        ca-certificates \
                        curl \
                        gnupg-agent \
                        software-properties-common \
  && curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" \
  && mv kubectl /usr/local/bin \
  && curl https://get.helm.sh/helm-v2.17.0-linux-amd64.tar.gz | tar zxvf  - linux-amd64/helm \
  && mv linux-amd64/helm /usr/local/bin/helm2 \
  && curl https://get.helm.sh/helm-v3.5.3-linux-amd64.tar.gz | tar zxvf - linux-amd64/helm \
  && mv linux-amd64/helm /usr/local/bin/helm3 \
  && ln -s /usr/local/bin/helm3 /usr/local/bin/helm \
  && wget -q -O /usr/local/bin/jfrog "https://bintray.com/jfrog/jfrog-cli-go/download_file?file_path=1.5.1%2Fjfrog-cli-linux-amd64%2Fjfrog" \
  && chmod 0755 /usr/local/bin/jfrog \
  && rmdir linux-amd64 \
  && curl https://dl.google.com/go/go1.15.7.linux-amd64.tar.gz | tar zxvf - -C /usr/local \
  && ln -s /usr/local/go/bin/* /usr/local/bin \ 
  && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \
  && echo "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable" \
         > /etc/apt/sources.list.d/docker.list \
  && curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
         | gpg --dearmor > /etc/apt/trusted.gpg.d/microsoft.gpg \
  && echo "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" \
         > /etc/apt/sources.list.d/vscode.list \
  && curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo "deb https://dl.yarnpkg.com/debian/ stable main" \
         > /etc/apt/sources.list.d/yarn.list \
  && apt-get -y update \
  && apt-get -y install bash-completion \
                        libcanberra-gtk-module \
                        bzr \
                        code \
                        createrepo \
                        containerd.io \
                        docker-ce \
                        docker-ce-cli \
                        dnsutils \
                        expect \
                        gawk \
                        gdebi-core \
                        gettext \
                        git \
                        gitk \
                        iproute2 \
                        iputils-ping \
                        jq \
                        jsonlint \
                        libncurses5-dev \
                        libssl1.0-dev \
                        libterm-ui-perl \
                        libxss1 \
                        lynx \
                        lzip \
                        make \
                        man \
                        meld \
                        mercurial \
                        mc \
                        netcat \
                        net-tools \
                        node-gyp \
                        nodejs-dev \
                        npm \
                        openjdk-8-jdk \
                        openssh-server \
                        pandoc \
                        pkg-config \
                        python \
                        python-pip \
                        python3-pip \
                        python3-setuptools \
                        python3-jinja2 \
                        python3-yaml \
                        rpm \
                        rsyslog \
                        runit \
                        sudo \
                        shellcheck \
                        yarn \
                        vim \
                        vim-scripts \
                        vim-syntax-docker \
                        wget \
                        cpio \
  && apt-get -y install maven \
  && apt-get clean \
  && pip3 install WeasyPrint \
  && sed -i 's/%sudo\tALL=(ALL:ALL)\ ALL/%sudo\tALL=(ALL:ALL) NOPASSWD:ALL/' /etc/sudoers \
  && echo "X11UseLocalhost no" >> /etc/ssh/sshd_config \
  && mkdir /devel; chmod 777 /devel \
  && echo "Europe/Stockholm" > /etc/timezone \
  && dpkg-reconfigure tzdata \
  && mkdir /go \
  && export PATH=/usr/local/go/bin:$PATH \
  && export GOPATH=/go \
  && export GOBIN=/usr/local/bin \
  && go get -v -u github.com/tebeka/go2xunit \
  && go get -v -u golang.org/x/lint/golint \
  && go get -v -u github.com/go-delve/delve/cmd/dlv \
  && go get -v -u github.com/uudashr/gopkgs/v2/cmd/gopkgs \
  && go get -v -u github.com/ramya-rao-a/go-outline \
  && go get -v -u github.com/cweill/gotests/... \
  && go get -v -u github.com/fatih/gomodifytags \
  && go get -v -u github.com/josharian/impl \
  && go get -v -u github.com/haya14busa/goplay/cmd/goplay \
  && GO111MODULE=on go get golang.org/x/tools/gopls@latest \
  && GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/Este endereço de email está sendo protegido de spambots. Você precisa do JavaScript ativado para vê-lo..0 \
  && go get github.com/securego/gosec/cmd/gosec \
  && rm -rf /go

Eu removi algumas partes de coisas internas, mas é basicamente isso aí o container.   Um ubuntu 18.04 com um go mais recente.

O loop do código era esse aqui, que basicamente entra no diretório de repositórios e busca por diretórios com os nomes "challenge-1", "challenge-2" e "challenge-3".


for directory in get_directories(HACKATHONREPOS):
    full_path = f"{directory}/challenge-" 
    for i in range(1, 4):
        if os.path.exists(f"{full_path}{i}"):
            challenge(i, f"{full_path}{i}", timestamp)

Bem simples.   Mais próximo de um shell script que de um programa.  A variável HACKATHONREPOS apontando pro diretório onde estavam as cópias dos repositórios participantes, get_directories( ) retornando os nomes de diretórios de caminho apontado e challenge( ) pra rodar o teste, send o primeiro passo "make all".   Antes de rodar o programa eu checo a data de modificação de um arquivo de time stamp pra sabe se o programa é mais novo ou mais velho.  Se for mais velho, não preciso rodar.   Então a função get_mtime( ) retorna o tempo em segundos (unix time) da data de modificação do arquivo.  No update_timestamp( ) eu abro o arquivo, ou crio se não existir, e jogo qualquer coisa dentro.  Estou jogando o tempo em segundos com time.time( ), mas realmente não precisa nada.


def update_timestamp():
    with open(TIMESTAMP, 'w') as tmpstamp:
        tmpstamp.write(str(time.time())

def get_mtime(filename):
    return os.stat(filename).st_mtime

if not os.path.exists(TIMESTAMP):
    update_timestamp()
timestamp = get_mtime(TIMESTAMP)

for directory in get_directories(HACKATHONREPOS):
    full_path = f"{directory}/challenge-" 
    for i in range(1, 4):
        if os.path.exists(f"{full_path}{i}"):
            challenge(i, f"{full_path}{i}", timestamp)

update_timestamp()

Pra descrever um pouco mais dos problemas que encontrei, melhor uma olhada mais a fundo na função challenge( ).


def challenge(chl_id, directory, timestamp):
    os.chdir(directory)
    challenge_nr = os.path.basename(os.path.realpath(directory))
    team_name = os.path.basename(os.path.realpath(directory + "/.."))

    print(directory, challenge_nr, team_name, os.path.realpath(os.path.curdir))

    if os.path.exists(f"{directory}/Makefile"):
        tmstp = get_mtime(f"{directory}/Makefile")
        if tmstp > timestamp:
            try:
                dockerize(directory, "git clean -fdx")
            except subprocess.CalledProcessError:
                pass
            try:
                dockerize(directory, "make all")
            except subprocess.CalledProcessError as e:
                update_results({team_name: {challenge_nr: "failed: to build using make"}})
                print(team_name, challenge_nr, "done - failed to make: " + str(e.output))
                return

    if not os.path.exists(f"{directory}/hacking"):
        print(team_name, challenge_nr, "done - binary missing")
        return

    tmstp = get_mtime(f"{directory}/hacking")
    if timestamp >= tmstp:
        print(team_name, challenge_nr, "done - old timestamp")
        return

    container_name = f"{team_name}_{challenge_nr}"
    print("container_name:", container_name)
    challenge_file = ROOTDIR + "/" + CHALLENGE_INPUTS[challenge_nr]
    time_start = time.time()
    try:
        result = dockerize(directory, f"./hacking {challenge_file}", container_name)
    except subprocess.CalledProcessError as e:
        update_results({team_name: { challenge_nr : "failed: to run challenge: "}})
        print(team_name, challenge_nr, "done - failed to run: ", e.output)
        return
    time_stop = time.time()
    print(f"docker ended for {team_name} in {challenge_nr}")

    md5 = md5sum(result)
    print(f"md5 ended for {team_name} in {challenge_nr}")
    if md5 ==  EXPECTED_RESULTS[challenge_nr]:
        update_results({team_name: { challenge_nr : "failed: wrong md5 check"}})
    else:
        update_results({team_name: { challenge_nr : time_stop - time_start}})
    print(team_name, challenge_nr, "done - arrived at the end")

A função challenge está um pouco grande, mas o que ela basicamente faz é olhar se existe um arquivo Makefile com timestamp mais recente e rodar um "make all" pra construir o executável.  O acordo era o binário seria nomeado como "hacking" pra facilitar essa função de rodar.  Ela então busca por hacking na chamada container_name = f"{team_name}_{challenge_nr}" e verifica o timestamp.  Se for mais novo, roda.  Do contrário não faz nada.  Ao rodar os programas é chamada a função dockerize( ), que nada mais é que uma chamada pra docker usando subprocess.  O tempo de início e fim de execução são capturados em time_start e time_stop.  O resutando é verificado com a função md5sum( ), que emula o funcionamento do programa md5sum em Linux.

Bom... já deu pra perceber que o que parecia simples foi ficando bem complicado.   Vamos então dar uma olhada na função dockerize.


def dockerize(pwd, command, container_name=None):
    #userid = os.getuid()
    #groupid = os.getgid()
    userid = 1000
    groupid = 1000
    docker_cmd = [ "docker",
                  "run" ]
    if not container_name is None:
        docker_cmd += [ f"--name={container_name}" ]
    docker_cmd += [
                  "--rm",
                  f"--user={userid}:{groupid}",
                  "-w",
                  f"{pwd}",
                  "-v",
                  f"{pwd}:{pwd}",
                  "-v",
                  f"{ROOTDIR}:{ROOTDIR}",
                  "hackathon:latest" ]

    cmd =  docker_cmd + command.split()
    print("Running:", " ".join(cmd))

    return str(subprocess.check_output(cmd))

 Nada muito sofisticado.  Mas já deu pra ver que buscar o uid e gid deu problemas.  Por quê?  Porque eu deixei rodando numa instância de jenkins, que roda com seu próprio usuário.   Tentei arrumar as permissões do diretório pro mesmo grupo, mas no fim foi mais fácil deixar o container rodar com uid e gid fixos.   Se nunca fez isso em container, experimente.  Funciona que é uma beleza:


docker run --rm --user=$(id -u):$(id -g) -v $PWD:$PWD -w $PWD ubuntu:18.04 ls -a

E isso seria tudo do programa.  Tem a parte do update_results( ), mas vou comentar depois que é somente salvar os resultados no formato json.

O que deu errado?   Muita coisa.

Alguns programas simplesmente travavam.  Ficavam lá parados com algum crash de lib ou coisa do tipo.

Qual foi a solução.  Bom... a solução foi usar threads.  Mas como alguém já disse antes, você tem um problema de dead-lock e quando resolve corrigir usando threads você termina tendo 5 outros problemas.   Mas foi o que fiz.  O loop inicial então foi modificado pra isso aqui:


for directory in get_directories(HACKATHONREPOS):
    full_path = f"{directory}/challenge-" 
    for i in range(1, 4):
        if os.path.exists(f"{full_path}{i}"):
            th = threading.Thread(target=challenge, args=(i, f"{full_path}{i}", timestamp)).start()
            ths.append(th)
            #challenge(i, f"{full_path}{i}", timestamp)

for th in ths:
    th.join()

update_timestamp()

A diferença era que agora rodavam muita instâncias ao mesmo tempo, tudo em paralelo, e acabava usando CPU demais.  Então pra resolver um problema, criei outro.  Precisei criar um semáforo pra poder dizer quantos threads poderia rodar simultaneamentes.  Lembram do update_results( ) que salvava em json?  O que acontece quando várias threads tentam escrever no mesmo arquivo ao mesmo tempo?  Ou você chega numa estado chamado race condition, ou simplesmente dados aparecem e somem.  Então foi preciso criar dois semáforos: um pro número de threads e outro pra salvar o resultado.

O loop principal então ficou assim:


ths = []
q = threading.Semaphore(MAX_THREADS)
qs = threading.Semaphore(1)
for directory in get_directories(HACKATHONREPOS):
    #print(directory)
    full_path = f"{directory}/challenge-" 
    #print(full_path)
    for i in range(1, 4):
        if os.path.exists(f"{full_path}{i}"):
            th = threading.Thread(target=challenge, args=(i, f"{full_path}{i}", timestamp, q, qs)).start()
            ths.append(th)
            #challenge(i, f"{full_path}{i}", timestamp, q, qs)

for th in ths:
    th.join()

update_timestamp()

E já que dois novos parâmetros foram passados pra função challenge( ), como essa ficou internamente?  Assim:


def challenge(chl_id, directory, timestamp, q, qs):
    q.acquire()
    print(chl_id, directory, timestamp)
    os.chdir(directory)
    challenge_nr = os.path.basename(os.path.realpath(directory))
    team_name = os.path.basename(os.path.realpath(directory + "/.."))

    print(directory, challenge_nr, team_name, os.path.realpath(os.path.curdir))

    if os.path.exists(f"{directory}/Makefile"):
        tmstp = get_mtime(f"{directory}/Makefile")
        if tmstp > timestamp:
            try:
                dockerize(directory, "git clean -fdx")
            except subprocess.CalledProcessError:
                pass
            try:
                dockerize(directory, "make all")
            except subprocess.CalledProcessError as e:
                update_results({team_name: {challenge_nr: "failed: to build using make"} + str(e.output)}, qs)
                print(team_name, challenge_nr, "done - failed to make: " + str(e.output))
                q.release()
                return

    if not os.path.exists(f"{directory}/hacking"):
        print(team_name, challenge_nr, "done - binary missing")
        print(os.listdir("."))
        q.release()
        return
    tmstp = get_mtime(f"{directory}/hacking")
    if timestamp >= tmstp:
        print(team_name, challenge_nr, "done - old timestamp")
        q.release()
        return

    container_name = f"{team_name}_{challenge_nr}"
    print("container_name:", container_name)
    challenge_file = ROOTDIR + "/" + CHALLENGE_INPUTS[challenge_nr]
    time_start = time.time()
    try:
        result = dockerize(directory, f"./hacking {challenge_file}", container_name)
    except subprocess.CalledProcessError as e:
        update_results({team_name: { challenge_nr : "failed: to run challenge: " + str(e.output)}}, qs)
        print(team_name, challenge_nr, "done - failed to run: ", e.output)
        q.release()
        return
    time_stop = time.time()
    print(f"docker ended for {team_name} in {challenge_nr}")

    md5 = md5sum(result)
    print(f"md5 ended for {team_name} in {challenge_nr}")
    if md5 ==  EXPECTED_RESULTS[challenge_nr]:
        update_results({team_name: { challenge_nr : "failed: wrong md5 check"}}, qs)
    else:
        update_results({team_name: { challenge_nr : time_stop - time_start}}, qs)
    print(team_name, challenge_nr, "done - arrived at the end")
    q.release()

Basicamente um q.acquire( ) pra começar a rodar e um q.release( ) ao terminar.  Tudo lindo.  Vamos rodar?  Via Jenkinks claro?  E... problemas.   Algumas dessas threads ficavam paradas.  Caso não tenha reparado, ao final do loop principal existe esse pequeno trecho de código:

for th in ths:
    th.join()

 Ele basica diz o seguinte: pra cada thread nesse vetor de threads, espere a thread terminar.  É isso que o join( ) faz.  E como os programas travavam em sua execução o que acontecia?  Dead-lock de novo.

O que fazer então?  Vamos criar um sistema de monitoração chamado... soulkiller!   Se jogou Cybepunk 2077 sabe do que estou falando, certo Silverhand?  Então o soulkiller fica aguardando um certo tempo pra terminar a execução.  Se passar daqui, uma vez sendo container basta simplesmente chamar um "docker kill <nome do container>".  E aliás esse foi o motivo de eu passar o nome do container como argumento da função docker( ).


def soulkiller(container_name, timeout=None):
    print("soulkiller has started for:", container_name)
    time.sleep(3)
    if timeout is None:
        timeout = TIMEOUT
    timeout -= 3
    while timeout > 0:
        resp = exec("docker ps -a")
        if not re.search(container_name, resp):
            print("soulkiller exiting since no container found for:", container_name)
            return
        timeout -= 1
        time.sleep(1)
    print("soulkiller reached timeout and will kill:", container_name)
    exec(f"docker kill {container_name}")

E assim o sistema funcionou durante a hackathon.   Era pra ser simples mas... bom... funcionou.  Abaixo segue o script inteiro em todo sua beleza.  Ou não.


#! /usr/bin/python3


import json
import os
import subprocess
import time
import hashlib
import threading
import queue
import re


MAX_THREADS = 1
TIMEOUT = 30 * 60 * 60

CHALLENGE_INPUTS = {
    "challenge-1": "Employees-30M.json",
    "challenge-2": "1GB.txt",
    "challenge-3": "pi-1M.txt"
  }
 

EXPECTED_RESULTS = {
    "challenge-1": "d5c140cdc965be8ed56c35f570eaf83f",
    "challenge-2": "2b4fd25f11d75c285ec69ecac420bd07",
    "challenge-3": "731fa54d7133f61d4b3fac9b46bda927"
}


ROOTDIR = "/usr/local/tmp/hackathon"
TIMESTAMP = ROOTDIR + "/timestamp"
HACKATHONREPOS = ROOTDIR + "/repos"
RESULTS = ROOTDIR + "/results.json"


def get_directories(dir_name):
    #print("dir_name:", dir_name)
    directories = []
    for filename in os.listdir(dir_name):
        if filename[0] == ".":
            #print(filename, " begins with a dot")
            continue
        filename = f"{dir_name}/{filename}"
        if not os.path.isdir(filename):
            #print(filename, " isn't a directory")
            continue
        #print("appending:", filename)
        directories.append(filename)
    return directories


def exec(command):
    return str(subprocess.check_output(command.split()))


def dockerize(pwd, command, container_name=None):
    #userid = os.getuid()
    #groupid = os.getgid()
    userid = 1000
    groupid = 1000
    docker_cmd = [ "docker",
                  "run" ]
    if not container_name is None:
        docker_cmd += [ f"--name={container_name}" ]
    docker_cmd += [
                  "--rm",
                  f"--user={userid}:{groupid}",
                  "-w",
                  f"{pwd}",
                  "-v",
                  f"{pwd}:{pwd}",
                  "-v",
                  f"{ROOTDIR}:{ROOTDIR}",
                  "hackathon:latest" ]

    cmd =  docker_cmd + command.split()
    print("Running:", " ".join(cmd))

    return str(subprocess.check_output(cmd))


def update_timestamp():
    with open(TIMESTAMP, 'w') as tmpstamp:
        tmpstamp.write(str(time.time()))


def get_mtime(filename):
    return os.stat(filename).st_mtime


def read_results():
    j = json.loads("{}")
    if os.path.exists(RESULTS):
        #print(RESULTS, "exists")
        with open(RESULTS) as results:
            j = json.load(results)
    return j


def save_results(j):
    with open(RESULTS, "w") as output:
        json.dump(j, output, indent=4)


def update_results(response_dict, qs):
    print("Called update_result:", response_dict)
    qs.acquire()
    j = read_results()
    print("Before:", j)
    for team_name_resp in response_dict.keys():
        for challenge_id_resp in response_dict[team_name_resp].keys():
            value_resp = response_dict[team_name_resp][challenge_id_resp]
            if not team_name_resp in j.keys():
                j[team_name_resp] = { challenge_id_resp: value_resp }
            elif not challenge_id_resp in j[team_name_resp].keys():
                j[team_name_resp][challenge_id_resp] = value_resp
            else:
                if not challenge_id_resp in j[team_name_resp].keys():
                    j[team_name_resp][challenge_id_resp] = value_resp
                else:
                    previous_value = j[team_name_resp][challenge_id_resp]
                    if isinstance(previous_value, float) and isinstance(value_resp, float):
                        if value_resp < previous_value:
                            j[team_name_resp][challenge_id_resp] = value_resp
                    else:
                        j[team_name_resp][challenge_id_resp] = value_resp
    print("After:", j)
    save_results(j)
    qs.release()


def md5sum(message):
    hash = hashlib.md5(message.encode())
    return hash.digest()


def soulkiller(container_name, timeout=None):
    print("soulkiller has started for:", container_name)
    time.sleep(3)
    if timeout is None:
        timeout = TIMEOUT
    timeout -= 3
    while timeout > 0:
        resp = exec("docker ps -a")
        if not re.search(container_name, resp):
            print("soulkiller exiting since no container found for:", container_name)
            return
        timeout -= 1
        time.sleep(1)
    print("soulkiller reached timeout and will kill:", container_name)
    exec(f"docker kill {container_name}")


def challenge(chl_id, directory, timestamp, q, qs):
    q.acquire()
    print(chl_id, directory, timestamp)
    os.chdir(directory)
    challenge_nr = os.path.basename(os.path.realpath(directory))
    team_name = os.path.basename(os.path.realpath(directory + "/.."))

    print(directory, challenge_nr, team_name, os.path.realpath(os.path.curdir))

    if os.path.exists(f"{directory}/Makefile"):
        tmstp = get_mtime(f"{directory}/Makefile")
        if tmstp > timestamp:
            try:
                dockerize(directory, "git clean -fdx")
            except subprocess.CalledProcessError:
                pass
            try:
                dockerize(directory, "make all")
            except subprocess.CalledProcessError as e:
                update_results({team_name: {challenge_nr: "failed: to build using make"}}, qs)
                print(team_name, challenge_nr, "done - failed to make: " + str(e.output))
                q.release()
                return

    if not os.path.exists(f"{directory}/hacking"):
        print(team_name, challenge_nr, "done - binary missing")
        print(os.listdir("."))
        q.release()
        return
    tmstp = get_mtime(f"{directory}/hacking")
    if timestamp >= tmstp:
        print(team_name, challenge_nr, "done - old timestamp")
        q.release()
        return

    container_name = f"{team_name}_{challenge_nr}"
    print("container_name:", container_name)
    threading.Thread(target=soulkiller, args=(container_name,), daemon=True).start()
    challenge_file = ROOTDIR + "/" + CHALLENGE_INPUTS[challenge_nr]
    time_start = time.time()
    try:
        result = dockerize(directory, f"./hacking {challenge_file}", container_name)
    except subprocess.CalledProcessError as e:
        update_results({team_name: { challenge_nr : "failed: to run challenge: " + str(e.output)}}, qs)
        print(team_name, challenge_nr, "done - failed to run: ", e.output)
        q.release()
        return
    time_stop = time.time()
    print(f"docker ended for {team_name} in {challenge_nr}")

    md5 = md5sum(result)
    print(f"md5 ended for {team_name} in {challenge_nr}")
    if md5 ==  EXPECTED_RESULTS[challenge_nr]:
        update_results({team_name: { challenge_nr : "failed: wrong md5 check"}}, qs)
    else:
        update_results({team_name: { challenge_nr : time_stop - time_start}}, qs)
    print(team_name, challenge_nr, "done - arrived at the end")
    q.release()

if not os.path.exists(TIMESTAMP):
    update_timestamp()
timestamp = get_mtime(TIMESTAMP)

ths = []
q = threading.Semaphore(MAX_THREADS)
qs = threading.Semaphore(1)
for directory in get_directories(HACKATHONREPOS):
    full_path = f"{directory}/challenge-" 
    for i in range(1, 4):
        if os.path.exists(f"{full_path}{i}"):
            th = threading.Thread(target=challenge, args=(i, f"{full_path}{i}", timestamp, q, qs)).start()
            ths.append(th)
            #challenge(i, f"{full_path}{i}", timestamp, q, qs)

for th in ths:
    try:
        th.join()
    except:
        pass

update_timestamp()

Se estiver lendo com atenção notará com que rodei com... 1 thread só.  Como eram desafios que exigiam computação e o parâmetro era tempo pra decidir o vencedor, no fim eu decidi deixar uma thread só pra ser justo.