Unable to delete a directory with square brackets in name using WinSCP and PowerShell - powershell

I am trying to write a PowerShell script that automatically deletes empty directories on our FTP server. I don't have any direct access to the server on which the directories reside - I can only access them via FTP. For this reason, I have written a PowerShell script that uses WinSCP .NET assembly to (try to) delete the empty directories. However, I have a problem, in that many of the directories on the server have square brackets in the directory name.
For example, the directory name might be called: [a]153432
There are lots of these directories, hence my desire to write a script to delete them. This occurs because they've been created by a program that uses a number to create the directories it requires. In order to work on this program, I created an empty directory
/latest/[b]test
My program goes like this:
# program to test deletion of directories with square-brackets in the name.
$HOSTNAME="myftpservername"
$USERNAME="myftpusername"
$PASSWORD="myftppassword"
$DLL_LOCATION="C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
$LOCAL_DIRECTORY="C:\testdir\extended\baseroot"
# Load WinSCP .NET assembly
[Reflection.Assembly]::LoadFrom($DLL_LOCATION) | Out-Null
# Session.FileTransferred event handler
try
{
$sessionOptions = New-Object WinSCP.SessionOptions
$sessionOptions.Protocol = [WinSCP.Protocol]::ftp
$sessionOptions.HostName = $HOSTNAME
$sessionOptions.UserName = $USERNAME
$sessionOptions.Password = $PASSWORD
$session = New-Object WinSCP.Session
try
{
# Connect
$session.Open($sessionOptions)
$remoteFileWithUnixPath = "/latest/[b]test"
$removalResult = $session.RemoveFiles($remoteFileWithUnixPath)
if ($removalResult.IsSuccess)
{
Write-Host ("Removal of remote file {0} succeeded" -f $remoteFileWithUnixPath)
}
else
{
Write-Host ("Removal of remote file {0} failed" -f $remoteFileWithUnixPath)
}
}
finally
{
# Disconnect, clean up
$session.Dispose()
}
exit 0
}
catch [Exception]
{
Write-Host $_.Exception.Message
exit 1
}
When I run it, it displays the following message:
PS C:\Users\dbuddrige\Documents\dps> .\delete-squarebracket-dir.ps1
Removal of remote file /latest/[b]test succeeded
However, the directory is not deleted.
I have also tried specifying the directory name with delimiters such as:
$remoteFileWithUnixPath = "/latest/\[b\]test"
But this also fails (but at least it says so):
PS C:\Users\dbuddrige\Documents\dps> .\delete-squarebracket-dir.ps1
Removal of remote file /latest/\[b\]test failed
BUT, if I change the directory name [and the variable $remoteFileWithUnixPath] to something like
/latest/foo
And then re-run the program, it deletes the directory /latest/foo just fine.
Does anyone have any ideas what I need to do to get this to work?
The answer to this question was pointed out by Martin Prikryl [Thanks!]
The fully working code follows:
# program to test deletion of directories with square-brackets in the name.
$HOSTNAME="myftpservername"
$USERNAME="myftpusername"
$PASSWORD="myftppassword"
$DLL_LOCATION="C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
$LOCAL_DIRECTORY="C:\testdir\extended\baseroot"
# Load WinSCP .NET assembly
[Reflection.Assembly]::LoadFrom($DLL_LOCATION) | Out-Null
# Session.FileTransferred event handler
try
{
$sessionOptions = New-Object WinSCP.SessionOptions
$sessionOptions.Protocol = [WinSCP.Protocol]::ftp
$sessionOptions.HostName = $HOSTNAME
$sessionOptions.UserName = $USERNAME
$sessionOptions.Password = $PASSWORD
$session = New-Object WinSCP.Session
try
{
# Connect
$session.Open($sessionOptions)
$remoteFileWithUnixPath = "/latest/[b]test"
$removalResult =
$session.RemoveFiles($session.EscapeFileMask($remoteFileWithUnixPath))
if ($removalResult.IsSuccess)
{
Write-Host ("Removal of remote file {0} succeeded" -f $remoteFileWithUnixPath)
}
else
{
Write-Host ("Removal of remote file {0} failed" -f $remoteFileWithUnixPath)
}
}
finally
{
# Disconnect, clean up
$session.Dispose()
}
exit 0
}
catch [Exception]
{
Write-Host $_.Exception.Message
exit 1
}

