DSCResources/cApplication/cApplication.psm1

Enum Ensure{
    Absent
    Present
}

function Get-TargetResource {
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    Param
    (
        [Parameter(Mandatory = $false)]
        [ValidateSet("Present", "Absent")]
        [string]
        $Ensure = 'Present',

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]
        $InstallerPath,

        [string]
        $ProductId,

        [string]
        $InstalledCheckFilePath,

        [bool]
        $Fuzzy = $false
    )

    # Get Application info
    # $InstalledCheckFilePath has highest priority
    if ($InstalledCheckFilePath) {
        Write-Verbose ('InstalledCheckFilePath is specified. Whether an application exists or not is judged by whether or not the path exists.')
        if (Test-Path $InstalledCheckFilePath) {
            Write-Verbose ('"{0}" is exist.' -f $InstalledCheckFilePath)
            $Program = @{
                DisplayName = $Name
            }
        }
        else {
            Write-Verbose ('"{0}" is not exist.' -f $InstalledCheckFilePath)
            $Program = $null
        }
    }
    # $ProductId take priority over $Name
    elseif ($ProductId) {
        $Program = Get-InstalledProgram -ProductId $ProductId
    }
    else {
        $Program = Get-InstalledProgram -Name $Name -Fuzzy:$Fuzzy
    }

    if (-not $Program) {
        Write-Verbose ('The application "{0}" is not installed.' -f $Name)
        $returnValue = @{
            Ensure        = [Ensure]::Absent
            Name          = ''
            InstallerPath = $InstallerPath
            Installed     = $false
        }
        return $returnValue
    }
    else {
        Write-Verbose ('The application "{0}" is installed.' -f $Program.DisplayName)
        $ProgramInfo = @{
            Ensure          = 'Present'
            Name            = $Program.DisplayName
            ProductId       = $Program.PSChildName
            Version         = $Program.DisplayVersion
            Publisher       = $Program.Publisher
            InstallerPath   = $InstallerPath
            UninstallString = $Program.UninstallString
            Installed       = $true
        }
        return $ProgramInfo
    }
} # end of Get-TargetResource


function Test-TargetResource {
    [CmdletBinding()]
    [OutputType([bool])]
    Param
    (
        [Parameter(Mandatory = $false)]
        [ValidateSet("Present", "Absent")]
        [string]
        $Ensure = 'Present',

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]
        $InstallerPath,

        [string]
        $ProductId,

        [string]
        $InstalledCheckFilePath,

        [string]
        $InstalledCheckScript,

        [bool]
        $Fuzzy = $false,

        [bool]
        $NoRestart = $false,

        [string]
        $Version,

        [string]
        $Arguments,

        [string]
        $ArgumentsForUninstall,

        [string]
        $WorkingDirectory,

        [bool]
        $UseUninstallString = $false,

        [PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # Return codes 1641 and 3010 indicate success when a restart is requested per installation
        [ValidateNotNullOrEmpty()]
        [UInt32[]]
        $ReturnCode = @( 0, 1641, 3010 ),

        [UInt32]
        $TimeoutSec = 900,

        [string]
        $FileHash,

        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')]
        [string]
        $HashAlgorithm = 'SHA256',

        [string]
        $PreAction,

        [string]
        $PostAction,

        [string]
        $PreCopyFrom,

        [string]
        $PreCopyTo
    )

    if ($InstalledCheckScript) {
        $local:scriptBlock = [ScriptBlock]::Create($InstalledCheckScript)
        return [bool]($local:scriptBlock.Invoke())
    }

    $private:GetParam = @{
        Ensure                 = $Ensure
        Name                   = $Name
        InstallerPath          = $InstallerPath
        ProductId              = $ProductId
        InstalledCheckFilePath = $InstalledCheckFilePath
        Fuzzy                  = $Fuzzy
    }

    $ProgramInfo = Get-TargetResource @GetParam -ErrorAction Stop

    if ($Ensure -eq 'Absent') {
        switch ($ProgramInfo.Ensure) {
            'Absent' {
                Write-Verbose ('Match desired state & current state. Return "True"')
                return $true
            }
            'Present' {
                Write-Verbose ('Mismatch desired state & current state. Return "False"')
                return $false
            }
            Default {
                Write-Error 'Test failed (unexpected error)'
            }
        }
    }
    else {
        switch ($ProgramInfo.Ensure) {
            'Absent' {
                Write-Verbose ('Mismatch desired state & current state. Return "False"')
                return $false
            }
            'Present' {
                if ($Version) {
                    if ($Version -ne $ProgramInfo.Version) {
                        Write-Verbose ('The application "{0}" is installed. but NOT match your desired version. (Desired version: "{1}", Installed version: "{2}")' -f $Name, $Version, $ProgramInfo.Version)
                        Write-Verbose ('Mismatch desired state & current state. Return "False"')
                        return $false
                    }
                }

                Write-Verbose ('Match desired state & current state. Return "True"')
                return $true
            }
            Default {
                Write-Error 'Test failed (unexpected error)'
            }
        }
    }
} # end of Test-TargetResource


