Building a Clipboard History Viewer Using PowerShell

I saw a question a while back in the Technet PowerShell Forum asking how one might start to build a clipboard viewer using PowerShell that met a few requirements:

  • Have an open window aside from the PowerShell console
  • Automatically list new clipboard items as they come in
  • Allow for filtering to find specific items

Not a lot of requirements here, but they are definitely some nice ones to have for this type of request.

I couldn’t really resist this type of challenge and found it to be a lot of fun. I already had a template of sorts from a previous UI that I created here.

[xml]$xaml = @"
<Window 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="Window" Title="Powershell Clipboard History Viewer" WindowStartupLocation = "CenterScreen" 
    Width = "350" Height = "425" ShowInTaskbar = "True" Background = "White">
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.Resources>
            <Style x:Key="AlternatingRowStyle" TargetType="{x:Type Control}" >
                <Setter Property="Background" Value="LightGray"/>
                <Setter Property="Foreground" Value="Black"/>
                <Style.Triggers>
                    <Trigger Property="ItemsControl.AlternationIndex" Value="1">
                        <Setter Property="Background" Value="White"/>
                        <Setter Property="Foreground" Value="Black"/>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </Grid.Resources>
        <Menu Width = 'Auto' HorizontalAlignment = 'Stretch' Grid.Row = '0'>
        <Menu.Background>
            <LinearGradientBrush StartPoint='0,0' EndPoint='0,1'>
                <LinearGradientBrush.GradientStops> 
                <GradientStop Color='#C4CBD8' Offset='0' /> 
                <GradientStop Color='#E6EAF5' Offset='0.2' /> 
                <GradientStop Color='#CFD7E2' Offset='0.9' /> 
                <GradientStop Color='#C4CBD8' Offset='1' /> 
                </LinearGradientBrush.GradientStops>
            </LinearGradientBrush>
        </Menu.Background>
            <MenuItem x:Name = 'FileMenu' Header = '_File'>
                <MenuItem x:Name = 'Clear_Menu' Header = '_Clear' />
            </MenuItem>
        </Menu>
        <GroupBox Header = "Filter"  Grid.Row = '2' Background = "White">
            <TextBox x:Name="InputBox" Height = "25" Grid.Row="2" />
        </GroupBox>
        <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"  
        Grid.Row="3" Height = "Auto">
            <ListBox x:Name="listbox" AlternationCount="2" ItemContainerStyle="{StaticResource AlternatingRowStyle}" 
            SelectionMode='Extended'>
            <ListBox.Template>
                <ControlTemplate TargetType="ListBox">
                    <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderBrush}">
                        <ItemsPresenter/>
                    </Border>
                </ControlTemplate>
            </ListBox.Template>
            <ListBox.ContextMenu>
                <ContextMenu x:Name = 'ClipboardMenu'>
                    <MenuItem x:Name = 'Copy_Menu' Header = 'Copy'/>      
                    <MenuItem x:Name = 'Remove_Menu' Header = 'Remove'/>  
                </ContextMenu>
            </ListBox.ContextMenu>
            </ListBox>
        </ScrollViewer >
    </Grid>
</Window>
"@

 

image

Now to connect to some controls so I can use them and their events later on.

$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )

#Connect to Controls
$listbox = $Window.FindName('listbox')
$InputBox = $Window.FindName('InputBox')
$Copy_Menu = $Window.FindName('Copy_Menu')
$Remove_Menu = $Window.FindName('Remove_Menu')
$Clear_Menu = $Window.FindName('Clear_Menu')

Ok, with the front end UI out of the way and I have connected to my controls, I can now work to add some code to handle some events and perform various actions based on those events.

But before that, I am going to wrap this UI up within a runspace so I can leave my PowerShell console open for other things.

$Runspacehash = [hashtable]::Synchronized(@{})
$Runspacehash.Host = $Host
$Runspacehash.runspace = [RunspaceFactory]::CreateRunspace()
$Runspacehash.runspace.ApartmentState = "STA"
$Runspacehash.runspace.Open() 
$Runspacehash.runspace.SessionStateProxy.SetVariable("Runspacehash",$Runspacehash)
$Runspacehash.PowerShell = {Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase}.GetPowerShell() 
$Runspacehash.PowerShell.Runspace = $Runspacehash.runspace 
$Runspacehash.Handle = $Runspacehash.PowerShell.AddScript({ 
# All my code for this UI
}).BeginInvoke()

 

