Programação Por Equipe DevBras

Capa do artigo: .NET no Raspberry Pi: publicar para ARM e manter o app de pé como serviço

.NET no Raspberry Pi: publicar para ARM e manter o app de pé como serviço

Descubra a arquitetura do Pi, publique com o RID certo, escolha entre runtime instalado ou app autocontido e rode como serviço systemd que volta depois do reboot.

Você publica a API do sensor no notebook, copia a pasta para o Raspberry Pi, roda ./SensorApi e recebe cannot execute binary file: Exec format error. Não é permissão, não é cartão corrompido. É arquitetura. A resposta curta: descubra se o sistema do Pi é ARM de 64 ou 32 bits, publique com o RID correspondente (linux-arm64 ou linux-arm), copie a pasta inteira e rode o app como serviço do systemd, numa porta alta, com log que não enche o cartão.

O executável gerado no PC foi compilado para x64, a língua do processador do notebook. O Pi fala ARM. O código C# é o mesmo, mas o arquivo que inicia o processo e o runtime por baixo dele precisam ser da arquitetura do aparelho.

Por que o binário do PC não roda no Pi

Um app .NET publicado tem duas partes. Os arquivos .dll com o seu código em IL são portáveis. Mas o executável que você chama na linha de comando (o apphost) e o runtime que interpreta e compila esse IL são nativos, feitos para uma arquitetura e um sistema operacional específicos. Esse par é o que o .NET chama de RID, o identificador de runtime.

Quando você publica sem dizer o RID, ou com o RID do PC, o apphost é x64. No Pi, o kernel olha o cabeçalho do arquivo, não reconhece a arquitetura e devolve o "Exec format error". Copiar o runtime x64 também não adianta, pelo mesmo motivo.

Há ainda uma restrição de hardware: o .NET exige ARMv7 ou superior. Raspberry Pi 1 e Pi Zero da primeira geração são ARMv6 e não rodam .NET moderno. Pi 3, Pi 4, Pi 5 e Pi Zero 2 W rodam.

Descobrir a arquitetura certa no aparelho

Rode dois comandos no Pi. O primeiro mostra a arquitetura do kernel, o segundo a dos pacotes do sistema, que é a que importa para o seu binário.

$ uname -m
aarch64

$ dpkg --print-architecture
arm64

Se o segundo comando responde arm64, publique para linux-arm64. Se responde armhf, o sistema é 32 bits e o RID é linux-arm, mesmo que o uname -m diga aarch64. Isso acontece em instalações com kernel de 64 bits e sistema de 32 bits, e é a armadilha que faz alguém publicar para arm64 e tomar o mesmo erro. Em projeto novo, prefira o Raspberry Pi OS de 64 bits.

Publicar: runtime instalado ou app autocontido

Há dois caminhos, e os dois funcionam. O que não funciona é misturar: app dependente de framework publicado para Windows rodando num Linux ARM.

Autocontido: o runtime vai junto

dotnet publish -c Release -r linux-arm64 --self-contained true -o ./publish

A pasta publish leva o runtime inteiro. Fica maior, algumas dezenas de megabytes a mais, mas o Pi não precisa ter .NET instalado e a versão do runtime é a que você testou. Para um aparelho único em campo, é o caminho mais previsível.

Dependente de framework: o Pi tem o runtime

# no Pi, instala o runtime do ASP.NET Core numa pasta do sistema
curl -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh
chmod +x dotnet-install.sh
sudo ./dotnet-install.sh --channel 10.0 --runtime aspnetcore --install-dir /opt/dotnet

# no PC
dotnet publish -c Release -r linux-arm64 --self-contained false -o ./publish

A publicação fica pequena e várias aplicações compartilham o mesmo runtime. Em troca, a versão maior do runtime instalado precisa atender a do projeto: um app feito para net10.0 não sobe num Pi que só tem o runtime 8, e a mensagem pede para instalar ou atualizar o .NET. Como o runtime ficou em /opt/dotnet, o processo precisa da variável DOTNET_ROOT apontando para lá, o que o serviço abaixo já faz.

Copiar e rodar como serviço

O caso de exemplo é a SensorApi, que lê um sensor de temperatura e expõe a leitura por HTTP para o painel da oficina. Ela precisa voltar sozinha depois de queda de energia, e não pode depender de um terminal aberto.

  1. Crie um usuário de sistema para o serviço, sem login: sudo useradd --system --no-create-home sensor.
  2. Copie a pasta publicada inteira: scp -r ./publish/* pi@oficina-pi.local:/tmp/sensor-api/, depois mova para /opt/sensor-api e ajuste o dono com sudo chown -R sensor:sensor /opt/sensor-api.
  3. Garanta a permissão de execução do apphost: sudo chmod +x /opt/sensor-api/SensorApi.
  4. Teste uma vez à mão: sudo -u sensor /opt/sensor-api/SensorApi.
  5. Crie a unidade do systemd e habilite.
# /etc/systemd/system/sensor-api.service
[Unit]
Description=API do sensor da oficina
After=network-online.target
Wants=network-online.target

[Service]
User=sensor
WorkingDirectory=/opt/sensor-api
ExecStart=/opt/sensor-api/SensorApi
Restart=always
RestartSec=5
Environment=ASPNETCORE_URLS=http://0.0.0.0:5000
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_ROOT=/opt/dotnet

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now sensor-api
systemctl status sensor-api
journalctl -u sensor-api -f

No app autocontido, a linha do DOTNET_ROOT é desnecessária, mas não atrapalha. Ao parar o serviço, o systemd manda SIGTERM, e o ASP.NET Core usa esse sinal para encerrar com calma; o porquê disso importar está em SIGTERM termina o pedido. Se quiser que o systemd saiba exatamente quando o app terminou de subir, o pacote Microsoft.Extensions.Hosting.Systemd integra o host com Type=notify.

Porta alta e proxy na frente

Portas abaixo de 1024 exigem privilégio no Linux. Rodar a API como root para escutar na 80 resolve o sintoma e cria um problema maior. Deixe a aplicação na 5000 e, se precisar da 80 ou da 443, coloque um Nginx ou Caddy na frente. O proxy cuida do TLS e a aplicação continua sem privilégio.

O cartão SD é o ponto fraco

Cartão SD aguenta mal escrita contínua e queda de energia no meio de uma gravação. Um app que registra cada leitura do sensor em log de texto, uma por segundo, enche e desgasta o cartão em meses. Três cuidados resolvem a maior parte:

  • Mande o log para o journald (a saída padrão já vai para lá com systemd) e limite o tamanho em /etc/systemd/journald.conf com SystemMaxUse=100M.
  • Reduza o nível de log em produção para Warning nas categorias barulhentas, pelo appsettings.Production.json.
  • Se o aparelho grava dados importantes, use SSD por USB ou envie os dados para fora com frequência. O cartão é o primeiro a falhar.

Se você prefere containers, dá para gerar a imagem para ARM no próprio PC com docker buildx build --platform linux/arm64. A regra é a mesma: a imagem precisa ser da arquitetura do aparelho. O começo desse caminho está em Docker no servidor: primeiro passo.

Erros comuns

  • Publicar sem -r: o apphost sai com a arquitetura do PC e o Pi responde "Exec format error".
  • Confiar só no uname -m: com sistema de 32 bits sobre kernel de 64, você publica para arm64 e o binário não roda.
  • Copiar só o executável: as .dll e o .runtimeconfig.json ficam para trás e o app falha ao iniciar.
  • Runtime de versão maior diferente da do projeto: o app dependente de framework se recusa a subir.
  • Rodar como root para usar a porta 80: qualquer falha na aplicação vira controle total do aparelho.
  • App rodando num terminal SSH aberto: fecha a sessão ou falta luz, e a API não volta.

Checklist

  • dpkg --print-architecture foi conferido no Pi.
  • O RID da publicação bate com a arquitetura do sistema.
  • A pasta publicada foi copiada inteira e o apphost tem permissão de execução.
  • O app roda com um usuário de sistema, não com root nem com o usuário pi.
  • Existe uma unidade systemd com Restart=always e enable feito.
  • A aplicação escuta numa porta acima de 1024.
  • O journald tem limite de tamanho configurado.
  • Depois de sudo reboot, a API respondeu sem ninguém tocar no aparelho.

Rode sudo reboot no seu Pi agora e espere dois minutos. A aplicação voltou sozinha, e o RID com que ela foi publicada é o que o dpkg --print-architecture mostra?

Avalie este artigo

Referência: https://edi.wang/post/2019/9/29/setup-net-core-30-runtime-and-sdk-on-raspberry-pi-4