Blog novo, vida velha
- publicado em 17 de agosto de 2025
Faz um tempo que eu já tava pensando: «Pô, pra que banco de dados, servidor de aplicação, um monte de coisa só pro meu blog? Seria muito melhor uns html queimado.», mas a preguiça sempre batia. Um dia fui pensar em colocar um feed vi que o blog ainda rodava em python3.5. Ia dar um trabalhão pra atualizar! Então chegou a hora de um blog novo.
Um blog com sphinx
Já que eu queria uns html queimado, o mais simples seria usar o sphinx, que já estou acostumado, é de boas escrever rst e já tinha um projeto de blog com sphinx, o ABlog. Com isso já deu uma animada, afinal eu não fui o único tonto que pensei em fazer um blog com sphinx.
A primeira coisa a fazer foi dar uma olhada no que o ABlog tem. E ele já tem basicamente metade do que eu preciva: já criava listagem de posts automáticamente e gerava feeds. Do que ele tem eu só precisaria mexer nas listas de posts pra paginar a coisa. Depois de paginar as listas de posts era só eu fazer o mesmo esquema que tem pros posts, só que pra fotos: Uma página pra cada foto e uma lista de fotos. Mamão-com-açúcar.
É hora de fazer
Chegou a hora do vamo vê e aí eu vi. Primeiro a estrutura blog -> catalog -> collection. Muito mais do que eu preciso, mas da hora. Depois tem o tal do init estranho que é _init e não __init__ que copia do __dict__ do cara. Serve pra um singleton durante todo o build. Esperto! Sobrescrevi Blog (com PhotoBlog), Catalog, Collection pra paginar as listas de posts, alterei a função que conecta no sinal do sphinx pra gerar os html e uso uma opção a mais para mostrar o post todo na listagem ao invés de só o começo. Foi de boas.
Agora era fazer uma listagem paginada de fotos. Alterei PhotoBlog pra ter uma collection
paginada de fotos, mas ainda me faltavam as fotos. Criei uma directive Photo
e aí
faltava juntar uma coisa com a outra. Quando comecei a olhar como o ablog fazia isso
foi quando começou minha tristeza. A função do ablog que faz isso é gigante, chatona
de ler e o pior, é uma função muito ensimesmada, digamos assim, então não dá muito pra
extender a parada. E essa não é a única, tem mais algumas peças nesse estilo. A preguiça
começou a bater, mas vamos lá…
O esquema era basicamente pegar todos os nodes do tipo foto, guardar isso no env do build e depois registrar nos PhotoBlog e boas, temos uma coleção de fotos no blog. Depois disso extendi o dirhtml builder do sphinx pra copiar os arquivos das fotos pro diretório de imagens do build e gerar as thumbnails que serão exibidas na lista de fotos. Nesse ponto eu já tinha a maioria das funcionalidades que eu queria, era hora de mexer no css. Aí a preguiça chegou de vez!
Os comentários
Eu fiquei um tempão sem mexer no blog, fazendo coisas mais úteis com meu tempo live (tipo jogar xadrez ou tocar guitarra :P) até que eu lembrei que todo blog precisa de comentários e animei de fazer os comentários.
Nota
O ABlog já tinha integração com o disqus e com o Isso, que é basicamente o que eu precisava, mas o disqus eu não queria usar e a velha mania de não ler a documentação direito me fez não ver que tinha integração com o Isso. Na verdade nunca tinha ouvido falar dele, então acabei fazendo o meu mesmo. Tonto tontando.
A ideia era fazer uma coisa bem simples pros comentários, então decidi fazer o esquema usando o sqlite de banco e uma api onde eu só preciso incluir um js na página e os cometários já aparecem. Até aí nada demais, umas tabelinhas no sqlite, um endpoint pra criar comentário, endpoint pra listar comentários (aqui dois na verdade, um que retorna um json e um que retorna um html), meia dúzia de linha de js (como o js deveria ser usado), o arroz com feijão da coisa. Até que o Bubbletea me chamou a atenção.
O esquema dos comentários - chamado parlante - funciona com clientes que tem permissão para certos domínios, então eu precisava cadastrar clientes e domínios e nada mais natural(?!) que fazer uma text user interface pra isso, não? Foi assim que o que era pra ser só uma paradinha de nada passou a ter dois executáveis: o parlante e o parlane-tui. Já não bastava ter feito o que não precisava, fez o desnecessário duas vezes! Mas tudo bem, foi. E agora eu tinha o que precisava pro blog. Ou quase…
Chegando nos finalmente
Depois de mais um tempão sem mexer no blog, peguei uns diazinhos de férias e decidi terminar essa parada. Dei um tapa na tui pra ficar mais bonitinha e foi pro css do blog. Eu já tinha o tema do sphinx que eu usava nas minhas documentações, a ideia era usar o mesmo tema pro blog, era só dar um tapa e mesmo assim e encheu o saco. A página de foto me deu uma canseira, foi difícil decidir como ela deveria ficar e depois demorei pra descobrir como usar a coisa do display:flex com order. Depois de mexer no tema me faltava o feed das fotos e um feed geral dos posts e fotos.
Comecei fazendo feed das fotos, que o conteúdo é basicamente o títudo da foto e a imagem. Depois foi fazer o feed geral e nesse momento eu me encontrei de novo com uma das funções feitas pra si mesma no ABlog, a parte que gera o feeds. No feed geral o item pode ser uma foto ou um post. Consegui reusar o item do feed de fotos, já o de posts não deu, tive que fazer um item de feed pra post específico pro feed geral. Passando por isso eu achava que já tava tudo pronto. Dei aquele rsync com o servidor e comecei a navegar no blog pra ver se tinha faltando algo. Tudo bem, tudo bom, até que a hostinger vem me atrapalhar.
A infra é sempre contra nóis, né?
Enquanto eu tava dando uma (o que eu esperava ser) útima olhada na parada, do nada o blog parou de responder, não pingava nem nada. No painel da hostinger a vps de pé, dava pra usar pelo ssh web dos cara, as regra de iptables tudo normal. Que porra que tava acontecendo? Troquei a minha conexão de casa pela do telefone e pimba! Foi! Fazendo um traceroute da coisa vi que quem tava dropando meus pacote era o úlitmo hop antes de chegar na minha vps, era a hostinger bloqueando meu ip! Agora passar pelo suporte que ia ser a dificuldade.
Primeiro tem que passar pelo bot maldito que te dá todos os passos que eu já tinha feito, depois quando chega num humano a resposta padrão é sempre a mesma: vps é um serviço auto gerenciado e a gente não pode fazer nada. Até alguém prestar atenção e entender que o bloqueio era por parte da hostinger foi difícil. Depois que entenderam, o chamado foi pra outra equipe, com outro prazo de atendimento. Depois de dois dias recebi um email dizendo que tinham feito alterações no firewall da hostiger e eu não deveria ter mais problemas. Funcionou até não funcionar mais.
Quando fui copiar o texto da página Sobre do blog antigo eu me lembrei que tinha um formulário de contato lá que na verdade nunca funcionou porque não tinha nem botão pra enviar a mensagem. Já que eu tava fazendo um blog novo essa era a hora de fazer funcionar. Implementei o contato no parlante e na hora de subir a nova versão, cadê que eu conseguia chegar no servidor? Bloquearam meu ip de novo! Vai lá eu falar com o suporte, toda aquela coisa (esse vai e volta com infra já tava me lembrando da firma) e até agora a coisa continua zuada.
Chegamos ao final
Depois de mais de uma semana desisti da hostinger. Fiz uma conta no oracle cloud e este bloguinho agora está no que diz ser uma vps pra sempre grátis. A máquina é bem meia boca, mas como só tem os html do blog e o parlante, dá e sobra. Depois de toda essa odisseia agora eu tenho um feed. Uau!
Ganhei algo com isso? Não. Vou ganhar algo com isso? Não. O blog é novo, mas a vidinha continua a mesma.
Uma jigajoga bacana
- publicado em 12 de novembro de 2024
Jigajoga |ó| (ji-ga-jo-ga) - Artifício, ludíbrio; mecanismo ou solução resultante de improvisação. Depois que eu aprendi essa palavra eu nunca mais consegui dizer hack ou gambiarra, só jigajoga. E hoje vou contar de uma jigajoga do trampo.
O galho
Esses tempos no trampo eu precisava identificar um usuário que chega no nosso
whatsapp. Em geral pra mandar alguém pro whatsapp você só manda url
https://wa.me/<telefone>?text=oi
. O problema aí é que eu não faço ideia de
como o usuário chegou no whatsapp. Ele pode ter acessado via um link ou ter
simplesmente chegado no whatsapp e falado oi. A única coisa que consigo saber
com isso é o texto que o usuário me mandou e o número do telefone dele.
A primeira parte da coisa é simples, ao invés de enviar o usuário para o link do whatsapp direto, manda um link pra mim, aí eu consigo gerar um fingerprint do usuário e depois redirecionar para o whatsapp. Mas depois que mandar o usuário pro whatsapp, como eu sei que o usuário que chegou lá é o usuário x?
A ideia
Logo de cara um colega me mostrou como outra empresa tava fazendo isso: mandando uma string XYZ na mensagem, pedindo para o usuário enviar essa mensagem com a string e essa string serviria de id pra identificar o usuário. Mas isso é feio pra caralho. Então minha primeira ideia foi usar caracteres “invisíveis” (non-printable) no meio da mensagem. E bom, se eu precisava de uma identificação única de usuário o óbvio seria uuid, no caso o 4.
Então peguei uma lista de 17 caracteres “invisíveis” (16 pra a-f e 1 pra “-“) e aí fazia a tradução da representação em string de um uuid v4 pra uma string invisível usando os caracteres non-printable. Subi a coisa rapidinho, testei no meu whatsapp, funcionou. Coisa linda.
O código ficou mais ou menos assim:
# -*- coding: utf-8 -*-
NON_PRINTABLE_CHARS = [
'\u200b',
'\u2060',
'\u2061',
'\u2062',
'\u2063',
'\u2064',
'\u2066',
'\u2067',
'\u2068',
'\u2069',
'\u206A',
'\u206B',
'\u206C',
'\u206D',
'\u206E',
'\u206F',
'\uFE06',
]
PRINTABLE_CHARS = '0123456789abcdef-'
def translate_uuid_to_invisible(u):
inv = ''
ustr = str(u)
for i in range(len(ustr)):
inv += NON_PRINTABLE_CHARS[PRINTABLE_CHARS.index(ustr[i])]
return inv
def translate_uuid_from_invisible(inv):
u = ''
for i in range(len(inv)):
u += PRINTABLE_CHARS[NON_PRINTABLE_CHARS.index(inv[i])]
return u
def put_fingerprint(text, uuid):
inv = translate_uuid_to_invisible(uuid)
t = text[0] + inv + text[1:]
return t
def get_fingerprint(t):
if t[1] not in NON_PRINTABLE_CHARS:
return ''
inv = t[1:37]
u = translate_uuid_from_invisible(inv)
return u
if __name__ == '__main__':
from uuid import uuid4
txt = 'Olá, mundo!'
u = uuid4()
t = put_fingerprint(txt, u)
fp = get_fingerprint(t)
assert str(u) == fp
Claro que nunca funciona de primeira
Depois com mais testes percebi que eu tinha um problema: no whatsapp web, a depender do uuid, ficava uns espaços em branco no meio do texto, algo tipo o i. e isso por causa da combinação de caracteres. Apesar dos caracteres que eu escolhi não terem uma representação, eles tem uma função e a maioria deles eu nem sei qual a função.
Pra corrigir isso, eu primeiro tentei ir substituindo os caracteres por outros até que desse certo, mas é um trabalhão danado, sem change. Então eu precisava diminuir o número de caracteres usados pra ficar mais fácil a coisa de tirar os espaços do whatsapp web.
Menos (caracteres) é mais (espaço)
Um uuid é um número de 128 bits, então eu pensei em escrever isso em “binário”, aí eu só precisaria de dois caracteres, mas em contrapartida eu teria 128 caracteres a mais em cada mensagem. Nada é grátis, mas era mais importante o texto ficar “certo” do que o tamanho da mensage.
Escolhi os caracteres u200b (zero width space) e u2060 (zero width word joiner), alerei o código pra pegar a string com a representação do número do uuid em binário e simplesmente troquei 0 e 1 pelos caracteres zero witdh escolhidos. Altera o código, sobre rapidinho, testa, testa, testa… Bada bim! Bada bam! Bada bum! Dessa fez funcionou. Merge no master, pusha e vai! Daqui a uns minutinhos tá em prd!
O código alterado ficou mais ou menos assim:
# -*- coding: utf-8 -*-
from uuid import UUID
NON_PRINTABLE_CHARS = [
'\u200b',
'\u2060',
]
PRINTABLE_CHARS = '01'
def translate_uuid_to_invisible(u):
inv = ''
bitstr = f'{u.int:128b}'.replace(' ', '0')
for i in range(len(bitstr)):
inv += NON_PRINTABLE_CHARS[PRINTABLE_CHARS.index(bitstr[i])]
return inv
def translate_uuid_from_invisible(inv):
bitstr = ''
for i in range(len(inv)):
bitstr += PRINTABLE_CHARS[NON_PRINTABLE_CHARS.index(inv[i])]
n = int(bitstr, 2)
u = UUID(int=n)
return u
def put_fingerprint(text, uuid):
inv = translate_uuid_to_invisible(uuid)
t = text[0] + inv + text[1:]
return t
def get_fingerprint(t):
if t[1] not in NON_PRINTABLE_CHARS:
return ''
inv = t[1:129]
u = translate_uuid_from_invisible(inv)
return u
if __name__ == '__main__':
from uuid import uuid4
txt = 'Olá, mundo!'
u = uuid4()
t = put_fingerprint(txt, u)
fp = get_fingerprint(t)
assert u == fp
Ainda tem problemas, 128 caracteres a mais pra um simples «oi» é feio, se eu printar isso num terminal ainda fica um espaço entre as letras, mas não é pra usar no terminal mesmo… E isso é uma jigajoga, não dá pra esperar muito, só que resolva o problema em mãos.
Moral da história: Não tem!
Uma passada de olhos em websockets
- publicado em 31 de outubro de 2024
Esses dias implementando websockets no tupi-proxy e precisava de um cliente e um servidor websocket pra poder testar e ao invés de pegar algo pronto eu escrevi o que eu precisava. Então pra não ficar parecendo que foi um trabalho inútil, vou escrever sobre websockets agora.
Websockets são uma conexão tcp normal onde assim que a conexão é estabelecida o cliente
envia os headers de uma requisição http, com os headers
Upgrade: websocket
e Connection: upgrade
.
Ao receber esses headers o servidor responde com esses mesmo headers indicando que suporta
websockets. Depois disso o cliente e servidor podem trocar mensagens.
Mas como assim?
Bom, primeiro o servidor tem que estar escutando em uma porta, aí o cliente faz uma conexão tcp e manda uma requisição http GET para o servidor. Assim:
GET /ws HTTP/1.1
Host: myhost.net:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
O header Sec-WebSocket-Key enviado pelo cliente é uma string de 16 caracteres ascii aleatórios encodados em base64.
Ao receber essa requisição o servidor deve responder assim:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
O header Sec-WebSocket-Accept enviado pelo servidor é obtida da seguinte maneira: o servidor concatena o Sec-WebSocket-Key enviado pelo cliente com a string mágica «258EAFA5-E914-47DA-95CA-C5AB0DC85B11», gera um hash sha1 com essa string concatenada e por fim encoda em base64.
Depois desse processo de handshake o servidor e o cliente devem manter a conexão aberta e aí podem trocar mensagens usando o formato descrito na RFC 6455.
O formato das mensagens
As mensagens trocadas via websockets são chamadas de frames. Os frames enviados tanto pelo servidor quanto pelo cliente tem o seguinte formato:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
Essa coisa feia aí quer dizer que cada frame é formado da seguinte maneira:
- Primeiro byte:
bit 0: Indica se o frame é um frame final de mensagem ou se a mensagem será continuada em outro frame
bits 1 a 3: Bits revervados à extensões.
bits 4 a 7: Opcode
- Segundo byte:
bit 0: Indica se uma máscara está sendo usada
bits 1 a 7: O tamanho do payload
- Próximos dois bytes:
O tamanho do payload se o tamanho no segundo byte for >= 126.
- Próximos 8 bytes:
O tamanho do payload se o tamanho no segundo byte for == 127
- Próximos 4 bytes:
A máscara se uma estiver sendo usada
O restante dos bytes (até o tamanho do payload) é o payload.
Opcodes e máscara
Os opcodes dão informação sobre o tipo do payload ou podem ser opcodes de controle. O opcode 0 indica que a mensagem é uma continuação da mensagem no frame anterior e o payload desse frame deve ser combinado com o payload do anterior; o opcode 1 indica que o payload é um texto encodado em utf-8; o opcode 2 indica que o payload é um binário; o opcode 8 é um opcode de controle usado para encerrar a conexão; o opcode 9 é um opcode de controle para ping e por fim o opcode 10 é um opcode de controle usado para pong.
A máscara são 32 bits aletórios que que vão encriptar os dados usando XOR. Os clientes obrigatóriamente devem usar máscara quando enviando dados pro servidor e o servidor não deve usar máscara quando enviando mensagens ao cliente.
Bom, é basicamente isso o protocolo de websockets. Agora ao que importa.
Uma implementaçãozinha
Primeiro uma implementação para wire encode e wire decode que vai ser usada tanto pelo cliente quanto pelo servidor.
// Frame é como a gente envia mensagens através do websocket.
// Essa struct representa aquele desenho feio lá de cima.
type Frame struct {
Opcode byte
Len uint
Payload []byte
Mask []byte
IsFinal bool
IsMasked bool
}
// WebSocket contém as operações básicas do protocolo
// performadas tanto pelo cliente quanto pelo servidor.
// Note que WebSocket não tem um método para criar uma
// conexão já que a conexão sempre tem que ser criada
// pelo cliente e nunca pelo servidor
type WebSocket struct {
Conn net.Conn
}
// Send wire encode um frame e envia os bytes em uma conxão
// já aberta
func (ws *WebSocket) Send(fr *Frame) error {
data, err := ws.WireEncode(fr)
if err != nil {
return err
}
_, err = ws.Conn.Write(data)
return err
}
// Recv lê da conexão aberta e retorna o frame recebido.
// Se Recv recebe um frame ping, envia um frame pong e
// volta a ler da conexão. Se recebe um frame close
// retorna um erro io.EOF.
// Note que Recv não fecha a conexão.
func (ws *WebSocket) Recv() (*Frame, error) {
for {
fr, err := ws.WireDecode()
if err != nil {
return &Frame{}, err
}
switch fr.Opcode {
case OpcodeClose:
return &Frame{}, io.EOF
case OpcodePing:
fr.Opcode = OpcodePong
err := ws.Send(fr)
if err != nil {
return &Frame{}, err
}
default:
return fr, nil
}
}
}
// RecvPayload retorna todo o payload da mensagem. Se a mensagem
// estiver divida em mais de um frame, lê todos os frames e
// só aí retorna o payload completo
func (ws *WebSocket) RecvPayload() ([]byte, byte, error) {
var unfinishedPayload []byte
unfinishedOpcode := byte(0xFF)
for {
fr, err := ws.Recv()
if err != nil {
return []byte{}, 0, err
}
if !fr.IsFinal {
unfinishedPayload = append(unfinishedPayload, fr.Payload...)
if unfinishedOpcode == 0xFF {
unfinishedOpcode = fr.Opcode
}
continue
}
if fr.Opcode == OpcodeCont {
unfinishedPayload = append(unfinishedPayload, fr.Payload...)
return unfinishedPayload, unfinishedOpcode, nil
}
return fr.Payload, fr.Opcode, nil
}
}
// Close manda um frame de controle close e fecha a conexão.
func (ws *WebSocket) Close() error {
msg := []byte("close connection")
fr := Frame{
Opcode: OpcodeClose,
Payload: msg,
Len: uint(len(msg)),
IsFinal: true,
}
ws.Send(&fr)
return ws.Conn.Close()
}
// WireEncode transforma um frame em uma sequencia de bytes
// que vai ser enviada pela conexão.
// WireEncode não força o uso de máscara
func (ws *WebSocket) WireEncode(fr *Frame) ([]byte, error) {
data := make([]byte, 2)
if fr.IsFinal {
// aqui se o frame for o frame final de uma mensagem
// a gente seta o primeiro bit pra zero.
data[0] = 0x00
} else {
// se não for um frame final a gente seta pra 1
data[0] = 0x80
}
// os quatro últimos bits do primeiro byte são
// o opcode
data[0] |= fr.Opcode
l := len(fr.Payload)
if l <= 125 {
// se o payload for menor que 126 bytes
// o temanho será os últimos 7 bits do
// primeiro byte
data[1] = byte(l)
} else if float64(l) < math.Pow(2, 16) {
// se o tamanho do payload couber em dois bytes a gente
// marca os sete últimos bits do segundo byte como 126
// e marca o tamanho do payload nos próximos dois.
data[1] = byte(126)
s := make([]byte, 2)
binary.BigEndian.PutUint16(s, uint16(l))
data = append(data, s...)
} else if float64(l) < math.Pow(2, 64) {
// se o tamanho do payload cabe em oito bytes marcamos
// nos próximos 8
data[1] = byte(127)
s := make([]byte, 8)
binary.BigEndian.PutUint64(s, uint64(l))
data = append(data, s...)
} else {
// muito grande. tem que dividir a mensagem em
// mais de um frame
return []byte{}, errors.New("Payload muito grande")
}
if fr.Mask != nil && len(fr.Mask) > 0 && len(fr.Mask) != 4 {
return []byte{}, errors.New("Invalid mask")
}
if fr.Mask != nil && len(fr.Mask) == 4 {
// Se uma mascara é usada setamos o primeiro bit
// do segundo byte para 1 e fazemos o XOR no payload
data[1] = 0x80 | data[1]
data = append(data, fr.Mask...)
xOR(fr.Payload, fr.Mask)
}
// e por fim o payload depois da tralha toda
data = append(data, fr.Payload...)
return data, nil
}
// WireDecode lê da conexão aberta e retorna o frame recebido.
// Aqui a gente tá basicamente fazendo o contrário do que fizemos
// em WireEncode
func (ws *WebSocket) WireDecode() (*Frame, error) {
fr := Frame{}
d := make([]byte, 2)
_, err := ws.Conn.Read(d)
if err != nil {
return nil, err
}
// verificando se o primeiro bit é 0 ou 1 pra saber
// se é um frame final. 0 == final
final := (d[0] & 0x80) == 0x00
// Pegando os últimos 4 bits do primeiro byte que
// são o opcode
opcode := d[0] & 0x0F
// Primeiro byte indica se tá usando máscara ou não
// 1 == tá usando
isMasked := (d[1] & 0x80) == 0x80
// os 7 últimos bits do segundo byte pro tamanho do
// payload. Se for <= 125 já será o tamanho real
len := d[1] & 0x7F
l := uint(len)
fr.Opcode = opcode
fr.IsFinal = final
fr.IsMasked = isMasked
if l == 126 {
// se o marcado no segundo byte é 126 então o tamanho
// está nos próximos dois bytes
d := make([]byte, 2)
_, err := ws.Conn.Read(d)
if err != nil {
return nil, err
}
l = uint(binary.BigEndian.Uint16(d))
} else if l == 127 {
// se o marcado no segundo byte é 127 então o tamanho
// está nos próximos 8 bytes
d := make([]byte, 8)
_, err := ws.Conn.Read(d)
if err != nil {
return nil, err
}
l = uint(binary.BigEndian.Uint64(d))
}
fr.Len = l
mask := make([]byte, 4)
if isMasked {
// se tá usando máscara, os próximos 4 bytes serão
// a máscara.
_, err = ws.Conn.Read(mask)
if err != nil {
return nil, err
}
}
// e por fim o payload do frame
payload := make([]byte, l)
_, err = ws.Conn.Read(payload)
if isMasked {
xOR(payload, mask)
fr.Mask = mask
}
fr.Payload = payload
return &fr, nil
}
Agora o código do websocket client:
// WebSocketClient é quem inicia a conexão de websocket.
type WebSocketClient struct {
WebSocket
URL *url.URL
}
// Handshake envia uma requisição http com headers upgrade
// perguntando se o servidor suporta websockets
func (ws *WebSocketClient) Handshake() error {
// O hash aqui são 16 caracteres ascii aleatórios encodados
// em base64
hash := getSecHashClient()
req := &http.Request{
URL: ws.URL,
Header: make(http.Header),
}
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Connection", "upgrade")
req.Header.Set("Sec-WebSocket-Accept", hash)
err := req.Write(ws.WebSocket.Conn)
if err != nil {
return err
}
reader := bufio.NewReaderSize(ws.Conn, 4096)
resp, err := http.ReadResponse(reader, req)
if err != nil {
return err
}
// O status que o servidor deve retornar informando
// que suporta websockets é o status 101
if resp.StatusCode != http.StatusSwitchingProtocols {
return errors.New("Server does not support websockets")
}
if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" ||
strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
return errors.New("Invalid response")
}
return nil
}
// Send envia um frame ao servidor e antes de enviar
// gera uma máscara para o frame
func (ws *WebSocketClient) Send(fr *Frame) error {
fr.Mask = getMask()
return ws.WebSocket.Send(fr)
}
// NewWebSocketClient retorna um cliente de websocket já
// connectado a um servidor que suporta websockets
func NewWebSocketClient(rawURL string) (*WebSocketClient, error) {
u, err := url.Parse(rawURL)
if err != nil {
return &WebSocketClient{}, nil
}
hostPort, err := getHostPort(u)
if err != nil {
return &WebSocketClient{}, err
}
conn, err := net.Dial("tcp", hostPort)
if err != nil {
return &WebSocketClient{}, err
}
ws := WebSocketClient{
WebSocket: WebSocket{
Conn: conn,
},
URL: u,
}
err = ws.Handshake()
if err != nil {
return &WebSocketClient{}, err
}
return &ws, nil
}
Agora o server que a única coisa que faz é retornar o que o cliente mandar, mas se for um texto retorna o texto invertido:
// WebSocketServer responde a uma conexão feita pelo cliente.
type WebSocketServer struct {
WebSocket
Header http.Header
}
// Handshake retorna status 101 indicando que aceita websockets
func (ws *WebSocketServer) Handshake() error {
secKey := ws.Header.Get("Sec-WebSocket-Key")
hash := getSecHashServer(secKey)
headers := []string{
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: upgrade",
"Sec-WebSocket-Accept: " + hash,
"",
"",
}
_, err := ws.Conn.Write([]byte(strings.Join(headers, "\r\n")))
return err
}
// Recv retorna o frame enviado pelo cliente. Se o cliente enviar
// um frame sem máscara Recv retorna um erro já que o cliente
// sempre tem que usar uma máscara
func (ws *WebSocketServer) Recv() (*Frame, error) {
fr, err := ws.WebSocket.Recv()
if err != nil {
return fr, err
}
if !fr.IsMasked {
return &Frame{}, errors.New("Clients must mask the payload")
}
return fr, err
}
// Echo simplesmente retorna a mensagem enviada pelo cliente
// invertendo a string se o payload for utf-8.
func (ws *WebSocketServer) Echo() error {
for {
payload, opcode, err := ws.RecvPayload()
if err != nil && errors.Is(err, io.EOF) {
log.Println("Connection closed")
return nil
}
if err != nil {
log.Println(err.Error())
return err
}
if opcode == OpcodeText {
runes := []rune(string(payload))
pl := len(runes)
reversed := make([]rune, pl)
for i := pl - 1; i >= 0; i-- {
j := (pl - 1) - i
reversed[j] = runes[i]
}
payload = []byte(string(reversed))
}
fr := Frame{
Payload: payload,
Opcode: opcode,
}
err = ws.Send(&fr)
if err != nil {
log.Println(err.Error())
return err
}
}
}
E por fim pra testar as coisas tudo junto a gente faz um http handler e uma cli.
func wsCli() {
ws, err := NewWebSocketClient("ws://localhost:8081")
if err != nil {
panic(err.Error())
}
var msg string
for {
fmt.Print(": ")
reader := bufio.NewReader(os.Stdin)
msg, err = reader.ReadString('\n')
if err != nil {
panic(err.Error())
}
frame := Frame{
Payload: []byte(msg),
IsFinal: true,
Opcode: OpcodeText,
}
ws.Send(&frame)
resp, err := ws.Recv()
if err != nil {
panic(err.Error())
}
fmt.Printf(string(resp.Payload) + "\n")
}
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
h, ok := w.(http.Hijacker)
if !ok {
w.WriteHeader(http.StatusInternalServerError)
return
}
conn, _, err := h.Hijack()
if err != nil {
log.Println(err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
ws := WebSocketServer{
WebSocket: WebSocket{
Conn: conn,
},
Header: r.Header,
}
defer ws.Close()
err = ws.Handshake()
if err != nil {
log.Println(err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
err = ws.Echo()
if err != nil {
log.Println(err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
}
A main function fica assim:
func main() {
server := flag.Bool("server", false, "start the server")
client := flag.Bool("client", false, "start the client")
flag.Parse()
if !*server && !*client {
panic("one of server or client must be true")
}
if *server && *client {
panic("only one of server and client can be true")
}
if *server {
log.Fatal(http.ListenAndServe(":8081", http.HandlerFunc(wsHandler)))
} else {
wsCli()
}
}
Agora só compilar assim:
$ go build -o ws ws.go
Inicie o servidor assim:
$ ./ws -server
E agora você pode usar o cliente para falar com o servidor via websockets
$ ./ws -client
: olá, mundo
odnum ,álo
:
O código completo pode ser baixado aqui.
E é isso!
Emacs é o editor mais legal
- publicado em 19 de outubro de 2024
Eu uso emacs desde o terceiro dia do meu primeiro emprego. Sempre pensei em escrever algo sobre aqui, mas aí sempe ficava naquelas: «Põ, é só um editor de texto, besteira escrever sobre isso». Mas esses dias no trampo fiz um esqueminha que na minha opnião é a melhor coisa do emacs: Elisp e poder fazer qualquer coisa com seu editor
É só executar uma query, caralho
Se sempre que vão contar a história do software livre metem uma história com impressora no meio, não tem problema nenhum minha história ser sobre só executar umas query, né?
A coisa começa com um serviço que temos lá no trampo e eu tinha feito umas apis pra consulta, o normal, tipo:
GET /api?fields=a,sum(1)&filter=b__gt=2|c=3&group=a HTTP/1.1
O problema com isso que é quando isso começa a crescer a coisa fica feia pra cacete e muito disso era feita na mão, alguém fazia uma query, mandava via curl ou postman e pegava o json da resposta. Então era muito ruim pro pessoal escrever isso. Um dia um colega me mostrou a api do salesforce que você basicamente manda um query sql na api. Aí implementei um sql parser (não sou doido de deixar umas query aberta na minha api) e começaram a uma api com um sql direto.
Isso acabou sendo bem mais prático de usar, de explicar pros outros, então a mesma coisa começou a ir pra outros serviços e em pouco tempo eu tinha uma meia dúzia de serviços com apis sql e aí acabou ficando um saco fazer as queries todas via postman ou curl.
Minha primeira solução foi fazer um cliente em python + readline que ficou melhor de usar do que a maneira que eu usava antes, mas ainda faltava coisa. Eu basicamente queria highlight syntax pro sql e pro json do retorno. O CodePrettifier faz highlight systax, mas só gera html e eu teria que mudar um monte de coisa. Trabalho demais pra uma coisa bem pouco útil.
Lá vem o Emacs
Nesse ponto eu me liguei: o emacs já tem o que eu quero, usemo-lo! E com isso eu precisava fazer bem menos coisa, só alterar o cliente em python que tinha feito para só aceitar uma query como parâmetro, fazer o request e cuspir o retorno (eu poderia fazer o request pra api de dentro do emacs, mas o outro já estava pronto, com o esquema de autenticação e talz, entõa assim seria mais fácil). Do lado do emacs seria só pegar uma query do buffer que eu estou usando, chamar o cliente com a query como param e pegar o retorno.
Explicando melhor o que foi a minha ideia aqui: eu escrevo as queries em um buffer em sql-mode no emacs pra poder ter o realce de sintaxe e quando eu usasse um atalho essa query seria executada, a janela dividia em duas e o retorno seria exibido ao lado do do buffer onde eu digito a query.
O código pra fazer isso ficou assim:
(defun sequela-exec ()
(interactive)
;; aqui a gente pega o texto do buffer atual
(setq sql (buffer-substring-no-properties (point-min) (point-max)))
;; aqui a gente pega uma linha do tipo
;; env: SomeWhere
;; onde SomeWhere é o serviço que a gente quer executar aquery
(string-match "^env: (.*)" sql)
(setq sequela-env (match-string 1 sql))
(unless sequela-env
(user-error "missing env"))
;; deixando só a query que vamos excutar, tirando comentários, env
;; e new lines.
(setq query (replace-regexp-in-string "--.*$\\|^env:.*$\\|\n" "" sql))
(setq buffname "sequela-idle")
(setq proc-buffname "sequela")
;; aqui divide a janela em duas e vai para a janela onde será exibido
;; o resultado
(pop-to-buffer buffname)
(erase-buffer)
(insert "sequelando...")
(with-current-buffer (get-buffer-create proc-buffname)
(erase-buffer))
;; executa o script que faz o acesso à api
(start-process proc-buffname proc-buffname "query.py" sequela-env query)
;; o sentinela do processo é chamado quando o estado do processo muda
(set-process-sentinel
(get-process proc-buffname)
(lambda (process event)
(when (string= event "finished\n")
;; aqui quando o processo terminar a gente formata o
;; resultado e copia para o buffer onde é exibido o resultado
(with-current-buffer proc-buffname
(json-pretty-print-buffer)
(copy-to-buffer (get-buffer-create buffname) (point-min) (point-max)))
(pop-to-buffer buffname)
(json-ts-mode)))))
Legal, né? Agora é só eu abrir um arquivo .sql, escrever a query que eu quiser, executar essa função e pronto! Um hackzinho vagabundo no emacs é mais fácil e fica melhor do que fazendo um programa separado!
A beleza do Common Gateway Interface
- publicado em 02 de outubro de 2024
Aqueles que já estão se aproximando da meia-idade vão se lembrar dos cgi scritps. Eles foram a primeira maneira de se fazer páginas dinâmicas por http, mas quando eu comecei a trabalhar como programador, cgi scripts já eram considerados ultrapassados, uma coisa que não se faz mais.
O fora de moda pode ser bom
Esses dias eu tava dando manutenção em um projetinho (não pergunta o porquê d’eu manter isso até hoje…) e queria subir pra vps nova. O problema era que eu usava o mod_perl do apache para servir via http. E aí eu precisaria instalar o apache que não uso pra nada dependências no meu código. Eu não tava nem um pouco feliz com isso, queria só rodar um script.
Foi aí que me lembrei do nosso bom e velho cgi. Aí implementei um plugin cgi para o tupi e é isso. Agora não preciso de nenhuma dependência, meu cgi script só precisa ler variáveis de ambiente, ler o stdin e o mandar o retorno pro stdout. Simples e bonito!
Nem tudo são flores
Apesar de serem legais, cgi scripts caíram em desuso por alguns motivos. Cada chamada cria um novo processo, os scripts precisam todos parsear o corpo por si só e por aí vai. Mas pelo menos no caso de um processo por chamada, num tempo onde tem muita gente usando esses serverless que sobe uma insância nova só pra rodar uma função, o que é um processinho comparado à isso?
Plugins in go (lang, não horse)
- publicado em 05 de julho de 2024
Eu gosto bastante da ideia de programas que potencialmente podem ser a junção e peças menores, unix pipes, serviços remotos e coisas do tipo e permitem escrever programas menores que se comunicam via uma interface definida. Nessa mesma linha, plugins podem ser bem úteis extendendo um programa sem que o programa principal saiba do que está acontecendo.
Em go existe o package plugin que permite escrever e carregar plugins dinamicamente. Basicamente o que precisa ser feito é definir o que o plugin precisa implementar (uma ou mais funções) e o programa principal carrega o plugin em tempo de execução e executa as funções implementadas pelo plugin.
O programa principal
A primeira coisa que precisamos é de um programa que seja capaz de carregar e executar plugins. Este será o programa principal que será extendidos pelos plugins. Para carregar o plugin se usa plugin.Open e plugin.Plugin.Lookup.
Então comecemos com um programa bem simples, sem suporte a plugins:
package main
import (
"flag"
"os"
)
func main() {
action := flag.String("action", "faz", "")
flag.CommandLine.Parse(os.Args[1:])
s := "coisa"
println(*action + " " + s)
}
$ ./programa
faz coisa
$./programa -action outra
outra coisa
Nada muito o que explicar aqui, né? Então agora vamos implementar o suporte a plugins.
A ideia agora é que a gente vai usar plugins baseados no parâmetro action que o usuário passar.
package main
import (
"flag"
"fmt"
"os"
v "plugin"
)
func main() {
action := flag.String("action", "faz", "")
flag.CommandLine.Parse(os.Args[1:])
s := "coisa"
var f plugin.Symbol
var fn func(string)
var ok bool
// Aqui plugin.Open recebe o caminho do plugin.
// A gente vai procurar por um plugin baseado
// na action que o usuário passou
fpath := fmt.Sprintf("./%s_plugin.so", *action)
p, err := plugin.Open(fpath)
if err != nil {
// Olha esse goto maroto engolindo os bug tudo!
goto PRINT
}
// Aqui a gente procura no plugin por um símbolo
// com o nome de Action
f, err = p.Lookup("Action")
if err != nil {
goto PRINT
}
// Aqui verifica se o símbolo é realmente do tipo
// que a gente espera
fn, ok = f.(func(string))
if !ok {
goto PRINT
}
fn(s)
return
PRINT:
println(*action + " " + s)
}
Por enquanto como não temos nenhum plugin nosso programa continua fazendo a mesma coisa:
$./programa
faz coisa
$./programa -action outra
outra coisa
O plugin
Para implementar o plugin a gente precisa só implementar uma função chamada Action que recebe uma string como parâmetro:
package main
func Action(s string) {
println("Aqui o plugin fazendo outra " + s)
}
Agora o plugin precisa ser compilado com a flag -buildmode=plugin
go build -buildmode=plugin outra_plugin.go
E agora nosso programa já pode usar o plugin:
$./programa
faz coisa
$./programa -action outra
Aqui o plugin fazendo outra coisa
E é isso!
Ninguém tem obrigação de aceitar seu código
- publicado em 06 de julho de 2023
Tem uma coisa que há algum tempo tem me incomodado um pouco. A ideia de que «qualquer coisa» serve como contribuição de software livre e a consequência disso que é uma galera achando que qualquer porcaria de pull request tem que ser aceito.
Programar dá trabalho
Como ouvi esses dias: «é triste, mas o óbvio tem que ser dito» e o óbvio aqui é que programar dá trabalho, mexer num projeto que você não conhece mais ainda. E o trabalho é dobrado porque além do trabalho de escrever o código alguém tem que verificar o que foi feito. Então o pr não é algo que chega de graça é trabalho dos dois lados.
E mais, pra chegar um pull request num repositório significa que o projeto já existe e tem utilidade pros outros. O trabalho já posto no projeto é muito maior que o trabalho posto no pr. Quando você manda um melhoria pra um projeto que já existe significa que alguém já pôs muito trabalho naquilo escrevendo, documentando, polindo. O seu pr é só uma gota no oceano. Sempre que alguém reclama que perdeu horas e teve o patch rejeitado eu lembro do Eric Idle dizendo: Oh, it took me hours.
O software é livre, o repo é meu
Quando você usa um programa ele tem uma licença, as de software livre em termos gerais dizem que você pode usar/alterar/distribuir da maneira que quiser não tem nada aí sobre o autor do software ser obrigado a usar código dos outros e um repositório não faz muita diferença, independente de onde o código estiver hospedado ainda vai dar pra usar. E tendo o seu fork do código ainda se pode colocar qualquer coisa, por mais super específica que for, que se fosse em um repositório compartilhado não caberia porque não interessa a mais ninguém, pode-se fazer experimentos sem quebrar nada, pode alterar tudo o que quiser. O direito de fork é a melhor coisa que tem.
Isso gera uma fragmentação da base de código, que geralmente é vista como ruim, mas no final das contas acaba tendo um bom resultado porque gera mais software útil, pra mais casos e quando as modificações do fork são importantes é capaz que o projeto original aproveite. Um repositório não deveria fazer diferença a não ser que a preocupação seja o nome.
Não me importo com seu marketing pessoal
E aqui é o que eu acho que mais estava me incomodando nessa coisa toda. Há um tempo eu já estava pensando em escrever algo sobre isso, mas esses dias vi um vídeo de um desses youtubers programadores que foi feio demais. Basicamente o cara teve um pr rejeitado e fez um vídeo choroso depois. Mas aí vamos ver qual foi a “contribuição” e a coisa pega. O que o cara fez foi tirar um comentário e alterar a variável que se usava em uma condição, uma mudança que por si só não melhora em nada o programa. O responsável pelo repo falou que uma mudança assim só seria aceita se viesse acompanhada de outras pra manter o padrão do projeto e fechou o pr. Aí veio o choro.
O cara aparentemente é alguém que já é profissional há anos, ele certamente sabe fazer mais que isso e sabe que o que fez não vai melhorar em nada o código. A única razão de fazer isso foi se promover, pra poder dizer «contribuo com open source» e pra gravar um vídeo pro youtube. Gravar o vídeo pro youtube rolou…
Eu não ligo que alguém queira se promover em cima do código que a pessoa fez, mas se quiser se promover faça algo decente, não vem com essa de alterar alguma besteira só pra depois ter seu nome nos colaboradores. Isso não ajuda o projeto em nada, só toma mais tempo dos envolvidos e isso pra um cara qualquer fazer sua propaganda.
O óbvio tem que ser dito e “comunidade” de software é feita principalmente em cima de software, a contribuição mais necessária é software. Quer fazer propaganda de si? Faz, mas faz direito.
Sobre a SSPL
- publicado em 22 de maio de 2023
Bom, eu sei que tô atrasado, mas eu sou lerdo mesmo, então vai.
A SSPL (Server Side Public Licence) é a licença criada pelo pessoal do mongodb para ser usada no lugar da AGPLv3 que era a licença do mongodb. A SSPL é basicamente a AGPLv3 com a cláusula 13 modificada. Esta cláusula diz respeito a como a licença se relaciona com a execução remota do programa licenciado.
A AGPL diz em sua cláusula 13 que um programa modificado, mesmo que seja acessado via rede, tem que ter o código fonte disponível e que um programa licenciado pela agpl pode ser combinado tranquilamente com um programa licenciado sob gpl. A SSPL por outro lado, além de exigir que um programa modificado tenha o código disponível assim como a agpl, também diz que se for oferecido um serviço de hospedagem do programa, todo o código de suporte ao serviço tenha que ser licenciado sob sspl de maneira que um usuário possa hospedar seu próprio serviço.
Por que mais uma licença?
A SSPL existe para lidar com as grandes de hospedagem (aws, microsoft e google) que oferecem serviços em cima do mongo e estão comendo uma fatiga gigante do mercado da mongo inc. tanto que a amazon anunciou um fork do mongodb assim que ele passou a ser licenciado com a sspl. A ideia da alteração feita na agpl é de forçar os cloud providers a abrirem sua infra, ou mais realisticamente, pagarem a licença pra mongo inc. E aqui nem vou entrar no mérito da briga de duas empresas multi-milionárias, a questão é mais a licença do software mesmo.
A licença foi submetida para analise da OSI e acabou não sendo aprovada, com basicamente o principal argumento que a licença é restritiva do uso do software assim violando o princípio de que o software deve poder ser usado da maneira que se quiser.
Restritiva como?
Um dos pontos do software livre é a não discriminação por uso do software. O exemplo clássico é o do aborto: um software livre não pode restringir o uso nem a uma clinica de aborto e nem a uma igreja anti-aborto e um dos argumentos era de que a licença restringia o uso baseado no tipo de uso, restringindo o uso baseado em ser um serviço ou não. E é aqui que eu acho estranho o argumento da osi.
Toda licença com copyleft tem alguma restrição na criação de trabalhos derivados, em geral exigindo que trabalhos derivados sejam lançados sobre a mesma licença e sendo assim «restringe» a liberdade de software proprietário, mas é uma restrição que faz bem ao ecossistema de software livre. No meu modo de ver a sspl para lidar com uma situação que não era contemplada com as licenças anteriores, isto é, um mundo com cloud providers gigantescos, como isto afeta a distribuição (ou falta de) dos programas. Me pareceu que o argumento da osi é o mesmo argumento que se usa contra licenças de copyleft.
A coisa do cachimbo e da boca torta
Como comentei no outro post, tem um tempo que as licenças com copyleft caíram em desuso e estão sempre “sob ataque”, e o open source é um dos responsáveis por isso. Quando do lançamento da GPLv3, que veio pra “tapar buracos” da GPLv2, um pessoal torceu o nariz. O pessoal não tinha como dizer que uma licença da fsf não é livre, mas fizeram questão de promover licenças mais amigávies aos negócios. E depois de tanto ser amigáveis aos negócios agora a osi passou a tomar lado na briga entre empresas.
No meu modo de ver, a sspl apesar de ser uma arma de negócios, é uma licença que adere aos princípios do software livre e expande isso, fazendo com que cloud providers também tenham que participar do jogo. Cinco anos já se passaram, o código do mongo continua aí disponível, pode-se usar o mongo da maneira que quiser, e se quiser montar um serviço de hospegagem de mongo também pode, só precisa abrir o seu código usado na infra. Acho justo e no espírito das licenças com copyleft.
Gera uma vez, gera outra!
- publicado em 11 de maio de 2023
Bom, seguindo com a minha ideia de escrever mais no blog nem que seja inútil, hoje vou escrever sobre geradores em go.
Geradores são funções que a cada interação retornam o próximo item de uma sequência. Eles dão a possibilidade de se trabalhar um item da sequencia de cada vez ao invés de ter que esperar pela sequencia completa para poder iterar sobre os itens. Uma maneira de se fazer isso em go é usando channels e goroutines e é isso que vamos fazer.
Uma primeira implementação
A ideia é criarmos uma função que retorna um channel que será alimentado por uma goroutine e assim o consumidor da função poderá iterar sobre o channel.
Um exemplo clássico (e inútil) sempre usado é a sequencia de fibonacci, então vamos fazer aqui também.
package main
func Fibs() chan int {
// Este canal será o consumido por quem usar esta função.
ch := make(chan int)
go func() {
// O Canal precisa ser fechado na função interna e não
// na externa porque senão o usuário não teria nada para
// consumir
defer close(ch)
i := 0
n := 1
for {
ch <- n
i, n = n, n+i
}
}()
return ch
}
func main() {
for n := range Fibs() {
println(n)
}
}
Rodando este código vemos dois problemas. Um que o código não para nunca até um C-c e quando acabam os números a sequencia para de crescer e começam a aparecer uns números negativos.
Lidando com erros
A primeira ideia para resolver isso é verifiar por erro e encerrar a goroutine interior.
package main
func Fibs() chan int {
// Este canal será o consumido por quem usar esta função.
ch := make(chan int)
go func() {
// O Canal precisa ser fechado na função interna e não
// na externa porque senão o usuário não teria nada para
// consumir
defer close(ch)
i := 0
n := 1
for {
ch <- n
i, n = n, n+i
// Aqui se o número for negativo a gente termina,
// o canal será fechado e o consumidor vai sair do loop.
if n < 0 {
break
}
}
}()
return ch
}
func main() {
for n := range Fibs() {
println(n)
}
}
Com isso o nosso programa já não fica rodando pra sempre e nem aparecem números estranhos, mas o consumidor também não sabe o que aconteceu, a sequencia simplesmente acabou. Se quisermos informar o consumidor sobre algum erro ocorrido precisamos de um canal que carrega uma struct com o valor e um erro.
package main
import "errors"
type GenItem struct {
Err error
Val int
}
func Fibs() chan GenItem {
// Este canal será o consumido por quem usar esta função.
// Agora o canal é um canal de GenItem para conter também
// informação sobre o erro
ch := make(chan GenItem)
go func() {
// O Canal precisa ser fechado na função interna e não
// na externa porque senão o usuário não teria nada para
// consumir
defer close(ch)
i := 0
n := 1
for {
item := GenItem{
Err: nil,
Val: n,
}
ch <- item
i, n = n, n+i
// Aqui se o número for negativo a gente termina
if n < 0 {
item := GenItem{
// A informação sobre o que de errado aconteceu
Err: errors.New("Cabou os número!"),
Val: 0,
}
ch <- item
break
}
}
}()
return ch
}
func main() {
for item := range Fibs() {
if item.Err != nil {
panic(item.Err.Error())
}
println(item.Val)
}
}
Assim o consumidor tem toda a segurança pra entrar em pânico tranquilamente. :)
Botando uma galera pra trampar
Até agora vimos somente uma goroutine alimentando o canal. Vamos fazer um pouco diferente dessa vez, vamos alimentar o canal com várias goroutines.
Imagine que temos uma lista de url e precisamos baixar o conteúdo de todas. Podemos usar mais de uma goroutine para baixar o conteúdo concorrentemente e ir alimentando o canal.
package main
import (
"io/ioutil"
"net/http"
"sync"
)
var URLS = []string{
"https://tupi.poraodojuca.dev/index.html",
"https://toxicbuild.poraodojuca.dev/index.html",
"https://mongomotor.poraodojuca.dev/index.html",
}
// Alteramos GenItem para conter as iformações do download
type GenItem struct {
Err error
Content []byte
Url string
}
func DownloadUrls() chan GenItem {
// Novamente o canal consumidor
ch := make(chan GenItem)
go func() {
// Note que o canal é fechado por esta goroutine
// e não pelas goroutines que fazem o downlaod as urls.
defer close(ch)
// Aqui usamos o WaitGroup para esperar até que todas as páginas
// tenham sido baixadas.
wg := new(sync.WaitGroup)
for _, url := range URLS {
// aqui pra cada url a gente dispara uma goroutine e seque a vida
// quem vai alimentar o canal é essa goroutina que baixa
// a página.
// Adicionamos 1 para cada goroutine que baixa uma página
wg.Add(1)
go func(url string) {
// Liberamos um do WaitGroup quando a função terminar
defer wg.Done()
content, err := DownloadUrl(url)
item := GenItem{
Err: err,
Content: content,
Url: url,
}
ch <- item
}(url)
}
// Aqui esperamos até que todas as páginas tenham sido baixadas
wg.Wait()
}()
return ch
}
// Isso aqui não importa, só faz um download normal mesmo
func DownloadUrl(url string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
c := http.Client{}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
func main() {
for item := range DownloadUrls() {
if item.Err != nil {
println("Erro baixando página " + item.Url)
} else {
println("Página " + item.Url + " baixada com sucesso!")
}
}
}
Aqui tivemos bastantes coisas diferentes: Primeiro que as funções que alimentam os canais são funções internas a função que cria o canal e também usamos um WaitGroup para aguardar todas as goroutines produtoras terminarem. É preciso esperar terminar porque caso contrário o canal seria fechado imediatamente.
Um wait group é um contador, para cada goroutine que queremos esperar adicionaos 1 ao wait group e o que estamos esperando deve remover um do grupo com Done() quando terminar a execução. Wait() bloqueia a execução até que o contador do wait group seja zerado. Voltando pro começo
Até agora vimos como consumir um gerador até que os produtores terminem, mas e se o consumidor quiser terminar antes? Se a gente simplesmente sair do loop o canal vai ficar aberto eternamente, então o que a gente precisa fazer é antes de sair do loop fechar o canal e tratar no produtor a tentativa de escrever no canal depois de fechado. Nosso primeiro exemplo fica assim:
package main
import (
"errors"
)
type GenItem struct {
Err error
Val int
}
func Fibs() chan GenItem {
// Este canal será o consumido por quem usar esta função.
// Agora o canal é um canal de GenItem para conter também
// informação sobre o erro
ch := make(chan GenItem)
go func() {
defer func() {
// Quando o consumidor fechar o canal a gente vai tentar escrever
// num canal fechado. Chamando recover() a gente recupera o controle
// da execução.
if r := recover(); r != nil {
println("Recovering!")
}
}()
defer close(ch)
i := 0
n := 1
for {
item := GenItem{
Err: nil,
Val: n,
}
ch <- item
i, n = n, n+i
// Aqui se o número for negativo a gente termina
if n < 0 {
item := GenItem{
// A informação sobre o que de errado aconteceu
Err: errors.New("Cabou os número!"),
Val: 0,
}
ch <- item
break
}
}
}()
return ch
}
func main() {
gen := Fibs()
for item := range gen {
println(item.Val)
if item.Val > 1000 {
// Aqui precisamos fechar o canal antes de sair do loop
// os consumidores vão ter pânico quando tentarem escrever
// no canal fechado
close(gen)
break
}
}
println("Fim!")
}
E acho que é isso.
Que isso, Python?
- publicado em 23 de abril de 2023
Essa é foda. O Python me mostrando traceback errado agora. Como pode?
Task exception was never retrieved
future: <Task finished name='Task-6939' coro=<BuildExecuter._run_build() done, defined at /home/pdjexception=KeyError('8ba5a94b-93bc-4a27-a4e4-05dc82c785cd')>
Traceback (most recent call last):
File /home/pdj/.virtualenvs/toxicbuild/lib/python3.11/site-packages/toxicbuild/master/build.py, line 848, in _run_build
await slave.build(build, **self.repository.envvars)
File /home/pdj/.virtualenvs/toxicbuild/lib/python3.11/site-packages/toxicbuild/master/slave.py, line 367, in build
build, envvars=envvars):
^^^^^^^^^^^^^^^^^^^
File /home/pdj/.virtualenvs/toxicbuild/lib/python3.11/site-packages/toxicbuild/master/client.py, line 125, in build
validate_cert=validate_cert)
File /home/pdj/.virtualenvs/toxicbuild/lib/python3.11/site-packages/toxicbuild/master/slave.py, line 409, in _process_info
async def _process_build_info(self, build, repo, build_info):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File /home/pdj/.virtualenvs/toxicbuild/lib/python3.11/site-packages/toxicbuild/master/slave.py, line 575, in _process_step_output_info
async def _get_step(self, build, step_uuid, wait=False):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File /home/pdj/.virtualenvs/toxicbuild/lib/python3.11/site-packages/toxicbuild/master/slave.py, line 560, in _update_build_step_info
return True
^^^^^^^^
KeyError: '8ba5a94b-93bc-4a27-a4e4-05dc82c785cd'
Na verdade esse aí foi o seguinte: Atualizei um código e antes de eu reuniciar o servidor deu um erro, então o python tá mostrando o erro em um fonte diferente. Mas foi engraçado. :P