-
Notifications
You must be signed in to change notification settings - Fork 4
/
LogGUI.ps1
149 lines (129 loc) · 5.98 KB
/
LogGUI.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# load the required assemblies and get the XML document to build the GUI
Add-Type -AssemblyName 'PresentationCore', 'PresentationFramework'
[Xml]$WpfXml = Get-Content -Path 'LogGUI.xaml'
# remove attributes from XML that cause problems with initializing the XAML object in Powershell
$WpfXml.Window.RemoveAttribute('x:Class')
$WpfXml.Window.RemoveAttribute('mc:Ignorable')
# initialize the XML Namespaces so they can be used later if required
$WpfNs = New-Object -TypeName Xml.XmlNamespaceManager -ArgumentList $WpfXml.NameTable
$WpfNs.AddNamespace('x', $WpfXml.DocumentElement.x)
$WpfNs.AddNamespace('d', $WpfXml.DocumentElement.d)
$WpfNs.AddNamespace('mc', $WpfXml.DocumentElement.mc)
# create a thread-safe Hashtable to pass data between the Powershell sessions/threads
$Sync = [Hashtable]::Synchronized(@{})
$Sync.Window = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $WpfXml))
# add a "sync" item to reference the GUI control objects to make accessing them easier
$Sync.Gui = @{}
foreach($Node in $WpfXml.SelectNodes('//*[@x:Name]', $WpfNs))
{
# get all the XML elements that have an x:Name attribute, these will be controls we want to interact with
$Sync.Gui.Add($Node.Name, $Sync.Window.FindName($Node.Name))
}
# create an ObservableCollection for the log window, when the contents change LogTextBox will be notified
$Sync.LogDataContext = New-Object -TypeName System.Collections.ObjectModel.ObservableCollection[string]
$Sync.LogDataContext.Add('')
$Sync.Gui.LogTextBox.DataContext = $Sync.LogDataContext
Function Write-Log ([string]$Message, [string]$Type = 'Information')
{
$Prefix = ''
if ($Type -eq 'Information') {$Prefix = 'INFO: '}
if ($Type -eq 'Error') {$Prefix = 'ERROR: '}
$Message = $Prefix + $Message + "`r`n"
# append to the first item of the ObservableCollection
$Sync.LogDataContext[0] += $Message
}
# prepare session state for Runspace
$SyncVariable = New-Object 'Management.Automation.Runspaces.SessionStateVariableEntry' `
-ArgumentList 'Sync', $Sync, ''
$WriteLogDefinition = Get-Content -Path 'function:\Write-Log'
$WriteLogFunction = New-Object 'Management.Automation.Runspaces.SessionStateFunctionEntry' `
-ArgumentList 'Write-Log', $WriteLogDefinition
$global:SessionState = [Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$SessionState.Variables.Add($SyncVariable)
$SessionState.Commands.Add($WriteLogFunction)
# save the state of $AutoScroll outside the event context so it is not reset on every event
$global:AutoScroll = $true
$Sync.Gui.LogScrollViewer.add_ScrollChanged({
if ($_.ExtentHeightChange -eq 0)
{
if ($Sync.Gui.LogScrollViewer.VerticalOffset -eq $Sync.Gui.LogScrollViewer.ScrollableHeight)
{
# if the ScrollViewer is scrolled to the end/bottom enable "auto-scroll"
$global:AutoScroll = $true
}
else
{
$global:AutoScroll = $false
}
}
if ($AutoScroll -eq $true -and $_.ExtentHeightChange -ne 0)
{
# scroll the ScrollViewer to the end/bottom
$Sync.Gui.LogScrollViewer.ScrollToVerticalOffset($Sync.Gui.LogScrollViewer.ExtentHeight)
}
})
# set an example value for the "Command" text box
$Sync.Gui.CommandTextBox.Text = 'Test-Connection -ComputerName 8.8.8.8 -Count 5'
# handle the click event for the "Run" button
$Sync.Gui.RunButton.add_click({
# create the extra Powershell session and add the script block to execute
$global:Session = [PowerShell]::Create().AddScript({
$ErrorActionPreference = 'Stop'
# make the $Error variable available to the parent Powershell session for debugging
$Sync.Error = $Error
# to access objects owned by the parent Powershell session a Dispatcher must be used
$Sync.Window.Dispatcher.Invoke([Action]{
# make $Command available outside this Dispatcher call to the rest of the script block
$script:Command = $Sync.Gui.CommandTextBox.Text
$Sync.Gui.RunButton.IsEnabled = $false
$Sync.Gui.CommandStatusText.Content = 'Running'
})
try
{
# by executing the command in this session the GUI owned by the parent session will remain responsive
Write-Log "Executing $Command"
$CommandOutput = (Invoke-Expression -Command $Command) | Out-String
}
catch
{
$ErrorText = $_.ToString()
Write-Log $ErrorText -Type 'Error'
}
finally
{
# now the command has executed the GUI can be updated again
$Sync.Window.Dispatcher.Invoke([Action]{
$Sync.Gui.CommandOutputTextBox.Text = $CommandOutput
$Sync.Gui.CommandStatusText.Content = 'Waiting'
$Sync.Gui.RunButton.IsEnabled = $true
})
}
}, $true) # set the "useLocalScope" parameter for executing the script block
# execute the code in this session
$Session.Runspace = $Runspace
$global:Handle = $Session.BeginInvoke()
})
$Sync.Window.add_Loaded({
# code here will be run at startup and be able to write to the log window if there is an error
# initalise Runspace
$global:Runspace = [RunspaceFactory]::CreateRunspace($SessionState)
$Runspace.ApartmentState = [Threading.ApartmentState]::STA
$Runspace.Open()
Write-Log "GUI initialized."
})
# check if a command is still running when exiting the GUI
$Sync.Window.add_closing({
if ($Session -ne $null -and $Handle.IsCompleted -eq $false)
{
[Windows.MessageBox]::Show('A command is still running.')
# the event object is automatically passed through as $_
$_.Cancel = $true
}
})
# close the runspace cleanly when exiting the GUI
$Sync.Window.add_closed({
if ($Session -ne $null) {$Session.EndInvoke($Handle)}
$Runspace.Close()
})
# display the GUI
$Sync.Window.ShowDialog() | Out-Null