From protheus-toolkit
Diagnostico e debugging de erros no Protheus (ADVPL/TLPP). Use quando o usuario pedir para debugar, diagnosticar, corrigir erros, analisar logs, stack traces, erros de compilacao, runtime, performance lenta, ou locks no Protheus.
How this skill is triggered — by the user, by Claude, or both
Slash command
/protheus-toolkit:advpl-debuggingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Skill prescritiva para diagnostico sistematico de erros em ADVPL/TLPP.
Skill prescritiva para diagnostico sistematico de erros em ADVPL/TLPP. Cobre erros de compilacao, runtime, performance, locks e analise de logs.
1. COLETAR → Obter mensagem de erro exata, stack trace, log do AppServer
2. CLASSIFICAR → Compilacao? Runtime? Performance? Lock? Integracao?
3. LOCALIZAR → Identificar arquivo, funcao e linha do erro
4. REPRODUZIR → Isolar cenario minimo que reproduz o problema
5. CORRIGIR → Aplicar fix, validar, documentar causa raiz
| Item | Onde encontrar |
|---|---|
| Mensagem de erro exata | Tela do SmartClient, console.log |
| Stack trace | console.log do AppServer, ErrorLog |
| Versao do AppServer | console.log (cabecalho) |
| RPO compilado? | SIGACFG > Compilar / TDS |
| Ultimo deploy | Verificar data do RPO |
| Ambiente (dev/hom/prod) | Perguntar ao usuario |
| Multiplos usuarios afetados? | Indica lock ou concorrencia |
Use esta tabela para diagnostico rapido antes de investigar a fundo.
| Sintoma | Causa provavel | Primeiro check |
|---|---|---|
| "Variable does not exist: CXXX" | Falta Local/Private/Public | Verificar declaracao da variavel na funcao |
| "Array index out of bounds" | AScan retornou 0, query vazia | Verificar If nPos > 0 antes de acessar array |
| "Type mismatch on +/-/==" | Leitura apos Eof(), campo NIL | Verificar If !(cAlias)->(Eof()) antes de ler |
| "THREAD ERROR" | Private nao declarada em Job | Declarar Private na funcao que chama, nao no caller |
| "WorkArea already in use" | Alias hardcoded ou leak | Usar GetNextAlias(), fechar aliases |
| "RecLock timeout" | Registro bloqueado por outro usuario/processo | Verificar quem tem o lock (ver secao 7) |
| "TOPCONN error" | Conexao com banco perdida | Verificar TCIsConnected(), reconectar |
| "Function not found: U_XXX" | Funcao nao compilada no RPO | Compilar o fonte no TDS/SIGACFG |
| "Error on OPEN TABLE" | Tabela inexistente ou corrompida | Verificar RetSqlName(), existencia no banco |
| Tela congela / "nao responde" | Loop infinito, falta DbSkip() | Verificar se todo While !Eof() tem DbSkip() |
| Dados errados / campo em branco | Leitura apos Eof() ou indice errado | Verificar DbSetOrder() e retorno do MsSeek |
| "Invalid column name" | Campo nao existe na tabela SQL | Verificar nome via RetSqlName() + SX3 |
| Job nao executa | Erro silencioso no RpcSetEnv | Verificar console.log do AppServer |
| Job processa duplicado | Sem LockByName | Adicionar controle de concorrencia |
| Lentidao progressiva | Memory leak de aliases abertos | Verificar DbCloseArea() em todos os caminhos |
| "Character length exceeds 1024K" | String crescendo em loop sem limite | Verificar concatenacao em loop |
| Erro de compilacao | Causa | Correcao |
|---|---|---|
Syntax error at 'xxx' | Palavra reservada como variavel, falta operador | Renomear variavel ou adicionar operador |
Undeclared variable 'xxx' | Variavel nao declarada | Adicionar Local cXxx := "" |
Function 'xxx' already defined | Funcao duplicada no RPO | Remover duplicata ou renomear |
#include file not found | .ch nao encontrado no includes do projeto | Verificar path no TDS, adicionar ao Includes |
Unbalanced BEGIN/END SEQUENCE | Falta End Sequence ou Recover | Verificar pareamento de blocos |
Missing EndIf/EndDo/Next | Bloco nao fechado | Verificar indentacao e fechar bloco |
Quando o compilador mostra MUITOS erros, corrija apenas o PRIMEIRO. Erros subsequentes frequentemente sao consequencia do primeiro (efeito cascata).
Causa raiz: Variavel usada sem declaracao Local/Private/Public.
// ERRADO — cStatus nunca foi declarado
User Function MinhaFunc()
cStatus := "A" // Runtime error: Variable does not exist
// CORRETO
User Function MinhaFunc()
Local cStatus := "A"
Em Jobs: Private deve ser declarada na mesma funcao ou na funcao que chama diretamente. Nao herda de funcoes anteriores da pilha.
// ERRADO — Private declarada no Entry, usada no Worker via MSExecAuto
User Function MeuJob()
Private lMsErroAuto := .F. // OK aqui
WORKER()
Static Function WORKER()
// lMsErroAuto existe aqui (herdada)
// MAS se MSExecAuto chamar sub-funcao que acessa, pode falhar
Causas comuns:
AScan retornou 0 e o resultado foi usado direto como indiceLen(aArray)// ERRADO
nPos := AScan(aCampos, {|x| x[1] == "XXX"})
cValor := aCampos[nPos][2] // Se nPos = 0, CRASH!
// CORRETO
nPos := AScan(aCampos, {|x| x[1] == "XXX"})
If nPos > 0
cValor := aCampos[nPos][2]
EndIf
Causa raiz: Operacao entre tipos incompativeis (ex: string + numerico).
// ERRADO — apos Eof(), campos retornam NIL em alias de query
(cAlias)->(DbSkip()) // Avancou para Eof
cNome := (cAlias)->A1_NOME // NIL!
cMsg := "Cliente: " + cNome // Type mismatch: string + NIL
// CORRETO — sempre checar Eof() ANTES de ler
While !(cAlias)->(Eof())
cNome := (cAlias)->A1_NOME
(cAlias)->(DbSkip())
EndDo
Outra causa comum: Val() em campo que pode conter letras.
// PERIGOSO — se cCampo tiver espacos ou letras
nVal := Val(cCampo) // Pode retornar 0 inesperadamente
// SEGURO — limpar antes
nVal := Val(AllTrim(cCampo))
Causa: Variavel Private acessada em thread diferente da que declarou.
// ERRADO — Private em thread principal, Worker em outra thread via FWIPCWait
Private lMsErroAuto := .F.
oIPC:Start("U_WORKER") // Worker roda em thread separada
// CORRETO — declarar Private DENTRO do Worker
User Function WORKER(aDados)
Private lMsErroAuto := .F.
Private lMsHelpAuto := .T.
Private lAutoErrNoFile := .T.
// ...
Local cErrorLog := ""
Local bOldError := ErrorBlock({|oError| ;
cErrorLog := oError:Description + CRLF + oError:ErrorStack, ;
Break(oError)})
Begin Sequence
// Codigo que pode gerar erro fatal
Recover
ConOut("ERRO FATAL: " + cErrorLog)
FwLogMsg("ERROR", , "MINHA_FUNC", FunName(), "", "01", ;
"Erro fatal: " + cErrorLog, 0, 0, {})
End Sequence
ErrorBlock(bOldError) // SEMPRE restaurar!
| Propriedade | Tipo | Descricao |
|---|---|---|
oError:Description | Character | Mensagem do erro |
oError:ErrorStack | Character | Stack trace completo |
oError:GenCode | Numeric | Codigo generico do erro |
oError:SubCode | Numeric | Subcoidgo do erro |
oError:SubSystem | Character | Subsistema que gerou o erro |
oError:OsCode | Numeric | Codigo de erro do SO |
oError:FileName | Character | Arquivo relacionado |
oError:Args | Array | Argumentos que causaram o erro |
// ERRADO — ErrorBlock que engole o erro silenciosamente
ErrorBlock({|e| Break(e)}) // Sem log, sem stack trace!
// ERRADO — ErrorBlock sem restaurar o anterior
ErrorBlock({|e| cErr := e:Description, Break(e)})
// ... codigo ...
// Esqueceu de restaurar! Proximo erro sera capturado aqui
// ERRADO — Begin Sequence vazio (engole erro)
Begin Sequence
// codigo perigoso
Recover
// NADA aqui — erro silencioso
End Sequence
// CORRETO — sempre logar e restaurar
Local bOld := ErrorBlock({|e| cErr := e:Description + CRLF + e:ErrorStack, Break(e)})
Begin Sequence
// codigo perigoso
Recover
FwLogMsg("ERROR", , cSource, FunName(), "", "01", cErr, 0, 0, {})
End Sequence
ErrorBlock(bOld)
| Fonte | Onde encontrar | Conteudo |
|---|---|---|
| Console.log | Diretorio do AppServer (rootpath) | Erros fatais, THREAD ERROR, stack traces |
| FwLogMsg | Console.log + FwMonitor | Log estruturado (INFO/WARNING/ERROR) |
| ConOut | Console.log | Output manual do desenvolvedor |
| ErrorLog | \system\ no rootpath | Erros nao tratados |
| DBAccess log | Configuracao do DBAccess | Queries SQL executadas |
// Formato tipico de erro no console.log:
[ERROR][2024/01/15 14:32:05] THREAD ERROR : Variable does not exist CMYVARIABLE
Called from U_MYFUNC(123) in source MYFUNC.PRW
Called from PROC_MYJOB(45) in source MYJOB.PRW
Called from U_MYJOB(28) in source MYJOB.PRW
Como ler:
// Gerar log estruturado
FwLogMsg(cSeverity, cTransactionId, cSource, cFunction, cOperation, cCode, cMessage, nP1, nP2, aExtras)
// Exemplo de saida no console.log:
// [INFO] [U_MEUJOB] [WORKER_MEUJOB] [] [01] Iniciando processamento.
// [ERROR] [U_MEUJOB] [WORKER_MEUJOB] [MATA410] [01] Erro ao gerar Pedido. NF: 001234 Cliente: 000001
Para buscar erros no log:
// Buscar por severidade:
grep "ERROR" console.log | grep "U_MEUJOB"
// Buscar por periodo:
grep "2024/01/15 14:3" console.log
// Debug temporario — REMOVER antes de deploy
ConOut(">>> DEBUG: cStatus = " + cValToChar(cStatus))
ConOut(">>> DEBUG: nRecno = " + cValToChar(nRecno))
ConOut(">>> DEBUG: Len(aItens) = " + cValToChar(Len(aItens)))
// Padrao para debug de query
ConOut(">>> QUERY: " + cQuery)
IMPORTANTE: Remover todos os ConOut de debug antes de enviar para producao.
Usar FwLogMsg para logs permanentes.
| Tipo | Mecanismo | Causa comum |
|---|---|---|
| RecLock timeout | RecLock("XXX", .F.) | Outro usuario/processo editando o registro |
| LockByName | LockByName(cName) | Job ja em execucao |
| SM0 lock | Tabela SM0 bloqueada | Operacao em empresa/filial em andamento |
| Deadlock SQL | Duas transacoes se bloqueiam | BEGIN TRANSACTION com tabelas cruzadas |
// 1. Identificar QUEM tem o lock
// No SQL Server:
cQuery := " SELECT request_session_id, resource_type, resource_description "
cQuery += " FROM sys.dm_tran_locks "
cQuery += " WHERE resource_database_id = DB_ID() "
// 2. Verificar se e processo ativo ou abandonado
// Locks abandonados geralmente sao de processos que crasharam
// 3. Solucao temporaria: matar sessao bloqueadora (com cautela!)
// Solucao definitiva: corrigir o codigo que nao libera o lock
// Se Job reporta "ja em execucao" mas nao esta rodando:
// Causa: UnLockByName nao foi chamado (crash anterior)
// Verificar no console.log se houve crash
// Se confirmado, liberar manualmente:
UnLockByName(cLockName, .F., .F., .T.)
// ERRADO — ordem de lock inconsistente entre funcoes
// Funcao A: lock SA1 -> lock SA2
// Funcao B: lock SA2 -> lock SA1 // DEADLOCK!
// CORRETO — sempre mesma ordem de lock
// Funcao A: lock SA1 -> lock SA2
// Funcao B: lock SA1 -> lock SA2 // Mesma ordem!
// Regra geral: lockar tabelas em ordem alfabetica
| Sintoma | Causa provavel | Diagnostico |
|---|---|---|
| Tela demora para abrir | Query sem indice / SELECT * | Verificar query no DBAccess log |
| Job leva horas | Loop N+1, query por registro | Refatorar para query unica |
| Servidor lento para todos | Memory leak / alias leak | Verificar aliases abertos |
| Query especifica lenta | Falta de indice SQL | Analisar plano de execucao |
// 1. Capturar tempo de execucao
Local nStart := Seconds()
MpSysOpenQuery(ChangeQuery(cQuery), cAlias)
Local nElapsed := Seconds() - nStart
ConOut(">>> Query executou em " + cValToChar(nElapsed) + " segundos")
// 2. Se > 2 segundos, verificar:
// a) SELECT * ? Trocar por campos explicitos
// b) Falta indice no WHERE? Verificar SIX
// c) Tabela muito grande sem filtro de filial?
// d) JOIN sem condicao adequada?
// ERRADO — 1 query por iteracao (N+1)
While !(cAlias)->(Eof())
cCodCli := (cAlias)->A1_COD
// Query DENTRO do loop!
cQuery := "SELECT E1_SALDO FROM " + RetSqlName("SE1") + " WHERE E1_CLIENTE = '" + cCodCli + "'"
MpSysOpenQuery(ChangeQuery(cQuery), cAlias2)
// ...
(cAlias2)->(DbCloseArea())
(cAlias)->(DbSkip())
EndDo
// CORRETO — JOIN na query principal
cQuery := " SELECT SA1.A1_COD, SA1.A1_NOME, SE1.E1_SALDO "
cQuery += " FROM " + RetSqlName("SA1") + " SA1 "
cQuery += " LEFT JOIN " + RetSqlName("SE1") + " SE1 "
cQuery += " ON SE1.E1_FILIAL = SA1.A1_FILIAL "
cQuery += " AND SE1.E1_CLIENTE = SA1.A1_COD "
cQuery += " WHERE SA1.D_E_L_E_T_ = ' ' "
// Verificar se o indice existe para o campo filtrado
// No dicionario SIX:
cQuery := " SELECT INDICE, ORDEM, CHAVE FROM " + RetSqlName("SIX")
cQuery += " WHERE X6_ARQUIVO = 'SA1' AND D_E_L_E_T_ = ' ' "
// Se o campo do WHERE nao esta em nenhum indice:
// Opcao 1: Adicionar indice via SIGACFG
// Opcao 2: Usar campo que JA tem indice no WHERE
// Opcao 3: Para queries ad-hoc, criar indice temporario (DBA)
// ERRADO — alias aberto em cada iteracao, nunca fechado
For nI := 1 To 1000
cAlias := GetNextAlias()
MpSysOpenQuery(cQuery, cAlias)
nVal := (cAlias)->CAMPO
// FALTA: (cAlias)->(DbCloseArea()) ← LEAK!
Next
// Diagnostico: verificar consumo de memoria crescente no AppServer
// AppServer pode negar novos aliases apos ~32.000 abertos
// SEMPRE verificar e logar o erro do MSExecAuto
Private lMsErroAuto := .F.
Private lMsHelpAuto := .T.
Private lAutoErrNoFile := .T.
MsExecAuto({|x,y,z| ROTINA(x,y,z)}, aCampos,, nOpc)
If lMsErroAuto
aLog := GetAutoGRLog()
cErro := ""
For nI := 1 To Len(aLog)
cErro += aLog[nI] + CRLF
Next
ConOut("MSExecAuto ERRO: " + cErro)
// Log com contexto
FwLogMsg("ERROR", , "MINHA_FUNC", FunName(), "MATA410", "01", ;
"Erro MSExecAuto: " + cErro, 0, 0, aLog)
EndIf
| Mensagem no GetAutoGRLog | Causa | Correcao |
|---|---|---|
| "Campo obrigatorio: XXX" | Campo obrigatorio faltando no array | Adicionar campo ao array |
| "Valor invalido para o campo XXX" | Valor fora do range do SX3 | Verificar tipo/tamanho no SX3 |
| "Registro ja existe" | Inclusao (nOpc=3) com chave duplicada | Verificar se registro ja existe |
| "Registro nao encontrado" | Alteracao (nOpc=4) sem posicionar antes | DbSelectArea + MsSeek antes |
| "Filial invalida" | xFilial nao bate com registro | Verificar filial corrente |
// Para nOpc = 4 (Alterar) ou 5 (Excluir), POSICIONAR antes!
DbSelectArea("SC5")
SC5->(DbSetOrder(1))
If SC5->(MsSeek(xFilial("SC5") + cNumPed))
MsExecAuto({|x,y,z| MATA410(x,y,z)}, aCampos,, 4) // Alterar
Else
ConOut("Registro nao encontrado para alteracao!")
EndIf
// 1. Log da requisicao
ConOut(">>> URL: " + cUrl)
ConOut(">>> METHOD: " + cMethod)
ConOut(">>> BODY: " + cBody)
// 2. Log da resposta
ConOut(">>> STATUS: " + cValToChar(nStatus))
ConOut(">>> RESPONSE: " + cResponse)
// 3. Verificar certificado SSL
// Se erro de SSL: verificar se certificado esta no AppServer
// 4. Verificar timeout
// FWRest padrao: 120 segundos
// Aumentar se necessario: oRest:SetTimeout(300)
| Status | Significado | Acao |
|---|---|---|
| 200 | OK | Processar resposta |
| 201 | Created | Recurso criado com sucesso |
| 400 | Bad Request | Verificar body/parametros |
| 401 | Unauthorized | Verificar token/credenciais |
| 403 | Forbidden | Verificar permissoes |
| 404 | Not Found | Verificar URL/endpoint |
| 408 | Timeout | Aumentar timeout ou verificar rede |
| 500 | Internal Server Error | Erro no servidor destino |
| 503 | Service Unavailable | Servidor destino fora do ar |
Jobs rodam sem interface. Estrategias especificas:
// Criar funcao de teste que chama o Worker diretamente
User Function TST_WORKER()
// Simular as Private que o Job declara
Private lMsErroAuto := .F.
Private lMsHelpAuto := .T.
Private lAutoErrNoFile := .T.
// Chamar o Worker diretamente (sem Scheduler, sem LockByName)
WORKER_MEUJOB()
MsgInfo("Concluido!")
Return
// Abrir console.log em tempo real (tail -f no Linux, ou abrir no editor)
// Filtrar por nome do Job:
// grep "U_MEUJOB" console.log
// No Windows: abrir console.log com editor que faz auto-refresh
// Ou usar PowerShell: Get-Content console.log -Wait | Select-String "U_MEUJOB"
// Executar Job manualmente via SmartClient para ver ConOut em tempo real
// 1. Abrir SmartClient
// 2. Programa Inicial: U_MEUJOB
// 3. Parametros: (nao preencher — vai cair no modo nao-Job)
// 4. Observar output no console do SmartClient
// Se precisar simular parametros do Scheduler:
User Function TST_JOB()
U_MEUJOB({"01", "0101"}) // Simula aDadosSistema
Return
Ao receber um erro para diagnosticar, siga esta checklist:
| Topico | Skill |
|---|---|
| Debug de locks e RecLock | protheus-data-model (secao 4) |
| Debug de Jobs sem UI | protheus-jobs (R10, secao Debug) |
| ErrorBlock em Jobs | protheus-jobs (variante ErrorBlock) |
| Debug de queries SQL | protheus-data-model (secao 1) |
| Debug de BeginSQL existente | advpl-embedded-sql |
| Debug de APIs REST | protheus-rest |
| Erros de MSExecAuto | protheus-data-model (secao 4.3) |
npx claudepluginhub guipegoraro/protheus-toolkit --plugin protheus-toolkitProvides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.