Pular para o conteúdo
Development

Controlando um iPhone bloqueado a partir de uma sessão de código

Por Victor Da Luz•
iosmusickitautomationdev-logdeep-cut-atlas

Venho construindo o Deep Cut Atlas, um app iOS que depende do MusicKit, o framework da Apple para conversar com o Apple Music. O simulador simplesmente não consegue tocar no MusicKit, ele lança erro ou vira um no-op silencioso dependendo da chamada. Toda checagem real de uma funcionalidade do MusicKit exigia um telefone físico, e se eu não estivesse do lado dele, o trabalho travava até que eu estivesse.

A solução a que cheguei nessa sessão: entregar um iPhone 13 sobressalente para um agente de código em tempo integral, para que ele possa tocar, capturar tela, e verificar contra o dispositivo real sem eu estar na sala.

O caminho que eu não segui

O caminho óbvio é o WebDriverAgent, o mesmo driver privado baseado em XCTest sobre o qual o Appium e a maioria das ferramentas de automação mobile são construídas. Eu de fato comecei a seguir esse caminho há algumas sessões, o go-ios para o túnel, um clone raso do WDA, o mobile-mcp conectado como um servidor MCP. Aí parei para pensar no que eu estava realmente me metendo.

O WDA precisa de um build assinado que pode expirar, um processo de túnel, um encaminhamento de porta, e (segundo as próprias issues do GitHub dele) ele vaza memória e morre em sessões longas. Nada disso é um impeditivo por si só. O que matou a ideia foi o paradoxo do bloqueio: para manter o telefone acessível, eu precisaria deixar o Auto-Lock em Never, o que significa um iPhone sempre ligado e sempre desbloqueado sentado numa mesa. Isso é um buraco de segurança real só para viabilizar automação de teste.

O que usei em vez disso

A Apple já vem com algo que resolve o problema exatamente oposto: o iPhone Mirroring deixa você controlar um iPhone a partir de um Mac enquanto o próprio telefone continua bloqueado. O truque foi achar um jeito de mandar toques e capturas de tela para dentro dessa janela espelhada de forma programática. O mirroir-mcp faz exatamente isso: ele controla a janela do iPhone Mirroring com CGEvent (a API de entrada de baixo nível do macOS) e lê a tela com OCR do Vision. Sem WebDriverAgent, sem assinatura de código, sem túnel. O devicectl continua cuidando de instalação, execução e logs, sem mudanças.

A configuração de duas contas é a outra metade disso. O espelhamento exige que a conta iCloud do telefone seja igual à do Mac, mas eu não queria que dados de teste (adições à biblioteca, playlists, histórico de reprodução) fossem parar na minha conta real do Apple Music. Então o iCloud do telefone é minha conta principal (para o pareamento do espelhamento), enquanto Media & Purchases está trocado para uma conta secundária da família. O próprio Apple Music segue o Media & Purchases, então qualquer coisa que o app escreve durante uma sessão de teste vai para uma biblioteca isolada.

Cinco testes de portão, em ordem

Antes de confiar nisso tudo para trabalho remoto de verdade, escrevi cinco testes de portão, e disse a mim mesmo para parar e reavaliar se algum deles falhasse:

  • G1 - a separação de contas realmente isola os dados do Apple Music. Passou, com um pequeno problema de incompatibilidade de região no caminho que precisou de correção.
  • G2 - devicectl install e a execução funcionam num telefone bloqueado com o espelhamento ativo. Essa era a única real incógnita ao começar. Passou, e também corrigiu meu modelo mental: o espelhamento não mostra a tela de bloqueio do telefone, ele mostra o app ao vivo, totalmente interativo, enquanto o dispositivo físico continua apagado. Esse é o comportamento esperado, não um bug.
  • G3 - reconectar depois de um timeout por inatividade, sem intervenção manual. A sessão de espelhamento já tinha ficado inativa quando sentei para testar isso. open -a "iPhone Mirroring" trouxe ela de volta sem ninguém tocar no telefone.
  • G4 - acessível por horas sem ninguém por perto. Decidi pular um teste dedicado de várias horas para esse. Se for realmente um problema, vai aparecer durante uma sessão remota de verdade e eu corrijo nessa hora.
  • G5 - o loop completo contra o app real: capturar tela, ler a tela, tocar, verificar. Isso funcionou de forma limpa, navegando entre abas e para dentro de uma sheet de detalhe, com o telefone bloqueado o tempo todo.

Duas coisas que me surpreenderam

A primeira foi uma pista falsa no meu próprio plano. Eu tinha escrito cursor_mode: "preserving" nas notas de configuração como um ajuste global para impedir que o cursor do mouse ficasse pulando de um lado para o outro durante a automação. Isso não existe como um ajuste global. Na verdade é um parâmetro da chamada individual de toque, e mesmo assim só restaura onde o ponteiro do mouse fica depois. Não faz nada em relação ao problema mais profundo.

Esse problema mais profundo é a segunda surpresa, e descobri isso da pior forma: no meio de uma sessão, um toque arrancou o foco do que eu estava fazendo. O próprio FAQ do mirroir-mcp diz isso: o macOS só roteia entrada para o app em primeiro plano, então todo toque precisa trazer o iPhone Mirroring para frente primeiro. Não existe API para mandar eventos para uma janela que não está em foco. A solução alternativa documentada é dar ao iPhone Mirroring seu próprio Space do macOS, o que mantém a posição do seu cursor e a seleção de texto intactas no Space em que você estava de fato trabalhando. Configurei isso e tentei de novo. Parou de perder meu lugar, mas ainda me arrastava para aquele Space toda vez que um toque disparava. Melhor, não resolvido. Registrei um spike de continuação para ver se há um jeito de evitar completamente essa troca visível.

Também, um detalhe menor mas do tipo que custa vinte minutos se você não percebe: npx -y mirroir-mcp mirroir doctor parece que funciona. Ele sai de forma limpa, imprime alguns logs de inicialização, e não dá nenhum erro. Também nunca roda as checagens de fato do doctor, porque o npx resolve para o binário padrão do pacote (o próprio servidor MCP) em vez da ferramenta de linha de comando mirroir empacotada junto. A correção é npx -y -p mirroir-mcp -- mirroir doctor, que diz explicitamente ao npx qual binário dentro do pacote rodar.

Onde isso parou

Quatro dos cinco portões passaram com evidência real, e tomei a decisão deliberada de pular o quinto em vez de queimar horas provando algo que eu vou perceber de qualquer jeito se quebrar. O fluxo de reconexão é um script de shell de cinco linhas que rodo manualmente antes de uma sessão, de propósito, não um item de login nem um watchdog. Já me queimei antes construindo infraestrutura de recuperação para falhas que eu ainda nem tinha observado de fato, e não quis repetir isso aqui.

O resultado é que agora posso entregar ao agente um telefone bloqueado e uma sessão de código, e ele pode ir checar se um fluxo do MusicKit realmente funciona, sem esperar eu ficar livre.

Leitura relacionada

Você também pode achar útil

RackNerd

RackNerd VPS

Hospedagem VPS econômica para serviços leves que funcionam continuamente.

Como afiliado da RackNerd, ganho com compras qualificadas.

Saiba mais
AdGuard

AdGuard para iOS

Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.

Como afiliado da AdGuard, ganho com compras qualificadas.

Saiba mais
Proton

Proton VPN

VPN comercial com filtragem NetShield e interruptor de desligamento automático.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais