Rápida introdução ao Python na BSD Day 2021
30 de Maio de 2021, 13:51 - sem comentários aindaNã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!
Limpando entradas de um disco removido do LVM
24 de Maio de 2021, 18:22 - sem comentários aindaE minhas aventuras com LVM continuam. Um dos HDDs começou a chiar. Mas chiar mesmo, fazendo barulho de marteladas e resets. Olhando nos logs eu vi que já tinha dado o que tinha que dar. E justamente o HDD que fazia parte do mirroring em que estão os jogos da steam.
Então procedi com o comando pra terminar a montagem em mirroring.
lvconvert -m 0 diskspace/steam /dev/sdb3
Infelizmente eu dei um reboot em seguida e não salvei nada pra poder postar aqui. Mas o mais importante foi depois de removido HDD e instalado um novo (também de 2 TB) que tinha parado aqui. Como eu esqueci de remover o /dev/sdb3 do LVM, claro que subiu com vários erros. Particionei o novo HDD (/dev/sdb) com uma só partição LVM. E fui adicionar quando...
root@goosfraba ~# pvcreate /dev/sdb1
WARNING: Couldn't find device with uuid CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa.
WARNING: VG diskspace is missing PV CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa (last written to /dev/sdb3).
Physical volume "/dev/sdb1" successfully created.
Epa! Que catzo de CvlXC4-não-sei-lá-o-que é esse? Sim, o disco que removi fisicamente e não tirei logicamente do LVM. Com "vgdisplay" pude ver que realmente o problema estava lá.
root@goosfraba ~# vgdisplay
WARNING: Couldn't find device with uuid CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa.
WARNING: VG diskspace is missing PV CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa (last written to /dev/sdb3).
--- Volume group ---
VG Name diskspace
System ID
Format lvm2
Metadata Areas 1
Metadata Sequence No 230
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 9
Open LV 2
Max PV 0
Cur PV 2
Act PV 1
VG Size 5.45 TiB
PE Size 4.00 MiB
Total PE 1429493
Alloc PE / Size 518912 /
Resolver não foi nada complicado. Bastou o seguinte comando:
root@goosfraba ~# vgreduce --removemissing diskspace
WARNING: Couldn't find device with uuid CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa.
WARNING: VG diskspace is missing PV CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa (last written to /dev/sdb3).
WARNING: Couldn't find device with uuid CvlXC4-LiEI-mr0c-vSky-oryk-Khrl-J1dyBa.
Wrote out consistent volume group diskspace.
Pronto! Metadados do disco antigo removidos.
root@goosfraba ~# vgdisplay
--- Volume group ---
VG Name diskspace
System ID
Format lvm2
Metadata Areas 1
Metadata Sequence No 231
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 9
Open LV 2
Max PV 0
Cur PV 1
Act PV 1
VG Size
Agora o trabalho seguinte foi adicionar o espaço novo dentro do VG (diskspace é o nome):
root@goosfraba ~# vgextend diskspace /dev/sdb1
Volume group "diskspace" successfully extended
root@goosfraba ~# vgdisplay
--- Volume group ---
VG Name diskspace
System ID
Format lvm2
Metadata Areas 2
Metadata Sequence No 232
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 9
Open LV 2
Max PV 0
Cur PV 2
Act PV 2
VG Size
Em seguida colocar o disco novo como mirroring novamente:
root@goosfraba ~# lvconvert -m 1 /dev/diskspace/steam /dev/sdb1
Are you sure you want to convert linear LV diskspace/steam to raid1 with 2 images enhancing resilience? [y/n]: y
Logical volume diskspace/steam successfully converted.
E pronto. Acabou. Isso mesmo. Foi fácil assim. Agora é só ir monitorando o progresso da cópia no disco novo.
root@goosfraba ~# lvs
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
home diskspace -wi-a----- 500.00g
opt diskspace -wi-a----- 2.00g
root diskspace -wi-ao---- 10.00g
steam diskspace rwi-a-r--- 750.00g 0.88
swap diskspace -wi-a----- 15.00g
tmp diskspace -wi-a----- 5.00g
usr diskspace -wi-ao---- 95.00g
usrlocal diskspace -wi-a----- 600.00g
var diskspace -wi-a----- 50.00g
root@goosfraba ~# lvs
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
home diskspace -wi-a----- 500.00g
opt diskspace -wi-a----- 2.00g
root diskspace -wi-ao---- 10.00g
steam diskspace rwi-a-r--- 750.00g 1.10
swap diskspace -wi-a----- 15.00g
tmp diskspace -wi-a----- 5.00g
usr diskspace -wi-ao---- 95.00g
usrlocal diskspace -wi-a----- 600.00g
var diskspace -wi-a----- 50.00g
Como são 750 GB a coisa demora um pouco. Mas com LVM, tudo é muito fácil.
Happy hacking :)
Rodando desafios de uma hackathon com Python
8 de Maio de 2021, 16:48 - sem comentários aindaEu 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:
- Os desafios no site wikimedia interno.
- 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).
- Eu copiava esses repositórios, e monitorava o arquivo Makefile.
- 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.
- Esse build deveria gerar um executável chamado "hacking". Podia ser binário ou script.
- Em seguida o código era rodado dentro do ambiente de container do devcon.
- 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/golangci-lint@v1.27.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.
Trabalhando de casa - atualização de 2021
16 de Abril de 2021, 12:23 - sem comentários aindaDesde que escrevi o artigo trabalhando de home-office para descrever como era meu ambiente de trabalho em casa eu fiz alguns updates e upgrades nas configurações. O primeiro e mais notável é que dei uma boa arrumada na estante à direita. E acreditem que não foi fácil. Mas ao menos agora durante as conferências e gravações do canal Unix Load On fica um fundo de tela decente.
Então vamos ao updates mais visualmente claros, começando pela cadeira.
A diferença básica dessa cadeira pra anterior é que ela mantém as costas retas e tem a almofada pra lombar. Isso ajuda pra quem passa muito tempo sentado, que é o meu caso. E, claro, comprei uma cadeira que aguenta até 125 Kg pra aguentar o peso extra adquirido durante essa pandemia.
Também fiz um upgrade nos hubs USB.
Agora com somente um único hub USB3 de 7 portas. Genérico da China. Pra trocar entre meu desktop pessoal e o laptop corporativo agora é somente mudar um cabo.
Outra melhoria, aparente mas subjetiva foi a mesa.
O sindicato negociou com a firma a aquisição de uma luminária e uma mesa com altura regulável. Achei o máximo, principalmente pra poder trabalhar em pé e dar um alívio pras costas. Mas essa não é exatamente a mesa que foi comprada. Como a tampa de madeira era menor eu meio que adaptei na mesa que eu tinha, que é do Ikea, e só coloquei as pernas reguláveis. Ficou até que bom. A luminária está entre os monitores. Infelizmente ficam esses montes de cabos pendurados... mas tá de bom tamanho. E por causa da mudança de altura eu precisei mudar o desktop pra cima da mesa. Ficou mais congestionado, mas ainda sobra espaço pra trabalhar.
Quem também sofreu upgrade foram tanto o braço mecânico quanto o microfone que uso pra gravar os webcasts do canal Unix Load On.
O de antes, que ainda está lá atrás desse e sem uso, ficava na frente da tela. Não tinha forma de ajustar que não atrapalhasse a visão. Já o microfone eu resolvi dar um upgrade na esperança de ter uma captação de voz melhor, não que isso conseguisse esconder minha voz afônica. Mas foi uma decepção. Não trouxe nenhum grande benefício e ainda é do tipo mono. Podia ter gastado esse dinheiro em cerveja. Comprei também a espuma pra minizar problemas de captura mas não posso dizer que tenha feito grande diferença. Mais uma cerveja perdida.
Já a câmera web, que ainda é a mesma Logitech c920, ganhou um braço mecânico pra sustentar próximo do monitor.
Privacy Shutter Lens Cap Hood Cover for Logitech HD Pro Webcam C920 C922 C930e
Já aproveitei e também mandei um filtro de privacidade na câmera. Mesmo só usando Linux, a gente nunca sabe...
Por fim o último, um mimo que recebi. Como o festival de rock Sweden Rock foi cancelado ano passado e também esse ano, eu tenho comprado parte do merchandise deles pra ajudar a manter o festival vivo. Na última compra eles enviaram esse copo de café ou chá que no fim eu adorei. Como ele é todo vedado não tem risco de derramar no teclado, o que já fiz algumas vezes. Tive sorte de que os copos estavam quase vazios.
Ao contrário de tudo que postei aqui, esse copo vedado eu não tenho link. Mesmo porque não comprei e veio de brinde. Mas deixo aqui o link com os acessórios disponíveis na loja do Sweden Rock (eu já comprei quase todos os ítens que estão lá).
https://merch.swedenrock.com/merch/accessoarer
E pra fechar eu vou deixar um foto no mesmo estilo do artigo anterior. Com camiseta de pijama de alguma empresa.
Clean code - os princípios para escrever-se um código legível
8 de Abril de 2021, 14:45 - sem comentários aindaNota: esse foi um artigo que escrevi pra Revista Espírito Livre, mas a edição nunca saiu. Mantive no mesmo formato e estilo que sairia na revista.
Algumas pessoas podem não acreditar ou até mesmo pensar que é loucura mas o software livre trata sobre... software! Nesse momento imagino já algumas pessoas revoltadas jogando cadeiras e ameaçando colocar fogo nas lixeiras mas infelizmente o software livre é sobre software. O software livre nos dá as diretivas pra termos um software que seja livre, mas não diz nada sobre como escrever esse software. Nenhuma direção, nada. Apenas sobre sua licença.
E software, apesar de ser uma forma de comunicação com um computador que interpretará as diretivas como uma linguagem de máquina e mandará sua CPU executar os passos descritos, pode e deve ser legível por seres humanos. Sim! Software é uma forma de escrita e como tal é imperativo para que seja lido e entendido tanto por CPUs quanto por humanos.
Existe então um movimento chamado "software craftmanship". A tradução remonta aos forjadores de armadura da idade média (não que o termo venha dessa época, é apenas a referencia feita). Uma tradução mais próxima seria algo como "forjadores de programação". Esse movimento tem seu manifesto explicitado em:
https://manifesto.softwarecraftsmanship.org/#/pt-br
Descrevendo o mesmo literalmente aqui:
Como aspirantes a verdadeiros Artesãos de Software, estamos ampliando as fronteiras da qualidade no desenvolvimento profissional de programas de computadores através da prática e ensinando outros a aprender nossa arte. Graças a esse trabalho, valorizamos:
Não apenas software em funcionamento,
mas software de excelente qualidade
Não apenas responder a mudanças,
mas agregar valor de forma constante e crescente
Não apenas indivíduos e suas interações,
mas uma comunidade de profissionais
Não apenas a colaboração do cliente,
mas parcerias produtivas.
Sendo assim descobrimos, que para atingir os objetivos à esquerda, os que estão à direita são indispensáveis.
A tradução existente no site usa o termo "artesãos de software" mas eu prefiro como "forjadores de software". Preciosidades linguísticas à parte, é possível ver que o foco é que todo código seja lido e interpretado por pessoas.
Um dos pais desse movimento é Robert C. Martin, ou como é mais conhecido, uncle Bob. Além de ter escrito os livros que são referências para clean code, Clean Code: A handbook of Agile Software Craftsmanship[1] e The Clean Coder: A Code of Conduct for Professional Porgrammers[2], ele também tem uma série de vídeos a respeito que ajudam a entender e aplicar os conceitos de clean code. O primeiro episódio[3] está disponível gratuitamente.
Clean code é uma prática ativa de software e um artigo somente não é o suficiente pra cobrir todo o assunto. Então descreverei apenas uma pequena parte referente sobre como escrever código de uma forma prática. Usarei shell script como exemplo por ser uma das primeiras linguagens de programação pra quem inicia no mundo Unix e ter uma curva de aprendizado mais baixa se comparada com linguagens como C.
Variáveis
Em clean code a ideia é escrever o código próximo da linguagem falada. Então algumas dicas são usar variáveis com nome significativo e lembrando substantivos. Então um loop for como esse:
for f in *txt
do
...
done
A variável "f" é claramente um padrão não desejado. O que significa "f"? De acordo com os fundamentos o melhor seria:
for fileName in *txt
do
...
done
Eu escolhi manter o padrão em inglês, mas claro que ali poderia ser "nomeArquivo". Eu usei a notação chamada "camelCase". Poderia ter usado for formato "file_name", snake case, mas esse tipo de detalhismo não é relevante pra clean code. O melhor seria seguir o padrão que o time usa no código em geral. No meu time trabalhamos muito com a linguagem go, que usa a forma "camelCase" e por isso acabo adotando no meu código.
Outra regra pras variáveis é tentar decidir se o nome é longo ou curto baseado em seu escopo de uso. Se a variável é muito usado, o melhor é ter um nome longo e bem descritivo. Se seu uso é curto, seu nome também pode ser curto. Então se dentro de um programa existir uma pequena função que apenas faça um sleep:
for i in $(seq 1 10)
do
echo $i
done
Esse não é lá um código muito útil, mas ilustra que pra um pequeno loop uma variável "i" pode ser aceita. Sempre lembando que depende do escopo de uso da variável.
Funções
Já as funções em clean code seguem a recomendação inversa. Se for muito usada no escopo do programa, o melhor é usar um nome mais curto. Se for pouco usada, um nome mais longo. E sempre usar um verbo pra funções. Ampliando o código do exemplo anterior, é possível criar uma função que retorne os nomes de todos os arquivos passados em seu argumento como padrão, algo como "*.txt". Como o exemplo é em shell script, não é possível usar return, mas um "echo" diretamente pra ler a resposta.
O código então poderia ser:
getFileNames() {
pattern=$1
for fileName in *$pattern
do
echo $fileName
done
}
allTXTFiles=$(getFileNames txt)
Então a função vira um "pegar os nomes de arquivos". O substantivo "allTXTFiles" tem o verbo "pegar os nomes de arquivos txt", ou ficando mais próximo da língua falada: "todos os arquivos TXT" é "pegar os nomes dos arquivos txt".
Booleanos
Booleanos são variáveis com valores sim ou não, verdadeiro ou falso. Seu uso em clean code pode ser traduzido como pergunta. Então parâmetros booleanos podem ser valores como "temperatura é alta", "isTemperatureHigh".
Um pequeno exemplo com um script pra verificar se é fim de semana.
getWeekend() {
weekDay=$(date +%a)
echo $weekDay | egrep -q "Sun|Sat"
echo $?
}
isWeekend=$(getWeekend)
if [ $isWeekend ]; then
echo "This is weekend."
else
echo "This is a working day."
fi
Comentários
Os comentários em código significam que ou seu código está tão mal escrito que precisa ter explicação. Então em clean code deve-se evitar o uso de comentários, mas se forem necessários, que sejam extremamente descritivos.
Então um exemplo de comentário pro meu código acima.
# it returns 0 for Sundays or Saturdays, 1 for every other day.
getWeekend() {
weekDay=$(date +%a)
echo $weekDay | egrep -q "Sun|Sat"
echo $?
}
Meu comentário não descreve a função, que o nome já faz isso. Apenas melhora o entendimento do que voltará como parâmetro.
Um exemplo real
Eu pedi um exemplo pra aplicar os princípios de clean code às pessoas do grupo de shell script no telegram[4]. Agradeço então ao usuário @fonini por ter fornecido seu código. O código em questão é esse (apenas uma parte do código):
ts="$(date +%Y%m%d.%H%M%S.%N)" # timestamp
if [[ ! -v DEBUGFILE ]]; then
printf "Set DEBUGFILE first.\n" 1>&2
exit 1
fi
if (( $# < 1 )); then
printf '%s\n%s\n' \
"usage: DEBUGFILE=filename debug format [args]" \
" e.g.: export DEBUGFILE=log.txt"
exit 2
fi
O que pode ser melhorado nesse código? Segue o que seria uma aplicação dos princípios de clean code no sell script.
checkForDebug(){
if [[ ! -v DEBUGFILE ]]; then
printf "Set DEBUGFILE first.\n" 1>&2
exit 1
fi
}
printUsage() {
printf '%s\n%s\n' \
"usage: DEBUGFILE=filename debug format [args]" \
" e.g.: export DEBUGFILE=log.txt"
}
validateNumberArguments() {
totalArguments=$1
if (( $totalArguments < 1 )); then
printUsage
exit 2
fi
}
timestamp="$(date +%Y%m%d.%H%M%S.%N)"
checkForDebug
numberOfArguments=$#
validateNumberArguments $numberOfArguments
Não mentirei que clean code é fácil. É uma disciplina: é preciso praticar o tempo todo pra conseguir aplicar. No começo não é fácil, mas com o tempo seu código vai melhorando a legibilidade. E isso é apenas uma pequena parte de clean code. Tentarei descrever mais nas próximas edições.
Sobre o autor:
|
Referências:
[1] Clean Code: A Handbook of Agile Software Craftsmanship - http://hl.eng.br/6cX
[2] The Clean Coder: A Code of Conduct for Professional Programmers - http://hl.eng.br/6cW
[3] Clean Code: Fundamentals, Episode 1 - https://cleancoders.com/video-details/clean-code-episode-1
[4] Shell Script (#!/bin/bash) - https://t.me/shellbr