Suppress Error Messages in PowerShell at get-package - windows

I use PowerShell to check, if specific apps are installed on the users PC:
$Application_MicrosoftEdge = get-package "Microsoft Edge" | % { $_.metadata['installlocation'] }
$Application_Microsoft365 = get-package *"Microsoft 365"* | % { $_.metadata['installlocation'] }
Write-Host "Microsoft Edge Path : $Application_MicrosoftEdge"
Write-Host "Microsoft 365 Path : $Application_Microsoft365"
This works quiet well, but if an application is not installed on the users PC, then an error message is shown (here "Microsoft 365" is not installed on the PC):
get-package : Für "*Microsoft 365*" wurde kein Paket gefunden.
In D:\Scripts\GetInstalledApp.ps1:12 Zeichen:32
+ $Application_Microsoft365 = get-package *"Microsoft 365"* | % { $ ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (Microsoft.Power...lets.GetPackage:GetPackage) [Get-Package], Exception
+ FullyQualifiedErrorId : NoMatchFound,Microsoft.PowerShell.PackageManagement.Cmdlets.GetPackage
I tried the following way, but here an error message is shown:
Try {
$Application_MicrosoftEdge = get-package "Microsoft Edge" | % { $_.metadata['installlocation'] }
$Application_Microsoft365 = get-package *"Microsoft 365"* | % { $_.metadata['installlocation'] }
}
Catch {
# Place action on error here
}
I also tried to wrap it in $( ... ) | out-null to suppress the error, but this is also not working.
`
Any idea?

The simplest way is to pass argument Ignore for common parameter -ErrorAction (-EA):
$Application_MicrosoftEdge = get-package "Microsoft Edge" -EA Ignore | % { $_.metadata['installlocation'] }
$Application_Microsoft365 = get-package *"Microsoft 365"* -EA Ignore | % { $_.metadata['installlocation'] }
if( $Application_MicrosoftEdge ) {
Write-Host "Microsoft Edge Path : $Application_MicrosoftEdge"
}
if( $Application_Microsoft365 ) {
Write-Host "Microsoft 365 Path : $Application_Microsoft365"
}
On error, the pipeline output will be empty, which converts to $false in a boolean context, which we test using the if statements.
Alternatively, as Abraham Zinala commented, to make try/catch working, pass argument Stop for common parameter -ErrorAction (-EA) or set the preference variable $ErrorActionPreference = 'Stop' at the beginning of your script:
try {
$Application_MicrosoftEdge = get-package "Microsoft Edge" -EA Stop | % { $_.metadata['installlocation'] }
$Application_Microsoft365 = get-package *"Microsoft 365"* -EA Stop | % { $_.metadata['installlocation'] }
}
catch {
# Place action on error here
}
Using -EA Stop, errors are turned into script-terminating errors (exceptions), which must be caught using try / catch, otherwise the script would end prematurely.
Note that this code skips the 2nd get-package call, if the 1st one failed! This is because execution flow jumps straight from the first error location into the catch block. To handle errors separately for both get-package calls, you'd need two try / catch blocks.

Related

How to get the List of Installed software

I am trying to get the availability of the software on pc. My condition is that I need to fetch whether the application is installed or not on my laptop if it is installed is it in working condition?
# Setting Execution policy for the Current User
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
$currentExecutionPolicy = Get-ExecutionPolicy
Write-Output "The Execution Ploicy is set to $currentExecutionPolicy"
$programFilePath = #(
'Google Chrome-C:\Program Files\Google\Chrome\Application\chrome.exe'
'Brackets Text Editor-C:\Program Files (x86)\Brackets\Brackets.exe'
'Microsoft Edge-C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
'Microsoft Excel-C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE'
#'Microsoft Outlook-C:\Program Files\Microsoft Office\root\Office16\OUTLOOK.EXE'
)
$result = foreach($program in $programFilePath) {
$splitString = $program -split ('-')
$program_name = $splitString[0]
$program_path = $splitString[1]
foreach($program in $program_path) {
if (Test-Path -Path $program) {
# Write-Output "Program Path Exists"
$programProcess = Start-Process -FilePath $program -PassThru -ErrorAction SilentlyContinue
timeout 5
try{
$myshell = New-Object -com "Wscript.Shell"
$myshell.sendkeys("{ENTER}")
timeout 1
$myshell = New-Object -com "Wscript.Shell"
$myshell.sendkeys("{ENTER}")
}
catch{
$runningProcess = Get-Process -Name $programProcess.ProcessName
}
if($runningProcess -eq $true) {
[pscustomobject]#{
Application_Name = $program_name
Application_Availability = 'Installed'
Application_Status = 'Working'
}
}
else {
[pscustomobject]#{
Application_Name = $program_name
Application_Availability = 'Installed'
Application_Status = 'Not Working. Error Log is generated as Application_Error_Log text file.'
}
Get-EventLog -LogName Application | Where-Object {$_.InstanceID -eq '1000'} | Tee-Object -FilePath .\Application_Error_Log.txt
}
<# Action to perform if the condition is true #>
} else {
[pscustomobject]#{
Application_Name = $program_name
Application_Availability = 'Not Installed'
Application_Status = $null
}
}
}
}
" :: System Software Audit Report :: " | Out-File .\System_Software_Details.txt
$result | Tee-Object -FilePath ".\System_Software_Details.txt" -Append
timeout 60
Although I am getting the application active which are working and functional but in my output in Text file application status shows : Application_Status = 'Not Working. Error Log is generated as although my application is working fine
My second concern is I am unable to handle the application which is giving me an error
$myshell = New-Object -com "Wscript.Shell"
$myshell.sendkeys("{ENTER}")
timeout 1
$myshell = New-Object -com "Wscript.Shell"
$myshell.sendkeys("{ENTER}")
I think checking filesystem paths is an option but a bad one - you cannot ensure in any case that the expected path is used. Checking the filesystem is only necessary for portable applications.
A better approach is to check the following registry keys, by doing so you get the same result as it is displayed under add/remove programs:
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall
Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -Name DisplayName,DisplayVersion,InstallSource,Publisher,UninstallString
Another way is to query wmi/cim:
Get-CimInstance -query "select * from win32_product"
But by doing so you generate foreach discovered software product an event in the windows event log (Log: Application, Source: MSIInstaller).
To verify if you can start successfully an application by using start-process you need also to specify the parameter -wait and then check if the return code is 0.
$runningProcess -eq $true doesn't necessarily work as $runningProcess is not a boolean but an object. Alas it always returns false.
TL;DR
If you look at your code you see that to get to "...Not Working..." you have to evaluate ($runningProcess -eq $true). Ergo it returns false.
There's always get-package.
$list = '*chrome*','*firefox*','*notepad++*'
$list | % { get-package $_ }
Name Version Source ProviderName
---- ------- ------ ------------
Google Chrome 104.0.5112.102 msi
Mozilla Firefox (x64 en-US) 104.0.1 Programs
Notepad++ (64-bit x64) 7.8.9 msi
Faster as an argument list and no wildcards:
get-package 'google chrome','mozilla firefox (x64 en-us)',
'notepad++ (64-bit x64)'
Or with the threadjob module:
$list = '*chrome*','*firefox*','*notepad++*'
$list | % { start-threadjob { get-package $using:_ } } |
receive-job -wait -auto

Delete an app on windows using powershell without querying win32

so I want to delete an app of which I do not know the product ID. I know you can delete an app by using msiexec.exe /x and then the product ID. How would I go about getting the product ID of a specific app also in commandline and storing the value in a variable so I can just place the variable in the delete command?
Thank in advance!!
There's always get-package. No one knows about it but me. This should work for msi installs.
get-package *software* | uninstall-package
If you know the exact name, this should work. Uninstall-package doesn't take wildcards.
uninstall-package 'Citrix HDX RealTime Media Engine 2.9.400'
Sometimes, annoyingly, it prompts to install nuget first:
install-packageprovider nuget -force
If it's not an msi install, but a programs install, it takes a little more string mangling. You may have to add a '/S' or something for silent install at the end.
$prog,$myargs = -split (get-package 'Remote Support Jump Client *' |
% { $_.metadata['uninstallstring'] })
& $prog $myargs
Maybe in this case just run it:
& "C:\Program Files (x86)\Citrix\Citrix WorkSpace 2202\TrolleyExpress.exe" /uninstall /cleanup /silent
Or
$uninstall = get-package 'Citrix Workspace 2202' |
% { $_.metadata['uninstallstring'] }
$split = $uninstall -split '"'
$prog = $split[1]
$myargs = -split $split[2]
$myargs += '/silent'
& $prog $myargs
I think I have a way that works with or without double-quotes for non-msi installs:
$uninstall = get-package whatever | % { $_.metadata['uninstallstring'] }
$prog, $myargs = $uninstall | select-string '("[^"]*"|\S)+' -AllMatches |
% matches | % value
$prog = $prog -replace '"',$null
$silentoption = '/S'
$myargs += $silentoption # whatever silent uninstall option
& $prog $myargs
Here's a script I use. You have to know what the display name of the package is...like if you went to Remove Programs, what it's name would be. It works for my MSI packages that I create with WiX, but not all packages. You might consider winget command if you are on Windows 10+. winget has an uninstall option.
param (
[Parameter(Mandatory = $true)]
[string] $ProductName,
[switch] $Interactive = $false,
[switch] $key = $false
)
$log_directory = "c:\users\public"
# $log_directory = "c:\erase\logs"
if ((-not $Interactive) -and (-not (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)))
{
$interactive = $true
# echo "Not elevated, needs to be interactive"
}
$found = $null
$productsKeyName = "SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products"
$rootKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($productsKeyName)
foreach ($productKeyName in $rootKey.GetSubKeyNames())
{
#$productKeyName
try
{
$installPropertiesKey = $rootKey.OpenSubKey("$productKeyName\InstallProperties")
if ($installPropertiesKey)
{
$displayName = [string] $installPropertiesKey.GetValue("DisplayName")
if ( (! [string]::IsNullOrEmpty($displayName)) -and ($displayName -eq $ProductName))
{
$found = $productKeyName
break
}
}
}
catch
{
}
finally
{
if ($installPropertiesKey) { $installPropertiesKey.Close() }
}
}
$rootKey.Close()
if (-not $found)
{
return "First search could not find $ProductName"
}
$localPackage = $null
if (! [string]::IsNullOrEmpty($found))
{
try
{
$regkey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey("$productsKeyName\$found\InstallProperties")
$localPackage = $regkey.GetValue("LocalPackage")
}
catch
{
}
finally
{
if ($regkey) { $regkey.Close() }
}
}
if ($key)
{
return "Found key: $found"
}
if (![string]::IsNullOrEmpty($localPackage) -and (Test-Path $localPackage))
{
$logflags = "/lv*"
$logname = (Join-Path $log_directory "$ProductName_uninstall.log")
$args = #($logflags, $logname, "/X", $localPackage)
if (!$Interactive) { $args += "/q" }
&msiexec $args
}
else
{
"Could not find uninstall package: $ProductName"
}

Script ran successfully but still software is not uninstalled

Even though the below script runs successfully to remove software, I
am still able to see the software in the control panel program list,
what I am doing wrong?
See the command below :
PS D:\Project> .\ScriptWithBelowCode.ps1 -ComputerName
"X-XXX50947" -AppGUID XXXXXX-A60B-4899-A369-XXXXXXX
Uninstallation command triggered successfully
[cmdletbinding()]
param (
[parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$ComputerName = $env:computername,
[parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
[string]$AppGUID
)
try {
$returnval = ([WMICLASS]"\\$computerName\ROOT\CIMV2:win32_process").Create("msiexec `/x$AppGUID `/norestart `/qn")
} catch {
write-error "Failed to trigger the uninstallation. Review the error message"
$_
exit
}
switch ($($returnval.returnvalue)){
0 { "Uninstallation command triggered successfully" }
2 { "You don't have sufficient permissions to trigger the command on $Computer" }
3 { "You don't have sufficient permissions to trigger the command on $Computer" }
8 { "An unknown error has occurred" }
9 { "Path Not Found" }
9 { "Invalid Parameter"}
}
Command to execute .\ScriptFileWithAboveContent.ps1 -ComputerName "X-15XX150947" -AppGUID 30F836D2-A60B-4899-A369-XXXXX884EAF
Command to get GUID
Get-WmiObject -Class Win32_Product ` -Filter "Name like '%Microsoft%'" | Select-Object -ExpandProperty IdentifyingNumber
Second try
$app = Get-WmiObject -Class Win32_Product | Where-Object {$_.IdentifyingNumber -match "XXXXXX-A60B-4899-XXXX-B0FXXX84EAF" }
$app.Uninstall()
I am getting error showing below
PS D:\Project> .\ScriptFileWithAboveContent.ps1
Exception calling "Uninstall" : "Operation is not valid due to the current state of the object."
At D:\MicrosoftAIM\ScriptFileWithAboveContent.ps1:2 char:5
+ $app.Uninstall()
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : WMIMethodException
Third try
The below script ran successfully, But still, the software is still in the program list.
echo "Getting product code"
$ProductCode = Get-WmiObject win32_product -Filter "Name like '%somesoftware%'" | Select-Object -Expand IdentifyingNumber
echo "removing Product"
# Out-Null argument is just for keeping the power shell command window waiting for msiexec command to finish else it moves to execute the next echo command
& msiexec /x $ProductCode | Out-Null
echo "uninstallation finished"
PS D:\MicrosoftAIM> .\ScriptWithAboveContent.ps1
Getting product code
removing Product
uninstallation finished
PS D:\MicrosoftAIM> Get-WmiObject -Class Win32_Product ` -Filter "Name like '%Microsoft%azure%info%'"