Some methods of the WinSCP .NET assembly, including the the Session.RemoveFiles, accept a file mask, not a simple path.
Square brackets have special meaning in the file mask.
You should always use the RemotePath.EscapeFileMask method on file paths, before passing them to assembly methods.

I'd use the escape sequence in the commandline:
Remove-Item 'C:\Scripts\Test`[1`].txt'
Or use the -LiteralPath parameter.
Remove-Item -LiteralPath C:\scripts\test[1].txt
The -LiteralPath parameter does not try to convert the string.

Related

Create Receive Location and Send Port in Existing BizTalk Application using Powershell

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.

Visual studio Build Task Issue for PowerShell inline task - Azure

I am running a vsts build inline PowerShell script task to create package for Azure cloud service. It works fine and create package file from my local machine, but when I try to run from VSTS PowerShell inline task it gives error :
##[error]Cannot find path ‘D:\a_tasks\InlinePowershell_31f040e5-e040-4336-878a-59a493355534\1.1.6\ServiceConfiguration.Cloud.Test.cscfg’ because it does not exist.
Here is my PowerShell inline script below, It fails on the following line:
Copy-Item $serviceConfigurationPath $packageOutDir
I really appreciate your help on this.
Thanks,
# This is the VSTS repository path
$workingDirectory = “$/DevCodeBase/ToolDevBranch1.33”
$webProjectName = “WebRole1”
$cloudProjectName = ‘ProjAzureDeployment’
$evv =’Test’
$cppack = ‘C:\Program Files\Microsoft SDKs\Azure\.NET SDK\v2.9\bin\cspack.exe’
$solutionDir = [string]::Format(“{0}”, $workingDirectory)
$webDir = [string]::Format(“{0}\{1}”, $workingDirectory, $webProjectName)
$packageOutDir = [string]::Format(“{0}\{1}”, $workingDirectory, $cloudProjectName)
$rolePropertyFile = [string]::Format(“{0}\{1}\{2}”, $workingDirectory, $cloudProjectName, “roleproperties.txt”)
# Create Role Properties File – This property file specifies the .Net framework against which webrole is going to run.
New-Item $rolePropertyFile -Type file -Force -Value “TargetFrameWorkVersion=v4.5” | Out-Null
New-Item $packageOutDir -Type directory -Force | Out-Null
# CSPack command Definition
$serviceDefinitionPath = [string]::Format(“{0}\{1}\ServiceDefinition.csdef”, $solutionDir, $cloudProjectName)
if ($evv -eq “Test”){
$serviceConfigurationPath = “ServiceConfiguration.Cloud.Test.cscfg”
}
else
{
$serviceConfigurationPath = [string]::Format(“{0}\{1}\ServiceConfiguration.Cloud.cscfg”, $solutionDir, $cloudProjectName)
}
$serviceRole = [string]::Format(“/role:{0};{1}”, $webProjectName, $webDir)
$rolePropertiesFile = [string]::Format(“/rolePropertiesFile:{0};{1}”, $webProjectName, $rolePropertyFile)
$sites = [string]::Format(“/sites:{0};Web;{1}”, $webProjectName, $webDir)
$packageOutput = [string]::Format(“/out:{0}\{1}.cspkg”, $packageOutDir, $cloudProjectName)
# $packageOutput = [string]::Format(“{0}\{1}.cspkg”, $packageOutDir, $cloudProjectName)
Write-Host $packageOutput
Write-Host $serviceConfigurationPath
# Build CSPKG file
& “C:\Program Files\Microsoft SDKs\Azure\.NET SDK\v2.9\bin\cspack.exe” $serviceDefinitionPath $serviceRole $rolePropertiesFile $sites $packageOutput /useCtpPackageFormat | Out-Null
Write-Host $serviceDefinitionPath
Write-Host $serviceRole
Write-Host $rolePropertiesFile
Write-Host $sites
Write-Host $packageOutput
Write-Host ‘before copy’
# Copy configuration file
Copy-Item $serviceConfigurationPath $packageOutDir
# Remove Role Properties File
Remove-Item -Path $rolePropertyFile -Force | Out-Null
In the VSTS task you'll have to specify an absolute path, otherwise the script will look in the temporary directory created for your inline powershell script.
For instance, you could supply the path to the file as a parameter like
-filepath "$(System.DefaultWorkingDirectory)\Solution\config.json"
(For a list of the variables you can use, have a peek here)
If you want to keep using a relative path, you can move to a file based (ie non-inline) script and use a relative path to that.

