Universal.psm1

Import-Module "$PSScriptRoot\UniversalDashboard.MaterialUI.psm1" -ErrorAction SilentlyContinue

if (-not $IsCoreCLR) {
    Import-Module "$PSScriptRoot\Assemblies\System.Runtime.CompilerServices.Unsafe.dll"
}

$TAType = [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
$TAtype::Add('File', 'PowerShellUniversal.PSUFile')
$TAtype::Add('Documentation', 'PowerShellUniversal.DocumentationAttribute')
$TAtype::Add('Component', 'PowerShellUniversal.ComponentAttribute')

function Start-PSUServer {
    [CmdletBinding(DefaultParameterSetName = "Service")]
    param(
        [Parameter(ParameterSetName = 'Path')]
        [string]$ExecutablePath,
        [Parameter(ParameterSetName = 'Path')]
        [string]$ListenAddress,
        [Parameter(ParameterSetName = 'Path')]
        [int]$Port,
        [Parameter(ParameterSetName = 'Path')]
        [ScriptBlock]$Configuration
    )
    
    if ($MyInvocation.BoundParameters.Count -eq 0)
    {
        Start-Service 'PowerShellUniversal'
        return
    }

    if ([UniversalAutomation.RemoteCommand]::Configuration) {
        & $Configuration
        return
    }

    if (-not $PSBoundParameters.ContainsKey("ExecutablePath")) {
        $ExecutablePath = "Universal.Server"
        if ($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) {
            $ExecutablePath = "Universal.Server.exe"
        }
    }

    $Command = Get-Command $ExecutablePath -ErrorAction SilentlyContinue
    if ($null -eq $Command) {
        $ExecutablePath = Join-Path $PSScriptRoot $ExecutablePath
        $Command = Get-Command $ExecutablePath -ErrorAction SilentlyContinue
        if ($null -eq $Command) {
            throw 'Unable to locate the Universal Server executable. You can use Install-PSUServer the server for your platform. Use the -AddToPath parameter to add the installation directory the the PATH.'
        }
    }

    if ($PSVersionTable.PSEdition -ne 'Desktop' -and -not $IsWindows) {
        try {
            chmod +x $ExecutablePath
        }
        catch {
            Write-Warning "Failed to set executable flag. You may have to run 'chmod +x' yourself on $ExecutablePath. $_"
        }
    }

    if ($ListenAddress) {
        $Env:Kestrel__Endpoints__HTTP__Url = $ListenAddress
    }
    elseif ($PSBoundParameters.ContainsKey("Port")) {
        $Env:Kestrel__Endpoints__HTTP__Url = "http://*:$port"
    }

    if ($Configuration) {
        $scriptName = (Get-PSCallStack | Select-Object -Last 1).ScriptName
        if (-not $scriptName) {
            $scriptName = (Get-PSCallStack | Select-Object -Last 1 -Skip 1).ScriptName
        }
        $Env:Data__ConfigurationScript = $scriptName
    }

    $Process = Start-Process -FilePath $ExecutablePath -PassThru

    $Process
}

function Install-PSUServer {
    <#
    .SYNOPSIS
    Install the PowerShell Universal server.
     
    .DESCRIPTION
    Install the PowerShell Universal server. This is a convenience function that will install the server for your platform. On Windows, it will install the
    server as a Windows service. On Linux, it will install the server as a systemd service. On Mac, it will install the server as a launchd service.
     
    .PARAMETER Path
    The path to store the PowerShell Universal binaries. If not specified, the default installation path will be used.
     
    .PARAMETER AddToPath
    Whether to add the path to the PATH environment variable.
     
    .PARAMETER Version
    The version of PowerShell Universal to install.
     
    .PARAMETER LatestVersion
    Install the most recent version.
     
    .EXAMPLE
    Install-PSUServer
    #>

    [CmdletBinding(DefaultParameterSetName = "Version")]
    param(
        [Parameter()]
        [string]$Path,
        [Parameter(ParameterSetName = "Version")]
        [string]$Version = (Get-Module Universal).Version,
        [Parameter(ParameterSetName = "Latest")]
        [Switch]$LatestVersion,
        [Parameter()]
        [string]$IISWebsite,
        [Parameter()]
        [string]$IISAppPool = "PowerShellUniversal",
        [Parameter()]
        [int]$IISPort
    )

    if ($IISWebsite -and ($IsLinux -or $IsMacOS)) {
        throw "IISWebsite is only supported on Windows."
    }

    if ($IISWebsite) {
        Import-Module WebAdministration -ErrorAction Stop
    }
    
    if ($platform -eq 'win-x64' -and -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
        throw 'You must be an administrator to install the Universal Server. Please run the command as an administrator.'
    }

    $platform = "win-x64";
    $folder = 'CommonApplicationData'
    if ($PSVersionTable.PSEdition -eq 'Core') {
        if ($IsLinux) {
            $platform = "linux-x64"
        }
        elseif ($IsMacOS) {
            $folder = 'ApplicationData'
            $platform = "osx-x64"
        }
    }

    if (-not $Path -and -not $IISWebsite) {
        $ProgramData = [System.Environment]::GetFolderPath($folder)
        $Path = [System.IO.Path]::Combine($ProgramData, "PowerShellUniversal", "Server")
    }

    if (-not $Path -and $IISWebsite) {
        $Path = "C:\inetpub\wwwroot\PowerShellUniversal"
        New-Item $Path -ItemType Directory | Out-Null
    }
    
    if ($LatestVersion) {
        $Version = (Invoke-WebRequest https://imsreleases.blob.core.windows.net/universal/production/v5-version.txt).Content
    }

    Write-Verbose "Downloading version $Version"

    if (($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) -and -not $IISWebsite) {
        $Temp = [System.IO.Path]::GetTempPath()
        $Msi = (Join-Path $Temp "Universal.$Version.$platform.msi")
        Remove-Item $Msi -Force -ErrorAction SilentlyContinue
        Invoke-WebRequest "https://imsreleases.blob.core.windows.net/universal/production/$version/PowerShellUniversal.$Version.msi" -OutFile $Msi  
        Write-Verbose "Download complete. Installing from MSI."
        Start-Process msiexec.exe -ArgumentList "/i $Msi /quiet /qn /norestart" -Wait
    } else {
        Write-Verbose "Installing server to $Path"
        $Temp = [System.IO.Path]::GetTempPath()
        $Zip = (Join-Path $Temp "Universal.$Version.$platform.zip")
        Remove-Item $Zip -Force -ErrorAction SilentlyContinue
        Invoke-WebRequest "https://imsreleases.blob.core.windows.net/universal/production/$version/Universal.$platform.$Version.zip" -OutFile $Zip
        Write-Verbose "Download complete. Unzipping to $Path"
        Expand-Archive -Path $Zip -DestinationPath $Path -Force
        Remove-Item $Zip -Force
    }

    if ($IISWebsite) {
        New-WebAppPool -Name $IISAppPool | Out-Null
        New-Website -Name $IISWebsite -Port $IISPort -PhysicalPath $Path -ApplicationPool $IISAppPool | Out-Null
        Start-Website -Name $IISWebsite
    }

    if ($IsMacOS -or $IsLinux) {
        $ServerPath = Join-Path $Path "Universal.Server"
        /bin/chmod +x $ServerPath
    }

    if ($IsLinux) {
        Write-Verbose "Creating and starting PowerShellUniversal service"
        touch /etc/systemd/system/PowerShellUniversal.service
        chmod 664 /etc/systemd/system/PowerShellUniversal.service
        "[Unit]
        Description=PowerShell Universal
         
        [Service]
        ExecStart=$Path/Universal.Server
         
        [Install]
        WantedBy=multi-user.target"
 | Out-File /etc/systemd/system/PowerShellUniversal.service

        systemctl daemon-reload
        systemctl start PowerShellUniversal
        systemctl enable PowerShellUniversal
    }
    
    if ($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) {
    }
    else {
        Write-Verbose "Adding $Path to `$PATH variable"
        $PathSeparator = ":"
        $envPath = [Environment]::GetEnvironmentVariable('PATH')
        $newpath = $envPath + $PathSeparator + $Path
        [Environment]::SetEnvironmentVariable("PATH", $newpath)
    }
    
    $Env:PATH += $PathSeparator + $Path
    
}

function Update-PSUServer {
    <#
    .SYNOPSIS
    Update the PowerShell Universal server.
     
    .DESCRIPTION
    Update the PowerShell Universal server. This is a convenience function that will update the server for your platform.
     
    .PARAMETER Path
    The path for the PowerShell Universal binaries. If not specified, the path will attempt to be resolved.
     
    .PARAMETER Version
    The version to upgrade to.
     
    .PARAMETER LatestVersion
    Upgrade to the latest version.
     
    .EXAMPLE
    Update-PSUServer
    #>

    [CmdletBinding(DefaultParameterSetName = "Version")]
    param(
        [Parameter()]
        [string]$Path,
        [Parameter(ParameterSetName = "Version")]
        [string]$Version = (Get-Module Universal).Version,
        [Parameter(ParameterSetName = "Latest")]
        [Switch]$LatestVersion,
        [Parameter()]
        [string]$IISWebsite
    )
    
    $platform = "win-x64";
    if ($PSVersionTable.PSEdition -eq 'Core') {
        if ($IsLinux) {
            $platform = "linux-x64"
        }
        elseif ($IsMacOS) {
            $platform = "osx-x64"
        }
    }

    if ($platform -eq 'win-x64' -and -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
        throw 'You must be an administrator to update the Universal Server. Please run the command as an administrator.'
    }

    if ($IISWebsite -and ($IsLinux -or $IsMacOS)) {
        throw "IISWebsite is only supported on Windows."
    }

    if ($IISWebsite) {
        Import-Module WebAdministration -ErrorAction Stop
    }
    
    if (-not $Path -and -not $IISWebsite) {
        if ($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) {
            $ServerPath = Get-Command "Universal.Server.exe" -ErrorAction SilentlyContinue
        }
        else {
            $ServerPath = Get-Command "Universal.Server" -ErrorAction SilentlyContinue
        }

        if (-not $ServerPath) {
            throw "Unable to locate existing PowerShell Universal installation. Use the -Path parameter to specify the folder of the previous installation."
        }

        $Path = [System.IO.Path]::GetDirectoryName($ServerPath.Source)
    }

    if (-not $Path -and $IISWebsite) {
        $Path = (Get-Website -Name $IISWebsite).PhysicalPath
    }
    
    if ($LatestVersion) {
        $Version = (Invoke-WebRequest https://imsreleases.blob.core.windows.net/universal/production/v5-version.txt).Content
    }

    if (($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) -and -not $IISWebsite) {
        $Temp = [System.IO.Path]::GetTempPath()
        $Msi = (Join-Path $Temp "Universal.$Version.$platform.msi")
        Remove-Item $Msi -Force -ErrorAction SilentlyContinue
        Invoke-WebRequest "https://imsreleases.blob.core.windows.net/universal/production/$version/PowerShellUniversal.$Version.msi" -OutFile $Msi
        Write-Verbose "Download complete. Installing from MSI."
        Start-Process msiexec.exe -ArgumentList "/i $Msi /quiet /qn /norestart" -Wait
    } else {
        Write-Verbose "Upgrading server installed at $Path"

        Write-Verbose "Downloading version $Version"

        $Temp = [System.IO.Path]::GetTempPath()
        $Zip = (Join-Path $Temp "Universal.$Version.$platform.zip")
        Remove-Item $Zip -Force -ErrorAction SilentlyContinue

        if ($IISWebsite) {
            $AppPool = (Get-Website -Name $IISWebsite).ApplicationPool
            Stop-Website -Name $IISWebsite
            Stop-WebAppPool -Name $AppPool
        }

        if ($IsLinux) {
            Write-Verbose "Stopped PowerShellUniversal service"
            systemctl stop PowerShellUniversal
            systemctl disable PowerShellUniversal
        }

        Remove-Item $Path -Force -Recurse
        Invoke-WebRequest "https://imsreleases.blob.core.windows.net/universal/production/$version/Universal.$platform.$Version.zip" -OutFile $Zip

        Write-Verbose "Download complete. Extracting to $Path"

        Expand-Archive -Path $Zip -DestinationPath $Path -Force
        Remove-Item $Zip -Force
        
        if ($IISWebsite) {
            Get-ChildItem $Path -Recurse | Unblock-File
            Start-Website -Name $IISWebsite
        }

        if ($IsMacOS -or $IsLinux) {
            $ServerPath = Join-Path $Path "Universal.Server"
            /bin/chmod +x $ServerPath
        }

        if ($IsLinux) {
            Write-Verbose "Started PowerShellUniversal service"
            systemctl start PowerShellUniversal
            systemctl enable PowerShellUniversal
        }
    }
}

function Uninstall-PSUServer {
    <#
    .SYNOPSIS
    Removes the PowerShell Universal server.
     
    .DESCRIPTION
    Removes the PowerShell Universal server. This is a convenience function that will remove the server for your platform.
     
    .PARAMETER Path
    The path to the PowerShell Universal binaries. If not specified, the path will attempt to be resolved.
     
    .EXAMPLE
    Remove-PSUServer
    #>

    [Alias("Remove-PSUServer")]
    [CmdletBinding(DefaultParameterSetName = "Version")]
    param(
        [Parameter()]
        [string]$Path,
        [Parameter()]
        [string]$IISWebsite
    )

    if (($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) -and -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
        throw 'You must be an administrator to remove the Universal Server. Please run the command as an administrator.'
    }

    if ($IISWebsite -and ($IsLinux -or $IsMacOS)) {
        throw "IISWebsite is only supported on Windows."
    }

    if ($IISWebsite) {
        Import-Module WebAdministration -ErrorAction Stop
    }

    if (($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) -and -not $IISWebsite)
    {
        Write-Verbose "Locating local package with Win32_Product"
        $Package = Get-CimInstance win32_product  -Filter "Name like 'PowerShell Universal%'" | Select-Object -First 1
        if ($Package)
        {
            Write-Verbose "Removing via msiexec."
            Start-Process msiexec.exe -ArgumentList "/x $( $Package.LocalPackage ) /quiet /qn /norestart" -Wait
        } else {
            throw "Unable to locate PowerShell Universal installation."
        }
    } 
    else 
    {
        if (-not $Path -and -not $IISWebsite) {
            $ServerPath = Get-Command "Universal.Server" -ErrorAction SilentlyContinue

            if (-not $ServerPath) {
                throw "Unable to locate existing PowerShell Universal installation. Use the -Path parameter to specify the folder of the previous installation."
            }

            $Path = [System.IO.Path]::GetDirectoryName($ServerPath.Source)
        }

        if ($IISWebsite) {
            $Path = (Get-Website -Name $IISWebsite).PhysicalPath
            $AppPool = (Get-Website -Name $IISWebsite).ApplicationPool
            Stop-Website -Name $IISWebsite
            Remove-Website -Name $IISWebsite
            Remove-WebAppPool $AppPool

            Write-Verbose "Removing application files"
            Remove-Item $Path -Force -Recurse
        }

        if ($IsLinux) {
            Write-Verbose "Stopped PowerShellUniversal service"
            systemctl stop PowerShellUniversal
            systemctl disable PowerShellUniversal
            Remove-Item /etc/systemd/system/PowerShellUniversal.service -Force
            systemctl daemon-reload

            Write-Verbose "Removing application files"
            Remove-Item $Path -Force -Recurse
        }
    }
}