PowerShell und Taskplaner

Die meisten PowerShell-Skripte werden von Administratoren interaktiv gestartet oder von Installationsroutinen (z.B. Exchange, Lync) oder Prozessen gestartet. Genaugenommen ist auch der Windows Taskplaner so ein "Prozess", der bestimmte Aufgaben zu vorgegebenen Zeiten starten kann. Häufig wiederkehrende Verwaltungsaufgaben lassen sich besonders gut per PowerShell erledigen. Aber beim Einsatz von PowerShell-Skripten als "geplanter Task" sollten Sie ein paar Dinge beachten:

Task planen

Der Windows Taskplaner kann schon länger einen Prozess nicht nur zu gewissen Zeiten starten, sondern mittlerweile auch viele andere Events als Startsignal nutzen:

Das Bild zeigt nur einen Ausschnitt. Wer hier z.B. "Bei einem Ereignis" auswählt, kann gezielt auf Windows Eventlog Meldungen bestimmte Tasks starten lassen. Ein Task kann als Aktion aber auch ein oder mehrere Skripte starten. Nun könnten Sie versucht sein, direkt hier die PS1-Datei zu starten, aber das ist eine denkbar schlechte Idee, denn Windows hat per Default nun mal "Notepad.exe" mit dem Skript verknüpft. Es würde nicht ausgeführt. Aber auch "PowerShell.exe" mit dem Skript als Parameter ist vielleicht nicht der optimale Weg. ich habe mit angewöhnt, immer noch eine CMD-Datei zu starten, die dann das PowerShell-Script ausführt, z.B.:

c:
cd "C:\Program Files (x86)\PRTGPause\"
PowerShell -file "C:\Program Files (x86)\PRTGPause\prtgpause.1.3.ps1"

Das hat zum einen den Vorteil, dass ich das CMD-File auch zum Test interaktiv ausführen kann. Ich kann in CMD-Dateien auch einfach mal ein "echo > logdatei" addieren, was quasi immer funktioniert und mir damit zeigt, dass zumindest das Script vom Taskplaner gestartet wurde. So lassen sich PowerShell-Probleme leichter lösen. Denken Sie dabei auch an die 32/64-Bit Problematik. Es gibt beide Interpreter und einige Skripte (z.B.: mit CDO-Zugriff) brauchen die 32bit PowerShell oder weitere Parameter. Die will ich alle nicht unbedingt jedes Mal wieder in der Aktion eintragen. Das Eingabefenster dazu ist nicht gerade sehr lang.

Ausführender Benutzer

Per Default werden die Skripte immer mit dem Benutzer gestartet, der das Skript auch eingerichtet hat. Hier muss ich aber aufpassen und warnen- Ich habe mir angewöhnt ein Skript entweder also "LocalSystem" oder mit einem explizit definierten Dienstkonto zu starten, aber nie mit einem Benutzer, der auch anderweitig verwendet wird, Das hat zwei Gründe

  • Nachvollziehbarkeit
    Wenn ein user "etwas" tut, z.B. Verzeichnisse anlegt, auf Webseiten zugreift etc. dann ist es immer einfacher einen bekannten Benutzer zu tracken.
  • Berechtigungen
    Jeder Nutzer, der Skript editieren kann, könnte dort Code addieren, der dann mit einem zu umfangreich privilegierten Konto ausgeführt wird. Natürlich sollten die Skripte an einer "sicheren Stelle" liegen, aber es wäre schon mehr als peinlich, wenn ein anderer "lokaler Admin" so ein Skript als Steigbügel nutzt, um erweiterte Rechte zu erlangen.
  • KennwortÄnderung
    Es wäre nicht das erste mal, dass jemand "sein Konto" auch nur temporär zum testen nutzt und natürlich vergisst, es später "richtig" zu machen. Sobald der Anwender dann sein Kennwort ändert, läuft das Skript nicht mehr. Ist es noch ein "geplantes Skript", dann könnten zu viele Fehlversuche auch das Konto selbst sperren.

Insofern betrachten Sie auch ein per Taskplaner gestartetes Programm genauso wie einen "Dienst". Wer kein Dienstkonto anlegen will, kann durchaus mal versuchen das Skript als "SYSTEM" zu starten. Auf dem lokalen System ist dieses Konto recht umfangreich berechtigt.

Error-Handling und Benachrichtigung

Ein Skript als Admin in einer PowerShell-Console zu starten ist einfach, da Sie es manuell mit ihren rechten starten und visuell überwachen. Rote "Errors" und gelbe "Warnings" fallen in den meisten Fällen auf und sie können direkt reagieren. Wenn das Skript sehr langsam wird oder sogar abbricht, bemerken sie dies auch. Sobald ein Skript aber "geplant" läuft, fällt dies nicht mehr direkt auf. Sie müssen sich unbedingt Gedanken zur Überwachung machen. Ohne Überwachung sollten Sie das Skript als "nicht wichtig" einstufen aber dann hätten Sie sich die Mühe ja gleich sparen können.