powershell and FTP, script not waiting for transfer to complete

I have a script that, in a nutshell, does the following:
copies required files to a temporary folder
compresses the files in the temporary folder to a .zip file
FTPs the .zip file to our FTP server
tidies up and deletes the temporary folder and .zip file
I have pinched the FTP code from a previous post:
Upload files with FTP using PowerShell
and modified it where necessary (keeping the basics in tact - I think).
The issue I have is that while the .zip file is being FTP'd the script doesn't wait until it is complete. It gets part way through, anywhere from 20Mb to 60Mb before it continues executing, tidies up and deletes the file it is transferring.
The temporary folder is always the same but the .zip filename varies depending on the date so I can't really reverse the order of operations.
Can anyone suggest how I might get the script to wait until the FTP process has completed, success or fail, before it moves on?
Cheers,
Andrew.
Edit: For those that asked....
function FTPtoServer ()
{
<#
What this function has to/should do:
- accept the right number of parameters,
minimum/mandatory: username, password, file
optional: proxy server address/port, proxy username and password
- check that the source file exists, then extract the filename.
- if a proxy is specified, set the appropriate parameters
- transmit the file
- if any errors occur, throw and return
#>
param(
[string]$sourcefile=$(throw 'A sourcefile is required, -sourcefile'), <#fully qualified zip file name#>
[string]$FTPUser =$(throw 'An FTP username is required, -ftpuser'),
[string]$FTPPass =$(throw 'An FTP password is required, -ftppass'),
#
[string]$proxyServer, #proxySocket?? it is an address and port
[string]$proxyUser,
[string]$proxyPass
)
#local variables
$FTPserver = "ftp://ftp.servername.com.au"
#check if the sourcefile exists, if not return/throw an error
# The sourcefile should contain the full path to the file.
if (-not (test-path $sourcefile)){
throw "the source file could not be located: $sourcefile"
}
# extract the filename from the sourcefile.
$filename = split-path -path $sourcefile -leaf
# create the FtpWebRequest and configure it
$ftp = [System.Net.FtpWebRequest]::Create("$FTPserver/$filename")
$ftp = [System.Net.FtpWebRequest]$ftp
$ftp.Method = [System.Net.WebRequestMethods+Ftp]::UploadFile
$ftp.Credentials = new-object System.Net.NetworkCredential($FTPUser,$FTPPass)
$ftp.UseBinary = $true
$ftp.UsePassive = $false
#proxy info
# ******** DANGER Will Robinson - this proxy config has not been
# tested and may not work.
if ($proxyServer){
$proxy = New-Object System.Net.WebProxy $proxyServer
if ($proxyUser -and $proxyPass){
$proxy.Credentials = new-object System.Net.NetworkCredential($proxyUser,$proxyPass)
}
$ftp.Proxy = $proxy
$ftp.UsePassive = $true #apparently, must usePassive if using proxy
}
#now we have checked and prepared everything, lets try and send the file.
# read in the file to upload as a byte array
try{
#work out how much we are sending
$content = [System.IO.File]::ReadAllBytes("$sourceFile")
$ftp.ContentLength = $content.Length
try {
# get the request stream, and write the bytes into it
$rs = $ftp.GetRequestStream()
$rs.Write($content, 0, $content.Length)
# be sure to clean up after ourselves
$rs.Close()
$rs.Dispose()
}
catch {
$errorMessage = "FTP failed. " + $_.exception.message
throw $errormessage
}
}
catch {
$errorMessage = "Unable to transmit file " + $sourceFile + "`r`n" + $_.exception.message
throw $errormessage
}
}
The above is in a separate file, but is called by the following:
try {
FTPtoServer -sourcefile $sourcefile -ftpuser $FTPUser -ftppass $FTPPass
}
catch {
$errorMessage = "FTPtoServer function failed with error: $_"
finishFail -failmessage $errorMessage
}
Cheers.
Found it.
I executed the FTP code above in isolation using a large file (~140Mb) and it threw the error; "The underlying connection was closed: An unexpected error occured on a receive."
I rebooted the FTP server, checked the user account etc etc.
I also tested the M$ FTP client with the same file and it transferred completely and correctly.
Anyway, I found this article: https://www.codeproject.com/Questions/597175/FileplusUploadplustoplusFTPplusserver which also has the error I received.
As it turns out, the Timeout value of FTPWebRequest is NOT -1 as in the doco but 100 seconds.
I checked my FTP logs and sure enough, time between logon and logoff was about 100 seconds.
I added the line: $ftp.Timeout = -1 to my code and first attempt transferred the entire file completely without error.
Previous transfers had worked as they fell below the 100 second timeout.
Many thanks for the posts and help.
I use an alternative oldschool method myself, it should work for you, and doesn't need any extra components on the server.
$ftp_user = "username"
$ftp_password = "password"
$ftp_address = "ftp.someserver.com"
$ftp_commands = #"
open $ftp_address
$ftp_user
$ftp_password
lcd c:\jobs\output
put estate_data_current.xml
bye
"#
set-content -encoding "ASCII" -path ftp_commands.txt -value $ftp_commands
ftp -s:ftp_commands.txt