The trick to the clipboard viewer is a little type call [Windows.Clipboard]  which is only available if you do the following (note this is already available if using the ISE):

Add-Type -AssemblyName PresentationCore

Now you can check out all of the cool static methods which are available.

[windows.clipboard] | 
Get-Member -Static -Type Method

image

I am only focused on Text related methods this time around (GetText() and SetText()) as well as Clear(). But with this I can view and manipulate the contents of the clipboard. I am going to create some custom functions that will utilize this as well as work with some of the UI as well to make things a little easier.

Function Get-ClipBoard {
    [Windows.Clipboard]::GetText()
}
Function Set-ClipBoard {
    $Script:CopiedText = @"
$($listbox.SelectedItems | Out-String)
"@
    [Windows.Clipboard]::SetText($Script:CopiedText)
}
Function Clear-Viewer {
    [void]$Script:ObservableCollection.Clear()
    [Windows.Clipboard]::Clear()
}

With these three functions, I can re-use them when it comes to multiple events that I have planned rather than re-writing the same code for more than one event.

The only thing left to do here is to start working with the events on the various controls that will handle all of the operations of this window.

The first event that I have is one of the most important as it is a Timer which checks the clipboard contents and adds them to the observable collection which in turn updates the listbox on the UI.

$Window.Add_SourceInitialized({
    #Create observable collection
    $Script:ObservableCollection = New-Object System.Collections.ObjectModel.ObservableCollection[string]
    $Listbox.ItemsSource = $Script:ObservableCollection

    #Create Timer object
    $Script:timer = new-object System.Windows.Threading.DispatcherTimer 
    $timer.Interval = [TimeSpan]"0:0:.1"

    #Add event per tick
    $timer.Add_Tick({
        $text =  Get-Clipboard
        If (($Script:Previous -ne $Text -AND $Script:CopiedText -ne $Text) -AND $text.length -gt 0) {
            #Add to collection
            [void]$Script:ObservableCollection.Add($text)
            $Script:Previous = $text
        }     
    })
    $timer.Start()
    If (-NOT $timer.IsEnabled) {
        $Window.Close()
    }
})

The timer is set to 100 milliseconds which should be plenty of time to handle items being added to the clipboard. The observable collection is binded to the listbox ItemsSource property (actually the view is binded, not the actual collection which is useful when we set up a filter later on in this article) which can auto update the UI as things are added to the collection. See this article on more examples of this.

 

Speaking of a filter, one of the requirements was being able to type in some text in a text box and have the contents start filtering automatically. This is accomplished using the TextChanged event on the textbox and using a Predicate for the DefaultCollectionView Filter property of the ListBox.

$InputBox.Add_TextChanged({
    [System.Windows.Data.CollectionViewSource]::GetDefaultView($Listbox.ItemsSource).Filter = [Predicate[Object]]{             
        Try {
            $args[0] -match [regex]::Escape($InputBox.Text)
        } Catch {
            $True
        }
    }    
})

As the text is changing in the textbox, the filter will look for anything that matches it. Pretty handy approach to real time filtering of data.

I also wanted to add some keystroke shortcuts as well to give it a more polished feel.

$Window.Add_KeyDown({ 
    $key = $_.Key  
    If ([System.Windows.Input.Keyboard]::IsKeyDown("RightCtrl") -OR [System.Windows.Input.Keyboard]::IsKeyDown("LeftCtrl")) {
        Switch ($Key) {
        "C" {
            Set-ClipBoard          
        }
        "R" {
            @($listbox.SelectedItems) | ForEach {
                [void]$Script:ObservableCollection.Remove($_)
            }            
        }
        "E" {
            $This.Close()
        }
        Default {$Null}
        }
    }
})

For this, I currently have the following shortcuts available:

  • Ctrl+E
    • Exits the application
  • Ctrl+R
    • Removes the selected items from the clipboard viewer
  • Ctrl+C
    • Copies the selected items from the clipboard viewer

The rest of the events handle things such as closing the application gracefully, handling the menu clicks on the context menu of the listbox and on the title menu as well as giving focus to the filter textbox at startup.