Für die Benachrichtigung und Logging gibt es viele Möglichkeiten:

  • Eventlog
    Das geht ganz einfach direkt in der PowerShell. Sie sollten auf jeden Fall den Start, das Ende und aufgetretene Fehler melden. Das geht mit Try/Catch recht einfach. Sie können aber auch einfach die $ErrorPreference auf "Continue" einstellen und nach jeder kritischen Aktion die $Error-Variable abfragen (Siehe auch MSXFAQ.DE:PS ErrHandling und MSXFAQ.DE:PS Eventlog)
  • Logfile
    Über ein "Start-Transcript" können sie die Aktionen des verdeckt laufenden PowerShell-Skripts in eine Datei loggen lassen. Wenn das Skript dauerhaft läuft, dann sollten Sie in die Schleife vielleicht etwas einbauen, um das Transcript z.B.: jede Stunde zu beenden und mit einem neuen Namen wieder zu starten. Hier wäre auch ein "Cleanup" alter Transcripte ganz sinnvoll.
  • Mail senden
    Es reicht nicht die Eventlogs oder Logdateien zu befüllen. Nachschauen und überwachen sollten Sie diese schon. Eventuell ist es daher sinnvoll bei größeren Problemen vielleicht direkt eine Mail zu senden. Das funktioniert meist, zumindest wenn der Mailserver erreichbar ist und die Zieladresse noch stimmt. Auch das sollte natürlich zukunftssicher sein, d.h. verwenden Sie einen DNS-Alias (z.B. smtp.<firmendomain> und als Zieladresse einen generischen Alias (<helpdesk>@<firmendomain>), so dass Wechsel der Zuständigkeiten und Server keine Probleme machen.
  • Performance Counter
    Für die ambitionierteren PowerShell Entwickler ist es auch Performance Counter selbst bereit zu stellen. Siehe auchPS Perfmon

Sicher fallen ihnen noch andere Optionen ein. Aber es ist schon noch ein Schritt von einem Skript, welches ein paar mal im interaktiven Einsatz seine Funktion gezeigt hat zu einem "geplanten Task", der viele Jahre problemlos arbeiten sollte.

Aus meiner Sicht der wichtigste Aspekt ist die Erkennung und Behandlung von Fehlersituationen, auch wenn das im Code einige Zeilen mehr bedeutet.

PowerShell im Dauereinsatz (Taskplaner)

Die meisten PowerShell-Skripte werden gestartet und beenden sich danach wieder. Wer hingegen PowerShell als "Dauerläufer" einsetzen will, muss sich genauso um die Speicherverwaltung kümmern, wie andere Programmierer auch. Wer Objekte instanziert und nicht explizit wieder frei gibt, der gibt auch den Speicher dazu nicht frei. Das gilt insbesondere für COM und NET-Objekte. Beachten Sie daher den Speicherbedarf ihrer Skripte, die nicht immer wieder manuell oder durch den Taskmanager gestartet und beendet werden.

# Variablen werden wie folgt frei gegeben
Remove-Variable Variablenname

# Freigeben von ADSI-Objekten
mySearchRoot.Dispose(); 
myDirectoryEntry.Dispose();

# COM-Objekte (z.B: Word) werden mit $null geleert
$objWord = $null

# Der Garbage Collection-Prozess muss manuell gestartet werden
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()

Dann aber sind nette Anwendungsfälle möglich, z.B. kleine Webserver

Mit der Lösung für PRTG (Siehe PRTG Pause) habe ich so ein Skript gebaut, welches als "Dauerprozess" im Hintergrund auf HTTP-Anfragen lauscht und darauf antwortet. Allerdings habe ich hier ausnahmsweise auf Eventlog, Logfiles etc. verzichtet, das es ein "unkritisches" Skript ist und eine Fehlfunktion sofort dem Nutzer auffällt. Indem er nichts sieht und dann die gewünschten Daten eben anders sich ermitteln muss. Hier ist aber keine "Gefahr im Verzug", wie das so schön heißt.

Wer hier etwas weiter gehen möchte, kann natürlich auch den Taskplaner dazu bringen, ein Skript z.B. nach 23:59 Stunden zwangsweise zu beenden und gleich wieder zu starten. Wenn Sie ihr Skript aber halbwegs sauber geschrieben haben, dann läuft es ohne Speicher zu fressen ganz schön lange. PRTGPause ist einmal 2 Monate am Stück gelaufen und der Speicherbedarf hat sich nicht messbar erhöht.

Task mit PowerShell planen

Natürlich müssen Sie nicht, wie am Anfang mit Bildern beschrieben, den Task manuell planen. Auch das kann per PowerShell erfolgen, so dass Sie im gleichen Skript sogar eine "Install-Routine" einbauen können. Das geht ab Windows 8.1 bzw. Windows 2012R2 sogar recht einfach:

# Ermitteln des Skriptnames
$taskaguments = '-nologo -nointeractive -WindowsStyle hidden -file "' + $MyInvocation.MyCommand.Definition *'"'

$taskaction = New-ScheduledTaskAction `
                  -execute 'PowerShell' `
                  -argument $taskaguments

$tasktrigger = New-ScheduledTaskTrigger `
                  -at "00:00" `
                  -RepetitionInterval (new-timespan -hours 1) `
                  -RepetitionDuration [timespan]::MaxValue

$tasksettings = New-ScheduledTaskSettingsSet `
                  -DontStopIfGoingOnBatteries `
                  -DontStopOnIdleEnd `
                  -Hidden `
                  -StartWhenAvailable `
                  -RunOnlyIfNetworkAvailable

$task = New-ScheduledTask `
           -Action $taskaction `
           -Trigger $tasktrigger `
           -Settings $tasksettings

Register-ScheduledTask "taskname" `
   -InputObject $task `
   -User "username" `
   -Password "password

Leider gibt es diese Befehle noch nicht bei früheren Windows Versionen.

Weitere Links