powershell input string not in correct format - powershell

I am having a bit of an annoying problem that I just can't seem to find the answer to. I am using a powershell script to uninstall programs and a batch file to run the script on the programs I want removed in a particular order. I found the script on the web and it works perfectly on my test machine. The problem begins when I run the script on another machine.
This is the error I get:
Error formatting a string: Input string was not in a correct format..
At C:\Users\currentuser\Desktop\uninstallScript.ps1:60 char:1
+ &msiexec `/qn `/x `{$stringer`}
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: ({{0}}}:String) [],
RuntimeException
+ FullyQualifiedErrorId : FormatError
And here is the PowerShell script:
###########################################
######## Written by DC 2012-02-13#########
###########################################
<#
.SYNOPSIS
Uninstalls software by only passing the Software Title.
Should work with all msiexec string uninstallers.
For uninstall commands that end in uninstall.exe or helper.exe a "/S" is
used as a switch.
.PARAMETER DisplayName
The complete or partial name of the software being uninstalled. Must appear
as shown in add / remove programs (case insenstive).
.EXAMPLE
Uninstall-Program Java
Will search the registry and uninstall all instances of Java from a machine.
#>
[cmdletBinding()]
Param
(
[String]$DisplayName = $(throw "DisplayName is Required")
)
Set-Variable -Name ThirtyMachine -Value
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Option Constant
Set-Variable -Name SixtyMachine -Value
"HKLM:\SOFTWARE\WOW6432NODE\Microsoft\Windows\CurrentVersion\Uninstall" -
Option Constant
Set-Variable -Name ThirtyUser -Value
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Option Constant
Set-Variable -Name SixtyUser -Value
"HKCU:\SOFTWARE\WOW6432NODE\Microsoft\Windows\CurrentVersion\Uninstall" -
Option Constant
$regs = $ThirtyMachine,$SixtyMachine,$ThirtyUser,$SixtyUser
foreach ($reg in $regs)
{
if(Test-Path $reg)
{
$SubKeys = Get-ItemProperty "$reg\*"
}
else
{
$SubKeys = $null
}
foreach($key in $SubKeys)
{
if($key.DisplayName -match "$DisplayName")
{
Write-Host "Found Software " $key.DisplayName
if($key.UninstallString -match "^msiexec")
{
$startGUID = $key.UninstallString.IndexOf("{") + 1
$endGuid = $key.UninstallString.IndexOf("}") - $startGUID
$stringer = $key.UninstallString.Substring($startGUID,$endGuid)
Write-Host "Uninstaller Known, now uninstalling"
&msiexec `/qn `/x `{$stringer`}
}
if($key.UninstallString.Replace('"',"") -match 'uninstall.exe\Z' -or
$key.UninstallString.replace('"',"") -match 'helper.exe\Z' )
{
$stringer = $key.UninstallString.Replace('"',"")
if(Test-Path $stringer )
{
Write-Host "Possible Uninstaller found. Trying" $key.UninstallString "/S"
&$stringer /S
}
}
}
}
}
I debugged the code in PowerShell ISE to verify that the variable $stringer contained the correct value. So I believe this has something to do with the version of powershell being run on the 2 machines. The test machine, where the script works, is version 2 whereas the other machine, where I get the error, is version 4. I've barely begun to learn powershell so I am scratching my head at this one. Hopefully it is simple to resolve. I appreciate any help offered.