IIIS WAS process cannot be stopped via Powershell

On a Windows Server 2008 R2, 64 bit-machine I am running the following code:
$global:arrServer = #("ph1", "ph2", "ph3")
$global:arrDienste = #("W3SVC", "WAS", "IISADMIN")
$global:strPfad = "D:\WASLogs\"
$global:strLogTime = Get-Date -Format "yyyy-MM-dd--hh-mm-ss"
$global:strLogDatei = $global:strPfad + "WARTUNG--" + $global:strLogTime + ".log"
Log_Abfrage_und_Generierung
Dienste_Stop
Function Dienste_Stop
{
echo "Stop of the services successful?" | Out-File $global:strLogDatei -Append -Force
foreach($strServer in $global:arrServer)
{
$strInterim2 = $strServer + " (" + $global:appServerNamen + ")"
echo " " $strInterim2 | Out-File $global:strLogDatei -Append -Force
foreach($strDienst in $global:arrDienste)
{
$objWmiService = Get-Wmiobject -Class "win32_service" -computer $strServer -filter "name = '$strDienst'"
if( $objWmiService.State )
{
$rtnWert = $objWmiService.stopService()
Switch ($rtnWert.returnvalue)
{
0 { echo "$strDienst stopped!" | Out-File $global:strLogDatei -Append -Force }
2 { echo "$strDienst throws: 'Access denied!'" | Out-File $global:strLogDatei -Append -Force }
3 { echo "Service $strDienst is not existing on $strServer!" | Out-File $global:strLogDatei -Append -Force }
5 { echo "$strDienst already stopped!" | Out-File $global:strLogDatei -Append -Force }
DEFAULT { echo "$strDienst service reports ERROR $($rtnWert.returnValue)" | Out-File $global:strLogDatei -Append -Force }
}
}
else
{
echo "Service $strDienst is not existing on $strServer!" | Out-File $global:strLogDatei -Append -Force
}
}
}
}
Function Log_Abfrage_und_Generierung
{
if([IO.Directory]::Exists($global:strPfad))
{
echo "Nothing happening here."
}
else
{
New-Item -ItemType directory -path $global:strPfad
}
}
This can be reproduced on all computers ph1, ph2 and ph3. However with some other code, WAS can be started, respectively the status can be seen.
Also to note:
All other services can be stopped? Does it has to do with the fact that the path for the WAS is like this? C:\Windows\system32\svchost.exe -k iissvcs
I use WMI on purpose.
What is going on here?
Tia
The problem could be that there are multiple services that depend on WAS which need to be stopped first. The StopService() method does not have an overload to stop dependent services. If this doesn't solve the issue check the response code from StopService to determine the problem in the link above.
It looks like you are handling the code 3 as 'service does not exist'. The docs show this code actually means 'The service cannot be stopped because other services that are running are dependent on it.'
Not sure why you're determined to use WMI when this capability is fully baked into powershell
Stop-Service WAS -Force

