Skip to main content

Thiago Lages de Alencar

(Interprocess Communication)

Existem diversas maneiras de fazer dois processos distintos se comunicarem e isto torna bem difícil de escolher qual deles utilizar sem antes conhecermos o mínimo delas.

Low Level

File

Escrever os dados em um arquivo e esperar que outro processo leia o arquivo.

Pode ser estranho por ser muito simples mas acontece que passar dados entre processos não precisa ser complicado.

note

Inclusive, é como eu implementei Mondot (GUI para MongoDB).

Exemplo:

  • Processo 1
    • Constantemente verifica se o arquivo possui conteúdo
    • Se notar que possui, exibe o conteúdo na tela e esvazia o arquivo
  • Processo 2
    • Constantemente verifica se o arquivo está vazio
    • Se notar que está vazio, escreve conteúdo no arquivo
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int BUFFER_SIZE = 256;

int main(void) {
FILE *file = fopen("input.txt", "r+");
char *buffer = (char *)malloc(BUFFER_SIZE);
int count = 0;

memset(buffer, 0, BUFFER_SIZE);

printf("Waiting message\n");

do {
fseek(file, 0, SEEK_SET);
count = fread(buffer, sizeof(char), BUFFER_SIZE, file);
} while (count == 0);

printf("Reading message\n\n");

do {
printf("%s", buffer);
count = fread(buffer, sizeof(char), BUFFER_SIZE, file);

if (count < BUFFER_SIZE) {
buffer[count] = '\0';
}

} while (count != 0);

printf("\n\nClearing file\n");
freopen("input.txt", "w", file);
fclose(file);

return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int BUFFER_SIZE = 256;

int main(void) {
FILE *file = fopen("../process1/input.txt", "w+");
char *buffer = (char *)malloc(BUFFER_SIZE);
int count = 0;

memset(buffer, 0, BUFFER_SIZE);

printf("Waiting file to be empty\n");

do {
fseek(file, 0, SEEK_SET);
count = fread(buffer, sizeof(char), BUFFER_SIZE, file);
} while (count != 0);

printf("Writing message\n");
fprintf(file, "Hello world");
fclose(file);

return 0;
}

Note que neste exemplo eu leio e escrevo no arquivo constantemente, porém isto é apenas um exemplo!

A realidade é que nós devemos ler ou escrever no arquivo na frequência que acharmos necessário para nosso programa. Só quero que você entenda que este método IPC é sobre processos usarem arquivos para interagir uns com os outros.

File Locking

Escrever os dados em um arquivo e esperar que outro processo leia o arquivo porém respeitando as travas.

Um grande problema da maneira anterior é dois processos interagirem exatamente no mesmo momento com o arquivo. Imagine que um processo comece a ler enquanto um outro não terminou de escrever, isso fará com que ele leia conteúdo incompleto.

A maneira de travar arquivos varia em cada sistema operacional. Por exemplo, no Linux temos:

  • flock
  • lockf
  • fcntl

Utilizaremos lockf para aprimorar o exemplo utilizado para arquivos:

  • Processo 1
    • Constantemente:
      • Espera obter a trava para o arquivo
      • Verifica se o arquivo possui conteúdo
      • Libera a trava do arquivo se ele não tiver
    • Se notar que possui, exibe o conteúdo na tela, esvazia o arquivo e libera a trava
  • Processo 2
    • Constantemente:
      • Espera obter a trava para o arquivo
      • Verifica se o arquivo possui conteúdo
      • Libera a trava do arquivo se ele tiver
    • Se notar que está vazio, escreve conteúdo no arquivo e libera a trava
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int BUFFER_SIZE = 256;

int main(void) {
int fd = open("input.txt", O_RDWR);
char *buffer = (char *)malloc(BUFFER_SIZE);
int count = 0;

memset(buffer, 0, BUFFER_SIZE);

printf("Waiting message\n");

while (1) {
lockf(fd, F_LOCK, 0);
lseek(fd, 0, SEEK_SET);
count = read(fd, buffer, BUFFER_SIZE);

if (count != 0) {
break;
}

lockf(fd, F_ULOCK, 0);
}

printf("Reading message\n\n");

do {
printf("%s", buffer);
count = read(fd, buffer, BUFFER_SIZE);

if (count < BUFFER_SIZE) {
buffer[count] = '\0';
}

} while (count != 0);

printf("\n\nClearing file\n");
ftruncate(fd, 0);
lockf(fd, F_ULOCK, 0);
close(fd);

return 0;
}
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int BUFFER_SIZE = 256;

char* MESSAGE = "Hello world";

int main(void) {
int fd = open("input.txt", O_RDWR);
char *buffer = (char *)malloc(BUFFER_SIZE);
int count = 0;

memset(buffer, 0, BUFFER_SIZE);

printf("Waiting file to be empty\n");

while (1) {
lockf(fd, F_LOCK, 0);
lseek(fd, 0, SEEK_SET);
count = read(fd, buffer, BUFFER_SIZE);

if (count == 0) {
break;
}

lockf(fd, F_ULOCK, 0);
}

printf("Writing message\n");
ftruncate(fd, 0);
lseek(fd, 0, SEEK_SET);
write(fd, MESSAGE, strlen(MESSAGE));
lockf(fd, F_ULOCK, 0);
close(fd);

return 0;
}

Signal

Enviar um signal ao processo/thread para uma função tratar

Diferente de outros IPC, signal não é focado em comunicação no nível de aplicação, então não é muito utilizado para enviar dados, normalmente apenas para notificar outro processo da ocorrência de algo.

A melhor maneira de ter uma idéia da utilidade dos signals é vendo o signal.h do sistema operacional. No caso do Linux:

bits/signum-generic.h
/* We define here all the signal names listed in POSIX (1003.1-2008);
as of 1003.1-2013, no additional signals have been added by POSIX.
We also define here signal names that historically exist in every
real-world POSIX variant (e.g. SIGWINCH).

Signals in the 1-15 range are defined with their historical numbers.
For other signals, we use the BSD numbers.
There are two unallocated signal numbers in the 1-31 range: 7 and 29.
Signal number 0 is reserved for use as kill(pid, 0), to test whether
a process exists without sending it a signal. */

/* ISO C99 signals. */
#define SIGINT 2 /* Interactive attention signal. */
#define SIGILL 4 /* Illegal instruction. */
#define SIGABRT 6 /* Abnormal termination. */
#define SIGFPE 8 /* Erroneous arithmetic operation. */
#define SIGSEGV 11 /* Invalid access to storage. */
#define SIGTERM 15 /* Termination request. */

/* Historical signals specified by POSIX. */
#define SIGHUP 1 /* Hangup. */
#define SIGQUIT 3 /* Quit. */
#define SIGTRAP 5 /* Trace/breakpoint trap. */
#define SIGKILL 9 /* Killed. */
#define SIGPIPE 13 /* Broken pipe. */
#define SIGALRM 14 /* Alarm clock. */

/* Archaic names for compatibility. */
#define SIGIO SIGPOLL /* I/O now possible (4.2 BSD). */
#define SIGIOT SIGABRT /* IOT instruction, abort() on a PDP-11. */
#define SIGCLD SIGCHLD /* Old System V name */

/* Not all systems support real-time signals. bits/signum.h indicates
that they are supported by overriding __SIGRTMAX to a value greater
than __SIGRTMIN. These constants give the kernel-level hard limits,
but some real-time signals may be used internally by glibc. Do not
use these constants in application code; use SIGRTMIN and SIGRTMAX
(defined in signal.h) instead. */

/* Include system specific bits. */
#include <bits/signum-arch.h>
bits/signum-arch.h
/* Adjustments and additions to the signal number constants for
most Linux systems. */

#define SIGSTKFLT 16 /* Stack fault (obsolete). */
#define SIGPWR 30 /* Power failure imminent. */

/* Historical signals specified by POSIX. */
#define SIGBUS 7 /* Bus error. */
#define SIGSYS 31 /* Bad system call. */

/* New(er) POSIX signals (1003.1-2008, 1003.1-2013). */
#define SIGURG 23 /* Urgent data is available at a socket. */
#define SIGSTOP 19 /* Stop, unblockable. */
#define SIGTSTP 20 /* Keyboard stop. */
#define SIGCONT 18 /* Continue. */
#define SIGCHLD 17 /* Child terminated or stopped. */
#define SIGTTIN 21 /* Background read from control terminal. */
#define SIGTTOU 22 /* Background write to control terminal. */
#define SIGPOLL 29 /* Pollable event occurred (System V). */
#define SIGXFSZ 25 /* File size limit exceeded. */
#define SIGXCPU 24 /* CPU time limit exceeded. */
#define SIGVTALRM 26 /* Virtual timer expired. */
#define SIGPROF 27 /* Profiling timer expired. */
#define SIGUSR1 10 /* User-defined signal 1. */
#define SIGUSR2 12 /* User-defined signal 2. */

/* Nonstandard signals found in all modern POSIX systems
(including both BSD and Linux). */
#define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */

Por padrão alguns signals já possuem comportamentos pré-definidos. Por exemplo: SIGINT.

info

Quando executando um programa pelo terminal, se você apertar Ctrl+C o signal enviado para o processo é o SIGINT.

Fica a sua escolha se você deseja sobreescrever o comportamento de um signal (caso ele tenha um comportamento padrão).

warning

Existem dois signals que não podem ter o comportamento sobreescrito: SIGKILL e SIGSTOP.

Quando um Signal é recebido pelo seu processo, o kernel pausa o fluxo normal do seu programa para executar a função que você definiu para aquele signal. Caso você não tenha definido alguma função, o comportamento padrão é executado (o qual pode ser ignorar o signal).

Signals permitem que você envie um inteiro ou ponteiro junto deles.

No caso de comunicação entre processos, apenas o inteiro costuma ser útil pois não podemos acessar o espaço de memória de outro processo.

note

Porém se estivermos utilizando para comunicação entre threads, enviar o endereço de um dado específico é bem útil.

Existem dois signals reservados para o uso da aplicação/usuário: SIGUSR1 e SIGUSR2. Podemos utiliza-los para a comunicação de nossos processo.

Por exemplo, enviar um número entre processos:

  • Processo 1
    • Exibe o PID no terminal
    • Espera em loop pelo signal com o número
    • Encerrar ao receber o número
  • Processo 2
    • Lê o PID passado por argumento
    • Envia o número para o processo desejado utilizadno o signal SIGUSR1
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

bool loop = true;

// Normally the function would only receive a number as paramter.
// But we setted flag SA_SIGINFO into our sigaction.
void on_user_signal(int signal_number, siginfo_t *signal_info, void *x) {
printf("Received signal: %d\n", signal_number);
printf("Together with number: %d\n", signal_info->si_value.sival_int);

loop = false;
}

int main(void) {
struct sigaction signal_action;
signal_action.sa_flags = SA_SIGINFO;
signal_action.sa_sigaction = on_user_signal;

// sigaction() is recommended nowadays instead of signal().
sigaction(SIGUSR1, &signal_action, NULL);

printf("My PID: %d\n", getpid());
printf("Waiting signal\n");

while (loop) {
}

return 0;
}
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

int NUMBER = 42;

int main(int argc, char **args) {
if (argc != 2) {
printf("Needs to pass PID through arguments\n");
return 1;
}

union sigval signal_value = {NUMBER};
int pid = atoi(args[1]);

printf("Sending signal to PID: %d\n", pid);
printf("Together with number: %d\n", NUMBER);
sigqueue(pid, SIGUSR1, signal_value);

return 0;
}

Pipe

Ler e escrever no pipe de outro processo filho/pai

warning

Para entender bem pipe, recomendo entender bem file descriptor (o que eu não entendia muito bem).

Recomendação: https://www.youtube.com/watch?v=rW_NV6rf0rM

O conceito de pipes é bem simples, você escreve em um lado do pipe e para alguém ler do outro lado dele.

Podemos criar um pipe com o comando pipe():

#include <stdio.h>
#include <unistd.h>

int main(void) {
int file_descriptors[2];

pipe(file_descriptors);

printf("Pipe input: %d\n", file_descriptors[0]);
printf("Pipe output: %d\n", file_descriptors[1]);

return 0;
}

O comando pipe() inseri dois file descriptors, um para a entrada do pipe e outro para a saída do pipe, no nosso array. O comando também retorna -1 em caso de erro, mas eu irei ignorar tratamentos de erros nesses exemplos.

info

O que é um file descriptor? É um número inteiro utilizado pelo seu processo para pedir ao sistema operacional por acesso a um arquivo. É preciso entender que quando você escreve/lê de um arquivo, você na verdade está pedindo para o sistema operacional fazer isto para você.

O sistema operacional possue uma tabela com todos os files descriptors de cada processo (e outras informações relacionadas ao arquivo).

processo idfile descriptorfile position...
103430...
567710...
394544959...
120343283...

Então toda vez que você deseja abrir um arquivo, o sistema operacional te entrega um file descriptor. Este file descriptor é como se fosse um ticket que permite você pedir ao sistema operacional por interações com aquele arquivo ("Oi sistema operacional, eu gostaria de escrever no arquivo relacionado a este ticket").

Começamos com o mínimo de IPC quando utilizando pipe() com fork():

#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
char message[3];
int file_descriptors[2];

pipe(file_descriptors);

if (fork()) {
// Parent
wait(NULL);
read(file_descriptors[0], message, 3);
printf("Message: %s\n", message);
} else {
// Child
write(file_descriptors[1], "hi", 3);
}

return 0;
}