The escape characters ``` in your msiexec call are causing the error and shouldn't be there. Here's a way to call it how you are properly:
& msiexec /qn "/x{$stringer}"
Alternatively, you can utilize Start-Process and wait on the process:
Start-Process -FilePath 'msiexec' -ArgumentList #('/qn','/x',"{$stringer}") -Wait

Related

Azure Pipelines: How to do successful BITS-Transfer in Powershell task?

Can't find the answer for this anywhere.
I need my pipeline to call a powershell script that has to do a BITS-Transfer multiple times. At first the script would always exit with this error:
Start-BitsTransfer : The operation being requested was not performed because the user has not
logged on to the
network. The specified service does not exist. (Exception from HRESULT: 0x800704DD)
Then, in the Powershell task, instead of calling the file, I changed it to do this inline script:
powershell -File $(Build.Repository.LocalPath)\installer\assembly.win10.x64.ps1 -Credential Get-Credential [Domain]\[Username]
Now the script runs, but I get this in my log in Azure Pipelines:
Microsoft Visual C++ 2015 Redistributable x64 : downloading...
Start-BitsTransfer : The operation being requested was not performed because the user has not
logged on to the
network. The specified service does not exist. (Exception from HRESULT: 0x800704DD)
At C:\agents\_work\3\s\installer\assembly.win10.x64.ps1:9 char:5
+ Start-BitsTransfer -Source $Url -Destination $Target
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Start-BitsTransfer], COMException
+ FullyQualifiedErrorId :
System.Runtime.InteropServices.COMException,Microsoft.BackgroundIntelligentTransfer.Management.NewBitsTransferCommand
Microsoft Visual C++ 2015 Redistributable x64 : download completed
The pipeline passes, but when I check the actual redistributable's .exe, it has a size of 0 bytes. So, it seems the folders are made and zipped, and the files are where they need to be, but the download didn't actually work. I also tested the powershell script on my machine locally, and there are no problems. How can I fix this?
Here is the relevant code. First three blocks are the download-related methods, then lastly is the method call:
function Get-FileSha256([string]$Src) {
return (Get-FileHash "$Src" -Algorithm SHA256).Hash
}
function Download-File([string]$Comment, [string]$Url, [string]$Target) {
Write-Host "$Comment : downloading..."
New-Item -ItemType File -Path "$Target" -Force | Out-Null
Import-Module BitsTransfer
Start-BitsTransfer -Source $Url -Destination $Target
Write-Host "$Comment : download completed"
}
function Download-FileIfNecessary([string]$Comment, [string]$Url,
[string]$Target, [string]$Sha256) {
If (!(Test-Path $Target)) {
Download-File "$Comment" "$Url" "$Target"
return
}
$TargetSha256 = Get-FileSha256 $Target
If ($TargetSha256 -eq $Sha256) {
Write-Host "$Comment : already downloaded"
}
Else {
Download-File "$Comment" "$Url" "$Target"
}
}
Download-FileIfNecessary "Microsoft Visual C++ 2015 Redistributable x64" `
"https://download.microsoft.com/download/6/A/A/6AA4EDFF-645B-48C5-81CC-ED5963AEAD48/vc_redist.x64.exe" `
"$target_downloaded_vc_redist_dir\2015.v14.0.24215\vc_redist.x64.exe" `
"da66717784c192f1004e856bbcf7b3e13b7bf3ea45932c48e4c9b9a50ca80965"
I'm going to offer an alternative to using BITS, as BITS is designed to be used when the requestor is a local authenticated user, not by remote sessions, service accounts, or other non-interactive contexts. I've re-written Download-File to make use of Invoke-WebRequest instead ofStart-BitsTransfer:
Function Download-File {
Param(
[string]$Comment,
[string]$Url,
[string]$Target
)
Write-Host "$Comment : downloading..."
# We need to check that the target folder exists later
$targetFolder = Split-Path -Parent $Target
# Prepare cmdlet arguments for splatting
$iwrArgs = #{
UseBasicParsing = $true
Uri = $Url
OutFile = $Target
}
$newDirArgs = #{
ItemType = 'Directory'
Path = $targetFolder
}
# Create the target parent dir, if it doesn't exist
if ( !( Test-Path -PathType Container $targetFolder ) ) {
Write-Host "Creating directory $targetFolder"
New-Item #newDirArgs | Out-Null
}
# Workaround progress bar performance bug with Invoke-WebRequest
# (also affects Invoke-RestMethod) by disabling it
$OldProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
# Use a try / finally block to guarantee the
# original progress preference is changed back
try {
Invoke-WebRequest #iwrArgs -EA Stop
}
finally {
$ProgressPreference = $OldProgressPreference
}
}
I've commented the code if you're interested in what each piece is doing. I can clarify if there are any questions.
You should be able to drop this in place of your current Download-File definition. I would recommend this over trying hacky OS tricks to make BITS work in a context it's not designed to. BITS is also not recommended for use with critical automation, as it downloads can be suspended by the OS at any time (e.g. when updates are being downloaded).
Note that what I've said is true for non-interactive logins. BITS should be safe to use from a scheduled task or user-run script, just not from a Windows service (which the Azure Agent is).

Take Screenshot with Firefox