PsExec Throws Error Messages, but works without any problems

So we are using PsExec a lot in our automations to install virtual machines, as we can't use ps remote sessions with our windows 2003 machines. Everything works great and there are no Problems, but PsExec keeps throwing errors, even every command is being carried out without correctly.
For example:
D:\tools\pstools\psexec.exe $guestIP -u $global:default_user -p $global:default_pwd -d -i C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -command "Enable-PSRemoting -Force"
Enables the PsRemoting on the guest, but also throws this error message:
psexec.exe :
Bei D:\Scripts\VMware\VMware_Module5.ps1:489 Zeichen:29
+ D:\tools\pstools\psexec.exe <<<< $guestIP -u $global:default_user -p $global:default_pwd -d -i C:\Windows\System32\WindowsPowerShell\
v1.0\powershell.exe -command "Enable-PSRemoting -Force"
+ CategoryInfo : NotSpecified: (:String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError
PsExec v1.98 - Execute processes remotely
Copyright (C) 2001-2010 Mark Russinovich
Sysinternals - www.sysinternals.com
Connecting to 172.17.23.95...Starting PsExec service on 172.17.23.95...Connecting with PsExec service on 172.17.23.95...Starting C:\Windows\
System32\WindowsPowerShell\v1.0\powershell.exe on 172.17.23.95...
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe started on 172.17.23.95 with process ID 2600.
These kinds of error messages apear ALWAYS no matter how i use psexec, like with quotes, with vriables/fixed values, other flags, etc. Does anybody has an idea how i could fix this? It is not a real problem, but it makes finding errors a pain in the ass, because the "errors" are everywhere. Disabling the error messages of psexec at all would also help...
This is because PowerShell sometimes reports a NativeCommandError when a process writes to STDERR. PsExec writes the infoline
PsExec v1.98 - Execute processes remotely
Copyright (C) 2001-2010 Mark Russinovich
Sysinternals - www.sysinternals.com
to STDERR which means it can cause this.
For more information, see these questions / answers:
https://stackoverflow.com/a/1416933/478656
https://stackoverflow.com/a/11826589/478656
https://stackoverflow.com/a/10666208/478656
redirect stderr to null worked best for me. see below link
Error when calling 3rd party executable from Powershell when using an IDE
Here's the relevant section from that link:
To avoid this you can redirect stderr to null e.g.:
du 2> $null
Essentially the console host and ISE (as well as remoting) treat the stderr stream differently. On the console host it was important for PowerShell to support applications like edit.com to work along with other applications that write colored output and errors to the screen. If the I/O stream is not redirected on console host, PowerShell gives the native EXE a console handle to write to directly. This bypasses PowerShell so PowerShell can't see that there are errors written so it can't report the error via $error or by writing to PowerShell's stderr stream.
ISE and remoting don't need to support this scenario so they do see the errors on stderr and subsequently write the error and update $error.
.\PsExec.exe \$hostname -u $script:userName -p $script:password /accepteula -h cmd /c $powerShellArgs 2> $null
I have created a psexec wrapper for powershell, which may be helpful to people browsing this question:
function Return-CommandResultsUsingPsexec {
param(
[Parameter(Mandatory=$true)] [string] $command_str,
[Parameter(Mandatory=$true)] [string] $remote_computer,
[Parameter(Mandatory=$true)] [string] $psexec_path,
[switch] $include_blank_lines
)
begin {
$remote_computer_regex_escaped = [regex]::Escape($remote_computer)
# $ps_exec_header = "`r`nPsExec v2.2 - Execute processes remotely`r`nCopyright (C) 2001-2016 Mark Russinovich`r`nSysinternals - www.sysinternals.com`r`n"
$ps_exec_regex_headers_array = #(
'^\s*PsExec v\d+(?:\.\d+)? - Execute processes remotely\s*$',
'^\s*Copyright \(C\) \d{4}(?:-\d{4})? Mark Russinovich\s*$',
'^\s*Sysinternals - www\.sysinternals\.com\s*$'
)
$ps_exec_regex_info_array = #(
('^\s*Connecting to ' + $remote_computer_regex_escaped + '\.{3}\s*$'),
('^\s*Starting PSEXESVC service on ' + $remote_computer_regex_escaped + '\.{3}\s*$'),
('^\s*Connecting with PsExec service on ' + $remote_computer_regex_escaped + '\.{3}\s*$'),
('^\s*Starting .+ on ' + $remote_computer_regex_escaped + '\.{3}\s*$')
)
$bypass_regex_array = $ps_exec_regex_headers_array + $ps_exec_regex_info_array
$exit_code_regex_str = ('^.+ exited on ' + $remote_computer_regex_escaped + ' with error code (\d+)\.\s*$')
$ps_exec_args_str = ('"\\' + $remote_computer + '" ' + $command_str)
}
process {
$return_dict = #{
'std_out' = (New-Object 'system.collections.generic.list[string]');
'std_err' = (New-Object 'system.collections.generic.list[string]');
'exit_code' = $null;
'bypassed_std' = (New-Object 'system.collections.generic.list[string]');
}
$process_info = New-Object System.Diagnostics.ProcessStartInfo
$process_info.RedirectStandardError = $true
$process_info.RedirectStandardOutput = $true
$process_info.UseShellExecute = $false
$process_info.FileName = $psexec_path
$process_info.Arguments = $ps_exec_args_str
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $process_info
$process.Start() | Out-Null
$std_dict = [ordered] #{
'std_out' = New-Object 'system.collections.generic.list[string]';
'std_err' = New-Object 'system.collections.generic.list[string]';
}
# $stdout_str = $process.StandardOutput.ReadToEnd()
while ($true) {
$line = $process.StandardOutput.ReadLine()
if ($line -eq $null) {
break
}
$std_dict['std_out'].Add($line)
}
# $stderr_str = $process.StandardError.ReadToEnd()
while ($true) {
$line = $process.StandardError.ReadLine()
if ($line -eq $null) {
break
}
$std_dict['std_err'].Add($line)
}
$process.WaitForExit()
ForEach ($std_type in $std_dict.Keys) {
ForEach ($line in $std_dict[$std_type]) {
if ((-not $include_blank_lines) -and ($line -match '^\s*$')) {
continue
}
$do_continue = $false
ForEach ($regex_str in $bypass_regex_array) {
if ($line -match $regex_str) {
$return_dict['bypassed_std'].Add($line)
$do_continue = $true
break
}
}
if ($do_continue) {
continue
}
$exit_code_regex_match = [regex]::Match($line, $exit_code_regex_str)
if ($exit_code_regex_match.Success) {
$return_dict['exit_code'] = [int] $exit_code_regex_match.Groups[1].Value
} elseif ($std_type -eq 'std_out') {
$return_dict['std_out'].Add($line)
} elseif ($std_type -eq 'std_err') {
$return_dict['std_err'].Add($line)
} else {
throw 'this conditional should never be true; if so, something was coded incorrectly'
}
}
}
return $return_dict
}
}

Resources