Konzolový výstup z Dynamics AX

Přestože Dynamics AX nabízí celou řadu možností, jak volat aplikační logiku (například Business Connector nebo WCF), velmi často je třeba použít přímo klienta (ax32.exe) z příkazové řádky. Týká se to zejména administrace a vývoje – automatizovaná konfigurace, kompilace, aktualizace křížových referencí a podobně.

Bohužel ax32.exe nevrací do konzole žádný výstup, je proto těžké zjistit, zda všechno běží nebo došlo k nějaké chybě. To je samozřejmě problém zejména pro automatizované skripty, protože ty se nemohou podívat, co se děje v GUI.

Jeden možný přístup je zapisovat stavové informace z Dynamics AX do nějakého logu (soubor, EventLog) a zpětně je analyzovat, ale zápis do konzole má řadu výhod – výstup je jasně svázán s konkrétním procesem a příkazem, není třeba řešit přístup k externím logům, konzole může výstup snadno dále zpracovávat (včetně přesměrování do souboru) a tak dále.

Na konzolový výstup se dá pohodlně zapisovat pomocí .NET třídy System.Console, vytvoříme si proto X++ třídu volající System.Console přes velmi zjednodušené rozhraní.

class SysConsole
{
    public static void writeLine(str _message)
    {
        ;
        System.Console::WriteLine(_message);
    }
    public static void writeErrorLine(str _message)
    {
        System.IO.TextWriter writer = System.Console::get_Error();
        ;
        writer.WriteLine(_message);
        writer.Close();
    }
}

Zprávy na konzolový výstup pak můžeme jednoduše zasílat voláním těchto metod, jako v následujícím zkušebním jobu:

SysConsole::writeLine("First!");
SysConsole::writeLine("Another message");
SysConsole::writeErrorLine("Something is wrong");

Pokud spustíte klienta Dynamics AX z příkazové řádky a následně testovací job, nic se bohužel nestane. Bude třeba zvolit trochu komplikovanější postup.

První problém je v tom, že příkazová řádka nečeká na výstup z programu, jen ho spustí a pokračuje s dalšími příkazy. Pouhé čekání na ukončení programu, jako v následujícím PowerShell skriptu, ale stále nic nezobrazuje.

Start-Process ax32.exe -Wait

Pokud ale přesměrujete výstupní a chybový proud do souboru, korektně se zapíší.

Start-Process ax32.exe -Wait -RedirectStandardOutput c:\Temp\out.txt -RedirectStandardError c:\Temp\error.txt

Výstup tedy získat lze a to je nejdůležitější zpráva. Pro některé případy je prostý zápis do souboru vyhovující (rozdíl oproti zápisu přímo z AX je v tom, že si jména souborů volí volající skript), ale stále to není „normální“ výstup do konzole.

V dalším pokusu vytvoříme proces přímo pomocí .NET třídy System.Diagnostics.Process, přesměrujeme výstup a budeme ho číst z vlastností procesu:

$process = New-Object System.Diagnostics.Process
$process.StartInfo.FileName = 'C:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin\ax32.exe'
$process.StartInfo.UseShellExecute = $false
$process.StartInfo.RedirectStandardOutput = $true
$process.StartInfo.RedirectStandardError = $true
 
$process.Start() | Out-Null
$process.WaitForExit()
 
Write-Host $process.StandardOutput.ReadToEnd()
Write-Host $process.StandardError.ReadToEnd() -ForegroundColor Red
 
$process.Close()

To skutečně zobrazí výstup z našeho testovacího jobu do konzole:

Toto řešení má ale stále nedostatky, zejména v tom, že se výstup zobrazí až po skončení procesu. To je sice dostatečné pro zobrazení konečných výsledků, ne ale pro jakékoli zobrazování průběhu zpracování.

V C# se nabízí celkem přímočaré řešení pomocí událostí OutputDataReceived a ErrorDataReceived – viz následující jednoduchou konzolovou aplikaci.

class Program
{
    static void Main(string[] args)
    {
        new Program().Run();
    }
 