Cada processo possui seus próprios file descriptors, que por sua vez levam a um arquivo/pipe/etc. Porém quando fazemos um fork(), nossas entradas na tabela de file descriptors também é clonada.

Por exemplo, vamos supor que o ID do processo pai é 1034 e o filho nasceu com o ID 1035. Quando o filho nasce, ele herda todos os files descriptors:

processo idfile descriptorfile position...leva ao pipe
103430...1000
103530...1000

Ou seja, o file descriptor com número 3 do pai e do filho irão levar ao mesmo arquivo/pipe/etc.

Isto não quer dizer que todos os file descriptors futuros seram compartilhados! Por exemplo:

#include <sys/wait.h>
#include <unistd.h>

int main(void) {
int file_descriptors[2];
int more_file_descriptors[2];

pipe(file_descriptors);

if (fork()) {
// Parent
pipe(more_file_descriptors);
wait(NULL);
} else {
// Child
pipe(more_file_descriptors);
}

return 0;
}

Ao chamar pipe() dentro do pai ou do filho, você está pedindo para o sistema operacional criar um pipe para aquele processo. O pai e filho receberam pipes distintos embora possuam o mesmo file descriptor (justamente pois file descriptors são identificadores únicos do processo).

processo idfile descriptorfile position...leva ao pipe
103430...1000
103440...2000
103450...2000
103530...1000
103540...3000
103550...3000

Named Pipe (FIFO)

Ler e escrever no pipe de outro processo

A diferença deste pipe para o anterior é que qualquer processo pode se ligar a ele, seja para escrever ou ler, pois ele é praticamente um arquivo no sistema.

O comando utilizado para criar o este pipe é mkfifo() e da mesma maneira que arquivos tem permissões... Você deve passar as permissões do arquivo como parâmetro, no nosso caso irei passar 0666 (rw-rw-rw).

Lembrando que um pipe só é um pipe se tiver pelo menos um lado de entrada e outro de saída, ou seja, funções de escrita/leitura do pipe irão ficar travadas até que o outro lado do pipe exista.

Enquanto não tiver ninguém lendo do pipe, o comando write() irá ficar em loop esperando alguém para ler.
Enquanto não tiver ninguém escrevendo no pipe, o comando read() irá ficar em loop esperando alguém começar a escrever.

  • Processo 1
    • Tenta criar o named pipe com devidas permissões
    • Abre o named pipe para escrita
    • Escreve no arquivo a mensagem
  • Processo 2
    • Tenta criar o named pipe com devidas permissões
    • Abre o named pipe para leitura
    • Le do arquivo a mensagem
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void) {
mkfifo("pipefile", 0666);

int file_descriptor = open("pipefile", O_WRONLY);
write(file_descriptor, "hi", 3);
printf("Message sent\n");

return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void) {
char message[3];

mkfifo("pipefile", 0666);

int file_descriptor = open("pipefile", O_RDONLY);
read(file_descriptor, &message, 3);
printf("Message: %s\n", message);

return 0;
}

É possível ter mais que um processo lendo do mesmo pipe mas é incerto de quem receberá o conteúdo ou se dois processos irão receber o mesmo conteúdo.

Também é possível não ficar em loop esperando alguém começar a ler/escrever do outro lado do pipe, basta fazer um or quando abrindo o pipe (O_WRONLY | O_NDELAY ou O_RDONLY | O_NDELAY).

info

Originalmente chamado de FIFO pelo comportamento clássico "first in, first out", porém atualmente é mais conhecido pelo nome named pipe que deixa implicito que se comporta basicamente igual a um pipe.

Originalmente criado utilizando a função mknod() e passando como argumento S_IFIFO para especificar o tipo de arquivo. Justamente por está função suportar diversos tipos:

#definevaluefile type
S_IFSOCK0140000socket
S_IFLNK0120000symbolic link
S_IFREG0100000regular file
S_IFBLK0060000block device
S_IFDIR0040000directory
S_IFCHR0020000character device
S_IFIFO0010000FIFO

Message Queue

Shared Memory

Socket

High Level

Remote Procedure Call

HTTP API

WebSocket

References

Thiago Lages de Alencar

Alocação de memória se trata de pedir ao sistema operacional por espaço de memória RAM para utilizarmos durante a execução do nosso programa.

note

Não confundir com dispositivos de armazenamentos como HDD e SSD, onde nossa interação com eles customa ser por escrita e leitura de arquivos.

Static Allocation

Se refere a alocar espaço para todos os dados que já se sabe que serão necessários desdo início do seu programa.

O espaço necessário é descoberto durante a compilação de um programa e armazenado em conjunto do binário para que já seja carregada na memória na inicialização do programa.

Compilador irá identificar:

  • Literais
  • Variáveis estáticas/globais
  • Código de Funções

Pegando o seguinte código como exemplo:

static int executions = 0;

void run(int p) {
printf("Running");
executions += 1;
}

int main(void) {
run(1);
run(5);
run(10);
return 0;
}

Para que seu programe funcione, o compilador consegue identificar que será necessário espaço para o literal "Running", variável estática executions e o código da função run().

A memória alocada é separada em dois segmentos:

  • Data Segment
    • Variáveis estáticas
  • Text Segment
    • Literais
    • Código de funções

Data segments funcionam como espaço de memória normal, onde podem ter seus valores atualizado/modificados.

Text segments são armazenados uma vez e apenas utilizados para leitura durante a execução do seu programa.


Variáveis estáticas caem na primeira categoria pois a qualquer momento podemos fazer algo como executions += 1.

Literais caem na segunda pois sempre precisaremos daquele exato literal quando o código passar por aquela linha de código printf("Running"), então não queremos que ele seja modificado de maneira nenhuma.

Código de funções são somente leitura pois estamos falando da base para se criar funções conforme o necessário. O que eu quero dizer com isto? Toda vez que executarmos uma função, utilizaremos o código da função como base para alocar memória para aquela execução da função!

info

Por que não fazer com que todas as chamadas da funções utilizem o mesmo espaço?

Cada chamada pode ter comportamento diferente por causa de parâmetros ou fatores externos. Isto quer dizer arriscariamos colisão entre as execuções, o que poderia trazer resultados diferentes.

Imagine que seu código possue uma função recursiva, agora você corre o risco das chamadas a ela mesma alterarem uma variável que era essencial dela.

Stack Allocation

Se refere a alocar espaço na Stack.

No início do programa, um espaço na memória é reservado para dados temporários ou curto tempo de vida, este espaço reservado é chamado de Stack. Utilizar mais espaço do que o reservado irá causar Stack Overflow.

Isto é necessário pois nosso código pode se ramificar de diversas maneiras, tornando impossível descobrir toda a memória que será utilizada durante a etapa de compilação.

Pegando o seguinte código como exemplo:

int func1(int a, int b) {
return a + b;
}

int func2(int a, int b) {
int x = a * 3;
int y = b * 2;
return x + y;
}

int run(int a, int b) {
if(a > 10) {
return func1();
} else {
return func2();
}
}

Quando uma função é chamada, o programa insere na Stack variáveis daquela função.

No caso da func1(): a, b.
No caso da func2(): a, b, x, y.

Ao sair da função, o programa remove esses valores da Stack.

É importante notar que como o espaço da Stack já foi alocada no início do programa, inserir e remover da Stack são operações rápidas.

Quando CPUs precisam de dados da memória RAM, elas pegam um bloco de dados de cada vez. O ideal é que nessa pegada já tivesse tudo que a CPU precisaria, para ajudar nisto Stacks seguem o modelo (LIFO, last-in, first-out).

Pegando a func2() como exemplo:

Stack
a
b
x
y

Podemos notar que inserimos na stack na ordem em que encontramos as variáveis, justamente para quando a CPU pegar um bloco de memória a chance de pegar tudo aumentar.

Por outro lado, note que essa arquitetura impede que nossas variáveis possam crescer de tamanho (b não poderia crescer de tamanho pois o espaço seguinte já está reservado por x), por isto os valores inseridos na Stack precisam ter tamanho fixo.

Explicit Memory Management

Se refere a alocar espaço na Heap.

Existem casos onde precisamos que a memória possa crescer ou diminuir tamanho, justamente pois não temos como saber o quanta memória será necessária (e armazenar toda a memória RAM para si mesmo seria rude).

É importante notar que o sistema operacional é responsável por gerenciar a memória, então precisamos pedir a ele por espaço de memória RAM para utilizar.

