Universal.psm1

Import-Module "$PSScriptRoot\UniversalDashboard.MaterialUI.psm1" -ErrorAction SilentlyContinue
Import-Module "$PSScriptRoot\classes.dll" -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')

function Start-PSUServer {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$ExecutablePath,
        [Parameter()]
        [string]$ListenAddress,
        [Parameter()]
        [int]$Port, 
        [Parameter()]
        [ScriptBlock]$Configuration
    )

    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()]
        [Switch]$AddToPath,
        [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 ($AddToPath) {
        Write-Warning "-AddToPath is obsolete and will be removed in the next major version."
    }

    if ($platform -eq 'win7-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 = "win7-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
    }

    Write-Verbose "Installing server to $Path"

    if ($LatestVersion) {
        $Version = (Invoke-WebRequest https://imsreleases.blob.core.windows.net/universal/production/v3-version.txt).Content
    }

    Write-Verbose "Downloading version $Version"

    $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 (($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) -and -not $IISWebsite) {
        Write-Verbose "Creating and starting PowerShellUniversal service"
        New-Service -Name 'PowerShellUniversal' -DisplayName 'PowerShell Universal' -Description 'PowerShell Universal Server' -BinaryPathName "$Path\Universal.Server.exe --service" -StartupType Automatic | Out-Null
        Get-ChildItem $Path -Recurse | Unblock-File
        Start-Service -Name 'PowerShellUniversal'
        Write-Host -ForegroundColor Green 'PowerShell Universal is running on port 5000. View the admin console by visiting http://localhost:5000'
    }

    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) {
        $PathSeparator = ";" 
        Write-Verbose "Adding $Path to %PATH% variable for the $Scope scope"
        $envPath = [Environment]::GetEnvironmentVariable('PATH', 'Machine')
        $newpath = $envPath + $PathSeparator + $Path
        [Environment]::SetEnvironmentVariable("PATH", $newpath, 'Machine')
    }
    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
    )

    if ($platform -eq 'win7-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
    }

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

    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
    }

    Write-Verbose "Upgrading server installed at $Path"

    if ($LatestVersion) {
        $Version = (Invoke-WebRequest https://imsreleases.blob.core.windows.net/universal/production/v3-version.txt).Content
    }

    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 (($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) -and -not $IISWebsite) {
        Write-Verbose "Stopped PowerShellUniversal service"
        Stop-Service -Name 'PowerShellUniversal'
    }

    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 (($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) -and -not $IISWebsite) {
        Get-ChildItem $Path -Recurse | Unblock-File
        Start-Service -Name 'PowerShellUniversal'
        Write-Verbose "Started PowerShellUniversal service"
    }

    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 Remove-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
    #>

    [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 (-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 (($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) -and -not $IISWebsite) {
        Write-Verbose "Stopped PowerShellUniversal service"
        Stop-Service -Name 'PowerShellUniversal'
        sc.exe delete "PowerShellUniversal" | Out-Null
    }

    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
    }

    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
}