Erste Gehversuche mit VB.NET Diensten

  • VB.NET

Es gibt 12 Antworten in diesem Thema. Der letzte Beitrag () ist von slice.

    Erste Gehversuche mit VB.NET Diensten

    Hallo zusammen,

    ich versuche aktuell meine ersten Gehversuche, einen Windows Dienst in VB.NET zu erstellen. Direkt zu Anfang stehe ich allerdings bereits vor zwei - wahrscheinlich trivialen - Problem.

    Zum einen bekomme ich den Dienste nicht installiert. Versucht habe ich das gemäß der Microsoft Anleitung mit installutil. Die Rückmeldung sieht dabei auch einigermaßen gut aus.
    installutil Ausgabe

    Quellcode

    1. Microsoft (R) .NET Framework-Installationsprogramm, Version 4.8.9037.0
    2. Copyright (C) Microsoft Corporation. Alle Rechte vorbehalten.
    3. Eine transaktive Installation wird ausgeführt.
    4. Die Installationsphase wird gestartet.
    5. Die Protokolldatei enthält den Fortschritt der Assembly C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.exe.
    6. Die Datei befindet sich in C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.InstallLog.
    7. Assembly C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.exe wird installiert.
    8. Betroffene Parameter:
    9. logtoconsole =
    10. logfile = C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.InstallLog
    11. assemblypath = C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.exe
    12. Keine öffentlichen Installer mit dem RunInstallerAttribute.Yes-Attribut in der Assembly C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.exe gefunden.
    13. Die Installationsphase ist abgeschlossen, und die Commitphase beginnt.
    14. Die Protokolldatei enthält den Fortschritt der Assembly C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.exe.
    15. Die Datei befindet sich in C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.InstallLog.
    16. Assembly C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.exe wird ausgeführt.
    17. Betroffene Parameter:
    18. logtoconsole =
    19. logfile = C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.InstallLog
    20. assemblypath = C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.exe
    21. Keine öffentlichen Installer mit dem RunInstallerAttribute.Yes-Attribut in der Assembly C:\Users\XXX\Desktop\ServiceTest\WindowsService1\bin\Debug\WindowsService1.exe gefunden.
    22. Die InstallState-Datei wird entfernt, da keine Installer vorhanden sind.
    23. Die Commitphase wurde erfolgreich abgeschlossen.
    24. Die transaktive Installation ist abgeschlossen.


    Allerdings erscheint der Dienst dann nicht in der Dienstverwaltung. Ein Dienstnamen hab ich in der vorgenerierten Sub InitializeComponent() bereits festgelegt

    VB.NET-Quellcode

    1. Private Sub InitializeComponent()
    2. Me.components = New System.ComponentModel.Container()
    3. Me.Timer1 = New System.Windows.Forms.Timer(Me.components)
    4. [...]
    5. Me.ServiceName = "MyTestService"
    6. End Sub


    Testweise habe ich den Dienst dann mit SC create angelegt, das hat funktioniert. Den Dienst kann ich auch starten. Grundsätzlich stellt sich mir hier auch die Frage, wie es bei der Dienstentwicklung mit Debugging aussieht. Den Dienst kann ich ja scheinbar nicht direkt aus Visual Studio starten. Wie komm ich bei dem Dienst an einigermaßen Debugging Informationen? Da Dienste keine GUI haben, kann ich das auch nicht selber abbilden.

    In meinem Testprojekt habe ich versucht das Timer Projekt von Microsoft nachzubauen. Testweise wollte ich alle 10 Sekunden einen Ordner mit der aktuellen Sekundenanzahl in C:\temp anlegen.
    Sourcecode

    VB.NET-Quellcode

    1. Imports System.Timers
    2. Public Class Service1
    3. Dim timer As Timer = New Timer()
    4. Protected Overrides Sub OnStart(ByVal args() As String)
    5. ' Code zum Starten des Dienstes hier einfügen. Diese Methode sollte Vorgänge
    6. ' ausführen, damit der Dienst gestartet werden kann.
    7. Timer.Interval = 10000 ' 60 seconds
    8. timer.Start()
    9. My.Computer.FileSystem.CreateDirectory("C:\TEMP\start_" + DateTime.Now.Second.ToString)
    10. End Sub
    11. Protected Overrides Sub OnStop()
    12. ' Hier Code zum Ausführen erforderlicher Löschvorgänge zum Beenden des Dienstes einfügen.
    13. End Sub
    14. Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
    15. My.Computer.FileSystem.CreateDirectory("C:\TEMP\" + DateTime.Now.Second.ToString)
    16. End Sub
    17. End Class


    Beim Starten des Dienstes wird der start_ Ordner zwar angelegt, allerdings die Ordner aus der Sub Timer1_Tick nicht.

    Kann mir zu den Themen jemand Starthilfe geben?

    Danke und Grüße!
    @HeizungAuf5 Ein Dienst kann nix mit Windows.Forms anfangen, ebensowenig wie dem Forms.Timer.
    Im Beispiel wird ein System.Timers verwendet.
    Probiere alternativ einen Threading.Thread.Timer.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    RodFromGermany schrieb:

    Ein Dienst hat nix mit Windows.Forms zu tun, ebensowenig wie dem Forms.Timer.

    Danke, das hab ich bereinigt, ich hatte mir testweise einen Timer aus der Toolbox rein gezogen um zu prüfen, ob die Problematik besser wird. Den "falschen" Timer hab ich eben gelöscht. Dadurch passt sich auch die InitializeComponent an:

    VB.NET-Quellcode

    1. Private Sub InitializeComponent()
    2. '
    3. 'Service1
    4. '
    5. Me.ServiceName = "MyTestService"
    6. End Sub



    Das Problem dass bei den Ticks "nichts" passiert, besteht aber weiter. Hierzu habe ich vorher einen anderen Thread gefunden, welcher das gleiche Problem hat. Ich konnte das Timer-Problem ebenfalls durch das Hinzufügen des Handler lösen
    AddHandler timer.Elapsed, AddressOf Timer1_Tick
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Protected Overrides Sub OnStart(ByVal args() As String)
    2. ' Code zum Starten des Dienstes hier einfügen. Diese Methode sollte Vorgänge
    3. ' ausführen, damit der Dienst gestartet werden kann.
    4. Dim timer As Timer = New Timer()
    5. timer.Interval = 5000 ' 60 seconds
    6. timer.Enabled = True
    7. timer.Start()
    8. AddHandler timer.Elapsed, AddressOf Timer1_Tick
    9. My.Computer.FileSystem.CreateDirectory("C:\TEMP\start_" + DateTime.Now.Second.ToString)
    10. End Sub

    @HeizungAuf5 Wie startest Du denn den Dienst als solchen?
    Welche Bedingungen müssen erfüllt sein, damit OnStart() ausgeführt wird?
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    RodFromGermany schrieb:

    Wie startest Du denn den Dienst als solchen?

    Aktuell nur über den Windows Dienste Manager (starten aus VS geht ja nicht).

    RodFromGermany schrieb:

    Welche Bedingungen müssen erfüllt sein, damit OnStart() ausgeführt wird?

    Da ich aktiv nichts hinterlegt habe, gehe ich davon aus dass es keine Bedingung gibt. Das was in OnStart() drin steht, wird zumindest direkt nach dem Start des Dienstes ausgeführt.
    @HeizungAuf5 Hänge mal das komplette bereinigte (ohne bin-, obj-, vs-Verzeichnisse) gezippte Projekt an.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Wie schaut die Syntax bei Vererbung in VB.Net aus?
    Mir fehlt da irgendwie der Punkt mit ServiceBase (class MyService :ServiceBase).

    Ist schon lange her das ich mir "rohen" Services gearbeitet habe, ich arbeite in dem Kontext mit IHostBuilder für Dependency Injection und Quartz.Net, für die periodische Ausführung.

    Program.cs

    C#-Quellcode

    1. internal class Program
    2. {
    3. static async Task Main(string[] args)
    4. {
    5. IHostBuilder builder = Host.CreateDefaultBuilder(args)
    6. .UseWindowsService(options => { options.ServiceName = "MyService"; })
    7. .ConfigureServices(services =>
    8. {
    9. services.AddSingleton<FoobarService>();
    10. });
    11. if (args.Contains("--debug"))
    12. {
    13. builder = builder.ConfigureServices(services =>
    14. {
    15. services.AddSingleton<MyJob>();
    16. services.AddHostedService<ConsoleHostedService>();
    17. });
    18. }
    19. else
    20. {
    21. builder = builder.ConfigureServices(services =>
    22. {
    23. services.Configure<QuartzOptions>(options =>
    24. {
    25. options.MisfireThreshold = TimeSpan.FromSeconds(2);
    26. });
    27. services.AddQuartz(q =>
    28. {
    29. q.AddJob<SyncJob>(j => { j.WithIdentity("sync_job", "sync_group"); });
    30. q.AddTrigger(t => { t.WithIdentity("sync_trigger", "sync_group").WithCronSchedule("0 0 5 ? * WED,SAT *", x => x.WithMisfireHandlingInstructionDoNothing()).ForJob("sync_job", "sync_group"); });
    31. });
    32. services.AddQuartzHostedService(options => { options.WaitForJobsToComplete = false; });
    33. });
    34. }
    35. await builder.Build().RunAsync();
    36. }
    37. }


    MyJob.cs

    C#-Quellcode

    1. [DisallowConcurrentExecution]
    2. internal class MyJob :IJob
    3. {
    4. private readonly ILogger<MyJob> logger;
    5. public MyJob(ILogger<MyJob> logger)
    6. {
    7. this.logger = logger;
    8. }
    9. public async Task Execute(IJobExecutionContext context)
    10. {
    11. // my stuff
    12. }
    13. }


    ConsoleHostedService.cs

    C#-Quellcode

    1. internal class ConsoleHostedService :IHostedService
    2. {
    3. private int? exitCode;
    4. private readonly ILogger<ConsoleHostedService> logger;
    5. private readonly IHostApplicationLifetime hostApplicationLifetime;
    6. private readonly MyJob myJob;
    7. public ConsoleHostedService(IHostApplicationLifetime hostApplicationLifetime, MyJob myJob, ILogger<ConsoleHostedService> logger)
    8. {
    9. this.logger = logger;
    10. this.hostApplicationLifetime = hostApplicationLifetime;
    11. this.myJob = myJob;
    12. }
    13. public Task StartAsync(CancellationToken cancellationToken)
    14. {
    15. this.hostApplicationLifetime.ApplicationStarted.Register(() =>
    16. {
    17. Task.Run(async () =>
    18. {
    19. try
    20. {
    21. await this.myJob.Execute(null);
    22. this.exitCode = 0;
    23. }
    24. catch (Exception ex)
    25. {
    26. this.logger.LogError(ex, "Unhandled exception");
    27. this.exitCode = 1;
    28. }
    29. finally
    30. {
    31. this.hostApplicationLifetime.StopApplication();
    32. }
    33. });
    34. });
    35. return Task.CompletedTask;
    36. }
    37. public Task StopAsync(CancellationToken cancellationToken)
    38. {
    39. this.logger.LogDebug("Exiting with return code: {exitCode}", this.exitCode.GetValueOrDefault(1));
    40. Environment.Exit(this.exitCode.GetValueOrDefault(1));
    41. return Task.CompletedTask;
    42. }
    43. }


    So kann das ganze Ding als Service laufen, mit Visual Studio ausgeführt werden oder direkt in der cmd/powershell (mittels --debug Parameter).

    Das hier müssten die benötigten Nuget-Packages sein:
    * Microsoft.Extensions.Hosting
    * Microsoft.Extensions.Hosting.WindowsServices
    * Quartz
    * Quartz.Extensions.Hosting

    Edit:
    Hier noch ein bissel Input zum Thema .NET Generic Host / IHostedService

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „slice“ ()

    @HeizungAuf5 Im ZIP deklarierst und instanziierest Du den Timer in der Prozedur OnStart(), das ist falsch.
    In Post #1 (letztes Snippet) deklarierst Du den Timer in der Klasse Service1, da gehört das hin.
    Sieh Dir mal diesen Artikel an:
    learn.microsoft.com/de-de/dotn…in-the-component-designer
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    RodFromGermany schrieb:

    Im ZIP deklarierst und instanziierest Du den Timer in der Prozedur OnStart() In Post #1 deklarierst Du den Timer in der Klasse Service1

    Ich habe in der Zwischenzeit im Projekt etwas "Fehlersuche" betrieben, daher vermutlich der Unterschiedliche Code.

    RodFromGermany schrieb:

    Sieh Dir mal diesen Artikel an:
    learn.microsoft.com/de-de/dotn…in-the-component-designer

    Danke. Hab ich durchgelesen. Was da leider gar nicht behandelt wird sind die Offenen punkte bzgl. Debugging. Hast Du dazu entsprechende Ressourcen? Aktuell bin ich in dem Testprojekt halt "blind" unterwegs, was das Testing und Debugging betrifft

    HeizungAuf5 schrieb:

    Grundsätzlich stellt sich mir hier auch die Frage, wie es bei der Dienstentwicklung mit Debugging aussieht. Den Dienst kann ich ja scheinbar nicht direkt aus Visual Studio starten. Wie komm ich bei dem Dienst an einigermaßen Debugging Informationen? Da Dienste keine GUI haben, kann ich das auch nicht selber abbilden.


    Grüße!

    HeizungAuf5 schrieb:

    Was da leider gar nicht behandelt wird sind die Offenen punkte bzgl. Debugging
    Windows Services lassen sich schlecht debuggen.
    Ich mache das üblicherweise so, dass ich eine DLL schreibe in dem der relevante Code abläuft.
    Diese DLL binde ich zum einen an den Service, zum anderen an eine Konsolanwendung.
    Beide machen nichts anderes als einen Einsprungpunkt in der DLL aufzurufen.
    Zum Testen und Debuggen arbeite ich mit der Konsolanwendung.

    Hab immer im Hinterkopf, dass ein Service keine Interaktion mit einem GUI haben darf!
    Also kein WriteLine und keine MessageBox oder andere Forms-Aufrufe.
    --
    If Not Program.isWorking Then Code.Debug Else Code.DoNotTouch
    --

    petaod schrieb:

    Ich mache das üblicherweise so, dass ich eine DLL schreibe in dem der relevante Code abläuft.
    Diese DLL binde ich zum einen an den Service, zum anderen an eine Konsolanwendung.

    Hast Du etwas dazu, wie sich das umsetzen lässt?


    petaod schrieb:

    Hab immer im Hinterkopf, dass ein Service keine Interaktion mit einem GUI haben darf!

    Genau deswegen mache ich es mir damit auch (zu) schwer. Aktuell lässt sich die Anwendung nicht Debuggen und scheint auch ein "eigenes" Fehlerverhalten zu haben. Testweise habe ich in der oben genannten Testumgebung versucht einen Ordner mit einem angehängten > anzulegen, was nicht geht, weil ungültiger Pfad. Eine GUI Anwendung stürzt dabei natürlich ab, wenn der Code ausgeführt wird. Der Dienst läuft dabei allerdings weiter und tut "nichts".
    Warum so kompliziert?
    In der Service1.Designer.vb wird der Code als Service gestartet:

    VB.NET-Quellcode

    1. Dim ServicesToRun() As System.ServiceProcess.ServiceBase
    2. ServicesToRun = New System.ServiceProcess.ServiceBase() {New Service1}
    3. System.ServiceProcess.ServiceBase.Run(ServicesToRun)

    Also an der Stelle einfach eine Prüfung einbauen, zum Beispiel via Argument oder ganz stumpf mittels Preprocessing Directives * und den Code direkt ausführen.

    * Preprocessing Directives

    VB.NET-Quellcode

    1. #if DEBUG then
    2. <do something>
    3. #else
    4. <do something else>
    5. #end if