Por exemplo, note como utilizamos a função malloc() para pedir ao sistema operacional por espaço para 3 inteiros:

int run() {
int *v = (int*)malloc(sizeof(int) * 3);
v[0] = 10;
v[1] = 100;
v[2] = 1000;
free(v);

return 0;
}

Fazer uma requisição por N espaços de memória ao sistema operacional, nos garante N espaço de memória, ou seja, podemos receber mais espaço de memória que o necessário (estou ignorando o caso onde a memória RAM está cheia).

Este comportamento tem como objeto minimizar a quantidade de requisições feitas ao sistema operacional por memória, pois estas requisições custam bastante tempo.

Vamos pegar outro exemplo:

int run() {
int *v = (int*)malloc(sizeof(int) * 3);
v[0] = 10;
v[1] = 100;
v[2] = 1000;
v[3] = 10000; // New line.
free(v);

return 0;
}

Existe a chance deste código dar erro e a chance de não dar, tudo depende de quanta memória RAM o sistema operacional nos deu. Se ele tiver nos dado exatamente 3, um erro de Segmentation fault vai aparecer pois o sistema operacional não nos permite acessar memória RAM que ele não nos entregou.

Por outro lado, grande chance de não dar erro pois sistema operacional costumam enviar bem mais que o necessário. O seguinte código tem bem mais chance de dar erro:

int run() {
int *v = (int*)malloc(sizeof(int) * 3);
v[0] = 10;
v[1] = 100;
v[2] = 1000;
v[3] = 10000;
v[100000] = 100000; // New line.
free(v);

return 0;
}

