Rodando desafios de uma hackathon com Python

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

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.

Acessos: 289