Save file in PowerShell script access denied

I have a PowerShell script that is intended to modify a web config transform as a pre-build event in a build definition. I've gotten it working for the most part, however when it goes to save the updated file I am getting access denied.
Is there a way to give the right access, without opening a window as this is done via the TFS build agent?
Here is the script:
param(
[string]$buildTarget="Dev",
[string]$projectName="SalesTools"
)
$VerbosePreference = "continue"
Write-Verbose "Params: buildTarget = '$($buildTarget)', projectName = '$($projectName)'"
# Make sure path to source code directory is available
if (-not $Env:TF_BUILD_SOURCESDIRECTORY)
{
Write-Error ("TF_BUILD_SOURCESDIRECTORY environment variable is missing.")
exit 1
}
elseif (-not (Test-Path $Env:TF_BUILD_SOURCESDIRECTORY))
{
Write-Error "TF_BUILD_SOURCESDIRECTORY does not exist: $Env:TF_BUILD_SOURCESDIRECTORY"
exit 1
}
Write-Verbose "TF_BUILD_SOURCESDIRECTORY: $Env:TF_BUILD_SOURCESDIRECTORY"
$webConfig = "$($Env:TF_BUILD_SOURCESDIRECTORY)\$($buildTarget)\SalesTools.Web\$($projectName)\web.$($buildTarget).config"
#$webConfig = "$($Env:TF_BUILD_SOURCESDIRECTORY)\$($buildTarget)\SalesTools.Web\ARCTools\web.$($buildTarget).config"
Write-Verbose "File Path: $($webConfig)"
$doc = (gc $webConfig) -as [xml]
$versionNumber = $doc.SelectSingleNode('//appSettings/add[#key="versionNumber"]/#value').'#text'
Write-Verbose "Current Version Number: $($versionNumber)"
if (($versionNumber))
{
$versionInfo = $versionNumber.Split(".")
$versionIteration = $versionInfo[1]
$minorVersion = $versionInfo[2] -as [int]
$minorVersion = $minorVersion + 1
$currentIteration = Get-Iteration
$newVersionInfo = ("v: 1.$($currentIteration).$($minorVersion)")
}
else
{
Write-Error "Could not get version info from config."
exit 1
}
$doc.SelectSingleNode('//appSettings/add[#key="versionNumber"]/#value').'#text' = $newVersionInfo
$doc.Save($webConfig)
Before you read & update the web.config, try to change the "Read-Only" attribute of web.config file. Because by default, all the source files are "Read-Only".
Add this line before "$doc = ....":
attrib -R $webConfig /S