$Clear_Menu.Add_Click({
    Clear-Viewer
})
$Remove_Menu.Add_Click({
    @($listbox.SelectedItems) | ForEach {
        [void]$Script:ObservableCollection.Remove($_)
    }
})
$Copy_Menu.Add_Click({
    Set-ClipBoard
})
$Window.Add_Activated({
    $InputBox.Focus()
})
$Window.Add_Closed({
    $Script:timer.Stop()
    $Script:ObservableCollection.Clear()
    $Runspacehash.PowerShell.Dispose()
})
$listbox.Add_MouseRightButtonUp({
    If ($Script:ObservableCollection.Count -gt 0) {
        $Remove_Menu.IsEnabled = $True
        $Copy_Menu.IsEnabled = $True
    } Else {
        $Remove_Menu.IsEnabled = $False
        $Copy_Menu.IsEnabled = $False
    }
})

After running the script…

.\ClipboardHistoryViewer.ps1

… we may see an item already being shown (it depends on if something is already in the clipboard) and can then start copying various amounts of text and see the viewer start filling up and also select multiple items and then right-click to copy or remove them (or using the keyboard shortcuts).

image

I can filter for just the script download location.

image

I can also use the File>Clear menu to clear out all of the contents of the viewer as well as the clipboard.

Currently these are all of the features that I have for this version, but if others would like to see new things, I can certainly see about updating it.

Download the PowerShell Clipboard History Viewer

http://gallery.technet.microsoft.com/scriptcenter/PowerShell-Clipboard-c414ec78

Posted in powershell | Tagged , , | Leave a comment

Guest Spot on Hey, Scripting Guy! Talking ForEach

Check out my latest guest spot on Hey, Scripting Guy! talking about using ForEach, ForEach-Object and … ForEach (the alias of ForEach-Object). Give it a read and let me know what you think!

http://blogs.technet.com/b/heyscriptingguy/archive/2014/07/08/getting-to-know-foreach-and-foreach-object.aspx

Posted in powershell | Tagged , , | Leave a comment

Quick Hits: Did I Really Lose My Output With Receive-Job By Not Using–Keep?

We have all probable been there at some point. We use Start-Job to run a command in the background like this:

Start-Job -ScriptBlock {
    Get-Process | Where {
        $_.WS -gt 100MB
    }
} -Name HighMemProcess

image

Take note of the HasMoreData property. This means that we have data available which can be used with Receive-Job to get the output stream of the background job.

When completed, we typically used Receive-Job to get the data from the output of the PSJob.

Receive-Job -Name HighMemProcess

image

 

Now let’s go back to the job and see what is there.

image

HasMoreData is now showing false, which means that if we use Receive-Job again against this job, no data will be returned.

image

That could be problematic if we didn’t use the –Keep parameter to ensure that the data will still be available in the PSJob because now we have to re-run the code again.

But wait! There is a way to still pull the data without having to re-run the job. Let’s examine the job a little more and see what is available.

Get-Job -Name HighMemProcess | Select *

image

What we are seeing is actually a parent job of the child job (Job3) which is actually doing the work. This parent job is really just monitoring the state of the child job and let’s you know when it has completed. With that knowledge, let’s take a look at the child job and see what is there.

$Job = Get-Job -Name HighMemProcess
$Job.ChildJobs | Select *

SNAGHTMLa7e8728

Check out the Output stream, all of the data we need that was originally available when using Receive-Job is still there!

$Job.ChildJobs.output

image

 

Now we know that even if we accidently forget to use the –Keep parameter on Receive-Job we can still dig into the child job of the parent job and pull the data from the Output stream.

Posted in powershell | Tagged , , , | 1 Comment

Changing Ownership of File or Folder Using PowerShell

While working on a project recently, I needed to find an easy way to take ownership of a profile folder and its subfolders to allow our support staff to either delete the profile or be able to traverse the folder to help troubleshoot issues. Typically, one could use Explorer to find the folder and then take ownership and be done with it. But the goal was to come up with a command line solution that not only worked quickly, but didn’t miss out on a file or folder.

The brief background on this is that roaming profiles sometimes would become inaccessible to our support staff in that only the user account and System would have access to the profile folder and its sub-folders and files. Also, ownership of those objects were by the user account. This created issues with deleting accounts and troubleshooting profile related issues.

Before showing the solution that I came up with, I will run down a list of attempts which never quite met my requirements and why.

