/
PSSoftware.psm1
3933 lines (3600 loc) · 121 KB
/
PSSoftware.psm1
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
Set-StrictMode -Version Latest
function Test-InstalledSoftware {
<#
.SYNOPSIS
This function is used as a quick check to see if a specific software product is installed on the local host.
.PARAMETER Name
The name of the software you'd like to query as displayed by the Get-InstalledSoftware function
.PARAMETER Version
The version of the software you'd like to query as displayed by the Get-InstalledSofware function.
.PARAMETER Guid
The GUID of the installed software
#>
[OutputType([bool])]
[CmdletBinding(DefaultParameterSetName = 'Name')]
param (
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$ComputerName,
[Parameter(ParameterSetName = 'Name')]
[ValidateNotNullOrEmpty()]
[string]$Name,
[Parameter(ParameterSetName = 'Name')]
[ValidateNotNullOrEmpty()]
[string]$Version,
[Parameter(ParameterSetName = 'Guid')]
[ValidateNotNullOrEmpty()]
[Alias('ProductCode')]
[string]$Guid
)
process {
try {
$getSoftwareParams = @{}
if ($PSBoundParameters.ContainsKey('ComputerName')) {
$getSoftwareParams.ComputerName = $ComputerName
}
if ($PSBoundParameters.ContainsKey('Name')) {
$getSoftwareParams.Name = $Name
}
$whereFilter = {'*'}
if ($PSBoundParameters.ContainsKey('Version')) {
$whereFilter = { $_.Version -eq $Version }
}
if ($PSBoundParameters.ContainsKey('Guid')) {
$getSoftwareParams.Guid = $Guid
}
if (-not ($SoftwareInstances = Get-InstalledSoftware @getSoftwareParams | Where-Object -FilterScript $whereFilter)) {
Write-Log -Message 'The software is NOT installed.'
$false
} else {
Write-Log -Message 'The software IS installed.'
$true
}
} catch {
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
function Get-InstalledSoftware
{
<#
.SYNOPSIS
Retrieves a list of all software installed
.EXAMPLE
Get-InstalledSoftware
This example retrieves all software installed on the local computer
.PARAMETER Name
The software title you'd like to limit the query to.
.PARAMETER Guid
The software GUID you'e like to limit the query to
#>
[OutputType([System.Management.Automation.PSObject])]
[CmdletBinding()]
param (
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$ComputerName = $ENV:COMPUTERNAME,
[ValidatePattern('\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')]
[string]$Guid
)
process
{
try
{
$scriptBlock = {
param($Name, $Guid)
$UninstallKeys = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null
$UninstallKeys += Get-ChildItem HKU: -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'S-\d-\d+-(\d+-){1,14}\d+$' } | ForEach-Object { "HKU:\$($_.PSChildName)\Software\Microsoft\Windows\CurrentVersion\Uninstall" }
if (-not $UninstallKeys) {
Write-Warning -Message 'No software registry keys found' -LogLevel '2'
} else {
foreach ($UninstallKey in $UninstallKeys) {
$friendlyNames = @{
'DisplayName' = 'Name'
'DisplayVersion' = 'Version'
}
if ($Name) {
$WhereBlock = { $_.GetValue('DisplayName') -like "$Name*" }
} elseif ($Guid) {
$WhereBlock = { $_.PsChildName -eq $Guid }
} else {
$WhereBlock = { $_.GetValue('DisplayName') }
}
$SwKeys = Get-ChildItem -Path $UninstallKey -ErrorAction SilentlyContinue | Where-Object $WhereBlock
foreach ($SwKey in $SwKeys) {
try {
$output = @{ }
foreach ($ValName in $SwKey.GetValueNames() | Where-Object { $_ }) {
if ($ValName -ne 'Version') {
Write-Verbose -Message $ValName
$output.InstallLocation = ''
if ($ValName -eq 'InstallLocation' -and ($SwKey.GetValue($ValName)) -and (@('C:', 'C:\Windows', 'C:\Windows\System32', 'C:\Windows\SysWOW64') -notcontains $SwKey.GetValue($ValName).TrimEnd('\'))) {
$output.InstallLocation = $SwKey.GetValue($ValName).TrimEnd('\')
}
[string]$ValData = $SwKey.GetValue($ValName)
if ($friendlyNames[$ValName]) {
$output[$friendlyNames[$ValName]] = $ValData.Trim() ## Some registry values have trailing spaces.
} else {
$output[$ValName] = $ValData.Trim() ## Some registry values trailing spaces
}
}
}
$output.GUID = ''
if ($SwKey.PSChildName -match '\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b') {
$output.GUID = $SwKey.PSChildName
}
New-Object –TypeName PSObject -Property $output
} catch {
Write-Error -Message $_.Exception.Message
}
}
}
}
}
Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock
}
catch
{
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
function Install-Software
{
<#
.SYNOPSIS
.NOTES
Created on: 6/23/2014
Created by: Adam Bertram
Filename: Install-Software.ps1
Credits:
Requirements: The installers executed via this script typically need "Run As Administrator"
Todos: Allow multiple software products to be installed
.EXAMPLE
Install-Software -MsiInstallerFilePath install.msi -InstallArgs "/qn "
.PARAMETER InstallShieldInstallerFilePath
This is the file path to the EXE InstallShield installer.
.PARAMETER MsiInstallerFilePath
This is the file path to the MSI installer.
.PARAMETER OtherInstallerFilePath
This is the file path to any other EXE installer.
.PARAMETER MsiExecSwitches
This is a string of arguments that are passed to the installer. If this param is
not used, it will default to the standard REBOOT=ReallySuppress and the ALLUSERS=1 switches. If it's
populated, it will be concatenated with the standard silent arguments. Use the -Verbose switch to discover arguments used.
Do NOT use this to pass TRANSFORMS or PATCH arguments. Use the MstFilePath and MspFilePath params for that.
.PARAMETER MstFilePath
Use this param if you've created a TRANSFORMS file and would like to pass this to the installer
.PARAMETER MspFilePath
Use this param if you have a patch to apply to the install
.PARAMETER InstallShieldInstallArgs
This is a string of arguments that are passed to the InstallShield installer. Default arguments are
"/s /f1$IssFilePath /SMS"
.PARAMETER OtherInstallerArgs
This is a string of arguments that are passed to any other EXE installer. There is no default.
.PARAMETER KillProcess
A list of process names that will be terminated prior to attempting the install. This is useful
in upgrade scenarios where you need to terminate the previous version's processes.
.PARAMETER ProcessTimeout
A value (in seconds) that the installer script will wait for the installation process to complete. If the installation
goes over this value, any processes (parent or child) will be terminated.
.PARAMETER LogFilePath
This is the path where the installer log file will be written. If not passed, it will default
to being named install.log in the system temp folder.
#>
[OutputType([void])]
[CmdletBinding(DefaultParameterSetName = 'MSI')]
param (
[Parameter(ParameterSetName = 'InstallShield', Mandatory = $true)]
[ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
[ValidatePattern('\.exe$')]
[ValidateNotNullOrEmpty()]
[string]$InstallShieldInstallerFilePath,
[Parameter(ParameterSetName = 'Other', Mandatory = $true)]
[ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
[ValidatePattern('\.exe$')]
[ValidateNotNullOrEmpty()]
[string]$OtherInstallerFilePath,
[Parameter(ParameterSetName = 'InstallShield', Mandatory = $true)]
[ValidatePattern('\.iss$')]
[ValidateNotNullOrEmpty()]
[string]$IssFilePath,
[Parameter(ParameterSetName = 'MSI', Mandatory = $true)]
[ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
[ValidateNotNullOrEmpty()]
[string]$MsiInstallerFilePath,
[Parameter(ParameterSetName = 'MSI')]
[ValidateNotNullOrEmpty()]
[string]$MsiExecSwitches,
[Parameter(ParameterSetName = 'MSI')]
[ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
[ValidatePattern('\.msp$')]
[ValidateNotNullOrEmpty()]
[string]$MspFilePath,
[Parameter(ParameterSetName = 'MSI')]
[ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
[ValidatePattern('\.mst$')]
[ValidateNotNullOrEmpty()]
[string[]]$MstFilePath,
[Parameter(ParameterSetName = 'InstallShield')]
[ValidateNotNullOrEmpty()]
[string]$InstallShieldInstallArgs,
[Parameter(ParameterSetName = 'Other')]
[ValidateNotNullOrEmpty()]
[Alias('OtherInstallerArguments')]
[string]$OtherInstallerArgs,
[Parameter()]
[ValidateNotNullOrEmpty()]
[string[]]$KillProcess,
[Parameter()]
[ValidateNotNullOrEmpty()]
[int]$ProcessTimeout = 600,
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$LogFilePath
)
process
{
try
{
## Common Start-Process parameters across all installers. We'll add to this hashtable as we go
$ProcessParams = @{
'NoNewWindow' = $true;
'Passthru' = $true
}
if ($PSBoundParameters.ContainsKey('MsiInstallerFilePath'))
{
$InstallerFilePath = $MsiInstallerFilePath
Write-Log -Message 'Creating the msiexec install string'
$InstallArgs = Get-MsiexecInstallString -InstallerFilePath $InstallerFilePath -MspFilePath $MspFilePath -MstFilePath $MstFilePath -LogFilePath $LogFilePath -ExtraSwitches $MsiExecSwitches
## Add Start-Process parameters
$ProcessParams['FilePath'] = 'msiexec.exe'
$ProcessParams['ArgumentList'] = $InstallArgs
}
elseif ($PSBoundParameters.ContainsKey('InstallShieldInstallerFilePath'))
{
$InstallerFilePath = $InstallShieldInstallerFilePath
$InstallArgs = Get-InstallshieldInstallString -InstallerFilePath $InstallerFilePath -LogFilePath $LogFilePath -ExtraSwitches $InstallShieldInstallArgs -IssFilePath $IssFilePath
$ProcessParams['FilePath'] = $InstallerFilePath
$ProcessParams['ArgumentList'] = $InstallArgs
}
elseif ($PSBoundParameters.ContainsKey('OtherInstallerFilePath'))
{
$InstallerFilePath = $OtherInstallerFilePath
Write-Log -Message 'Creating a generic setup install string'
## Nothing fancy here. Since we don't know any common switches to run I'll just take whatever
## arguments are provided as a parameter.
if ($PSBoundParameters.ContainsKey('OtherInstallerArgs'))
{
$ProcessParams['ArgumentList'] = $OtherInstallerArgs
}
$ProcessParams['FilePath'] = $OtherInstallerFilePath
}
## Thiw was added for upgrade scenarios where the previous version would be running and the installer
## itself isn't smart enough to kill it.
if ($PSBoundParameters.ContainsKey('KillProcess'))
{
Write-Log -Message 'Killing existing processes'
$KillProcess | ForEach-Object { Stop-MyProcess -ProcessName $_ }
}
Write-Log -Message "Starting the command line process `"$($ProcessParams['FilePath'])`" $($ProcessParams['ArgumentList'])..."
$Result = Start-Process @ProcessParams
## This is required because sometimes depending on how the MSI is packaged, the parent process will exit
## but will leave child processes running and the function will exit before the install is finished.
if ($PSBoundParameters.ContainsKey('MsiInstallerFilePath'))
{
Wait-WindowsInstaller
}
else
{
## No special msiexec.exe waiting here. We'll just use Wait-MyProcess to report on the waiting
## process.
Write-Log "Waiting for process ID $($Result.Id)"
$WaitParams = @{
'ProcessId' = $Result.Id
'ProcessTimeout' = $ProcessTimeout
}
Wait-MyProcess @WaitParams
}
}
catch
{
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
function Remove-Software
{
<#
.SYNOPSIS
This function removes any software registered via Windows Installer from the local computer
.NOTES
Created on: 6/4/2014
Created by: Adam Bertram
Requirements: The msizap utility (if user would like to run)
.DESCRIPTION
This function searches a local computer for a specified application matching a name. Based on the
parameters given, it can either remove services, kill proceseses and if the software is
installed, it uses the locally cached MSI to initiate an uninstall and has the option to
ensure the software is completely removed by running the msizap.exe utility.
.EXAMPLE
Remove-Software -Name 'Adobe Reader' -KillProcess 'proc1','proc2'
This example would remove any software with 'Adobe Reader' in the name and look for and stop both the proc1
and proc2 processes
.EXAMPLE
Remove-Software -Name 'Adobe Reader'
This example would remove any software with 'Adobe Reader' in the name.
.EXAMPLE
Remove-Software -Name 'Adobe Reader' -RemoveService 'servicename' -Verbose
This example would remove any software with 'Adobe Reader' in the name, look for, stop and remove any service with a
name of servicename. It will output all verbose logging as well.
.EXAMPLE
Remove-Software -Name 'Adobe Reader' -RemoveFolder 'C:\Program Files Files\Install Folder'
This example would remove any software with 'Adobe Reader' in the name, look for and remove the
C:\Program Files\Install Folder, attempt to uninstall the software cleanly via msiexec using
the syntax msiexec.exe /x PRODUCTMSI /qn REBOOT=ReallySuppress which would attempt to not force a reboot if needed.
If it doesn't uninstall cleanly, it would run copy the msizap utility from the default path to
the local computer, execute it with the syntax msizap.exe TW! PRODUCTGUID and remove itself when done.
.PARAMETER Name
This is the name of the application to search for. This can be multiple products. Each product will be removed in the
order you specify.
.PARAMETER MsiExecSwitches
Specify a string of switches you'd like msiexec.exe to run when it attempts to uninstall the software. By default,
it already uses "/x GUID /qn". You can specify any additional parameters here.
.PARAMETER LogFilePath
The file path where the msiexec uninstall log will be created. This defaults to the name of the product being
uninstalled in the system temp directory
.PARAMETER InstallshieldLogFilePath
The file path where the Installshield log will be created. This defaults to the name of the product being
uninstalled in the system temp directory
.PARAMETER RunMsizap
Use this parameter to run the msizap.exe utility to cleanup any lingering remnants of the software
.PARAMETER MsizapParams
Specify the parameters to send to msizap if it is needed to cleanup the software on the remote computer. This
defaults to "TWG!" which removes settings from all user profiles
.PARAMETER MsizapFilePath
Optionally specify where the file msizap utility is located in order to run a final cleanup
.PARAMETER IssFilePath
If removing an InstallShield application, use this parameter to specify the ISS file path where you recorded
the uninstall of the application.
.PARAMETER InstallShieldSetupFilePath
If removing an InstallShield application, use this optional paramter to specify where the EXE installer is for
the application you're removing. This is only used if no cached installer is found.
#>
[OutputType([void])]
[CmdletBinding(DefaultParameterSetName = 'None')]
param (
[Parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = 'FromPipeline')]
[ValidateNotNullOrEmpty()]
[object]$Software,
[Parameter(Mandatory = $true,
ValueFromPipelineByPropertyName = $true,
ParameterSetName = 'FromValue')]
[string]$Name,
[Parameter(ParameterSetName = 'MSI')]
[Parameter(ParameterSetName = 'FromPipeline')]
[Parameter(ParameterSetName = 'FromValue')]
[string]$MsiExecSwitches,
[Parameter()]
[Parameter(ParameterSetName = 'FromPipeline')]
[Parameter(ParameterSetName = 'FromValue')]
[string]$LogFilePath,
[Parameter(ParameterSetName = 'ISS')]
[Parameter(ParameterSetName = 'FromPipeline')]
[Parameter(ParameterSetName = 'FromValue')]
[string]$InstallshieldLogFilePath,
[Parameter(ParameterSetName = 'Msizap')]
[Parameter(ParameterSetName = 'FromPipeline')]
[Parameter(ParameterSetName = 'FromValue')]
[switch]$RunMsizap,
[Parameter(ParameterSetName = 'Msizap')]
[Parameter(ParameterSetName = 'FromPipeline')]
[Parameter(ParameterSetName = 'FromValue')]
[string]$MsizapParams = 'TWG!',
[Parameter(ParameterSetName = 'Msizap')]
[Parameter(ParameterSetName = 'FromPipeline')]
[Parameter(ParameterSetName = 'FromValue')]
[ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
[string]$MsizapFilePath = 'C:\MyDeployment\msizap.exe',
[Parameter(ParameterSetName = 'ISS',
Mandatory = $true)]
[Parameter(ParameterSetName = 'FromPipeline')]
[Parameter(ParameterSetName = 'FromValue')]
[ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
[ValidatePattern('\.iss$')]
[string]$IssFilePath,
[Parameter(ParameterSetName = 'ISS')]
[Parameter(ParameterSetName = 'FromPipeline')]
[Parameter(ParameterSetName = 'FromValue')]
[ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
[string]$InstallShieldSetupFilePath
)
process
{
try
{
if ($PSCmdlet.ParameterSetName -eq 'FromValue')
{
Write-Debug -Message "Getting installed software matching [$($Name)]"
$Software = Get-InstalledSoftware -Name $Name
}
if ($Software.InstallLocation)
{
Write-Log -Message "Stopping all processes under the install folder $($Software.InstallLocation)..."
Stop-SoftwareProcess -Software $Software
}
if ($Software.UninstallString)
{
$InstallerType = Get-InstallerType $Software.UninstallString
}
else
{
Write-Log -Message "Uninstall string for $($Software.Name) not found" -LogLevel '2'
}
if (-not $PsBoundParameters['LogFilePath'])
{
$script:LogFilePath = "$(Get-SystemTempFolderPath)\$($Software.Name).log"
Write-Log -Message "No log file path specified. Defaulting to $script:LogFilePath..."
}
if (-not $InstallerType -or ($InstallerType -eq 'Windows Installer'))
{
Write-Log -Message "Installer type detected to be Windows Installer or unknown for $($Software.Name). Attempting Windows Installer removal" -LogLevel '2'
$params = @{ }
if ($PSBoundParameters.ContainsKey('MsiExecSwitches'))
{
$params.MsiExecSwitches = $MsiExecSwitches
}
if ($Software.GUID)
{
$params.Guid = $Software.GUID
}
else
{
$params.Name = $Software.Name
}
Uninstall-WindowsInstallerPackage @params
}
elseif ($InstallerType -eq 'InstallShield')
{
Write-Log -Message "Installer type detected as Installshield."
$Params = @{
'IssFilePath' = $IssFilePath;
'Name' = $Software.Name
'SetupFilePath' = $InstallShieldSetupFilePath
}
if ($InstallshieldLogFilePath)
{
$Params.InstallshieldLogFilePath = $InstallshieldLogFilePath
}
Uninstall-InstallShieldPackage @Params
}
if (Test-InstalledSoftware -Name $Software.Name)
{
Write-Log -Message "$($Software.Name) was not uninstalled via traditional uninstall" -LogLevel '2'
if ($RunMsizap.IsPresent)
{
Write-Log -Message "Attempting Msizap..."
Uninstall-ViaMsizap -Guid $Software.GUID -MsizapFilePath $MsizapFilePath -Params $MsiZapParams
}
else
{
Write-Log -Message "$($Software.Name) failed to uninstall successfully" -LogLevel '3'
}
}
$outputProps = @{ }
if (-not (Test-InstalledSoftware -Name $Software.Name))
{
Write-Log -Message "Successfully removed $($Software.Name)"
}
else
{
throw "Failed to remove $($Software.Name)"
}
}
catch
{
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
function Import-Certificate
{
<#
.SYNOPSIS
This function imports a certificate into any certificate store on a local computer
.EXAMPLE
PS> Import-Certificate -Context LocalMachine -StoreName My -FilePath C:\certificate.cer
This example will import the certificate.cert certificate into the Personal store for the
local computer
.EXAMPLE
PS> Import-Certificate -Context CurrentUser -StoreName TrustedPublisher -FilePath C:\certificate.cer
This example will import the certificate.cer certificate into the Trusted Publishers store for the
currently logged on user
.PARAMETER Context
This is the Context (either CurrentUser or LocalMachine) where the store is located which the certificate
will go into.
.PARAMETER StoreName
This is the certificate store that the certificate will be placed into
.PARAMETER FilePath
This is the path to the certificate file that you'd like to import
#>
[OutputType([void])]
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateSet('CurrentUser', 'LocalMachine')]
[string]$Context,
[Parameter(Mandatory = $true)]
[ValidateScript({
if ($Context -eq 'CurrentUser')
{
(Get-ChildItem Cert:\CurrentUser | Select-Object -ExpandProperty name) -contains $_
}
else
{
(Get-ChildItem Cert:\LocalMachine | Select-Object -ExpandProperty name) -contains $_
}
})]
[string]$StoreName,
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-Path $_ -PathType Leaf })]
[string]$FilePath
)
begin
{
$ErrorActionPreference = 'Stop'
try
{
[void][System.Reflection.Assembly]::LoadWithPartialName('System.Security')
}
catch
{
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
process
{
try
{
$Cert = Get-Item $FilePath
$Cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $Cert
$X509Store = New-Object System.Security.Cryptography.X509Certificates.X509Store $StoreName, $Context
$X509Store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$X509Store.Add($Cert)
$X509Store.Close()
}
catch
{
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
function Test-Process
{
<#
.SYNOPSIS
This function is called after the execution of an external CMD process to log the status of how the process was exited.
.PARAMETER Process
A System.Diagnostics.Process object type that is output by using the -Passthru parameter on the Start-Process cmdlet
#>
[OutputType([bool])]
[CmdletBinding()]
param (
[Parameter()]
[System.Diagnostics.Process]$Process
)
process
{
try
{
if (@(0, 3010) -notcontains $Process.ExitCode)
{
Write-Log -Message "Process ID $($Process.Id) failed. Return value was $($Process.ExitCode)" -LogLevel '2'
$false
}
else
{
Write-Log -Message "Process ID $($Process.Id) exited with successfull exit code '$($Process.ExitCode)'."
$true
}
}
catch
{
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
function Get-ChildProcess
{
<#
.SYNOPSIS
This function childs all child processes a parent process has spawned
.PARAMETER ProcessId
The potential parent process ID
#>
[OutputType([System.Management.ManagementObject])]
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$ProcessId
)
process
{
try
{
Get-WmiObject -Class Win32_Process -Filter "ParentProcessId = '$ProcessId'"
}
catch
{
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
function Stop-MyProcess
{
<#
.SYNOPSIS
This function stops a process while provided robust logging of the activity
.PARAMETER ProcessName
One more process names
#>
[OutputType([void])]
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[Parameter(Mandatory = $true)]
[string[]]$ProcessName
)
process
{
try
{
$ProcessesToStop = Get-Process -Name $ProcessName -ErrorAction 'SilentlyContinue'
if (-not $ProcessesToStop)
{
Write-Log -Message "-No processes to be killed found..."
}
else
{
foreach ($process in $ProcessesToStop)
{
Write-Log -Message "-Process $($process.Name) is running. Attempting to stop..."
$WmiProcess = Get-WmiObject -Class Win32_Process -Filter "name='$($process.Name).exe'" -ErrorAction 'SilentlyContinue' -ErrorVariable WMIError
if ($WmiError)
{
throw "process $($process.Name). WMI query errored with `"$($WmiError.Exception.Message)`""
}
elseif ($WmiProcess)
{
foreach ($p in $WmiProcess)
{
if ($PSCmdlet.ShouldProcess("Process ID: $($p.ProcessId)", 'Stop'))
{
$WmiResult = $p.Terminate()
if ($WmiResult.ReturnValue -eq 1603)
{
Write-Log -Message "Process $($p.name) exited successfully but needs a reboot."
}
elseif ($WmiResult.ReturnValue -ne 0)
{
throw "-Unable to stop process $($p.name). Return value was $($WmiResult.ReturnValue)"
}
else
{
Write-Log -Message "-Successfully stopped process $($p.Name)..."
}
}
}
}
}
}
}
catch
{
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
function Stop-SoftwareProcess
{
[OutputType([void])]
[CmdletBinding(SupportsShouldProcess = $true)]
param
(
[Parameter(Mandatory = $true,ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[object]$Software
)
begin {
$ErrorActionPreference = 'Stop'
}
process {
try
{
$Processes = (Get-Process | Where-Object { $_.Path -like "$($Software.InstallLocation)*" } | Select-Object -ExpandProperty Name)
if ($Processes)
{
Write-Log -Message "Sending processes: $Processes to Stop-MyProcess..."
## Check to see if the process is still running. It's possible the termination of other processes
## already killed this one.
$Processes = $Processes | Where-Object { Get-Process -Name $_ -ErrorAction 'SilentlyContinue' }
if ($PSCmdlet.ShouldProcess("Process ID $($Processes)", 'Stop'))
{
Stop-MyProcess $Processes
}
}
else
{
Write-Log -Message 'No processes running under the install folder path'
}
}
catch
{
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
function Wait-MyProcess
{
<#
.SYNOPSIS
This function waits for a process and waits for all that process's children before releasing control
.PARAMETER ProcessId
A process Id
.PARAMETER ProcessTimeout
An interval (in seconds) to wait for the process to finish. If the process hasn't exited within this timeout
it will be terminated. The default is 600 seconds (5 minutes) so no process will run longer than that.
.PARAMETER ReportInterval
The number of seconds between when it is logged that the process is still pending
#>
[OutputType([void])]
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[int]$ProcessId,
[Parameter()]
[ValidateNotNullOrEmpty()]
[int]$ProcessTimeout = 600,
[Parameter()]
[ValidateNotNullOrEmpty()]
[int]$ReportInterval = 15
)
process
{
try
{
Write-Log -Message "Finding the process ID '$ProcessId'..."
$Process = Get-Process -Id $ProcessId -ErrorAction 'SilentlyContinue'
if ($Process)
{
Write-Log -Message "Process '$($Process.Name)' ($($Process.Id)) found. Waiting to finish and capturing all child processes."
## While waiting for the initial process to stop, collect all child IDs it spawns
$ChildProcessesToLive = @()
## Start the timer to ensure we have a point to get total time from
$Timer = [Diagnostics.Stopwatch]::StartNew()
$i = 0
## Do this while the parent process is still running
while (-not $Process.HasExited)
{
## Find any and all child processes the parent process spawned
$ChildProcesses = Get-ChildProcess -ProcessId $ProcessId
if ($ChildProcesses)
{
Write-Log -Message "Found [$(@($ChildProcesses).Count)] child process(es)"
## If any child processes are found, collect them all
$ChildProcessesToLive += $ChildProcesses
}
if ($Timer.Elapsed.TotalSeconds -ge $ProcessTimeout)
{
Write-Log -Message "The process '$($Process.Name)' ($($Process.Id)) has exceeded the timeout of $ProcessTimeout seconds. Killing process."
$Timer.Stop()
Stop-MyProcess -ProcessName $Process.Name
}
elseif (($i % $ReportInterval) -eq 0) ## Use a modulus here to write to the log every X seconds
{
Write-Log "Still waiting for process '$($Process.Name)' ($($Process.Id)) after $([Math]::Round($Timer.Elapsed.TotalSeconds, 0)) seconds"
}
Start-Sleep -Milliseconds 100
$i++
}
Write-Log "Process '$($Process.Name)' ($($Process.Id)) has finished after $([Math]::Round($Timer.Elapsed.TotalSeconds, 0)) seconds"
if ($ChildProcessesToLive) ## If any child processes were spawned while the parent process was running
{
$ChildProcessesToLive = $ChildProcessesToLive | Select-Object -Unique ## Ensure we didn't accidently capture duplicate PIDs
Write-Log -Message "Parent process '$($Process.Name)' ($($Process.Id)) has finished but still has $(@($ChildProcessesToLive).Count) child processes ($($ChildProcessesToLive.Name -join ',')) left. Waiting on these to finish."
foreach ($Process in $ChildProcessesToLive)
{
Wait-MyProcess -ProcessId $Process.ProcessId
}
}
else
{
Write-Log -Message 'No child processes found spawned'
}
Write-Log -Message "Finished waiting for process '$($Process.Name)' and all child processes"
}
else
{
Write-Log -Message "Process ID '$ProcessId' not found. No need to wait on it."
}
}
catch
{
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
function Get-InstallshieldInstallString
{
[OutputType([string])]
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$InstallerFilePath,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$IssFilePath,
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$LogFilePath,
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$ExtraSwitches
)
begin {
$ErrorActionPreference = 'Stop'
}
process {
try
{
Write-Log -Message 'Creating the InstallShield setup install string'
## We're adding common InstallShield switches here. -s is silent, -f1 specifies where the
## ISS file we createed previously lives, -f2 specifies a log file location and /SMS is a special
## switch that prevents the setup.exe was exiting prematurely.
if (-not $PSBoundParameters.ContainsKey('LogFilePath'))
{
$LogFilePath = "$(Get-SystemTempFolderPath)\$($InstallerFilePath | Split-Path -Leaf).log"
}
if (-not $ExtraSwitches)
{
$InstallArgs = "-s -f1`"$IssFilePath`" -f2`"$LogFilePath`" /SMS"
}
else
{
$InstallArgs = "-s -f1`"$IssFilePath`" $ExtraSwitches -f2`"$LogFilePath`" /SMS"
}
}
catch
{
Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
$PSCmdlet.ThrowTerminatingError($_)
}
}
}
function Uninstall-InstallShieldPackage
{
<#
.SYNOPSIS
This function runs an uninstall for any InstallShield packaged software. This function utilitizes an
InstallShield ISS file to silently uninstall the application.
.PARAMETER Name
One or more software titles of the InstallShield package you'd like to uninstall.
.PARAMETER IssFilePath
The file path where the pre-built silent answer file (ISS) is located.
.PARAMETER SetupFilePath
The file path where the EXE InstallShield installer is located.
.PARAMETER LogFilePath
The log file path where the InstallShield installer will log results. If not log file path
is specified it will be created in the system temp folder.
#>
[OutputType([void])]
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string[]]$Name,
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
[string]$IssFilePath,
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })]
[string]$SetupFilePath,