function Set-TargetResource {
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false)]
        [ValidateSet("Present", "Absent")]
        [string]
        $Ensure = 'Present',

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]
        $InstallerPath,

        [string]
        $ProductId,

        [string]
        $InstalledCheckFilePath,

        [string]
        $InstalledCheckScript,

        [bool]
        $Fuzzy = $false,

        [bool]
        $NoRestart = $false,

        [string]
        $Version,

        [string]
        $Arguments,

        [string]
        $ArgumentsForUninstall,

        [string]
        $WorkingDirectory,

        [bool]
        $UseUninstallString = $false,

        [PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # Return codes 1641 and 3010 indicate success when a restart is requested per installation
        [ValidateNotNullOrEmpty()]
        [UInt32[]]
        $ReturnCode = @( 0, 1641, 3010 ),

        [UInt32]
        $TimeoutSec = 900,

        [string]
        $FileHash,

        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')]
        [string]
        $HashAlgorithm = 'SHA256',

        [string]
        $PreAction,

        [string]
        $PostAction,

        [string]
        $PreCopyFrom,

        [string]
        $PreCopyTo
    )

    if (($Ensure -eq 'Absent') -and (!$UseUninstallString) -and (!$InstallerPath)) {
        Write-Error ("InstallerPath is not specified. Skip Set-Configuration.")
        return
    }
    elseif (($Ensure -eq 'Present') -and (!$InstallerPath)) {
        Write-Error ("InstallerPath is not specified. Skip Set-Configuration.")
        return
    }

    #PreCopy
    if ([string]::IsNullOrWhiteSpace($PreCopyFrom) -and $PreCopyTo) {
        Write-Warning ('PreCopyTo parameter is specified, but PreCopyFrom is empty. You should specify both PreCopyFrom and PreCopyTo.')
    }
    elseif ($PreCopyFrom -and [string]::IsNullOrWhiteSpace($PreCopyTo)) {
        Write-Warning ('PreCopyFrom parameter is specified, but PreCopyTo is empty. You should specify both PreCopyFrom and PreCopyTo.')
    }
    elseif ($PreCopyFrom -and $PreCopyTo) {
        Write-Verbose ('PreCopy From:"{0}" To:"{1}"' -f $PreCopyFrom, $PreCopyTo)
        Get-RemoteFile -Path $PreCopyFrom -DestinationFolder $PreCopyTo -Credential $Credential -TimeoutSec $TimeoutSec -Force -ErrorAction Stop >$null
    }

    #PreAction
    try {
        Invoke-ScriptBlock -ScriptBlockString $PreAction
    }
    catch [Exception] {
        Write-Error -Exception $_.Exception
    }

    $private:TempFolder = $env:TEMP
    $private:UseWebFile = $false
    $private:Installer = ''
    $private:strInOrUnin = ''
    $private:msiOpt = ''
    $private:Arg = New-Object 'System.Collections.Generic.List[System.String]'
    $private:tmpDriveName = [Guid]::NewGuid()

    try {
        if ($Ensure -eq 'Absent') {
            Write-Verbose ('Ensure = "Absent". Try to uninstall an application.')
            $strInOrUnin = 'Uninstall'
            $msiOpt = 'x'
            $Arguments = $ArgumentsForUninstall

            if ($UseUninstallString) {
                $private:GetParam = @{
                    Ensure        = $Ensure
                    Name          = $Name
                    InstallerPath = $InstallerPath
                    ProductId     = $ProductId
                    Fuzzy         = $Fuzzy
                }
                $private:ProgramInfo = Get-TargetResource @GetParam -ErrorAction Stop

                if (-not $ProgramInfo.UninstallString) {
                    throw ("Couldn't get UninstallString.")
                }

                Write-Verbose ('Use UninstallString for uninstall. ("{0}")' -f $ProgramInfo.UninstallString)
                $UseWebFile = $false
                if ($ProgramInfo.UninstallString -match '^(?<path>.+\.[a-z]{3})(?<args>.*)') {
                    $Installer = $Matches.path
                    $Arg.Add($Matches.args)
                }
                else {
                    throw ("Couldn't parse UninstallString.")
                }
            }
        }
        else {
            Write-Verbose ('Ensure = "Present". Try to install an application.')
            $strInOrUnin = 'Install'
            $msiOpt = 'i'
        }

        if (($Ensure -eq 'Absent') -and $UseUninstallString) {
        }
        else {
            Write-Verbose ('Use Installer ("{0}") for {1}. (if the path of installer as http/https/ftp. will download it)' -f $InstallerPath, $strInOrUnin)
            if ($InstallerPath -match '^msiexec[.exe]?') {
                #[SpecialTreat]If specified 'msiexec.exe', replace 'C:\Windows\System32\msiexec.exe'
                $InstallerPath = (Join-Path $env:windir '\system32\msiexec.exe')
            }
            $private:tmpPath = [System.Uri]$InstallerPath
            if ($tmpPath.IsLoopback -or $tmpPath.IsUnc) {
                Write-Verbose ('"{0}" is local file or remote unc file.' -f $tmpPath.LocalPath)
                $UseWebFile = $false
                if ($PSBoundParameters.Credential) {
                    New-PSDrive -Name $tmpDriveName -PSProvider FileSystem -Root (Split-Path $tmpPath.LocalPath) -Credential $Credential -ErrorAction Stop > $null
                }
                $Installer = $tmpPath.LocalPath
            }
            else {
                $UseWebFile = $true
                $Installer = (Get-RemoteFile -Path $InstallerPath -DestinationFolder $TempFolder -Credential $Credential -TimeoutSec $TimeoutSec -Force -PassThru -ErrorAction Stop)
            }

            if ($FileHash) {
                if (-not (Assert-FileHash -Path $Installer -FileHash $FileHash -Algorithm $HashAlgorithm)) {
                    throw ("File '{0}' does not match expected hash value" -f $Installer)
                }
                else {
                    Write-Verbose ("Hash check passed")
                }
            }
        }

        $Arg.Add($Arguments)
        if (-not (Test-Path $Installer -PathType Leaf)) {
            throw ("Installer file not found. ('{0}')" -f $Installer)
        }

        if ([System.IO.Path]::GetExtension($Installer) -eq '.msi') {
            $Arg.Insert(0, ('/{0} "{1}"' -f $msiOpt, $Installer))
            $Installer = 'msiexec.exe'
        }

        $CommandParam = @{
            FilePath     = $Installer
            ArgumentList = $Arg
        }
        if ($WorkingDirectory) {
            $CommandParam.WorkingDirectory = $WorkingDirectory
            Write-Verbose ("{2} start. Installer:'{0}', Args:'{1}', WorkDir:'{3}'" -f $Installer, $Arg, $strInOrUnin, $WorkingDirectory)
        }
        else {
            Write-Verbose ("{2} start. Installer:'{0}', Args:'{1}'" -f $Installer, $Arg, $strInOrUnin)
        }
        $ExitCode = Start-Command @CommandParam -ErrorAction Stop
        Write-Verbose ("{1} end. ExitCode: '{0}'" -f $ExitCode, $strInOrUnin)

        if (-not ($ReturnCode -contains $ExitCode)) {
            throw ("The exit code {0} was not expected. Configuration is likely not correct" -f $ExitCode)
        }
        else {
            Write-Verbose ('{0} process exited successfully' -f $strInOrUnin)
        }

        if (-not $NoRestart) {
            $private:serverFeatureData = Invoke-CimMethod -Name 'GetServerFeature' -Namespace 'root\microsoft\windows\servermanager' -Class 'MSFT_ServerManagerTasks' -Arguments @{ BatchSize = 256 } -ErrorAction 'Ignore' -Verbose:$false
            $private:registryData = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -ErrorAction 'Ignore'
            if (($serverFeatureData -and $serverFeatureData.RequiresReboot) -or $registryData -or ($exitcode -eq 3010) -or ($exitcode -eq 1641)) {
                Write-Verbose "The machine requires a reboot"
                $global:DSCMachineStatus = 1
            }
        }
    }
    catch [Exception] {
        Write-Error -Exception $_.Exception
    }
    finally {
        if ($PreCopyTo -and (Test-Path $PreCopyTo -ErrorAction SilentlyContinue)) {
            Write-Verbose ("Remove PreCopied file(s)")
            Remove-Item $PreCopyTo -Force -Recurse > $null
        }
        if ($UseWebFile -and $Installer -and (Test-Path $Installer -PathType Leaf -ErrorAction SilentlyContinue)) {
            Write-Verbose ("Remove temp files")
            Remove-Item $Installer -Force -Recurse > $null
        }
        if (Get-PSDrive | Where-Object { $_.Name -eq $tmpDriveName }) {
            Remove-PSDrive -Name $tmpDriveName -Force -ErrorAction SilentlyContinue
        }
    }

    #PostAction
    try {
        Invoke-ScriptBlock -ScriptBlockString $PostAction
    }
    catch [Exception] {
        Write-Error -Exception $_.Exception
    }

} # end of Set-TargetResource


