I'm trying to convert a .ps1 file to run as a windows service. This needs to run as a service as it's requirements for Business Continuity (scheduled task is not an option). i've always used NSSM to wrap the .ps1 as it will then run via NSSM as an exe.
This works for different scripts in Windows Server 2012, but this script is slightly different and i'm required to get this service to work on Windows Server 2016. The script itself, connects to a large amount of other servers (in total i'll have 3 services - Windows Service / Windows Process / Linux Process) which all work when just running within PowerShell.
Below is an example of the start of the script so you get an idea how it works (may not be relevant);
while ($test = 1)
{
[string]$query
[string]$dbServer = "DBSERVER" # DB Server (either IP or hostname)
[string]$dbName = "DBNAME" # Name of the database
[string]$dbUser = "CONNECTIONUSER" # User we'll use to connect to the database/server
[string]$dbPass = "CONNECTIONPASSWORD" # Password for the $dbUser
$conn = New-Object System.Data.Odbc.OdbcConnection
$conn.ConnectionString = "Driver={PostgreSQL Unicode(x64)};Server=$dbServer;Port=5432;Database=$dbName;Uid=$dbUser;Pwd=$dbPass;"
$conn.open()
$cmd = New-object System.Data.Odbc.OdbcCommand("select * from DBNAME.TABLENAME where typeofcheck = 'Windows Service' and active = 'Yes'",$conn)
$ds = New-Object system.Data.DataSet
(New-Object system.Data.odbc.odbcDataAdapter($cmd)).fill($ds) | out-null
$conn.close()
$results = $ds.Tables[0]
$Output = #()
foreach ($result in $results)
{
$Hostone = $Result.hostone
$Hosttwo = $Result.hosttwo
$Hostthree = $Result.hostthree
$Hostfour = $Result.hostfour
Write-Output "checking DB ID $($result.id)"
#Host One Check
if (!$result.hostone)
{
$hostonestatus = 17
$result.hostone = ""
}
else
{
try
{
if(Test-Connection -ComputerName $result.hostone -quiet -count 1)
{
$hostoneres = GET-SERVICE -COMPUTERNAME $result.hostone -NAME $result.ServiceName -ErrorAction Stop
$hostonestatus = $hostoneres.status.value__
$Result.HostOneCheckTime = "Last checked from $env:COMPUTERNAME at $(Get-date)"
}
else
{
$hostonestatus = 0
$result.hostonestatus = "Failed"
$Result.HostOneCheckTime = "Last checked from $env:COMPUTERNAME at $(Get-date)"
}
}
catch
{
$hostonestatus = 0
$result.hostonestatus = "Failed"
$Result.HostOneCheckTime = "Last checked from $env:COMPUTERNAME at $(Get-date) Errors Found"
}
if ($hostonestatus -eq 4)
{
$result.hostonestatus = "Running"
}
if ($hostonestatus -eq 1)
{
$result.hostonestatus = "Stopped"
}
elseif ($hostonestatus -eq 0)
{
$result.hostonestatus = "Failed"
}
}
As mentioned, the exact script running standalone works seamlessly.
Whats the best way to run this as a service or are there any known issues with NSSM when using it with Windows Server 2016?
I've also found the below which may be pointing in the right direction as i've intermittently got these in the logs;
DCOM event ID 10016 is logged in Windows
Windows sysadmin here.
Quiet a few different ways to accomplish this from a purely service-orientated perspective.
--- 1 ---
If you are using Server 2016, I believe that the Powershell command 'New-Service' may be one of the cleanest ways. Have a look at the following for syntax and if it suits your use case --
https://support.microsoft.com/en-au/help/137890/how-to-create-a-user-defined-service
This CMDlet takes a credential parameter, so depending on your use case may be good to access resources on other foreign machines.
--- 2 ---
Another way is to use the old trusty in-built SC.exe utility in windows.
SC CREATE <servicename> Displayname= "<servicename>" binpath= "srvstart.exe <servicename> -c <path to srvstart config file>" start= <starttype>
An example --
SC CREATE Plex Displayname= "Plex" binpath= "srvstart.exe Plex -c C:PlexService.ini" start= auto
As far as I can tell, this will create a service that will execute under the Local System context. For more information, have a look at the following:
https://support.microsoft.com/en-au/help/251192/how-to-create-a-windows-service-by-using-sc-exe
https://www.howtogeek.com/50786/using-srvstart-to-run-any-application-as-a-windows-service/
--- 3 ---
You may want to consider manually injecting some registry keys to create your own service (which is essentially what SC.exe does under the hood).
Although I'm unfortunately in no position at the moment to provide boiler-plate code, I'd encourage that you have a look at the following resource:
https://www.osronline.com/article.cfm%5Eid=170.htm
NOTE - you will need to provide all required sub-keys for it to work.
As with any registry changes, please make a backup of your registry and perform edits at your own risk prior to making any changes. I can only recommend to try this on a spare/test VM if possible prior to implementing to prod.
Related
I am trying to build my own script to check some Windows services (status and start mode) and I am facing an issue on the IF ...
For example even if the service is "Running", it will never run the code inside the IF...
let me share my code below (I am a newbie on powershell so be gentle xD)
For info, I will do more actions inside the IF and ELSE, it is just for the example.
# import computers list, 1 by line
$Computers = get-content .\computers.txt
# define variable of services we want to check
$ServiceNetbios = "netbt"
# define variable to ask credentials
$Cred = Get-Credential
# declare Function to open a session a remote computer
Function EndPSS { Get-PSSession | Remove-PSSession }
EndPSS
########################################################
# BEGINNING OF SCRIPT #
# by xxx #
# 2022-02-03 #
########################################################
# loop for each computer imported from the file
foreach ($computer in $computers) {
# show name of computer in progress
$computer
# connect remotely to the computer
$session = New-PSSession -ComputerName $computer -Credential $Cred
# check Netbios service
$StatusServiceNetbios = Invoke-Command -Session $session -ScriptBlock { Get-Service -Name $Using:ServiceNetbios | select -property * }
# Check Netbios service started or not
write-host $StatusServiceNetbios.Status
if ($StatusServiceNetbios.Status -eq 'Running')
{
Write-host "IF Running"
}
else
{
write-host "IF NOT Running"
}
EndPSS
}
and what return my script :
computername
Running (<= the variable $StatusServiceNetbios.Status )
IF NOT Running (<= the ELSE action)
Thanks you in advance for your help,
this drive me crazy and maybe this is very simple...
To complement Cpt.Whale's helpful answer, this is likely to be caused by the serialization and deserialization done by Invoke-Command:
using namespace System.Management.Automation
$service = Get-Service netbt
$afterInvokeCmd = [PSSerializer]::Deserialize(([PSSerializer]::Serialize($service)))
$service.Status -eq 'Running' # => True
$afterInvokeCmd.Status -eq 'Running' # => False
$afterInvokeCmd.Status.Value -eq 'Running' # => True
$afterInvokeCmd.Status.ToString() -eq 'Running' # => True
To put some context to my answer, this is a nice quote from about_Remote_Output that can better explain why and what is happening:
Because most live Microsoft .NET Framework objects (such as the objects that PowerShell cmdlets return) cannot be transmitted over the network, the live objects are "serialized". In other words, the live objects are converted into XML representations of the object and its properties. Then, the XML-based serialized object is transmitted across the network.
On the local computer, PowerShell receives the XML-based serialized object and "deserializes" it by converting the XML-based object into a standard .NET Framework object.
However, the deserialized object is not a live object. It is a snapshot of the object at the time that it was serialized, and it includes properties but no methods.
This is probably because of the way powershell creates service objects - (Get-Service netbt).Status has a child property named Value:
$StatusServiceNetbios.Status
Value
-----
Running
# so Status is never -eq to 'Running':
$StatusServiceNetbios.Status -eq 'Running'
False
# use the Value property in your If statement instead:
$StatusServiceNetbios.Status.Value -eq 'Running'
True
I am needing to create receive locations and send ports in an already existing BizTalk application using Powershell. I have only seen some documentation on how to create an application but not to call upon one. Any suggestions would be beneficial. There are some things that are commented out, and that is because I cannot disclose that information. I added at the last part on what I have learned of how to create an application, but that is not something that I want for my script. The program below is what I have so far:
#===Create a receive port and location function===#
Function CreateRPandRL ()
{
#Creating Receive Port
$myReceivePort = $catalog.AddNewReceivePort($false)
$myReceivePort.Name = "My Receive Port"
#Creating Receive Location
$myReceiveLocation = $myReceivePort.AddNewReceiveLocation()
foreach ($handler in $catalog.ReceiveHandlers)
{
if ($handler.TransportType.Name -eq "FILE")
{
$myReceiveLocation.ReceiveHandler = $handler
break
}
}
#Associate a transport protocol and file location with receive location
$myReceiveLocation.TransportType = $catalog.ProtocolTypes["FILE"]
$myReceiveLocation.Address = #pick-up file location
#Assign the first receive pipeline found to process the message
foreach ($pipeline in $catalog.Pipelines)
{
if ($pipeline.Type -eq [Microsoft.BizTalk.ExplorerOM.PipelineType] "File_Receive")
{
$myReceiveLocation.ReceivePipeline = $pipeline
break
}
#Enable the receive location
$myReceiveLocation.Enable = $true
}
#Try to commit the changes made so far. If the commit fails, roll back changes
$catalog.SaveChanges()
}
Function CreateSendPorts($Catalog)
{
#=== Register a trap handler to discard changes on exceptions ===#
$ErrorActionPreference="silentlycontinue"
trap { "Exception encountered:`r`n"; $_; "`r`nDiscarding Changes.`r`n";$Catalog.DiscardChanges();exit; }
#=== create a new static one-way send port using FILE transport ===#
$mySendPort = $Catalog.AddNewSendPort($false,$false)
$mySendPort.Name = "My Send Port"
$mySendPort.PrimaryTransport.TransportType = $catalog.ProtocolTypes["FILE"]
$mySendPort.PrimaryTransport.Address = #drop-off file location
$mySendPort.SendPipeline = $Catalog.Pipelines["Microsoft.BizTalk.DefaultPipelines.EdiSend"]
#=== Persist new ports to BizTalk configuration database ===#
Write-Host "Adding $mySendPort.Name..."
$catalog.SaveChanges();
Write-Host "`r`n $mySendPort.Name has been created."
#=== specify filters for content-based routing ===#
Write-Host $mySendPort.Name: Adding a filter
$mySendPort.Filter = "<Filter><Group>" +
"<Statement Property='EDI.ISA06' Operator='0' Value='9999999999'/>" +
"<Statement Property='EDI.ST01' Operator='0' Value='999'/>" +
"<Statement Property='EDI.IsSystemGeneratedAck' Operator='0' Value='true'/>" +
"<Statement Property='BTS.ReceivePortName' Operator='0' Value= $myReceivePort.Name/>" +
"</Group></Filter>"
#=== Persist all changes to BizTalk configuration database ===#
Write-Host $mySendPort.Name + ": Saving changes"
$catalog.SaveChanges();
Write-Host "`r`nFilters for $mySendPort.Name created"
#===========Changing Send Port status===========#
#Register a trap handler to discard changes on exceptions
$ErrorActionPreference="silentlycontinue"
trap { "Exception encountered:`r`n"; $_; "`r`nDiscarding Changes.`r`n";$Catalog.DiscardChanges();exit; }
#start the send port to begin processing messages
$mySendPort.Status = [Microsoft.BizTalk.ExplorerOM.PortStatus] "Started"
Write-Host "Changing" + $mySendPort.Name + "status to ($mySendPort.Status)..."
$catalog.SaveChanges()
Write-Host "Complete."
}
#===Main Script===#
#make sure the ExplorerOM assembly is loaded
[void][System.reflection.Assembly]::LoadwithPartialName("Microsoft.BizTalk.ExplorerOM")
#Connect to the BizTalk management database
$Catalog = New-Object Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer
$Catalog.ConnectionString = "SERVER = #server_address; DATABASE=BizTalkMgmtDB; Integrated Security=SSPI"
#Implementing functions
CreateRPandRL
CreateSendPorts
Start-Sleep -Seconds 60
<#
#===BizTalk Application===#
$app = $Catalog.AddNewApplication()
$app.Name = "TestingPowershellScript"
$app.CreateRPandRL()
$app.CreateSendPorts()
#>
Hoo boy, this takes me back a few years, I'm glad I'm not the only one to struggle with this. You want to leave that alone and switch to the BizTalk PowerShell Extensions (information on this is sketchy), they are sooooooo much easier to work with in PowerShell.
I cobbled this together from some scripts I used, and left out some of the fancy stuff, but what you want is basically:
$InitializeDefaultBTSDrive = $false
Import-Module "$env:BTSINSTALLPATH\SDK\Utilities\PowerShell\BizTalkFactory.PowerShell.Extensions.dll" -WarningAction Ignore
New-PSDrive -Name BizTalk -PSProvider BizTalk -Root BizTalk:\ -Instance $DatabaseName -Database $BizTalkMgmtDb
This opens up a whole world of goodies, because it's loaded as a PSDrive, you can navigate round it, create things, delete things, use it all as native as any other drive/filesystem, such as:
Get-ChildItem "BizTalk:\All Artifacts\Receive Locations"
Get-ChildItem "BizTalk:\All Artifacts\Receive Locations" | Disable-ReceiveLocation
Get-ChildItem "BizTalk:\Platform Settings\Host Instances" | Stop-HostInstance
Get-ChildItem "BizTalk:\Platform Settings\Host Instances" | Where-Object { $_.IsDisabled -eq $false } | Start-HostInstance
Get-ChildItem "BizTalk:\All Artifacts\Receive Locations" | Enable-ReceiveLocation
Get-ChildItem -Path "BizTalk:\Health and Activity\Service Instances"
There's so much more than the above, and none of this is what you really asked for, what you actually want is:
Import-Bindings -Path "BizTalk:" -Source $bindings
Where $bindings is your XML bindings file.
My advice, don't even try this. Most of the useful settings for Adapters are not exposed by any API so this will get you maybe half way at most.
Instead, script the import of a binding file which does support all settings for all Adapters.
I just finished the initial tests phase on automating our product release to Azure Virtual Machines using DSC, particularly with the commands described in this article, which are part of the Azure PowerShell SDK.
I can push a DSC configuration fine using PowerShell, but since this process is automated, I wanted to get feedback on how the configuration process progressed. When I call Update-AzureVM, I get an ok but the DSC configuration happens after that, asynchronously, and I don't know how it is going unless I log into the machine (or look at the updated Azure Portal which now shows this).
I'd like to fail my automated process if the configuration fails. How can I check the status of the configuration from my script and gracefully detect success or failure?
We have added a new cmdlet Get-AzureVMDscExtensionStatus to obtain status of the running DSC configuration
This article explains the same http://blogs.msdn.com/b/powershell/archive/2015/02/27/introducing-get-azurevmdscextensionstatus-cmdlet-for-azure-powershell-dsc-extension.aspx
There are a few ways to do this. You can call the REST-based API as I described in a recent post here.
You could also use Get-AzureVM to drill into the value (just like parsing the REST response) like so:
((Get-AzureVM -ServiceName "" -Name "").ResourceExtensionStatusList | Where-Object { $_.HandlerName -eq 'Microsoft.PowerShell.DSC' }).ExtensionSettingStatus.Status
Based on #David's suggestion, I ended up creating a polling function to detect the status changes and report back to my main script.
First, I needed to find what the terminating status codes where (I need to finish my loop as soon as I detect a successful DSC operation or if any error happens).
I drilled down in the files used by the DSC Extension in the VM to find the possible status codes and based my condition on that. The status codes can be found at C:\Packages\Plugins\Microsoft.Powershell.DSC\1.4.0.0\bin\DscExtensionStatus.psm1 in any virtual machine with the DSC Extension installed. Here are the status codes as of version 1.4.0.0 of the DSC Extension:
$DSC_Status = #{
Initializing = #{
Code = 1
Message = "Initializing DSC extension."
}
Completed = #{
Code = 2
Message = "DSC configuration was applied successfully."
}
Enabled = #{
Code = 3
Message = "PowerShell DSC has been enabled."
}
RebootingInstall = #{
Code = 4
Message = "Rebooting VM to complete installation."
}
RebootingDsc = #{
Code = 5
Message = "Rebooting VM to apply DSC configuration."
}
Applying = #{
Code = 6
Message = "Applying DSC configuration to VM."
}
#
# Errors
#
GenericError = 100; # The message for this error is provided by the specific exception
InstallError = #{
Code = 101
Message = "The DSC Extension was not installed correctly, please check the logs on the VM."
}
WtrInstallError = #{
Code = 102
Message = "WTR was not installed correctly, please check the logs on the VM."
}
}
The logic in the function is somewhat convoluted because the state changes are persistent, i.e. they are not from a single DSC operation, but from the whole extension itself. Because of that, I needed to pick the status first to then try to find updates. I'm using the timestamp field to detect a new status. Here is the code:
function Wait-AzureDSCExtensionJob
{
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[string] $ServiceName,
[int] $RefreshIntervalSeconds = 15
)
Begin
{
$statusFormat = `
#{Label = 'Timestamp'; Expression = {$_.TimestampUtc}},
#{Label = 'Status'; Expression = {"$($_.Code) - $($_.Status)"}}, `
#{Label = 'Message'; Expression = {$_.FormattedMessage.Message}}
Write-Verbose 'Getting starting point status...'
$previousStatus = Get-AzureDscStatus -ServiceName:$ServiceName
Write-Verbose "Status obtained: $($previousStatus | Format-List $statusFormat | Out-String)"
Write-Verbose 'This status will be used as the starting point for discovering new updates.'
}
Process
{
do
{
Write-Verbose "Waiting for the next check cycle. $RefreshIntervalSeconds seconds left..."
Start-Sleep -Seconds:$RefreshIntervalSeconds
$currentStatus = Get-AzureDscStatus -ServiceName:$ServiceName
if ($previousStatus.TimestampUtc -eq $currentStatus.TimestampUtc)
{
Write-Verbose 'Status has not changed since the last check.'
$statusUpdated = $false
}
else
{
Write-Verbose "Status updated: $($currentStatus | Format-List $statusFormat | Out-String)"
$previousStatus = $currentStatus
$statusUpdated = $true
}
# Script with default message codes for the DSC Extension:
# On Target VM: "C:\Packages\Plugins\Microsoft.Powershell.DSC\1.4.0.0\bin\DscExtensionStatus.psm1"
} until ($statusUpdated -and (($currentStatus.Code -eq 2) -or ($currentStatus.Code -ge 100)))
}
End
{
switch ($currentStatus.Code)
{
2 {Write-Verbose 'Configuration finished successfully.'; break}
default {throw "Configuration failed: $($currentStatus.Status)"}
}
}
}
function Get-AzureDscStatus
{
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[string] $ServiceName
)
Begin
{
$vm = Get-AzureVM -ServiceName:$ServiceName
$dscExtensionStatus = $vm.ResourceExtensionStatusList | where { $_.HandlerName -eq 'Microsoft.PowerShell.DSC' }
if (-not $dscExtensionStatus)
{
throw 'Could not find the PowerShell DSC Extension on the VM'
}
$dscExtensionStatus.ExtensionSettingStatus
}
}
I'm not very proficient in PowerShell yet, so this could probably look a bit better and be easier to read. Still, I hope it can be of use to someone in the same situation as me.
UPDATE 28/11/2014:
Microsoft has updated the DSC Extension to version 1.5.0.0 and my function broke, how nice of them. I mean... it's not as if changing the response codes is a breaking change or anything like that ;)
Here are the new status codes:
$DSC_Status = #{
Success = #{
Code = 1
Message = 'DSC configuration was applied successfully.'
}
Initializing = #{
Code = 2
Message = 'Initializing DSC extension.'
}
Enabled = #{
Code = 3
Message = 'PowerShell DSC has been enabled.'
}
RebootingInstall = #{
Code = 4
Message = 'Rebooting VM to complete installation.'
}
RebootingDsc = #{
Code = 5
Message = 'Rebooting VM to apply DSC configuration.'
}
Applying = #{
Code = 6
Message = 'Applying DSC configuration to VM.'
}
#
# Errors
#
GenericError = 1000 # The message for this error is provided by the specific exception
InstallError = #{
Code = 1001
Message = 'The DSC Extension was not installed correctly, please check the logs on the VM.'
}
WtrInstallError = #{
Code = 1002
Message = 'WTR was not installed correctly, please check the logs on the VM.'
}
OsVersionNotSupported = #{
Code = 1003
Message = 'The current OS version is not supported. The DSC Extension requires Windows Server 2012 or 2012 R2, or Windows 8.1.'
}
}
For some reason they swapped the codes and now 1 is success, while errors went up from 100 to 1000 (they surely are expecting a lot of errors on this one).
Does anyone have a link or script that uses PowerShell to inventory the Scheduled Tasks on a server, including the Action?
I am able to get the Scheduled Service com object and what I would call "top level" properties (name, state, lastruntime), but would like to also get information from the "Actions" part of the Schedule Tasks (essentially, the name of Scheduled Task and its commandline).
For example:
$schedule = new-object -com("Schedule.Service")
$schedule.connect()
$tasks = $schedule.getfolder("\").gettasks(0)
$tasks | select Name, LastRunTime
foreach ($t in $tasks)
{
foreach ($a in $t.Actions)
{
$a.Path
}
}
The above snippet of code works in terms of listing the tasks; but the loop on the Actions simply does not seem to do anything, no error, no output whatsoever.
Any help would be appreciated.
This is probably very similar to current answers, but I wrote a quick script to get you going. The problem with your current script is that there is no Actions property in a task. You need to extract it from the xml task-definition that the comobject provides. The following script will return an array of objects, one per scheduled task. It includes the action if the action is to run one or more command. It's just to get you going, so you need to modify it to include more of the properties if you need them.
function getTasks($path) {
$out = #()
# Get root tasks
$schedule.GetFolder($path).GetTasks(0) | % {
$xml = [xml]$_.xml
$out += New-Object psobject -Property #{
"Name" = $_.Name
"Path" = $_.Path
"LastRunTime" = $_.LastRunTime
"NextRunTime" = $_.NextRunTime
"Actions" = ($xml.Task.Actions.Exec | % { "$($_.Command) $($_.Arguments)" }) -join "`n"
}
}
# Get tasks from subfolders
$schedule.GetFolder($path).GetFolders(0) | % {
$out += getTasks($_.Path)
}
#Output
$out
}
$tasks = #()
$schedule = New-Object -ComObject "Schedule.Service"
$schedule.Connect()
# Start inventory
$tasks += getTasks("\")
# Close com
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($schedule) | Out-Null
Remove-Variable schedule
# Output all tasks
$tasks
Ex. of output
PS > .\Untitled1.ps1 | ? { $_.Name -eq "test" }
Actions : notepad.exe c:\test.txt
calc.exe
Path : \test
Name : test
LastRunTime : 30.12.1899 00:00:00
NextRunTime : 17.03.2013 13:36:38
Get the PowerShellPack from the W7 RK, and try get-scheduledtask
http://archive.msdn.microsoft.com/PowerShellPack
Excerpt From MSDN:
The Windows 7 Resource Kit PowerShell Pack contains 10 modules to do all sorts of interesting things with PowerShell. Import-Module PowerShellPack actually imports 10 modules for you to use. Here’s a brief overview of each of the modules.
WPK Create rich user interfaces quick and easily from Windows PowerShell. Think HTA, but easy. Over 600 scripts to help you build quick user interfaces
TaskScheduler List scheduled tasks, create or delete tasks
FileSystem Monitor files and folders, check for duplicate files, and check disk space
IsePack Supercharge your scripting in the Integrated Scripting Environment with over 35 shortcuts
DotNet Explore loaded types, find commands that can work with a type, and explore how you can use PowerShell, DotNet and COM together
PSImageTools Convert, rotate, scale, and crop images and get image metadata
PSRSS Harness the FeedStore from PowerShell
PSSystemTools Get Operating System or Hardware Information
PSUserTools Get the users on a system, check for elevation, and start-processaadministrator
PSCodeGen Generates PowerShell scripts, C# code, and P/Invoke
Another way would be a script I wrote called Get-ScheduledTask.ps1, available in this article:
How-To: Use PowerShell to Report on Scheduled Tasks
In this way you only need this single script and you don't need to download or install anything else.
Bill
I know I'm late to the party, but the answer provided by #Frode F., while it works, is technically not correct.
You can access items of the Actions collection of a scheduled task via PowerShell, it's just not immediately obvious. I had to figure this out myself today as well.
Here's the code to do this all in PowerShell, without having to muck around with XML:
# I'm assuming that you have a scheduled task object in the variable $task:
$taskAction = $task.Definition.Actions.Item.Invoke(1) # Collections are 1-based
That's all there is to getting a single item out of the collection without using foreach.
Because the Actions property is a collection which contains a parameterized property Item (e.g. in C# you would write myTask.Actions[0] or in VB myTask.Actions.Item(1)), PowerShell represents the Item property as a PSParameterizedProperty object. To call the methods associated with the property, you use the Invoke method (for the getter) and InvokeSet method (for the setter).
I ran a quick test running the OP's code and it worked for me (I'm running PowerShell 4.0, however, so maybe that has something to do with it):
$schedule = new-object -com("Schedule.Service")
$schedule.connect()
$tasks = $schedule.getfolder("\").gettasks(0)
$tasks | select Name, LastRunTime
foreach ($t in $tasks)
{
foreach ($a in $t.Actions)
{
Write-Host "Task Action Path: $($a.Path)" # This worked
Write-Host "Task Action Working Dir: $($a.workingDirectory)" # This also worked
}
$firstAction = $t.Actions.Item.Invoke(1)
Write-Host "1st Action Path: $($firstAction.Path)"
Write-Host "1st Action Working Dir: $($firstAction.WorkingDirectory)"
}
HTH.
here a quick one based on: https://blogs.technet.microsoft.com/heyscriptingguy/2015/01/17/weekend-scripter-use-powershell-to-document-scheduled-tasks/
Uses Powershell: Get-ScheduledTask and Get-ScheduledTaskInfo
### run like >> Invoke-Command -ComputerName localhost, server1, server2 -FilePath C:\tmp\Get_WinTasks.ps1
$taskPath = "\"
$outcsv = "c:\$env:COMPUTERNAME-WinSchTaskDef.csv"
Get-ScheduledTask -TaskPath $taskPath |
ForEach-Object { [pscustomobject]#{
Server = $env:COMPUTERNAME
Name = $_.TaskName
Path = $_.TaskPath
Description = $_.Description
Author = $_.Author
RunAsUser = $_.Principal.userid
LastRunTime = $(($_ | Get-ScheduledTaskInfo).LastRunTime)
LastResult = $(($_ | Get-ScheduledTaskInfo).LastTaskResult)
NextRun = $(($_ | Get-ScheduledTaskInfo).NextRunTime)
Status = $_.State
Command = $_.Actions.execute
Arguments = $_.Actions.Arguments }} |
Export-Csv -Path $outcsv -NoTypeInformation
Recently, I made a script to list all the installed applications in local & remote machine & give the output in a structured manner in an excelsheet.
It looks like this:
$a = Read-Host "Enter machine name" | Out-File -filepath C:\machine.txt
$computerName = Get-Content C:\machine.txt
$a = New-Object -comobject Excel.Application
$a.visible = $True
$b = $a.Workbooks.Add()
$c = $b.Worksheets.Item(1)
$c.Cells.Item(1,1) = "Name"
$c.Cells.Item(1,2) = "Publisher"
$c.Cells.Item(1,3) = "InstalledDate"
$c.Cells.Item(1,4) = "Version"
$c.Cells.Item(1,5) = "UninstallString"
$d = $c.UsedRange
$d.Interior.ColorIndex = 19
$d.Font.ColorIndex = 11
$d.Font.Bold = $True
$i = 2
function Get-InstalledAppReg ([string]$ComputerName) {
$RegPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
$BaseKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $ComputerName)
$OpenSubKey = $BaseKey.OpenSubKey($RegPath)
$i =2
$OpenSubKey.GetSubKeyNames() | ForEach {
$Path = "$RegPath\$_"
$BaseKey.OpenSubKey($Path).GetValue("DisplayName")
$BaseKey.OpenSubKey($Path).GetValue("Publisher")
$BaseKey.OpenSubKey($Path).GetValue("InstalledDate")
$BaseKey.OpenSubKey($Path).GetValue("Version")
$BaseKey.OpenSubKey($Path).GetValue("UninstallString")
$c.Cells.Item($i,1) = $BaseKey.OpenSubKey($Path).GetValue("DisplayName")
$c.Cells.Item($i,2) = $BaseKey.OpenSubKey($Path).GetValue("Publisher")
$c.Cells.Item($i,3) = $BaseKey.OpenSubKey($Path).GetValue("InstalledDate")
$c.Cells.Item($i,4) = $BaseKey.OpenSubKey($Path).GetValue("Version")
$c.Cells.Item($i,5) = $BaseKey.OpenSubKey($Path).GetValue("UninstallString")
$i ++
}
}
Get-InstalledAppReg($computerName)
$d.EntireColumn.AutoFit()
$b.SaveAs("c:\softhive.xlsx")
$b.Close()
$a.Quit()
Get-Process | Where { $_.Name -Eq "Excel" } | Kill
This script ran perfectly for all remote machines which has XP as a OS.
Problem started when I started running it in windows & machines remotely.
Initially it gave wrong path error, when I realized that for windows 7, I probably have to use
"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" instead of
"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall".
With this different path, when I run the same script again, I get an error:
Exception calling "OpenRemoteBaseKey" with "2" argument(s): "The network path was not found.
"
At :line:24 char:62
$BaseKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey( <<<< "LocalMachine", $ComputerName)
Probably, I need to change other things too in the script?
My machine, from where I run the script, is a windows XP SP3 machine.
Unfortunately the WMI Win32_Product class does not report all apps found in Control Panel's "Add or Remove Programs"...
The registry walk seems to be unavoidable, see:
http://powergui.org/thread.jspa?threadID=17068
http://learningpcs.blogspot.fr/2011/10/powershell-get-installed-software.html
Rather than comb the registry, I would use WMI for this. See Win32_Product and friends e.g.:
Get-WmiObject Win32_Product
Note that if I run this on my Windows 7 x64 system in a 64bit PowerShell prompt it shows all installed apps (32-bit and 64-bit):
Get-WmiObject Win32_Product| sort Vendor | Format-Table Name,InstallDate,Vendor
To see all the properties available execute:
Get-WmiObject Win32_Product | Select -First 1 | Format-List *
I remember a while back I did something like this at an IT firm and we simply searched the C: directory for the names of all programs ending in .exe, in order to optimize we would hone in on specific apps that we were looking for. We set up a batch that would pass or fail based on if what we wanted. Keep in mind this is a batch file, however the idea is similar.
echo ================= >>Software_Scan.txt
echo Below is a list of all wireless networks. Saved networks will be found in the Wireless Profiles folder
set filePath=
for /R "C:\Program Files (x86)" /D %%a in (*) do if exist "%%a\YahooMessenger.exe" set filePath=%%a& goto continue
:continue
if defined filePath echo %COMPUTERNAME% FAIL Yahoo Messenger >> Software_Scan.txt
if NOT defined filePath echo %COMPUTERNAME% PASS Yahoo Messenger >> Software_Scan.txt