Using Takeown.exe

This was actually my initial idea as I allows for recursive actions and lets me specify to grant ownership to Builtin\Administrators. Sure it wasn’t a PowerShell approach, but it met the requirements of what I wanted to do…or so I thought.

image

The first problem is that it is slow. I kicked it off on my own profile (because it is always more fun to test on yourself than others) and found that it would take upwards of 10 minutes vs. the ~2 minute UI approach. Obviously this is an issue if I expect to have this used as part of my project for others to take ownership on profiles which would more than likely have more items than my profile. I still decided to press forward with this and later found the second issue: takeown.exe would not reliably grant ownership completely down the tree of subfolders. This was a huge issue and would not be acceptable with the customer.

Take Ownership using PowerShell and Set-ACL

The next idea was to grab the ACL object of a folder elsewhere in the user’s home directory that had good permissions and then change the owner in that ACL object to ‘Builtin\Administrators” and the apply it to the profile folder.

$ACL = Get-ACL .\smithb
$Group = New-Object System.Security.Principal.NTAccount("Builtin", "Administrators")
$ACL.SetOwner($Group)
Set-Acl -Path .\smithb\profile.v2 -AclObject $ACL

Sounds good, right? Well, not really due to some un-foreseen issues. Because the accounts do not have the proper user rights (seTakeOwnershipPrivilege, SeRestorePrivilege and SeBackupPrivilege), this would fail right away with an ‘Access Denied’ error. Fine, I can add those privileges if needed and continue on from there. Well, it doesn’t quite work that way either because only the directories would propagate these permissions but the files wouldn’t get ownership.

Set-Owner Function

The final thing that I came up with followed a similar idea as my second attempt, but makes sure to allow for recursion and files and folders as well as allowing either ‘Builting\Administrators’ or another account to have ownership of files and folders. To do this I dove into the Win32 API to first allow the account to elevate the tokens that I have mentioned before.