function Get-RemoteFile {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, Position = 0)]
        [Alias("Uri")]
        [Alias("SourcePath")]
        [System.Uri[]] $Path,

        [Parameter(Mandatory = $true, Position = 1)]
        [string]$DestinationFolder,

        [Parameter()]
        [AllowNull()]
        [pscredential]$Credential,

        [Parameter()]
        [int]$TimeoutSec = 0,

        [Parameter()]
        [switch]$Force,

        [Parameter()]
        [switch]$PassThru
    )
    begin {
        if (-not (Test-Path $DestinationFolder -PathType Container)) {
            Write-Verbose ('DestinationFolder "{0}" is not exist. Will create it.' -f $DestinationFolder)
            New-Item $DestinationFolder -ItemType Directory -Force -ErrorAction Stop > $null
        }
    }

    Process {
        foreach ($private:tempPath in $Path) {
            try {
                $private:OutFile = ''
                $private:valid = $true
                $private:tmpDriveName = [Guid]::NewGuid()

                if ($null -eq $tempPath.IsLoopback) {
                    $valid = $false
                    throw ("{0} is not valid uri." -f $tempPath)
                }

                # Depending on the location of the installer processing branch (local or shared folder or Web)
                if ($tempPath.IsLoopback -and (!$tempPath.IsUnc)) {
                    # Local file
                    Write-Verbose ('"{0}" is local file.' -f $tempPath.LocalPath)
                    $valid = $true
                    $OutFile = $tempPath.LocalPath
                    Write-Verbose ("Copy file from '{0}' to '{1}'" -f $tempPath.LocalPath, $DestinationFolder)
                    Copy-Item -Path $tempPath.LocalPath -Destination $DestinationFolder -ErrorAction Stop -Force:$Force -Recurse -PassThru:$PassThru
                }
                elseif ($tempPath.IsUnc) {
                    # Shared folder
                    # When using credentials it is necessary to map the drive first
                    if ($PSBoundParameters.Credential) {
                        New-PSDrive -Name $tmpDriveName -PSProvider FileSystem -Root (Split-Path $tempPath.LocalPath) -Credential $Credential -ErrorAction Stop > $null
                    }
                    # Copy to Local
                    $OutFile = Join-Path $DestinationFolder ([System.IO.Path]::GetFileName($tempPath.LocalPath))
                    if (Test-Path $OutFile -PathType Leaf) {
                        if ($tempPath.LocalPath -eq $OutFile) {
                            if ($PassThru) {
                                if (Test-Path $OutFile) {
                                    Get-Item $OutFile
                                }
                            }
                            continue
                        }
                        elseif ($Force) {
                            Write-Warning ('"{0}" will be overwritten.' -f $OutFile)
                        }
                        else {
                            $valid = $false
                            throw ("'{0}' is exist. If you want to replace existing file, Use 'Force' switch." -f $OutFile)
                        }
                    }

                    Write-Verbose ("Copy file from '{0}' to '{1}'" -f $tempPath.LocalPath, $DestinationFolder)
                    Copy-Item -Path $tempPath.LocalPath -Destination $DestinationFolder -ErrorAction Stop -Force:$Force -Recurse
                }
                elseif ($tempPath.Scheme -match 'http|https|ftp') {
                    # Download from Web
                    if ($redUri = Get-RedirectedUrl $tempPath.AbsoluteUri -ErrorAction SilentlyContinue) {
                        # When it is not a file direct link, obtain the file name of the redirect destination(issue #1)
                        $OutFile = Join-Path $DestinationFolder ([System.IO.Path]::GetFileName($redUri.LocalPath))
                    }
                    else {
                        $OutFile = Join-Path $DestinationFolder ([System.IO.Path]::GetFileName($tempPath.LocalPath))
                    }
                    if (Test-Path $OutFile -PathType Leaf) {
                        if ($Force) {
                            Write-Warning ('"{0}" will be overwritten.' -f $OutFile)
                        }
                        else {
                            $valid = $false
                            throw ("'{0}' is exist. If you want to replace existing file, Use 'Force' switch." -f $OutFile)
                        }
                    }

                    Write-Verbose ("Download file from '{0}' to '{1}'" -f $tempPath.AbsoluteUri, $OutFile)
                    #Suppress Progress bar for faster download
                    $private:origProgress = $ProgressPreference
                    $ProgressPreference = 'SilentlyContinue'
                    Invoke-WebRequest -Uri $tempPath.AbsoluteUri -OutFile $OutFile -Credential $Credential -TimeoutSec $TimeoutSec -ErrorAction stop
                    $ProgressPreference = $private:origProgress
                }
                else {
                    $valid = $false
                    throw ("{0} is not valid uri." -f $tempPath)
                }

                if ($valid -and $OutFile -and $PassThru) {
                    if (Test-Path $OutFile) {
                        Get-Item $OutFile
                    }
                }
            }
            catch [Exception] {
                Write-Error -Exception $_.Exception
            }
            finally {
                if (Get-PSDrive | Where-Object { $_.Name -eq $tmpDriveName }) {
                    Remove-PSDrive -Name $tmpDriveName -Force -ErrorAction SilentlyContinue
                }
            }
        }
    }
}