PowerShell script to check an application that's locking a file?

Using in PowerShell, how can I check if an application is locking a file?
I like to check which process/application is using the file, so that I can close it.
You can do this with the SysInternals tool handle.exe. Try something like this:
PS> $handleOut = handle
PS> foreach ($line in $handleOut) {
if ($line -match '\S+\spid:') {
$exe = $line
}
elseif ($line -match 'C:\\Windows\\Fonts\\segoeui\.ttf') {
"$exe - $line"
}
}
MSASCui.exe pid: 5608 ACME\hillr - 568: File (---) C:\Windows\Fonts\segoeui.ttf
...
This could help you: Use PowerShell to find out which process locks a file. It parses the System.Diagnostics.ProcessModuleCollection Modules property of each process and it looks for the file path of the locked file:
$lockedFile="C:\Windows\System32\wshtcpip.dll"
Get-Process | foreach{$processVar = $_;$_.Modules | foreach{if($_.FileName -eq $lockedFile){$processVar.Name + " PID:" + $processVar.id}}}
You should be able to use the openfiles command from either the regular command line or from PowerShell.
The openfiles built-in tool can be used for file shares or for local files. For local files, you must turn on the tool and restart the machine (again, just for first time use). I believe the command to turn this feature on is:
openfiles /local on
For example (works on Windows Vista x64):
openfiles /query | find "chrome.exe"
That successfully returns file handles associated with Chrome. You can also pass in a file name to see the process currently accessing that file.
You can find a solution using Sysinternal's Handle utility.
I had to modify the code (slightly) to work with PowerShell 2.0:
#/* http://jdhitsolutions.com/blog/powershell/3744/friday-fun-find-file-locking-process-with-powershell/ */
Function Get-LockingProcess {
[cmdletbinding()]
Param(
[Parameter(Position=0, Mandatory=$True,
HelpMessage="What is the path or filename? You can enter a partial name without wildcards")]
[Alias("name")]
[ValidateNotNullorEmpty()]
[string]$Path
)
# Define the path to Handle.exe
# //$Handle = "G:\Sysinternals\handle.exe"
$Handle = "C:\tmp\handle.exe"
# //[regex]$matchPattern = "(?<Name>\w+\.\w+)\s+pid:\s+(?<PID>\b(\d+)\b)\s+type:\s+(?<Type>\w+)\s+\w+:\s+(?<Path>.*)"
# //[regex]$matchPattern = "(?<Name>\w+\.\w+)\s+pid:\s+(?<PID>\d+)\s+type:\s+(?<Type>\w+)\s+\w+:\s+(?<Path>.*)"
# (?m) for multiline matching.
# It must be . (not \.) for user group.
[regex]$matchPattern = "(?m)^(?<Name>\w+\.\w+)\s+pid:\s+(?<PID>\d+)\s+type:\s+(?<Type>\w+)\s+(?<User>.+)\s+\w+:\s+(?<Path>.*)$"
# skip processing banner
$data = &$handle -u $path -nobanner
# join output for multi-line matching
$data = $data -join "`n"
$MyMatches = $matchPattern.Matches( $data )
# //if ($MyMatches.value) {
if ($MyMatches.count) {
$MyMatches | foreach {
[pscustomobject]#{
FullName = $_.groups["Name"].value
Name = $_.groups["Name"].value.split(".")[0]
ID = $_.groups["PID"].value
Type = $_.groups["Type"].value
User = $_.groups["User"].value.trim()
Path = $_.groups["Path"].value
toString = "pid: $($_.groups["PID"].value), user: $($_.groups["User"].value), image: $($_.groups["Name"].value)"
} #hashtable
} #foreach
} #if data
else {
Write-Warning "No matching handles found"
}
} #end function
Example:
PS C:\tmp> . .\Get-LockingProcess.ps1
PS C:\tmp> Get-LockingProcess C:\tmp\foo.txt
Name Value
---- -----
ID 2140
FullName WINWORD.EXE
toString pid: 2140, user: J17\Administrator, image: WINWORD.EXE
Path C:\tmp\foo.txt
Type File
User J17\Administrator
Name WINWORD
PS C:\tmp>
I was looking for a solution to this as well and hit some hiccups.
Didn't want to use an external app
Open Files requires the local ON attribute which meant systems had to be configured to use it before execution.
After extensive searching I found.
https://github.com/pldmgg/misc-powershell/blob/master/MyFunctions/PowerShellCore_Compatible/Get-FileLockProcess.ps1
Thanks to Paul DiMaggio
This seems to be pure powershell and .net / C#
You can find for your path on handle.exe.
I've used PowerShell but you can do with another command line tool.
With administrative privileges:
handle.exe -a | Select-String "<INSERT_PATH_PART>" -context 0,100
Down the lines and search for "Thread: ...", you should see there the name of the process using your path.
Posted a PowerShell module in PsGallery to discover & kill processes that have open handles to a file or folder.
It exposes functions to: 1) find the locking process, and 2) kill the locking process.
The module automatically downloads handle.exe on first usage.
Find-LockingProcess()
Retrieves process information that has a file handle open to the specified path.
Example: Find-LockingProcess -Path $Env:LOCALAPPDATA
Example: Find-LockingProcess -Path $Env:LOCALAPPDATA | Get-Process
Stop-LockingProcess()
Kills all processes that have a file handle open to the specified path.
Example: Stop-LockingProcess -Path $Home\Documents
PsGallery Link: https://www.powershellgallery.com/packages/LockingProcessKiller
To install run:
Install-Module -Name LockingProcessKiller
I like what the command prompt (CMD) has, and it can be used in PowerShell as well:
tasklist /m <dllName>
Just note that you can't enter the full path of the DLL file. Just the name is good enough.
I've seen a nice solution at Locked file detection that uses only PowerShell and .NET framework classes:
function TestFileLock {
## Attempts to open a file and trap the resulting error if the file is already open/locked
param ([string]$filePath )
$filelocked = $false
$fileInfo = New-Object System.IO.FileInfo $filePath
trap {
Set-Variable -name filelocked -value $true -scope 1
continue
}
$fileStream = $fileInfo.Open( [System.IO.FileMode]::OpenOrCreate,[System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None )
if ($fileStream) {
$fileStream.Close()
}
$obj = New-Object Object
$obj | Add-Member Noteproperty FilePath -value $filePath
$obj | Add-Member Noteproperty IsLocked -value $filelocked
$obj
}
If you modify the above function slightly like below it will return True or False
(you will need to execute with full admin rights)
e.g. Usage:
PS> TestFileLock "c:\pagefile.sys"
function TestFileLock {
## Attempts to open a file and trap the resulting error if the file is already open/locked
param ([string]$filePath )
$filelocked = $false
$fileInfo = New-Object System.IO.FileInfo $filePath
trap {
Set-Variable -name Filelocked -value $true -scope 1
continue
}
$fileStream = $fileInfo.Open( [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None )
if ($fileStream) {
$fileStream.Close()
}
$filelocked
}