Try {
[void][TokenAdjuster]
} Catch {
$AdjustTokenPrivileges = @"
using System;
using System.Runtime.InteropServices;

public class TokenAdjuster
{
    [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
    internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall,
    ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);
    [DllImport("kernel32.dll", ExactSpelling = true)]
    internal static extern IntPtr GetCurrentProcess();
    [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
    internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr
    phtok);
    [DllImport("advapi32.dll", SetLastError = true)]
    internal static extern bool LookupPrivilegeValue(string host, string name,
    ref long pluid);
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    internal struct TokPriv1Luid
    {
        public int Count;
        public long Luid;
        public int Attr;
    }
    internal const int SE_PRIVILEGE_DISABLED = 0x00000000;
    internal const int SE_PRIVILEGE_ENABLED = 0x00000002;
    internal const int TOKEN_QUERY = 0x00000008;
    internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
    public static bool AddPrivilege(string privilege)
    {
        try
        {
            bool retVal;
            TokPriv1Luid tp;
            IntPtr hproc = GetCurrentProcess();
            IntPtr htok = IntPtr.Zero;
            retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
            tp.Count = 1;
            tp.Luid = 0;
            tp.Attr = SE_PRIVILEGE_ENABLED;
            retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid);
            retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
            return retVal;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
    public static bool RemovePrivilege(string privilege)
        {
        try
        {
            bool retVal;
            TokPriv1Luid tp;
            IntPtr hproc = GetCurrentProcess();
            IntPtr htok = IntPtr.Zero;
            retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
            tp.Count = 1;
            tp.Luid = 0;
            tp.Attr = SE_PRIVILEGE_DISABLED;
            retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid);
            retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
            return retVal;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}
"@
Add-Type $AdjustTokenPrivileges
}

#Activate necessary admin privileges to make changes without NTFS perms
[void][TokenAdjuster]::AddPrivilege("SeRestorePrivilege") #Necessary to set Owner Permissions
[void][TokenAdjuster]::AddPrivilege("SeBackupPrivilege") #Necessary to bypass Traverse Checking
[void][TokenAdjuster]::AddPrivilege("SeTakeOwnershipPrivilege") #Necessary to override FilePermissions

This allows me to traverse the directory tree and set ownership on the files and folders. If I cannot take ownership on a file or folder (because inheritance is not allowed from the parent folder), then it moves up a level to grant Full Control to to parent folder, thus allowing me to take ownership on the folder or file below it.

Process {
    ForEach ($Item in $Path) {
        Write-Verbose "FullName: $Item"
        #The ACL objects do not like being used more than once, so re-create them on the Process block
        $DirOwner = New-Object System.Security.AccessControl.DirectorySecurity
        $DirOwner.SetOwner([System.Security.Principal.NTAccount]$Account)
        $FileOwner = New-Object System.Security.AccessControl.FileSecurity
        $FileOwner.SetOwner([System.Security.Principal.NTAccount]$Account)
        $DirAdminAcl = New-Object System.Security.AccessControl.DirectorySecurity
        $FileAdminAcl = New-Object System.Security.AccessControl.DirectorySecurity
        $AdminACL = New-Object System.Security.AccessControl.FileSystemAccessRule('Builtin\Administrators','FullControl','ContainerInherit,ObjectInherit','InheritOnly','Allow')
        $FileAdminAcl.AddAccessRule($AdminACL)
        $DirAdminAcl.AddAccessRule($AdminACL)
        Try {
            $Item = Get-Item -LiteralPath $Item -Force -ErrorAction Stop
            If (-NOT $Item.PSIsContainer) {
                If ($PSCmdlet.ShouldProcess($Item, 'Set File Owner')) {
                    Try {
                        $Item.SetAccessControl($FileOwner)
                    } Catch {
                        Write-Warning "Couldn't take ownership of $($Item.FullName)! Taking FullControl of $($Item.Directory.FullName)"
                        $Item.Directory.SetAccessControl($FileAdminAcl)
                        $Item.SetAccessControl($FileOwner)
                    }
                }
            } Else {
                If ($PSCmdlet.ShouldProcess($Item, 'Set Directory Owner')) {                        
                    Try {
                        $Item.SetAccessControl($DirOwner)
                    } Catch {
                        Write-Warning "Couldn't take ownership of $($Item.FullName)! Taking FullControl of $($Item.Parent.FullName)"
                        $Item.Parent.SetAccessControl($DirAdminAcl) 
                        $Item.SetAccessControl($DirOwner)
                    }
                }
                If ($Recurse) {
                    [void]$PSBoundParameters.Remove('FullName')
                    Get-ChildItem $Item -Force | Set-Owner @PSBoundParameters
                }
            }
        } Catch {
            Write-Warning "$($Item): $($_.Exception.Message)"
        }
    }
}
End {  
    #Remove priviledges that had been granted
    [void][TokenAdjuster]::RemovePrivilege("SeRestorePrivilege") 
    [void][TokenAdjuster]::RemovePrivilege("SeBackupPrivilege") 
    [void][TokenAdjuster]::RemovePrivilege("SeTakeOwnershipPrivilege")
}

Using this approach, I was able to accurately take ownership on all of the items as well as not facing major slowdown (it was roughly 30 seconds slower than the UI approach). Seemed like a good tradeoff to me.

Here are a couple of examples of the function in action:

Set-Owner -Path .\smithb\profile.v2 -Recurse -Verbose

image

Set-Owner -Path .\smithb\profile.v2 -Recurse -Verbose -Account 'WIN-AECB72JTEV0\proxb'

image

The function is available to download from the following link:

http://gallery.technet.microsoft.com/scriptcenter/Set-Owner-ff4db177

Posted in powershell | Tagged , , | 5 Comments

PowerShell User Group in Omaha Nebraska!

That’s right! We now have a PowerShell Users group in Omaha, Nebraska. Jacob Benson (Blog | Twitter) and myself are kicking off the inaugural session on 29 July @ 6:30 PM at the Microsoft office. If you are in Omaha or nearby, then you should most definitely be here to join the PowerShell community!

Oh yea, we also have an excellent speaker by the name of Don Jones (Blog | Twitter), whom you may have heard of Winking smile.  We will be working to line up more speakers for other meetings each month and will have those posted on the Powershell.org page.

Sign up here:

http://www.eventbrite.com/e/omaha-powershell-user-group-initial-meeting-tickets-11958049849 

If you have any questions, please contact either myself or Jacob and we will answer any questions that you may have. Also make sure that you follow the Omaha PowerShell Users Group on twitter as well!

Hope to see you there!

Posted in powershell | Tagged , , , | Leave a comment