SoftwareInstallManager.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
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(ParameterSetName = 'Name')]
        [ValidateNotNullOrEmpty()]
        [string]$Name,
        
        [Parameter(ParameterSetName = 'Name')]
        [ValidateNotNullOrEmpty()]
        [string]$Version,
        
        [Parameter(ParameterSetName = 'Guid')]
        [ValidateNotNullOrEmpty()]
        [Alias('ProductCode')]
        [string]$Guid
    )
    process
    {
        try
        {
            
            if ($PSBoundParameters.ContainsKey('Name'))
            {
                if ($PSBoundParameters.ContainsKey('Version'))
                {
                    $SoftwareInstances = Get-InstalledSoftware -Name $Name | Where-Object { $_.Version -eq $Version }
                }
                else
                {
                    $SoftwareInstances = Get-InstalledSoftware -Name $Name
                }
            }
            elseif ($PSBoundParameters.ContainsKey('Guid'))
            {
                $SoftwareInstances = Get-InstalledSoftware -Guid $Guid
            }
            
            
            if (-not $SoftwareInstances)
            {
                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([PSObject])]
    [CmdletBinding()]
    param (
        [string]$Name,
        
        [ValidatePattern('\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')]
        [string]$Guid
    )
    process
    {
        try
        {
            
            $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: | Where-Object { $_.Name -match 'S-\d-\d+-(\d+-){1,14}\d+$' } | ForEach-Object { "HKU:\$($_.PSChildName)\Software\Microsoft\Windows\CurrentVersion\Uninstall" }
            if (-not $UninstallKeys)
            {
                Write-Log -Message 'No software registry keys found' -LogLevel '2'
            }
            else
            {
                foreach ($UninstallKey in $UninstallKeys)
                {
                    $friendlyNames = @{
                        'DisplayName' = 'Name'
                        'DisplayVersion' = 'Version'
                    }
                    Write-Log -Message "Checking uninstall key [$($UninstallKey)]"
                    if ($PSBoundParameters.ContainsKey('Name'))
                    {
                        $WhereBlock = { $_.GetValue('DisplayName') -like "$Name*" }
                    }
                    elseif ($PSBoundParameters.ContainsKey('GUID'))
                    {
                        $WhereBlock = { $_.PsChildName -eq $Guid }
                    }
                    else
                    {
                        $WhereBlock = { $_.GetValue('DisplayName') }
                    }
                    $SwKeys = Get-ChildItem -Path $UninstallKey -ErrorAction SilentlyContinue | Where-Object $WhereBlock
                    if (-not $SwKeys)
                    {
                        Write-Log -Message "No software keys in uninstall key $UninstallKey"
                    }
                    else
                    {
                        foreach ($SwKey in $SwKeys)
                        {
                            $output = @{ }
                            foreach ($ValName in $SwKey.GetValueNames())
                            {
                                if ($ValName -ne 'Version')
                                {
                                    $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-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()]
    [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 = New-MsiexecInstallString -InstallerFilePath $InstallerFilePath -MspFilePath $MspFilePath -MstFilePath $MstFilePath -LogFilePath $LogFilePath
                
                ## Add Start-Process parameters
                $ProcessParams['FilePath'] = 'msiexec.exe'
                $ProcessParams['ArgumentList'] = $InstallArgs
            }
            elseif ($PSBoundParameters.ContainsKey('InstallShieldInstallerFilePath'))
            {
                $InstallerFilePath = $InstallShieldInstallerFilePath
                
                $InstallArgs = New-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
            }
            
            $outputProps = @{ }
            if ($Result.ExitCode -notin @(0, 3010))
            {
                throw "Failed to install software. Installer exited with exit code [$($Result.ExitCode)]"
            }
        }
        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()]
    [CmdletBinding(DefaultParameterSetName = 'MSI')]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = 'FromPipeline')]
        [ValidateNotNullOrEmpty()]
        [object]$Software,
    
        [Parameter(Mandatory = $true,
                   ValueFromPipelineByPropertyName = $true)]
        [string]$Name,
        
        [Parameter(ParameterSetName = 'MSI')]
        [string]$MsiExecSwitches,
        
        [Parameter()]
        [string]$LogFilePath,
        
        [Parameter(ParameterSetName = 'ISS')]
        [string]$InstallshieldLogFilePath,
        
        [Parameter(ParameterSetName = 'Msizap')]
        [switch]$RunMsizap,
        
        [Parameter(ParameterSetName = 'Msizap')]
        [string]$MsizapParams = 'TWG!',
        
        [Parameter(ParameterSetName = 'Msizap')]
        [ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
        [string]$MsizapFilePath = 'C:\MyDeployment\msizap.exe',
        
        [Parameter(ParameterSetName = 'ISS',
                   Mandatory = $true)]
        [ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
        [ValidatePattern('\.iss$')]
        [string]$IssFilePath,
        
        [Parameter(ParameterSetName = 'ISS')]
        [ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
        [string]$InstallShieldSetupFilePath
    )
    process
    {
        try
        {
            
            
            if ($PSCmdlet.ParameterSetName -ne 'FromPipeline')
            {
                $Software = Get-InstalledSoftware -Name $Name
            }
            
            if (-not $Software)
            {
                Write-Log -Message "The software [$($Name)] was not found"
            }
            else
            {
                try
                {
                    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 $Name not found" -LogLevel '2'
                    }
                    if (-not $PsBoundParameters['LogFilePath'])
                    {
                        $script:LogFilePath = "$(Get-SystemTempFolderPath)\$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 $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 = $Name
                        }
                        
                        Uninstall-WindowsInstallerPackage @params
                        
                    }
                    elseif ($InstallerType -eq 'InstallShield')
                    {
                        Write-Log -Message "Installer type detected as Installshield."
                        $Params = @{
                            'IssFilePath' = $IssFilePath;
                            'Name' = $Name;
                            'SetupFilePath' = $InstallShieldSetupFilePath
                        }
                        if ($InstallshieldLogFilePath)
                        {
                            $Params.InstallshieldLogFilePath = $InstallshieldLogFilePath
                        }
                        Uninstall-InstallShieldPackage @Params
                    }
                    if (Test-InstalledSoftware -Name $Name)
                    {
                        Write-Log -Message "$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 "$Name failed to uninstall successfully" -LogLevel '3'
                        }
                    }

                    $outputProps = @{ }
                    if (-not (Test-InstalledSoftware -Name $Name))
                    {
                        Write-Log -Message "Successfully removed $Name!"
                    }
                    else
                    {
                        throw "Failed to remove $Name"
                    }
                }
                catch
                {
                    Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
                    $PSCmdlet.ThrowTerminatingError($_)
                }
            }
        }
        catch
        {
            Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3'
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}