    void Run()
    {
        string file = @"c:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin\ax32.exe";
 
        using (Process ax = new Process())
        {
            ax.StartInfo.FileName = file;
            ax.StartInfo.UseShellExecute = false;
            ax.StartInfo.RedirectStandardOutput = true;
            ax.StartInfo.RedirectStandardError = true;
            ax.OutputDataReceived += new DataReceivedEventHandler(outputDataReceived);
            ax.ErrorDataReceived += new DataReceivedEventHandler(errorDataReceived);
            ax.Start();
 
            ax.BeginOutputReadLine();
            ax.BeginErrorReadLine();
            ax.WaitForExit();
        }
        Console.ReadLine();
    }
    private void outputDataReceived(object sender, DataReceivedEventArgs e)
    {
        if (!String.IsNullOrEmpty(e.Data))
        {
            Console.WriteLine(e.Data);
        }
    }
    private void errorDataReceived(object sender, DataReceivedEventArgs e)
    {
        if (!String.IsNullOrEmpty(e.Data))
        {
            Console.WriteLine("Error: {0}", e.Data);
        }
    }
}

Přestože Powershell umožňuje zpracovávat ty samé události, musel jsem zvolit trochu komplikovanější řešení.

$process = New-Object System.Diagnostics.Process
$process.StartInfo.FileName = "ax32.exe"
$process.StartInfo.UseShellExecute = $false
$process.StartInfo.RedirectStandardOutput = $true
$process.StartInfo.RedirectStandardError = $true
 
Register-ObjectEvent -InputObject $process -EventName OutputDataReceived -SourceIdentifier AxOutput
Register-ObjectEvent -InputObject $process -EventName ErrorDataReceived -SourceIdentifier AxError
 
$process.Start() | Out-Null
$process.BeginOutputReadLine()
$process.BeginErrorReadLine()
 
Function GetAxMessages
{
    Get-Event -SourceIdentifier AxOutput -ErrorAction SilentlyContinue | %{
        if ($_.SourceEventArgs.Data)
        {
            $_.SourceEventArgs.Data
        }
        Remove-Event -EventIdentifier $_.EventIdentifier
    }
 
    Get-Event -SourceIdentifier AxError -ErrorAction SilentlyContinue | %{
        if ($_.SourceEventArgs.Data)
        {
            Write-Error $_.SourceEventArgs.Data
        }
        Remove-Event -EventIdentifier $_.EventIdentifier
    }
}
 
while (!$process.WaitForExit(1000))
{
    GetAxMessages
}
 
$process.WaitForExit()
$process.Close()

Skript spustí ax32.exe, přihlásí se k událostem RedirectStandardOutput a RedirectStandardError a každou vteřinu zpracovává přidané události. Možná by se to dalo napsat jednodušeji, každopádně to dělá přesně to, co od toho potřebuji, a to je to hlavní. Veškeré zprávy zapsané z AX do výstupního nebo chybového proudu jsou odchyceny a se zanedbatelným zpožděním zapsány do konzole Powershellu. Chyby jsou zapsány pomocí Write-Error, takže mohou být zpracovávany běžným způsobem (třeba použít parametr -ErrorAction Stop k zastavení na první chybě).

Výše uvedený Powershellový kód určitě není něco, co byste chtěli ručně psát do konzole. Naštěstí jsem to již stihl začlenit do DynamicsAxCommunity modulu, takže jediné, co musíte udělat, je zavolat Start-AXClient s parametrem -Wait.  (Stáhněte si poslední verzi z repozitáře, zatím to není v žádném releasu.)

Zápis do konzole lze využít mnoha různými způsoby – automatizované skripty mohou dostávat zprávy z Dynamics AX, lze zobrazovat průběh dlouhotrvajících procesů (např. aktuálně kompilovaný objekt) nebo třeba pro ladící zprávy.

Můžete dokonce zapisovat všechny chyby z Dynamics AX do chybového proudu (tzn. volat SysConsole::writeErrorLine() z Info.add()). Pokud dojde k chybě, volající skript o ní dostane informaci a může nějak reagovat. Bez odchycení chyby (ať už do konzole nebo jinak) by zkrátka proces dále běžel, nebo (jako v následujícím příkladu) skončil bez indikace chyby.

Pro jistotu ještě upozorňuji, že Dynamics AX na konzoli normálně nezapisuje (alespoň pokud vím) a ani DynamicsAxCommunity žádný takový kód neobsahuje. Stejně tak třída SysConsole není součást Dynamics AX. Aby vám fungoval příklad na předchozím obrázku, musíte sami naimplementovat volání v Dynamics AX.