function Assert-FileHash {
    [CmdletBinding()]
    [OutputType([bool])]
    Param(
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $FileHash,

        [Parameter()]
        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')]
        [string]
        $Algorithm = 'SHA256'
    )

    Process {
        $private:hash = Get-FileHash -Path $Path -Algorithm $Algorithm | Select-Object Hash
        if ($FileHash -eq $hash.Hash) {
            Write-Verbose ('Match file hash of "{1}". ({0})' -f $hash.Hash, $Path)
            return $true
        }
        else {
            Write-Verbose ('Not match file hash of "{1}". ({0})' -f $hash.Hash, $Path)
            return $false
        }
    }
}


function Get-InstalledProgram {
    [CmdletBinding(DefaultParameterSetName = 'Name')]
    Param(
        [Parameter(Mandatory, ParameterSetName = 'Name')]
        [string] $Name,

        [Parameter(Mandatory, ParameterSetName = 'Id')]
        [string] $ProductId,

        [Parameter(ParameterSetName = 'Name')]
        [switch] $Fuzzy,

        [switch] $Wow64,

        [switch] $FallbackToWow64 = $true
    )

    $local:Program = $null
    switch ($Wow64) {
        $true {
            $UninstallRegMachine = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
            $UninstallRegUser = "HKCU:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
        }

        $false {
            $UninstallRegMachine = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
            $UninstallRegUser = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall"
        }
    }

    $local:InstalledPrograms = @()
    $local:InstalledPrograms += Get-ChildItem $UninstallRegMachine | ForEach-Object { Get-ItemProperty $_.PSPath } | Where-Object { $_.DisplayName }
    if (Test-Path $UninstallRegUser) {
        $local:InstalledPrograms += Get-ChildItem $UninstallRegUser | ForEach-Object { Get-ItemProperty $_.PSPath } | Where-Object { $_.DisplayName }
    }

    switch ($PsCmdlet.ParameterSetName) {
        'Name' {
            if ($Fuzzy) {
                $Program = $InstalledPrograms | Where-Object { $_.DisplayName -match $Name } | Select-Object -First 1
            }
            else {
                $Program = $InstalledPrograms | Where-Object { $_.DisplayName -eq $Name } | Select-Object -First 1
            }
            break
        }
        'Id' {
            $ProductId = Format-ProductId -ProductId $ProductId
            $Program = $InstalledPrograms | Where-Object { $_.PSChildName -eq $ProductId } | Select-Object -First 1
            break
        }
    }

    if ($Program) {
        $Program
    }
    elseif ((!$Wow64) -and $FallbackToWow64 -and (Test-Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall")) {
        Get-InstalledProgram @PSBoundParameters -Wow64
    }
}


function Format-ProductId {
    [CmdletBinding()]
    [OutputType([String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ProductId
    )

    try {
        $private:identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper()
        return $identifyingNumber
    }
    catch {
        Write-Error ("The specified ProductId ({0}) is not a valid Guid" -f $ProductId)
    }
}


function Invoke-ScriptBlock {
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [AllowEmptyString()]
        [string]$ScriptBlockString,

        [Parameter()]
        [AllowEmptyCollection()]
        [string[]]$Arguments
    )

    if (-not $ScriptBlockString) { return }

    try {
        $scriptBlock = [ScriptBlock]::Create($ScriptBlockString).GetNewClosure()
        Write-Verbose ('Execute ScriptBlock')
        if (@($Arguments).Count -ge 1) {
            $scriptBlock.Invoke($Arguments) | Out-String -Stream | Write-Verbose
        }
        else {
            $scriptBlock.Invoke() | Out-String -Stream | Write-Verbose
        }
    }
    catch {
        throw $_
    }
}


function Get-RedirectedUrl {
    [CmdletBinding()]
    [OutputType([System.Uri])]
    Param (
        [Parameter(Mandatory, Position = 0)]
        [string]$URL
    )

    try {
        $request = [System.Net.WebRequest]::Create($URL)
        $request.AllowAutoRedirect = $false
        $response = $request.GetResponse()

        if ($response.StatusCode -eq "Found") {
            [System.Uri]$response.GetResponseHeader("Location")
        }
    }
    catch {
        Write-Error -Exception $_.Exception
    }
}


function Start-Command {
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0)]
        [string] $FilePath,

        [Parameter(Position = 1)]
        [string[]]$ArgumentList,

        [Parameter()]
        [string]$WorkingDirectory,

        [int]$Timeout = [int]::MaxValue
    )
    $ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
    $ProcessInfo.FileName = $FilePath
    $ProcessInfo.UseShellExecute = $false
    $ProcessInfo.Arguments = [string]$ArgumentList
    if ($PSBoundParameters.ContainsKey('WorkingDirectory')) {
        if (-not (Test-Path -LiteralPath $WorkingDirectory -PathType Container)) {
            Write-Warning ('Specified working directory path is not exist.')
        }
        else {
            $ProcessInfo.WorkingDirectory = $WorkingDirectory
        }
    }
    $Process = New-Object System.Diagnostics.Process
    $Process.StartInfo = $ProcessInfo
    $Process.Start() > $null
    if (!$Process.WaitForExit($Timeout)) {
        $Process.Kill()
        Write-Error ('Process timeout. Terminated. (Timeout:{0}s, Process:{1})' -f ($Timeout * 0.001), $FilePath)
    }
    $Process.ExitCode
}

Export-ModuleMember -Function *-TargetResource