Importante notar que v contém o endereço da memória RAM requisitada ao sistema operacional (o endereço na Heap), porém o valor de v, o endereço` é armazenado na Stack pois é uma espaço de memória fixo (um endereço tem um tamanho fixo de memória).

O grande problema que aparece com o uso da Heap é garantir que o seu programa libere a memória obtida, pois é bem comum de usuários da linguagem esquecerem de devolver a memória (free()).

Para evitar este tipo de problema, algumas técnicas para gerênciar memória foram criados:

  • Garbage Collection
  • Reference Counting
  • Ownership Model

Garbage Collection

É uma técnica onde toda a responsabilidade de alocar e liberar memória (malloc() e free()) é passada ao Garbage Collector, onde ele deve conseguir detectar que a memória não está mais sendo usada e libera-la.

info

A requisição de memória (malloc()) sempre é feita pelo usuário, mesmo em linguagens que possuem Garbage Collector embutido.

Pode não ser tão claro notar estes pedidos de alocação:

  • Python
    • example = []
  • GDScript
    • var example = []
  • Java
    • Obj example = new ArrayList<Obj>();

É possível implementar um Garbage Collector em linguagens que não possuem um embutido, porém por não ser embutido, bibliotecas de terceiros podem acabar por não utiliza-lo e vazamento de memória pode acontecer de qualquer maneira.

note

Por exemplo, para a linguagem de programação C podemos encontrar este pequeno projeto:
https://github.com/orangeduck/tgc

A grande desvantagem desta técnica é que pausas no seu programa precisam ser feitas para que o Garbage Collector tenha tempo de análisar memórias que não estão mais em uso.

Reference Counting

Nesta técnica, toda alocação de memória inclue um contador para sabermos quantas vezes aquele espaço alocado está sendo referenciado. Quando o contador chega a zero, uma chamada para liberar a memória é feita (free()).

A quantidade de referências aumenta a qualquer momento que alguém aponta para aquele espaço de memória. Por exemplo:

func _ready() -> void:
var x = RefCounted.new()
var y = x
var z = [x]
print(x.get_reference_count()) # Three
y = null
z = []
print(x.get_reference_count()) # One

Cada vez que alguém referência o espaço de memória alocado por pedido da segunda linha, o contador cresce.

func _ready() -> void:
var x = RefCounted.new()
var y = x
var z = [x]
print(x.get_reference_count()) # Three
y = null
z = []
print(x.get_reference_count()) # One

Cada vez que alguém para de referênciar aquele espaço de memória, o contador desce.

func _ready() -> void:
var x = RefCounted.new()
var y = x
var z = [x]
print(x.get_reference_count()) # Three
y = x
x = null
print(y.get_reference_count()) # One
y = x
x = null
print(y.get_reference_count()) # One

É importante notar que o contador não existe com a variável inicial, no caso x, então a qualquer momento podemos fazer com que a variável inicial deixe de referênciar e continuaremos sem problemas de usar aquela memória!

O lado negativo é que a cada referência a está variável, precisamos aumentar/diminuir o contador. O que pode ser custoso quando tem que se fazer isso para toda memória da Heap.

A técnica mais simples de reference counting também não é bom em lidar com reference cycles. Quando referências apontam entre sim, o que faz com que os contadores nunca cheguem a zero.

Ownership Model

Diferente das maneiras anteriores onde o programador não precisa pensar sobre a liberação de memória, neste caso temos que seguir regras que no final ajudam o compilador a determinar quando que a memória deve ser liberada.

Como isto é feito durante a etapa de compilação, a execução do seu software não sofre perda de desempenho e qualquer erro relacionado ao assunto é pego durante a compilação.

A documentação da linguagem Rust deixa claro as regras:

  • Cada valor possue um dono
  • Valores apenas podem possuir um dono
  • Quando o dono sai do escopo, o valor é liberado
info

A primeira regra parece ser apenas uma introdução de que existe o conceito de dono.
A segunda é para deixar claro que um valor não pode ter múltiplos donos.
A terceira nos deixa claro quando o compilador irá adicionar a liberação de memória.

References

Thiago Lages de Alencar

The Elm Architecture

(MVU, Model-View-Update)

A arquitetura se resume a 3 partes:

  • Model: Conjuntos de dados sobre o estado da aplicação
  • View: Converte os dados do model para algo visual
  • Update: Reage a atualizações para atualizar os dados do model

Seja qual for a sua aplicação (CLI, TUI, GUI, ...), ela é responsável por receber o que o usuário deve visualizar e por notificar atualizações vindas do usuário:

MVU

Bubbletea

É responsável por rodar em loop a arquitetura Elm. Todo o código para isto se resume a:

package main

import tea "github.com/charmbracelet/bubbletea"

func main() {
program := tea.NewProgram(
MyStruct{} // Here you pass your model.
)

_, err := program.Run()

if err != nil {
print(err.Error())
}
}

Entenda que Model é terminologia para representar uma coleção de dados e é por isto que Bubbletea utiliza structs para representar models.

Models Structure

Models dentro do Bubbletea devem seguir a seguinte estrutura:

type MyStruct struct {
// Fields.
}

func (m MyStruct) Init() tea.Cmd {
// Run on initialization.
}

func (m MyStruct) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Process a message.
}

func (m MyStruct) View() string {
// Write to screen.
}

Podemos ver que é dividido em 4 partes:

  • Uma struct responsável por representar o Model da arquitetura Elm
  • Um método Init() único da estrutura bubbletea
  • Um método Update() responsável por representar o Update da arquitetura Elm
  • Um método View() responsável por representar o View da arquitetura Elm
info

Em muitos tutoriais, pessoas também incluem uma função responsável por criar o model e configura-lo com os padrões desejados:

func newMyStruct() MyStruct {
return MyStruct{
// Set default fields values here
}
}

func main() {
program := tea.NewProgram(newMyStruct()) // Use here
}

Keys

Teclas pressionadas no teclado vão para o mesmo lugar que todas interações (Update()), lá resta a você reconhecer como sendo um pressionamento de tecla.

func (m MyStruct) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
}

return m, nil
}

No exemplo podemos ver que possuimos um switch para nos ajudar a identificar o tipo da mensagem, isso é necessário pois a mensagem pode ter qualquer tipo (não necessariamente uma mensagem de pressionamento de tecla).

Mensagens de teclas dentro do Bubbletea podem ser convertido para strings, o que nos da uma string de fácil entedimento porém nem sempre uma ação está ligada a apenas um atalho, então pode ser útil sabermos como ligar diversos atalhos a uma ação:

var quit = key.NewBinding(
key.WithKeys("ctrl+c", "ctrl+q", "q")
)

// ...

func (m MyStruct) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, quit):
return m, tea.Quit
}
}

return m, nil
}
tip

Em vez de criar uma variável para cada atalho, você pode agrupar todas em uma que simbolize todo o mapeamento dos atalhos:

var keyMap struct {
Up key.Binding
Down key.Binding
Quit key.Binding
}

var keys = keyMap{
Up: key.NewBinding(
key.WithKeys("up", "w"),
),
Down: key.NewBinding(
key.WithKeys("down", "s"),
),
Quit: key.NewBinding(
key.WithKeys("ctrl+c", "ctrl+q", "q"),
),
}

// ...

func (m MyStruct) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, keys.Quit):
return m, tea.Quit
case key.Matches(msg, keys.Down):
// Do something
case key.Matches(msg, keys.Up):
// Do something
}

return m, nil
}

Bubbles

É uma coleção de models com parte da lógica já preparada.

Olhando a assinatura das funções e structs do spinner, podemos ver que ele implementa:

  • Model: type Model struct
  • Update: func (m Model) Update(msg tea.Msg) (Model, tea.Cmd)
  • View: func (m Model) View() string

Ele implementa quase todas as partes necessárias para ser executado diretamente no Bubbletea, a única parte que está faltando é Init(). Por que isso?

Acontece que spinner não tem intenção de ser um modelo perfeito para ser utilizado diretamente pelo Bubbletea, pois não faz sentido criar um programa Bubbletea que apenas exibe um spinner.

Porém podemos criar nosso próprio model que utiliza este spinner:

type MyStruct struct {
spinner spinner.Model
}

func newSpinner() MyStruct {
return MyStruct{
spinner: spinner.New(),
}
}

func (m MyStruct) Init() tea.Cmd {
return m.spinner.Tick // spinner requires this to start
}

func (m MyStruct) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd

switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
default:
m.spinner, cmd = m.spinner.Update(msg)
}

return m, cmd
}

func (m MyStruct) View() string {
return m.spinner.View()
}

func main() {
program := tea.NewProgram(
MyStruct{
spinner: spinner.New(),
}
)

_, err := program.Run()

if err != nil {
print(err.Error())
}
}

Note como basicamente reutilizamos tudo do spinner para fazer nosso model:

  • Nossa struct é o spinner sozinho
  • Nossa View() apenas chama a View() do spinner
  • Nosso Update() chama Update() do spinner e detecta se o atalho para sair foi pressionado

Como podemos ver os componentes do Bubbles foram feitos para serem utilizados pelos seus models (e não pelo Bubbletea diretamente). Imagine o susto de alguém executando o spinner diretamente e ele nunca fechando pois ninguém processa ctrl+c para sair.

References

Thiago Lages de Alencar

1 - Performance

Mude o modo de renderização para Compatibility, não é como se você fosse precisar de qualquer renderização além do básico para desenhar uma janela na tela.

Compatibility

Ative Application > Run > Low Processor Mode para que tenha um delay entre as renderizações da janela e apenas renderizar se alguma mudança for detectada. Em jogos a tela muda constantemente, então esse tipo de delay e validação só atrapalham mas como estamos falando de GUI que altera bem menos, isto ajuda muito.

Low Processor Mode

2 - Window Title Bar

Por padrão o nosso sistema operacional nos providência o gráfico básico de uma janela e nos deixa responsável por desenhar o conteúdo dentro dela.

Not borderless

O lado positivo é que isto nos providência o básico de uma janela, como aqueles 3 botões no topo da direita (minimizar, máximizar, fechar, ...).

Porém note que, dependendo do sistema operacional, mais opções podem estar disponíveis! Se eu clicar com o botão direito na title bar do topo, podemos ver mais ações:

Not borderless again

Se ativarmos Display > Window > Size > Borderless, o sistema operacional deixará de adicionar a title bar no topo:

Borderless

Basicamente ele está assumindo que você mesmo irá desenhar a title bar no topo caso queira (normalmente em jogos isto não faz sentido).

info

Borderless ou não, ainda se trata de uma janela no seu sistema operacional então alguns atalhos podem continuar funcionando (Super + Up/Down/Left/Right, Alt + Space).

3 - Multiple Windows

Janelas abertas são tratadas como processos filhos, ou seja, o encerramento de uma janela pai irá encerrar os filhos.

Caso queiramos ter múltiplas janelas idênticas, igual a editores de textos e navegadores, precisamos ter certeza que a janela principal (processo inicial) não possa ser encerrado da maneira padrão (clickando no botão de fechar).

Podemos resolver isto escondendo a janela principal e apenas exibindo as subwindows.

Ative Display > Window > Size > Transparent para que o fundo cinza padrão não seja renderizado durante a execução.

Transparent background

note

Acredito que a cor padrão do canvas é preto, por isto deixar de pintar vai deixar a janela preta

Ative Display > Window > Per Pixel Transparency > Allowed para que o fundo realmente seja transparente (caso contrário vai ficar o canvas preto).

Per Pixel Transparency

warning

Existe uma configuração que eu ainda não entendi a necessidade: Rendering > Viewport > Transparent Background.

Mas a documentação menciona ela como necessária.

Como visto na sessão anterior...

Ative Display > Window > Size > Borderless para o sistema operacional deixará de adicionar a title bar no topo.

Invisible

Embora ela esteja transparente, ela ainda é uma janela como as outras. Podemos conferir que ela ainda aparece quando apertando Alt+Tab (ou apenas apertando Alt no Ubuntu).

Window invisible but exist

Agora nós temos que tratar inputs!

Primeiro podemos notar que se está janela for posta na frente de outra, ela não irá deixar de consumir os seus clicks (mesmo que você queira selecionar algo na janela de trás).

Para resolver isto podemos alterar janela raiz (criada quando seu programa inicia) para repassar adiante clicks do mouse.

func _ready() -> void:
get_window().mouse_passthrough = true

Segundo podemos notar que ela ainda está processando teclas (pode ser selecionada pelo Alt + Tab, fechada por Alt + F4, maximizada com Super + Up, etc).

Ative Display > Window > Size > No Focus para que ela não possa ser focada (até por atalhos).

Process invisible

Lembre que fechar o processo pai fecha todos os filhos, porém fechar todos os filhos não fecha o pai.

Isto quer dizer que o processo pai continua rodando mesmo se o usuário fechar todas as janelas filhas. Agora o usuário apenas conseguiria encerrar o programa pelo "gerenciador de tarefas" ou terminal.

Para tratar isto podemos ligar um signal a um método responsável por notar quando a quantidade de filhos mudar e encerrar o programa se necessário.

Signal child_order_changed

func _on_child_order_changed() -> void:
if get_child_count() == 0:
get_tree().quit()
note

Isto é apenas uma maneira de tratar!

Nós poderiamos checar a cada frame se todas as janelas foram fechadas, poderiamos fazer os filhos avisarem o pai quando fossem encerrados, etc.

Agora precisamos entender que cada janela aberta é uma subwindow. Existem dois tipos de subwindows:

  1. Subwindows
    • Quando sua janela pede ao sistema operacional para criar uma janela filha dela
    • Sua janela filha vai possuir a title bar padrão de janelas
  2. Embed subwindows
    • Quando sua janela simula outra janela dentro dela mesmo
    • Isto impossibilita ela de ser mover para fora da janela pai

Se estamos tratando de uma aplicação que possui múltiplas janelas, precisamos que ela se mova para fora da janela pai. Caso contrário isso ocorreria:

Subwindow + Half subwindow

A janela 2 está saindo do limite da janela pai.

Poderiamos inicializar a janela pai maximizada para evitar isto porém outros problemas iriam aparecer, por exemplo: Janela pai ignorar clicks e teclas, tornando impossível interagir com as janelas simuladas nele.

Desative Display > Window > Subwindows > Embed Subwindows para que as subwindows sejam tratadas como janelas reais pelo sistema operacional (em vez de simuladas pela janela pai).

Subwindows

Mas se quisermos ter uma title bar de janela única para as nossas janelas? Podemos fazer o mesmo que fizemos com janela principal, torna-la borderless.

subwindows borderless

Dentro das propriedades da Janela, ative Flags > Borderless.

subwindows borderless activate

Agora nós seriamos responsáveis por criar a title bar no topo da janela. Desta maneira poderiamos fazer uma title bar única igual ao Google Chrome ou Steam!

4 - Custom Title Bar

Ter uma title bar própria é relativamente raro hoje em dia, pois muitas vezes requer reinventar a roda sem trazer benifícios reais.

Mas isto não quer dizer que nenhuma aplicação faz isto:
Custom Title Bars
(Steam, GNOME Files, Google Chrome)

Note que as 3 aplicações aproveitaram o espaço para providênciar mais informações e funcionalidades ao usuário. Porém nós vamos focar em pelo menos reproduzir o básico:

  1. Exibir titulo
  2. Providênciar botões de minimizar, maximizar e fechar
  3. Double click maximizar
  4. Arrastar a title bar deve mover a janela
  5. Redimensionar janela se arrastar as bordas

Depois disso você deve ser capaz de adicionar ou remover mais utilidades conforme a sua vontade.

warning

Estarei partindo do princípio que queremos customizar uma title bar na janela principal, por isto o código utiliza get_window(), mas adaptações podem ser necessárias caso esteja tratando subwindows.

Exibir Titulo

Basta utilizar o node Label.

Minimize, Maximize, Close Buttons

Basta utilizar 3 nodes Button tratando o signal pressed:

func _on_minimize_pressed() -> void:
get_window().mode = Window.MODE_MINIMIZED


func _on_maximize_pressed() -> void:
if get_window().mode == Window.MODE_MAXIMIZED:
get_window().mode = Window.MODE_WINDOWED
else:
get_window().mode = Window.MODE_MAXIMIZED


func _on_close_pressed() -> void:
get_tree().quit()
subwindows tip

É importante tratar o signal close_requested vindo da janela, pois é por ele que você recebe notificações que o usuário tentou fechar de outras maneiras (taskbar do windows, etc).

Double Click Maximize

Container não possui signal para isto diretamente porém podemos utilizar o signal mais geral gui_input.

func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_title_bar_mouse_button(event)


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
_on_title_bar_double_click()


func _on_title_bar_double_click() -> void:
match get_window().mode:
Window.MODE_MAXIMIZED:
get_window().mode = Window.MODE_WINDOWED
_:
get_window().mode = Window.MODE_MAXIMIZED

Já estamos dividindo em funções menores pois os passos seguintes irão adicionar mais funcionalidades nestas funções gerais.

Drag Window

A princípio, arrastar a janela pode ser resumido em saber duas coisas:

  • Saber se o click do mouse está sendo pressionado
  • Onde que o click estava quando começou
var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_title_bar_mouse_button(event)
elif event is InputEventMouseMotion:
_on_title_bar_mouse_motion(event)


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
_on_title_bar_double_click()
elif event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_title_bar_dragging = true
_title_bar_dragging_start = get_global_mouse_position()
elif event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
_title_bar_dragging = false


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(_event: InputEventMouseMotion) -> void:
if _title_bar_dragging:
_on_title_bar_dragged()


func _on_title_bar_dragged() -> void:
match get_window().mode:
Window.MODE_WINDOWED:
get_window().position += get_global_mouse_position() as Vector2i - _title_bar_dragging_start
subwindows tip

Primeiro: Talvez seja bom mover para o centro da tela a janela pois a posição poder não estar correta durante a inicialização (bug?):

func _ready() -> void:
get_window().move_to_center()

Segundo: Talvez seja necessário utilizar get_local_mouse_position() em vez de get_global_mouse_position() pois deve ser necessário o canvas da própria subwindow.

Esse foi apenas o essencial sobre arrastar, agora podemos pensar em implementar detalhes sobre a ação de arrastar janelas.

Por exemplo: Quando o usuário tentar arrastar uma janela máximizada, ela automaticamente sai do máximizado e se posiciona para que o mouse esteja proporcionalmente na posição correta.

var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i

var _title_bar_dragging_adjustment: float = 0


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
...


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
...


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(event: InputEventMouseMotion) -> void:
...


func _on_title_bar_dragged() -> void:
match get_window().mode:
Window.MODE_WINDOWED:
get_window().position += get_global_mouse_position() as Vector2i - _title_bar_dragging_start
Window.MODE_MAXIMIZED:
_title_bar_dragging_adjustment = get_global_mouse_position().x / get_window().size.x
get_window().mode = Window.MODE_WINDOWED


func _on_resized() -> void:
if _title_bar_dragging_adjustment != 0:
get_window().position += (get_global_mouse_position() as Vector2i)
get_window().position.x -= get_window().size.x * _title_bar_dragging_adjustment
_title_bar_dragging_start = get_global_mouse_position()
_title_bar_dragging_adjustment = 0
note

Lembre de se conectar ao sinal _on_resized.

Resize Window (old)

Redimensionar pode ser facilmente implementado se utilizarmos o node MarginContainer que nos permite adicionar bordas às laterais, estas serão nossas bordas que devem reagir ao mouse.

Nodes do tipo Control possuem lógica para lidar com inputs do mouse, eles podem consumir ou passar ao node de cima as input do mouse.

Isso quer dizer que qualquer input do mouse na nossa janela (que não tiver sido consumida) chegará ao nosso MarginContainer. Isto não é o que queremos, para nós só é interessante que chegue inputs interagindo com a borda do nosso container.

Podemos resolver isto parando o consumo de inputs no container logo abaixo do MarginContainer:

Margin

note

Existem Controls que por padrão param o consumo do mouse neles, por exemplo: Panel.

Agora temos certeza que interações vindo do signal gui_input são interações diretas com o MarginContainer.

enum Margin {
NONE,
TOP,
RIGHT,
BOTTOM,
LEFT,
TOP_RIGHT,
TOP_LEFT,
BOTTOM_RIGHT,
BOTTOM_LEFT,
}

var _margin_dragging: bool = false

var _margin_dragging_edge_start: Vector2i

var _margin_dragging_origin_limit: Vector2i

var _margin_selected: Margin

var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i

var _title_bar_dragging_adjustment: float = 0

func _get_current_margin(mouse_position: Vector2) -> Margin:
var margin: Margin = Margin.NONE

if get_global_mouse_position().x < get_theme_constant("margin_left"):
margin = Margin.LEFT
elif get_global_mouse_position().x > size.x - get_theme_constant("margin_right"):
margin = Margin.RIGHT

if get_global_mouse_position().y < get_theme_constant("margin_top"):
match margin:
Margin.LEFT:
return Margin.TOP_LEFT
Margin.NONE:
return Margin.TOP
Margin.RIGHT:
return Margin.TOP_RIGHT
elif get_global_mouse_position().y > size.y - get_theme_constant("margin_bottom"):
match margin:
Margin.LEFT:
return Margin.BOTTOM_LEFT
Margin.NONE:
return Margin.BOTTOM
Margin.RIGHT:
return Margin.BOTTOM_RIGHT

return margin


func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_mouse_button(event)
elif event is InputEventMouseMotion:
_on_mouse_motion(event)


func _on_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_margin_dragging = true
_margin_selected = _get_current_margin(event.position)
_margin_dragging_edge_start = get_window().position + get_window().size
_margin_dragging_origin_limit = _margin_dragging_edge_start - get_window().min_size
elif event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
_margin_dragging = false


func _on_mouse_motion(event: InputEventMouseMotion) -> void:
if _margin_dragging:
_on_dragged(event)
else:
_on_hover(event)


func _on_dragged(event: InputEventMouseMotion) -> void:
if get_window().mode != Window.MODE_WINDOWED:
return

match _margin_selected:
Margin.TOP:
get_window().position.y = min(
get_window().position.y + event.position.y,
_margin_dragging_origin_limit.y
)

get_window().size.y = _margin_dragging_edge_start.y - get_window().position.y
Margin.RIGHT:
get_window().size.x = event.position.x
Margin.BOTTOM:
get_window().size.y = event.position.y
Margin.LEFT:
get_window().position.x = min(
get_window().position.x + event.position.x,
_margin_dragging_origin_limit.x
)

get_window().size.x = _margin_dragging_edge_start.x - get_window().position.x
Margin.TOP_RIGHT:
get_window().position.y = min(
get_window().position.y + event.position.y,
_margin_dragging_origin_limit.y
) # Top

get_window().size = Vector2i(
event.position.x, # Right
_margin_dragging_edge_start.y - get_window().position.y, # Top
)
Margin.TOP_LEFT:
get_window().position = Vector2i(
min(
get_window().position.x + event.position.x,
_margin_dragging_origin_limit.x
), # Left,
min(
get_window().position.y + event.position.y,
_margin_dragging_origin_limit.y
), # Top
)

get_window().size = Vector2i(
_margin_dragging_edge_start.x - get_window().position.x, # Left
_margin_dragging_edge_start.y - get_window().position.y, # Top
)
Margin.BOTTOM_RIGHT:
get_window().size = Vector2i(
event.position.x, # Right
event.position.y, # Bottom
)
Margin.BOTTOM_LEFT:
get_window().position.x += event.position.x # Left
get_window().size = Vector2i(
_margin_dragging_edge_start.x - get_window().position.x, # Left
event.position.y, # Bottom
)


func _on_hover(event: InputEventMouseMotion) -> void:
match _get_current_margin(event.position):
Margin.NONE:
mouse_default_cursor_shape = Control.CURSOR_ARROW
Margin.TOP:
mouse_default_cursor_shape = Control.CURSOR_VSIZE
Margin.RIGHT:
mouse_default_cursor_shape = Control.CURSOR_HSIZE
Margin.BOTTOM:
mouse_default_cursor_shape = Control.CURSOR_VSIZE
Margin.LEFT:
mouse_default_cursor_shape = Control.CURSOR_HSIZE
Margin.TOP_RIGHT:
mouse_default_cursor_shape = Control.CURSOR_BDIAGSIZE
Margin.TOP_LEFT:
mouse_default_cursor_shape = Control.CURSOR_FDIAGSIZE
Margin.BOTTOM_RIGHT:
mouse_default_cursor_shape = Control.CURSOR_FDIAGSIZE
Margin.BOTTOM_LEFT:
mouse_default_cursor_shape = Control.CURSOR_BDIAGSIZE


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
...


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
...


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(_event: InputEventMouseMotion) -> void:
...


func _on_title_bar_dragged() -> void:
...


func _on_resized() -> void:
...

Dentro das funções novas, muitas possuem a mesma lógica utilizada para arrastar janela. Porém duas possuem lógica nova: _get_current_margin() e _on_dragged()

A primeira é responsável por identificar a borda a qual o mouse se encontra (varias validações para identificar a posição do mouse em relação as bordas).

A segunda é a lógica de redimensionar, para resolver ela é recomendado primeiro resolver a lógica para cima, direita, baixo e esquerda (as diagonais são combinações das lógicas das outras).

info

Por que redimensionar não é suave igual a outras aplicações?

Isto ocorre pois nem sempre a movimentação e o redimensionamento não ocorrem na mesma frame.

Por enquanto não é possível de se resolver isto pelo Godot.

Resize Window (new)

A maneira anterior adiciona um grande problema: Utilizar MarginContainer adiciona margins vazias a sua janela e tornava impossível delas encostarem nas bordas do monitor.

Está outra maneira envolve estruturar uma cena com todas as bordas necessárias.

Margin Scene

enum Margin {
TOP,
RIGHT,
BOTTOM,
LEFT,
TOP_RIGHT,
TOP_LEFT,
BOTTOM_RIGHT,
BOTTOM_LEFT,
}

@export var margin_top: int = 0:
set(m):
margin_top = m

if not get_node_or_null("%Top"):
return

%TopLeft.custom_minimum_size.y = m
%Top.custom_minimum_size.y = m
%TopRight.custom_minimum_size.y = m

@export var margin_right: int = 0:
set(m):
margin_right = m

if not get_node_or_null("%Right"):
return

%TopRight.custom_minimum_size.x = m
%Right.custom_minimum_size.x = m
%BottomRight.custom_minimum_size.x = m

@export var margin_bottom: int = 0:
set(m):
margin_bottom = m

if not get_node_or_null("%Bottom"):
return

%BottomLeft.custom_minimum_size.y = m
%Bottom.custom_minimum_size.y = m
%BottomRight.custom_minimum_size.y = m

@export var margin_left: int = 0:
set(m):
margin_left = m

if not get_node_or_null("%Left"):
return

%TopLeft.custom_minimum_size.x = m
%Left.custom_minimum_size.x = m
%BottomLeft.custom_minimum_size.x = m

var _margin_dragging: bool = false

var _margin_dragging_edge_start: Vector2i

var _margin_dragging_origin_limit: Vector2i

var _margin_hovered: Margin

var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i

var _title_bar_dragging_adjustment: float = 0

func _process(_delta: float) -> void:
if _margin_dragging:
_on_dragged()


func _on_dragged() -> void:
if get_window().mode != Window.MODE_WINDOWED:
return

var mouse_position: Vector2i = get_global_mouse_position() as Vector2i

match _margin_hovered:
Margin.TOP:
get_window().position.y = min(
get_window().position.y + mouse_position.y,
_margin_dragging_origin_limit.y
)

get_window().size.y = _margin_dragging_edge_start.y - get_window().position.y
Margin.RIGHT:
get_window().size.x = mouse_position.x
Margin.BOTTOM:
get_window().size.y = mouse_position.y
Margin.LEFT:
get_window().position.x = min(
get_window().position.x + mouse_position.x,
_margin_dragging_origin_limit.x
)

get_window().size.x = _margin_dragging_edge_start.x - get_window().position.x
Margin.TOP_RIGHT:
get_window().position.y = min(
get_window().position.y + mouse_position.y,
_margin_dragging_origin_limit.y
) # Top

get_window().size = Vector2i(
mouse_position.x, # Right
_margin_dragging_edge_start.y - get_window().position.y, # Top
)
Margin.TOP_LEFT:
get_window().position = Vector2i(
min(
get_window().position.x + mouse_position.x,
_margin_dragging_origin_limit.x
), # Left,
min(
get_window().position.y + mouse_position.y,
_margin_dragging_origin_limit.y
), # Top
)

get_window().size = Vector2i(
_margin_dragging_edge_start.x - get_window().position.x, # Left
_margin_dragging_edge_start.y - get_window().position.y, # Top
)
Margin.BOTTOM_RIGHT:
get_window().size = Vector2i(
mouse_position.x, # Right
mouse_position.y, # Bottom
)
Margin.BOTTOM_LEFT:
get_window().position.x += mouse_position.x # Left
get_window().size = Vector2i(
_margin_dragging_edge_start.x - get_window().position.x, # Left
mouse_position.y, # Bottom
)


func _on_top_left_gui_input(event: InputEvent) -> void:
_margin_hovered = Margin.TOP_LEFT
_on_margin_gui_input(event)


func _on_top_gui_input(event: InputEvent) -> void:
_margin_hovered = Margin.TOP
_on_margin_gui_input(event)


func _on_top_right_gui_input(event: InputEvent) -> void:
_margin_hovered = Margin.TOP_RIGHT
_on_margin_gui_input(event)


func _on_left_gui_input(event: InputEvent) -> void:
_margin_hovered = Margin.LEFT
_on_margin_gui_input(event)


func _on_right_gui_input(event: InputEvent) -> void:
_margin_hovered = Margin.RIGHT
_on_margin_gui_input(event)


func _on_bottom_left_gui_input(event: InputEvent) -> void:
_margin_hovered = Margin.BOTTOM_LEFT
_on_margin_gui_input(event)


func _on_bottom_gui_input(event: InputEvent) -> void:
_margin_hovered = Margin.BOTTOM
_on_margin_gui_input(event)


func _on_bottom_right_gui_input(event: InputEvent) -> void:
_margin_hovered = Margin.BOTTOM_RIGHT
_on_margin_gui_input(event)


func _on_margin_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_margin_mouse_button(event)


func _on_margin_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_margin_dragging = true
_margin_dragging_edge_start = get_window().position + get_window().size
_margin_dragging_origin_limit = _margin_dragging_edge_start - get_window().min_size
elif event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
_margin_dragging = false


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
...


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
...


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(_event: InputEventMouseMotion) -> void:
...


func _on_title_bar_dragged() -> void:
...


func _on_resized() -> void:
...
warning

Eu não passei a limpo o código acima, usei como base o de outro projeto que eu possuia.

Isto quer dizer que posso ter esquecido de alterar algum nome de função e variável corretamente ou seguindo o mesmo padrão visto anteriormente.

5 - Drag and Drop (DND)

Podemos dividir em dois tipos:

  • Drag from Godot
  • Drag from Operating System

Entenda que não é possível simplesmente arrastar um item de uma aplicação para outra e esperar que a receptora entenda aquele tipo de dado.

Por exemplo, imagine que nós puxemos a aba do terminal do VSCode para o Godot.

VSCode terminal

Embora VSCode nos permita arrastar está aba e reposiciona-la dentro do próprio VSCode, o Godot não entende o que é está aba (definitivamente não é um Node ou Control).

Para resolver este problema, o sistema operacional age como intermediários entre as aplicações, forçando a aplicação a formatar de uma maneira esperada pelo OS antes de transferir entre aplicações

note

Isto quer dizer que cada sistema operacional possue seu formato de transferência (normalmente as bibliotecas abstraem isto).

Por outro lado, quando toda a operação de DND é dentro do Godot, não precisamos nos preocupar com formatar da maneira que o sistema operacional deseja e podemos passar os dados em um formato conhecido pelo Godot.

Drag from Godot

DND Godot

No momento que você começa a arrastar qualquer Control, Godot irá chamar o método _get_drag_data() daquele Control.

Exemplo:

extends TextureRect


func _get_drag_data(at_position: Vector2) -> Variant:
return texture
  • Se o método retornar null, Godot entenderá que não existe conteúdo sendo arrastado
    • Por padrão este método virtual retorna null
  • Se o método retornar qualquer outro dado, Godot entenderá que existe conteúdo sendo arrastado

Neste momento Godot lança a notificação NOTIFICATION_DRAG_BEGIN para todos os Nodes.

info

Este tipo de notificação é muito utilizada em GUI's pois nos permite destacar uma área onde o conteúdo pode ser solto.

Por exemplo, Godot detecta que você está arrastando algo válido para aquele campo e cria uma borda azul para deixar claro que é possível soltar o conteúdo lá.

Godot blue border

Agora que estamos no estado "dragging", sempre que passarmos o mouse em cima de um Control, Godot irá chamar o método _can_drop_data() para saber se é possível soltar conteúdo nele.

Exemplo:

extends Button


func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
return data is Texture
  • Se o método retornar false, Godot entenderá que não suporta o conteúdo sendo arrastado
    • Por padrão este método virtual retorna false
  • Se o método retornar true, Godot entenderá que suporta o conteúdo sendo arrastado
info

É normal ver o mouse mudar de aparência para destacar que o conteúdo pode ser largado naquele local.

No momento que soltarmos o conteúdo, Godot irá chamar o método _drop_data() apenas se passou na validação do método _can_drop_data().

Exemplo:

extends Button


func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
return data is Texture


func _drop_data(at_position: Vector2, data: Variant) -> void:
icon = data

Após soltar o click, independente se tiver sido de algo válido, Godot irá emitir a notificação NOTIFICATION_DRAG_END para todos os Nodes.

info

É possível conseguir informações ao vivo do estado do dragging na Viewport.

Drag from OS

DND OS

No momento Godot apenas suporta drop do file manager, ao fazer isto sua janela irá receber os para os arquivos passados.

info

Esta ação ocorre entre sistema operacional e as janelas da nossa aplicação, mas é importante entender que está janela não pode ser embedded.

Isso é necessário pois "embed window" é apenas uma simulação de janela dentro da nossa aplicação, logo não é vista como janela pelo OS.

O node Window possui o sinal files_dropped para avisar quando um ou mais arquivos são largados na janela.

Sabendo isto podemos conectar uma função a este sinal da janela principal:

func _ready() -> void:
get_window().files_dropped.connect(_on_files_dropped)


func _on_files_dropped(files: PackedStringArray) -> void:
pass
note

Se estivessemos falando de uma subwindow, poderiamos utilizar a própria interface do Godot para conectar.

Um detalhe a se notar é que neste caso não recebemos a posição onde os arquivos foram largados, então precisariamos calcular manualmente se está dentro da área esperada.

func _ready() -> void:
get_window().files_dropped.connect(_on_files_dropped)


func _on_files_dropped(files: PackedStringArray) -> void:
if Rect2(global_position, size).has_point(get_global_mouse_position()):
pass

Este código pode ser adicionado a qualquer Control para que ele trate drops nele.

6 - Debug

É possível literalmente visualizar o quanto a sua aplicação se redesenha na tela.

Debug canvas options

Isto pode ser útil para te alertar se algum control seu está se redesenhando demais.

warning

Atualmente isto apenas funciona quando utilizando o modo de renderização "Mobile" e este modo afeta o desempenho do Godot.

Minha recomendação seria apenas utilizar durante as etapas de testes.

References

Thiago Lages de Alencar

Crawler: Responsável por navegar entre websites utilizando links encontrados neles mesmos
Scraper: Responsável por extrair informações importantes dos websites navegados

Crawling é essêncial para scraping, pois você não tem como conseguir extrair informações de um site sem navegar para ele antes.
Scraping não é essêncial para crawling, pois os dados do site podem ser irrelevantes para você.

Por exemplo:

  • Google utiliza um crawler para navegar a internet e indexar páginas delas, porém não extrai as informações dos websites em si
  • OpenAI utiliza scraper para pegar os videos do youtube e utilizar na inteligência artificial deles

Se eu tivesse que separar crawlers em categorias, seria:

  • Browser: Utilizando um browser real para crawlear
  • Raw: Simulando um browser atráves de requisições pela internet

Hoje em dia simular um navegador é incrivelmente difícil então a maneira raw é bem menos potente e fornece muito menos funcionalidades.

Browser

Existem 3 ferramentas famosas de automação de navegadores:

É importante notar que elas todas se declaram para usos de testes, porém ainda assim são muito utilizadas para web scraping.

Selenium

Criado em ~2004 mas que continua a oferecer suporte para todos os navegadores e diversas linguagens (não necessariamente bem).

Muito do seu estilo vem do fato de ter sido criado utilizando Java e depois adaptado para outras linguagens.

javascript
import { Browser, Builder, By } from "selenium-webdriver";

const driver = await new Builder().forBrowser(Browser.CHROME).build()
await driver.get('https://www.etiquetaunica.com.br/')
await driver.manage().setTimeouts({implicit: 1000});

let hamburguerButton = await driver.findElement(By.xpath('//*[@id="headerWrapper"]/div[1]/button[1]'))
await hamburguerButton.click()

let brandButton = await driver.findElement(By.xpath('//*[@id="menu"]/div/div[2]/ul/li[7]/a'))
await brandButton.click()

Puppeteer

Criado pela Google em ~2017 para fornecer automação ao Google Chrome utilizando JavaScript.

Note que o locator favorito deles envolve utilizar CSS.

javascript
import puppeteer from "puppeteer";

const browser = await puppeteer.launch({headless: false})
const page = await browser.newPage()

await page.setViewport({ width: 1600, height: 1024 })
await page.goto('https://gringa.com.br/')
await page.locator('#section-header > div > div.Header__FlexItem.Header__FlexItem--fill.Header__FlexItem_left--mobile > nav > ul > li:nth-child(3) > a').click()

Playwright

Criado pela Microsoft em ~2020 para fornecer automação em diversos navegador. O estilo é bem parecido ao do Puppeteer pois boa parte dos desenvolvedores vieram do Puppeteer 🤣.

A linguagem primária de programação dele é JavaScript, porém fornece suporte a diversas outras (não necessariamente bem).

javascript
import { chromium, devices } from "playwright";

const browser = await chromium.launch({ channel: 'chrome', headless: false })
const context = await browser.newContext(devices['Desktop Chrome'])
const page = await context.newPage()

await page.goto('https://canseivendi.com.br/')
await page.getByRole('link', {name: 'Marcas'}).click()

Raw

Neste caso o mais importante é você possuir uma boa quantidade de bibliotecas que o ajudem a realizar a tarefa! O básico é conseguir requisitar a página na internet e parsear o conteúdo HTML.

python
import httpx
from parsel import Selector

USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"

response = httpx.get(
"https://canseivendi.com.br/bolsas",
headers={"user-agent": USER_AGENT},
)

selector = Selector(text=response.text)
names = selector.xpath('//div[@class="name"]/a/text()').getall()
links = selector.xpath('//div[@class="name"]/a/@href').getall()
prices = selector.xpath('//div[@class="cash"]/text()').getall()

Neste exemplo escolhi utilizar a biblioteca httpx (criada por Encode) e parsel (criado por Scrapy), mas fica a sua escolha as bibliotecas para as tarefas.

note

Navegadores fazem muito mais que apenas uma requisição! Uma página leva uma reação em cadeia de requisições por conteúdos delas.

Por exemplo, ao receber uma página HTML e o navegador identificar uma imagem nela (<img src="photo.jpg">), ele precisa fazer uma requisição dessa imagem ao site.

Agora imagina que isto acontece para diversos tipos de conteúdos da página:

  • Imagens: <img src="myimage.jpg">
  • Audio: <audio></audio>
  • Videos: <video></video>
  • CSS: <link rel="stylesheet" href="mystyle.css">
  • JavaScript: <script src="myscripts.js"></script>
  • Iframe: <iframe src="url"></iframe>

Cat and Mouse Game

Ter os dados do seu site scrapeado por bots não é algo bom, pois eles geram grande tráfego e nenhum lucro (não estou falando de scalping).

Por isto é normal ver websites tentando identificar bots para bloquea-los e bots fingindo serem usuários normais do dia a dia.

Acontece que muitas vezes isso envolve simular comportamentos de um usuário e simular um navegador, onde ambos não são tarefas fáceis.

Aqui uma lista pequena de coisas a se pensar:

  • Simular Navegador
    • Construir Headers
    • Analisar HTML
    • Executar JavaScript
    • Variar fingerprint
  • Simular Usuário
    • Resolver Captchas
    • Movimento do mouse
    • Velocidade digitando
  • Após ser bloqueado
    • Alterar comportamento/tática
      • Para não ser bloqueado novamente
    • Utilizar Proxy

Uma da melhor maneira de saber como atacar é sabendo como os sites se protegem... O que é algo que eu tenho pouco conhecimento então vou terminar aqui 🤣.

References

Thiago Lages de Alencar
note

Pesquisa limitada pelo fato de Twitter ainda estar bloqueado no Brasil.

Aproveitando que eu estava olhando o drama em cima de Godot e assistindo Ace Attorney... Resolvi brincar com o site https://objection.lol.

Video

Details

Twitter

Discord

Godot Foundation

Website

jun-17

jul-08

oct-11

Fork

SMS

Docs

Twitter++

BlueSky

Google

Discord++

Youtube

Thiago Lages de Alencar

Está é uma lista com minha opnião sobre diversas ferramentas de terminal.
Aqueles marcados com estrela (⭐) são ainda utilizados por mim.

CLI

  • nala
    • Substitui: apt
    • Uso diário: Alto
    • Nota: Torna muito melhor a visualização e entendimento da instalação de programas
  • duf
    • Substitui: df
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois acabo olhando em GUIs essas informações
  • dust
    • Substitui: du
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois acabo olhando em GUIs essas informações
  • fastfetch
    • Substitui: neofetch
    • Uso diário: Baixo
    • Nota: Providência mais informação e o projeto não foi abandonado
  • jq
    • Substitui: ---
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois trato JSON por código

TUI

  • bottom
    • Substitui: htop, top
    • Uso diário: Baixo
    • Nota: Praticamente nunca uso pois é muito chato ter que lembrar de todos os atalhos, acabo voltando para htop
  • superfile
    • Substitui: ---
    • Uso diário: Baixo
    • Nota: Nunca uso pois dificilmente tenho necessidade de usar file manager no terminal
  • yazi
    • Substitui: ---
    • Uso diário: Baixo
    • Nota: Nunca uso pois dificilmente tenho necessidade de usar file manager no terminal
  • micro
    • Substitui: nano
    • Uso diário: Alto
    • Nota: Muito mais parecido com um editor de texto do dia a dia

Shells

  • fish
    • Substitui: bash
    • Nota 1: Providência uma ótima experiência logo de cara
    • Nota 2: Sintaxe melhor que bash porém ainda não da vontade de aprender
  • nushell
    • Substitui: bash, fish
    • Nota 1: Providência uma ótima experiência logo de cara
    • Nota 2: Sintaxe muito mais agradável
    • Nota 3: Remove a necessidade de possuir jq
    • Nota 4: Remove a necessidade de possuir curl pois possui o comando http
    • Nota 5: Remove a necessidade de possuir df/duf pois possui o comando sys disks

Em shells "uso diário" é 100% de quando você utilizar o shell, então sempre é alto.

Prompt

  • Starship
    • Opnião 1: Permite grande costumização do prompt com facilidade, tornando o prompt mais agradável
    • Opnião 2: Quando lidando com Git possue um peso considerativo

Em prompts "subsititui" é sempre sobre substituir o padrão.
Em prompts "uso diário" é 100% de quando você utilizar o shell, então sempre é alto.

References

Thiago Lages de Alencar

Imagem mostrando camada do App, OS e Hardwares

Quando você quer ler o arquivo, você pede ao sistema operacional pelo conteúdo do arquivo.

Quando você quer escrever no arquivo, você pede ao sistema operacional para inserir o conteúdo no arquivo.

É importante saber que o sistema operacional toma diversos cuidados para que desenvolvedores não acessem diretamente o hardware, ou seja, por baixo dos panos você está pedindo para o sistema operacional ler/escrever.

  • C
    • fgets()
    • fwrite()
  • Python
    • file.read()
    • file.write()
  • Rust
    • file.read_to_string()
    • file.write_all()
  • Go
    • os.ReadFile()
    • os.WriteFile()

Veremos como garantir a segurança de um arquivo quando se tem múltiplos processos querendo altera-lo.

Process

A função utilizada para se criar processos é fork(), está função faz com que o atual processo crie um processo filho quase idêntico e executando o mesmo código que o pai.

Olhe este código que printa duas vezes "Hi":

#include <stdio.h>
#include <unistd.h>

int main() {
fork();
printf("Hi\n");
}

Se você executa-lo irá notar que o filho é tão igual ao pai que ele continua exatamente do mesmo local que o pai se encontrava (logo após fork() retornar um valor). Se tivessemos variáveis, poderiamos ver que até o valor delas são idênticos ao do pai.

No entanto, precisamos de uma maneira de reconhecer quem é o pai e filho, caso contrário este código executária exatamente a mesma coisa para ambos (não seria nada produtivo). Acontece que a função fork() retorna um valor e este valor é utilizado para sabermos se estamos no pai ou no filho.

#include <stdio.h>
#include <unistd.h>

int main() {
int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
printf("I'm the child process\n");
} else {
printf("I'm the parent process\n");
}
}

A função fork() vai retornar ao pai o PID do filho (ou -1 em caso de error).
A função fork() vai retornar ao filho zero.

tip

Normalmente o código do pai e filho são inseridos em funções em vez de deixar tudo dentro de um if/else.

if(pid == 0) {
child_code();
} else {
parent_code();
}

Ou se utiliza funções exec para transformar completamente o código executado naquele processo.

Problem

Imagem ilustrativa do app 1 lendo do arquivo, app 2 lendo do arquivo, app 1 escrevendo no arquivo, app 2 escrevendo no arquivo

Quando dois processos interagem com o mesmo arquivo, pode acontecer da informação ser preenchida incorretamente? Afinal, precisamos primeiramente descobrir se isso é possível ou não de acontecer.

Como o escalonamento pode ser imprevisivel, uma maneira de testar se durante a interação com um arquivo houve troca de processo é repetindo a ação diversas vezes e ver se pelo menos uma vez ocorreu.

O seguinte código irá ser executado para o processo pai e filho:

int count = 0;
FILE* file = fopen("example.txt", "w+");

while(count < 10000) {
int i;

fseek(file, 0, SEEK_SET);
fscanf(file, "%d", &i);
fseek(file, 0, SEEK_SET);
fprintf(file, "%d ", ++i);

count++;
}

fclose(file);

O código irá garantir que o cursor está no início do arquivo, ler o atual número do arquivo, mover o cursor para o início do arquivo e sobreescrever o número.

note
fprintf(file, "%d     ", ++i);

Por que inserir espaço após o número? Foi uma maneira de evitar que o número de ambos processos se misturem.

Por exemplo: Processo 1 escreve 5000 e processo 2 escreve 9, o arquivo irá conter "9000" pois o 9 foi escrito em cima do 5.

Agora só precisamos adicionar a lógica de criar processo vista anteriormente:

#include <stdio.h>
#include <unistd.h>

void code() {
int count = 0;
FILE* file = fopen("example.txt", "w+");

while(count < 10000) {
int i;

fseek(file, 0, SEEK_SET);
fscanf(file, "%d", &i);
fseek(file, 0, SEEK_SET);
fprintf(file, "%d ", ++i);

count++;
}

fclose(file);
}

int main() {
FILE* file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

Quando executei este código para 10 iterações, o valor final do arquivo foi 20.
Quando executei este código para 1000 iterações, o valor final do arquivo foi 1000.
Quando executei este código para 10000 iterações, o valor final do arquivo foi 10015.

O que somos capaz de deduzir com isto?

  • O resultado é imprevisível pois não temos controle de quando o escalonador vai trocar os processos
  • Dependendo do volume de iterações e da máquina do usuário, um processo pode ou não conseguir fazer a tarefa antes do escalonador trocar o processo
  • Se houver troca durante uma tarefa, pode corromper o resultado do arquivo

Quais as chances disto acontecer? Depende do software, pois existem arquivos que a chance de dois softwares interagirem ao mesmo tempo é 0%.

Locks

Acontece que existe mais de uma maneira de aplicar locks no Linux.

Uma pessoa bem surpresa

  • flock
    • file lock
    • Origem do sistema operacional BSD
  • lockf
    • lock file
    • POSIX
    • É uma versão simplificada do fcntl "Advisory record locking"
  • fcntl "Advisory record locking"
    • file control
    • POSIX
    • Uma função capaz de fazer diversas operações sobre file descriptors
      • Uma delas é utilizar "Advisory record locking"
  • fcntl "Open file description locks (non-POSIX)"
    • file control
    • Linux specific
      • Existem propostas para ser adicionado ao padrões POSIX
    • Uma função capaz de fazer diversas operações sobre file descriptors
      • Uma delas é utilizar "Open file description locks (non-POSIX)"

Se quiser saber mais sobre cada um, é bom ler o post no blog: https://gavv.net/articles/file-locks/
Incrivel como um blog de 8 anos atrás me ajudou mais do que pesquisas no Google.

flock

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

while (count < 10000) {
int i;

flock(fd, LOCK_EX);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
flock(fd, LOCK_UN);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

lockf

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

while (count < 10000) {
int i;

lockf(fd, F_LOCK, 0);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
lockf(fd, F_ULOCK, 0);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

fcntl - "Advisory record locking"

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 256

void code() {
int count = 0;
struct flock fl;
char *buffer = malloc(sizeof(char) * BUFFER_SIZE);
int fd = open("example.txt", O_RDWR);

fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;

while (count < 10000) {
int i;

fl.l_type = F_WRLCK;
fcntl(fd, F_SETLKW, &fl);
lseek(fd, 0, SEEK_SET);
read(fd, buffer, BUFFER_SIZE);
i = atoi(buffer) + 1;
sprintf(buffer, "%d ", i);
lseek(fd, 0, SEEK_SET);
write(fd, buffer, strlen(buffer));
fl.l_type = F_UNLCK;
fcntl(fd, F_SETLKW, &fl);

count++;
}

close(fd);
}

int main() {
FILE *file = fopen("example.txt", "w");
fputc('0', file);
fclose(file);

int pid = fork();

if (pid == -1) {
printf("Failed to create child process\n");
} else if (pid == 0) {
code();
printf("Child finished\n");
} else {
code();
printf("Parent finished\n");
}
}

References

Extra

Por diversão, eu experimentei replicar o mesmo problema em outras linguagens.

package main

import (
"fmt"
"os"
"os/exec"
)

func code() {
var count int = 0
file, _ := os.OpenFile("example.txt", os.O_RDWR, 0666)

for count < 10000 {
var i int

file.Seek(0, 0)
fmt.Fscanf(file, "%d", &i)
file.Seek(0, 0)
fmt.Fprintf(file, "%d ", i+1)

count++
}

file.Close()
}

func main() {
if len(os.Args) >= 2 {
code()
fmt.Println("Child finished")
} else {
os.WriteFile("example.txt", []byte{'0'}, 0600)

command := exec.Command("/usr/bin/go", "run", "main.go", "fork")
command.Start()
code()
command.Wait()

fmt.Println("Parent finished")
}
}

Thiago Lages de Alencar

My english is trash :v

Remember the old days where we would immerse ourselfs while reading books?

I don't... I suck at books.

Person laughing

But you know the feeling, right? We just have to change books for movies/games/sports...

Pretty sure that after the title and text above, you can guess everything that I'm going to write about (if you reflect deep enough, you don't even have to read this post...).

Storytelling

What do theaters, movie theaters and libraries have in common? When you make noise, people go "Shh".

Person making &quot;shhhh!&quot;

I'm joking... They are storytelling places.

Where you go to read, watch, see, or hear a predetermined story. The author tries to build a path for how you should feel in that storytelling (sometimes is not even close).

Video games are storytelling? If you put a story in it... Yes! They may not be the best storytelling because it's hard to control what players feel, but that's why people insert cinematics in games.

Cinematics are a way of telling a story without letting you ruin everything (I'm being dramatic). Just think about what stupid thing players could if you let them free in the cinematic (probably throw themselves from a high place... I don't know).

People love to test the boundaries of the games or try to find something that the author didn't count on.

Anyway, I'm getting off track. It's harder than movies and books because it's not just storytelling, but it's still possible to do a great job at it.

info

"It's not just storytelling"

Not saying that storytelling is easy, just that when you have to count everything that the player can do... Telling a good story becomes difficult.

Think about a great book or movie that somebody attempted to transform into a game... It's really hard to tell the same story. The developer may want them to feel like a great hero, but maybe they are pissed after dying 20 times to the same boss.

¯\_(ツ)_/¯

Immerse our selfs

Remember when I joked about places where people "Shh" you? I wasn't joking.

Person watching a monitor/tv

Is kind of obvious that people "Shh" in those places because it's really hard to immerse in something, but it's very easy to lose focus.

When I say "immerse", think about the situation where you are so focused on something that you ignore somebody calling your name.

Obviously, is important for storytelling games (except comedy, which is a free ticket to "f*ck it" town), but it's also important for competitive games! You don't have to immerse yourself in feelings but in the game mechanics (thinking about everything that others players can do).

That's probably why we get more angry or sad after losing a competitive game where we invest ourselfs to win. Over time, we learn to manage these feelings better, but it's still not a happy feeling.

note

"I don't get sad or angry"

Err... Not even fustrated? Are you sure that you were invested in the game? Maybe you just didn't care about the game.

Attention problem

Attention is a limited resource... We can't make 20 tasks at the same time and get the same quality of making one with all our attention (exceptions exist, but I don't think you are one of them).

Person being distracted
(It was supposed to be someone getting distracted and dying in the game...)

A classic example is your parents trying to talk to you while you are playing. In this cases, you attempt to multitask between playing and answering them (if you are like me, you are probably dead in the game).

In case of games/movies/books storytelling the same can happen... How can you get in the horror game mood of "I'm going to die, I want to run" if you have to stop to do something for your parents just before the scary moment?

note

Stop blaming the player, is also devs fault.

You want me to blame developers? Okay, it's also the developers fault for not making great storytelling that locks you into the screen... But this post is more about our problem, I can't just start badmouthing developers now (this post would get BIG and I don't have time to complain about everyone (I actually have... I'm unemployed... Sad...)).

Streamers

Streamer playing

"Making games for streamers seems hard as f***"

How do you capture the attention of someone who may stop to read others people messages and thinking about an answer to them?

How do you reproduce into another the feeling that you had when children where you would play in front of TV without noticing your parents calling you?

How do you tell about the struggles of your character if the other person is busy reflecting their life to other people?

Streamers are not "just" players, they are entertainment too! This means that they need to share their attention with their viewers.

  • Look at the chat to check if the stream didn't go offline
  • Interact with the chat to show that you cares about them
  • Thanks the subscrpitions and donations
  • Express what is on their mind while playing because the viewers want to know

I'm not here to blame anyone, just pointing the difficulties.

Compared to other options for playing games:

  • Playing the game alone in the room
    • You don't have to say a word
  • Recording a video for Youtube
    • People normally want to know what is on your mind
    • I didn't reflect enough on this so I don't have another point... Sorry Youtubers

Conclusion

I don't have any...

I don't know how developers attack this... They invest less in long immersive moments? They do more short content? They always assume that the player is a streamer? I mean, you can't just ignore the fact that a streamer could badmouth your game (this can affect sales, right?).

note

It's always good to see that streamers know that this is something that can happen.

https://youtu.be/5LtQAcGxQlM?t=193

Thiago Lages de Alencar
warning

Este post parte do princípio que o leitor viu o post anterior!

Além disto este post for reescrito pois o formato não estava do meu agrado (igual ao post anterior).

O repositório com exemplos continua vivo em:
https://github.com/thiagola92/learning-authentication

A lógica básica é a mesma que o post anterior, porém usando o email no lugar do username.

UI de login com email e password

O que temos de extra é que podemos utilizar o email do usuário para nos dar mais confiança que a pessoa acessando é o dono da conta.

Register

Client

Segue exatamente o mesmo passo a passo do post anterior (utilizando o email em vez de usuário).

Server

Segue exatamente o mesmo passo a passo do post anterior (utilizando o email em vez de usuário) porém não cadastramos o usuário ao final!

Carta representando um email

O motivo é bem simples: O email fornecido é realmente da pessoa que cadastrou a conta?

Imagina se nós terminassemos o cadastro acreditando naquele email... Dia seguinte aquele email poderia receber emails nosso, achar super esquisito e nos marcar como spam. Se isso acontecesse muito o Google iria acabar marcando nosso email corporativo como spam!

Para resolver esse problema basta cobrarmos uma prova que a pessoa é a dona do email. Podemos fazer isso enviando um email para ela e esperando que ela nos diga algo que possue no email.

note

Partindo do princípio que a pessoa dona do email é a única que deveria ter acesso ao email.

O modo clássico é gerar um código no servidor e enviar para o email da pessoa este código, agora ela precisa acessar o email e nos dizer o código para provar que ela conseguiu o código certo.

Visão geral do processo

Email é bom pois nós não precisamos ser dependentes de terceiros para utilizar (Gmail/Outlook/etc).

import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["Subject"] = "Confirmation code"
message["From"] = "noreply@yourwebsite.com"
message["To"] = "user@example.com"
message.set_content("Your confirmation code is 123")

# I'm running my own email server for tests
with smtplib.SMTP("localhost", 8025) as s:
s.send_message(message)
info

Utilizei a biblioteca aiosmtpd para criar o server de test:
python -m aiosmtpd -n

Se tudo foi feito corretamente, todo email novo deve aparecer lá no formato:

---------- MESSAGE FOLLOWS ----------
Subject: Confirmation code
From: noreply@yourwebsite.com
To: user@example.com
Content: Your confirmation code is 123
X-Peer: ('127.0.0.1', 52020)

------------ END MESSAGE ------------

Porém o exemplo acima é inseguro, pois não utiliza a SLL (que ajuda a criar uma camada de segurança entre nós e o serviço). Hoje em dia isso é obrigatório para interagir com quase todos os serviços de email (eles não vão aceitar emails sem essa segurança).

import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["Subject"] = "Confirmation code"
message["From"] = "noreply@yourwebsite.com"
message["To"] = "user@example.com"
message.set_content("Your confirmation code is 123")

with smtplib.SMTP("localhost", 8024) as s:
s.starttls()
s.send_message(message)
info

Levantei outro server de email e nele estou utilizando TLS.

Criei meu certificado auto-assinado:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'

E utilizei ele durante a inicialização do server:
python -m aiosmtpd -n --tlscert cert.pem --tlskey key.pem -l localhost:8024

Então é isso? Podemos mandar email para provedores como Gmail? Nope.

Rosto chorando

Deve ser bem óbvio que embora este código funcione localmente, ele jamais passaria por qualquer segurança do nível de aplicação.

Ele possui TLS para proteger contra man-in-the-middle mas como que o provedor vai saber que nós somos os donos daquele email? Se nós botarmos noreply@gmail.com, nós temos que provar que temos acesso à aquela conta no Gmail.

A maneira clássica é logar na conta.

import smtplib
from email.message import EmailMessage

# Removed "From" because providers will use the email used in s.login()
message = EmailMessage()
message["Subject"] = "Confirmation code"
message["To"] = "user@example.com"
message.set_content("Your confirmation code is 123")

with smtplib.SMTP("localhost", 8024) as s:
s.starttls()
s.login("noreply@gmail.com", "password")
s.send_message(message)

Este código não vai funcionar!

Hoje em dia já sabemos que usuário e senha sozinhos não providênciam uma segurança forte durante a autenticação, por isso que Google nos cobra outros métodos de MFA para provar que nós somos nós mesmos durante os logins (telefone, passkeys, authenticators, ...).

Google requer que você explicitamente adicione uma senha para aquele "app" e mesmo assim ele não recomenda fazer isto!
Link para a resposta no StackOverflow

Bem, vamos parar por aqui pois eu apenas tenho interesse em ver o conceito de segurança com o email (não quero ensinar a se autenticar em diversos provedores com email).

Client 2

Existe duas opções aqui, a primeira é o server ter armazenado numa tabela temporária seu email, código enviado por email e hash da senha.

emailcodehash
thiagola92@email.com48935e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
darklord@email.com38922d2c3f7eb9152d67258cd1068a64a746c130d4cca3f571bd28a86d7f7589aa25
juninho@email.com0283b7e94be513e96e8c45cd23d162275e5a12ebde9100a425c4ebcdd7fa4dcd897c

Neste caso basta o usuário enviar o código para o server validar a conta.

import httpx
from urllib.parse import urlencode

body = urlencode({"code": code})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/register/code", headers=headers, content=body)
note

Embora eu tenha feito como um requisição POST, não existe nenhuma obrigação de ser assim.

Uma maneira mais familiar é o usuário receber uma URL com o código/token para o usuário acessar e isto seria o suficiente para confirmar.

http://127.0.0.1:8000/register/{code}

A outra é ele não ter armazenado o hash e só esperar que você envie a senha novamente.

emailcode
thiagola92@email.com4893
darklord@email.com3892
juninho@email.com0283
import httpx
from urllib.parse import urlencode

body = urlencode({"email": email, "password": password, "code": code})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/register/code", headers=headers, content=body)
note

Em ambas as tabelas é normal de ter a data de criação do código, para que ele não fique válido para sempre (não queremos que ninguém chute todas as opções de código).

Server 2

Segue exatamente o mesmo passo a passo do post anterior (utilizando o email em vez de usuário).

Recovery

Client

A grande vantagem de ter email é adicionar um ponto de recuperação de senha.

Usuário se perguntando qual a senha

Dado que o usuário esqueceu a senha, ele só precisa requisitar o resete de senha da conta relacionada a aquele email.

import httpx
from urllib.parse import urlencode

body = urlencode({"email": email})
headers = {"Content-Type": "application/x-www-form-urlencoded"}

httpx.post("http://127.0.0.1:8000/recovery", headers=headers, content=body)

Como estamos falando do caso em que o usuário não lembra a senha, não podemos cobrar nenhuma autenticação... Em outras palavras, qualquer pessoa má intencionada pode ficar requisitando e resta ao server tomar cuidados para elas não conseguirem acesso.

Server

É a mesma ideia do login, queremos confirmar que é o dono da conta então mandamos para o email dele um código/token para ele utilizar na alteração da senha atual dele.

Lembrando que é importante botarmos tempo para o usuário fazer essa alteração e limite de tentativas, pois não queremos que usuários má intencionados usem força bruta para descobrir o código/token.

Client 2

Basicamente idêntico ao registrar, porém temos que fornecer a nova senha. Isto pode ser feito tudo em uma requisição ou em duas (acessar o URL com token e inserir a nova senha em um form).

Server 2

Valide o código/token!

Carimbo

Cada tentativa errada é um sinal de perigo, por isso é importante lembrar:

  • Invalidar o código/token depois de certo tempo
    • Por isto alguns tokens são grandes, pois nunca é possível chutar todas as possibilidades antes do tempo esgotar
  • Contar o número de tentativas de acertar o código/token para um email
    • Importante invalidar o código depois de certo número de tentativas, não queremos X máquinas tentando acertar o código

References