I'm having a heck of a time figuring out why this simple command is not working.
I'm attempting to take screenshots of a list of domains using PowerShell and Firefox per [this article][1].
Currently I have the following code, but it does not produce screenshots and I'm unsure what is wrong code wise. Any assistance and/or a point in the correct direction is greatly appreciated.
$screenshotdir = "$PSScriptRoot\FF_Screenshots"
If(!(Test-Path -Path $screenshotdir)) {New-Item -Path $PSScriptRoot -Name "FF_Screenshots" -ItemType Directory}
function getFireFoxScreenShot() {
$importedCSV = Import-Csv .\Domains.csv
foreach ($url in $importedCSV) {
$domainName = $url.Name #example google.com
$domain = $url.Domain #example google (no tld)
if (-not ([string]::IsNullOrEmpty($domainName))){
Echo "Getting Screen Shot for: $domainName"
Start-Process -FilePath "C:\Program Files\Mozilla Firefox\firefox.exe " -ArgumentList " --screenshot $screenshotdir\$domain.png ", "$domainName" -Wait
}
}
}
getFireFoxScreenShot
[1]: https://www.bleepingcomputer.com/news/software/chrome-and-firefox-can-take-screenshots-of-sites-from-the-command-line/
Make sure to specify protocol (https:// or http://) as it is in the article you linked to:
# Tested with Developer Edition of Firefox
$domain = "example"
$domainName = example.com"
$screenshotdir = "C:\SO\56572800"
# This works
Start-Process -FilePath "C:\Program Files\Firefox Developer Edition\firefox.exe" -ArgumentList "--screenshot $screenshotdir\$domain-with-https.png", "https://$domainName" -Wait
# But doesn't work
Start-Process -FilePath "C:\Program Files\Firefox Developer Edition\firefox.exe " -ArgumentList " --screenshot $screenshotdir\$domain-no-https.png ", "$domainName" -Wait
From what I checked, if you don't specify https:// prefix (or http:// if applicable), it'll hang for a long time so you might have an impression that it's working.
As #lloyd mentioned in comments, you have to make sure that value of $screenshotdir is properly assigned and available to the function.
Also, it's a good practice to trim leading/trailing spaces from your command, even though in your example it still works with the spaces. I mean these ones:
HERE | HERE | HERE |
Start-Process -FilePath "C:\Program Files\Mozilla Firefox\firefox.exe " -ArgumentList " --screenshot $screenshotdir\$domain.png ", "$domainName" -Wait

Add logging quickly to an existing set of powershell scripts

I've got a collection of powershell scripts, some of which call others. Some of these subscripts can also be called on their own as needed. How can I quickly add logging to all of the scripts so that any script invocation results in a log file for later examination?
There are a number of questions dealing with logging with some great answers, like this one. But I wanted to see what we could come up with that:
required minimal touching of the existing powershell files
automatically dealt with script A.ps1 calling script B.ps1. If you call
A.ps1, A.ps1 needs to start and finish the logging. But if you call B.ps1
directly, B.ps1 does.
I came up with my answer below, and wanted to share and see if there were other ideas on how to approach this, or suggestions for improvement on my answer.
The support code I write (further down) allows for just adding the following to each ps1 file. It automatically gives me logging regardless of if a script is called at top-level or by another script:
#any params for script
. "$PSScriptRoot\ps_support.ps1"
StartTranscriptIfAppropriate
try
{
#all of the original script
}
finally
{
ConditionalStopTranscript
}
The code that powers this is in ps_support.ps1, sitting next to my collection of powershell files that need logging. It uses Get-Variable and Set-Variable to manipulate a couple variables at the caller's scope level:
Logging__TranscriptStarted is normal so sub-scopes can see that
logging is already happening and not try to start it again.
Logging__TranscriptStartedPrivate is private so a scope can know if
it is responsible for stopping the logging.
Here is ps_support.ps1:
Set-Variable -name TranscriptStartedPropertyName -opt ReadOnly -value 'Logging__TranscriptStarted'
Set-Variable -name TranscriptStartedPrivatePropertyName -opt ReadOnly -value 'Logging__TranscriptStartedPrivate'
function StartTranscriptIfAppropriate
{
$transcriptStarted = [bool](Get-Variable -name $TranscriptStartedPropertyName -ErrorAction Ignore)
if (-not $transcriptStarted)
{
$callstack = get-pscallstack
$fullPath = $callstack[$callstack.count-2].ScriptName
$name = Split-Path -Path $fullPath -Leaf
$directory = Split-Path -Path $fullPath
$logDirectory = [IO.Path]::GetFullPath("$directory\..\scripts_logs")
md -force $logDirectory | out-null
$logFinalPath = "$logDirectory\$(Get-Date -Format o | foreach {$_ -replace ":", "."})_$name.log"
Set-Variable -scope 1 -name $TranscriptStartedPropertyName -value $True
Set-Variable -scope 1 -option private -name $TranscriptStartedPrivatePropertyName -value $True
Start-Transcript $logFinalPath | Write-Host
}
$immediateCallerPath = Get-Variable -scope 1 -name PSCommandPath -ValueOnly
Write-Host "Starting script at $immediateCallerPath"
}
function ConditionalStopTranscript
{
$immediateCallerPath = Get-Variable -scope 1 -name PSCommandPath -ValueOnly
Write-Host "Stopping script at $immediateCallerPath"
$transcriptStartedByMe = [bool](Get-Variable -scope 1 -name $TranscriptStartedPrivatePropertyName -ErrorAction Ignore)
if ($transcriptStartedByMe)
{
Stop-Transcript | Write-Host
}
}

How Do I run Powershell x86 from Powershell?

I read this answer: How to Open Powershell from Powershell
start powershell
This opens the base, large resolution PS instance. How do I open PS(x86)?
Start-Process $Env:WINDIR\SysWOW64\WindowsPowerShell\v1.0\powershell.exe
I recommend Caleb's answer. But personally, I have a function in the PowerShell profile that loads on startup and launches a new PowerShell x86 shell when running x86 as this is so commonly required.
Function x86{
Start-Process $($env:SystemRoot + "\syswow64\WindowsPowerShell\v1.0\powershell.exe")
}
NB: $env:windir and $env:SystemRoot are equivalent here. Maybe not always
For a quick fix I think this solution will help you
start 'C:\Users\(Your-username here)\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Windows PowerShell\Windows PowerShell (x86).lnk'
Please note this is just a quick fix.
The following code will Dynamically switch Powershell to run in 64-bit mode
if ($env:PROCESSOR_ARCHITEW6432 -eq "AMD64") {
    write-warning "Y'arg Matey, we're off to 64-bit land....."
    if ($myInvocation.Line) {
        &"$env:WINDIR\sysnative\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile $myInvocation.Line
    }else{
        &"$env:WINDIR\sysnative\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile -file "$($myInvocation.InvocationName)" $args
    }
exit $lastexitcode
}
 
 
write-host "Main script body"
You will need the complete path to the x86 Powershell executable. If you are launching it from the command prompt (CMD.EXE), you would use
start "" "%SystemRoot%\SysWOW64\WindowsPowerShell\v1.0\powershell.exe"
If you were starting it from a PowerShell session, you would use
start "" "$env:SystemRoot\SysWOW64\WindowsPowerShell\v1.0\powershell.exe"
or
Start-Process -FilePath "$env:SystemRoot\SysWOW64\WindowsPowerShell\v1.0\powershell.exe"
When I last had to run a 32-bit version of PowerShell it was for something specific (there was no 64-bit version of a DLL that I needed to access, reference: View All Certificates On Smart Card). When that was the case I simply executed the needed code as a background job using the -RunAs32 switch for New-Job. Full code that I ended up using can be found in the referenced question, but the general concepts are:
$RunAs32Bit = {
Do some stuff that requires 32-bit
}
#Run the code in 32bit mode if PowerShell isn't already running in 32bit mode
If($env:PROCESSOR_ARCHITECTURE -ne "x86"){
Write-Warning "Non-32bit architecture detected, collecting certificate information in separate 32bit process."
$Job = Start-Job $RunAs32Bit -RunAs32
$SCStore = $Job | Wait-Job | Receive-Job
}Else{
$SCStore = $RunAs32Bit.Invoke()
}
Download PSExec
Then, run this in PowerShell: PATH_TO_PSEXEC\psexec.exe -i powershell
The core structure including passing of parameters in either scenario is given below
Param(
[String] $Param1 =#("Param1"),
[String] $Param2 =#("Param2")
)
$ScriptLocation = Split-Path $script:MyInvocation.MyCommand.Path -Parent
Write-Host $ScriptLocation
$RunAs32Bit = {
Param(
[String] $Param1 =#("Param1"),
[String] $Param2 =#("Param2")
)
...
return $Result
}
#Run the code in 32bit mode if PowerShell isn't already running in 32bit mode
If($env:PROCESSOR_ARCHITECTURE -ne "x86"){
Write-Warning "Non-32bit architecture detected, processing original request in separate 32bit process."
$Job = Start-Job $RunAs32Bit -RunAs32 -ArgumentList ($Param1, $Param2, $ScriptLocation)
$Result = $Job | Wait-Job | Receive-Job
}Else{
$Result = Invoke-Command -ScriptBlock $RunAs32Bit -ArgumentList ($Param1, $Param2, $ScriptLocation)
}

Powershell to install .\file errors, but when given full location C:\Users.... doesn't error

I have a script below that errors when trying to access a file, however if I change the location of the .msi file in the -argumentlist to a full address it succeeds, but I can't have it run like that as the address will change when I submit it to be packaged for SCCM deployment.
Function Get-OSCComputerOU
{
$ComputerName = $env:computername
$Filter = "(&(objectCategory=Computer)(Name=$ComputerName))"
$DirectorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$DirectorySearcher.Filter = $Filter
$SearcherPath = $DirectorySearcher.FindOne()
$DistinguishedName = $SearcherPath.GetDirectoryEntry().DistinguishedName
$OUName = ($DistinguishedName.Split(","))[1]
$OUMainName = $OUName.SubString($OUName.IndexOf("=")+1)
$OUMainName
}
$strOU = Get-OSCComputerOU
$strTrueOU=$strOU.split('_')[1]
$strCSV=Import-Csv \\SERVER\SHARE\FOLDER\CSV.csv
$strRoomChannel=$strCSV | where {$_.Room -eq $strTrueOU} | % channel
IF ($strRoomChannel){
$strRoomFoundArg="/i .\Installers\MSI.msi CHANNEL=$strRoomChannel"
Start-Process msiexec -ArgumentList $strRoomFoundArg -wait
} ELSE {
msg * "Channel is missing, and can not install correctly, please call tech support on Ext: to have this rectified, it's a quick fix."
}
When I use a full address such as below, it installs fine.....what's the deal.
Function Get-OSCComputerOU
{
$ComputerName = $env:computername
$Filter = "(&(objectCategory=Computer)(Name=$ComputerName))"
$DirectorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$DirectorySearcher.Filter = $Filter
$SearcherPath = $DirectorySearcher.FindOne()
$DistinguishedName = $SearcherPath.GetDirectoryEntry().DistinguishedName
$OUName = ($DistinguishedName.Split(","))[1]
$OUMainName = $OUName.SubString($OUName.IndexOf("=")+1)
$OUMainName
}
$strOU = Get-OSCComputerOU
$strTrueOU=$strOU.split('_')[1]
$strCSV=Import-Csv \\SERVER\SHARE\FOLDER\CSV.csv
$strRoomChannel=$strCSV | where {$_.Room -eq $strTrueOU} | % channel
IF ($strRoomChannel){
$strRoomFoundArg="/i C:\Users\USERNAME\Desktop\Installers\MSI.msi CHANNEL=$strRoomChannel"
Start-Process msiexec -ArgumentList $strRoomFoundArg -wait
} ELSE {
msg * "Channel is missing, and can not install correctly, please call tech support on Ext: to have this rectified, it's a quick fix."
}
I get this error:
The difference between the two is that '.' is going to be resolved by the process you are calling, msiexec, which, like most processes, is going to use the process's CurrentDirectory for '.', which is different than the current location in PowerShell. You can see the difference if you compare Get-Location and [Environment]::CurrentDirectory] in PowerShell. They will be different if you start powershell and change the directory using Set-Location (aka cd).
The solution is to resolve the path in PowerShell before sending it over to msiexec:
$path = Convert-Path .\Installers\MSI.msi
$strRoomFoundArg = "/i `"$path`" CHANNEL=$strRoomChannel"
Start-Process msiexec -ArgumentList $strRoomFoundArg -wait
Turns out the script wasn't happy with the .\ in front of the MSI file.
If I kept the .\ I would get the error.
If I removed the .\ and just had MSI.msi then it worked fine.
I failed to mention that I had changed the active directory to my desktop to execute the script, my apologies #mike z
Thank you very much for your input however.