Wsl-Instance/Wsl-Instance.Cmdlets.ps1

# Copyright 2022 Antoine Martin
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

function Set-WslDefaultInstance {
    <#
    .SYNOPSIS
    Sets the default WSL instance.

    .PARAMETER Name
    The name of the WSL instance to set as default.

    .PARAMETER Instance
    The WSL instance object to set as default.

    .EXAMPLE
    Set-WslDefaultInstance -Name "alpine"
    Sets the default WSL instance to "alpine".

    .EXAMPLE
    Set-WslDefaultInstance -Instance $myInstance
    Sets the default WSL instance to the specified instance object.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([WslInstance])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $false, ParameterSetName = 'InstanceName', Position=0)]
        [string]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $false, ParameterSetName = 'InstanceObject')]
        [WslInstance]$Instance
    )

    if ($PSCmdlet.ParameterSetName -eq 'InstanceName') {
        $Instance = Get-WslInstance -Name $Name
    }

    if ($PSCmdlet.ShouldProcess($Name, "Set default WSL instance")) {
        $baseKey = $null
        try {
            $baseKey = Get-WslRegistryBaseKey
            $key = $baseKey.GetSubKeyNames() |
                Where-Object {
                    $subKey = $baseKey.OpenSubKey($_, $false)
                    try {
                        $subKey.GetValue('DistributionName') -eq $Instance.Name
                    } finally {
                        if ($null -ne $subKey) {
                            $subKey.Close()
                        }
                    }
                } | Select-Object -First 1
            $baseKey.SetValue('DefaultDistribution', $key)
            Success "Default instance set to $($Instance.Name)"
        } finally {
            if ($null -ne $baseKey) {
                $baseKey.Close()
            }
        }
    }
    return $Instance
}


function Get-WslInstance {
    <#
    .SYNOPSIS
        Gets the WSL instances installed on the computer.
    .DESCRIPTION
        The Get-WslInstance cmdlet gets objects that represent the WSL instances on the computer.
        This cmdlet wraps the functionality of "wsl.exe --list --verbose".
    .PARAMETER Name
        Specifies the instance names of instances to be retrieved. Wildcards are permitted. By
        default, this cmdlet gets all of the instances on the computer.
    .PARAMETER Default
        Indicates that this cmdlet gets only the default instance. If this is combined with other
        parameters such as Name, nothing will be returned unless the default instance matches all the
        conditions. By default, this cmdlet gets all of the instances on the computer.
    .PARAMETER State
        Indicates that this cmdlet gets only instances in the specified state (e.g. Running). By
        default, this cmdlet gets all of the instances on the computer.
    .PARAMETER Version
        Indicates that this cmdlet gets only instances that are the specified version. By default,
        this cmdlet gets all of the instances on the computer.
    .INPUTS
        System.String
        You can pipe a instance name to this cmdlet.
    .OUTPUTS
        WslInstance
        The cmdlet returns objects that represent the instances on the computer.
    .EXAMPLE
        Get-Wsl
        Name State Version Default
        ---- ----- ------- -------
        Ubuntu Stopped 2 True
        Ubuntu-18.04 Running 1 False
        Alpine Running 2 False
        Debian Stopped 1 False
        Get all WSL instances.
    .EXAMPLE
        Get-WslInstance -Default
        Name State Version Default
        ---- ----- ------- -------
        Ubuntu Stopped 2 True
        Get the default instance.
    .EXAMPLE
        Get-WslInstance -Version 2 -State Running
        Name State Version Default
        ---- ----- ------- -------
        Alpine Running 2 False
        Get running WSL2 instances.
    .EXAMPLE
        Get-WslInstance Ubuntu* | Stop-WslInstance
        Terminate all instances that start with Ubuntu
    .EXAMPLE
        Get-Content instances.txt | Get-WslInstance
        Name State Version Default
        ---- ----- ------- -------
        Ubuntu Stopped 2 True
        Debian Stopped 1 False
        Use the pipeline as input.
    #>

    [CmdletBinding()]
    [OutputType([WslInstance])]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string[]]$Name,
        [Parameter(Mandatory = $false)]
        [Switch]$Default,
        [Parameter(Mandatory = $false)]
        [WslInstanceState]$State,
        [Parameter(Mandatory = $false)]
        [int]$Version
    )

    process {
        $instances = Get-WslHelper
        if ($Default) {
            $instances = $instances | Where-Object {
                $_.Default
            }
        }

        if ($PSBoundParameters.ContainsKey("State")) {
            $instances = $instances | Where-Object {
                $_.State -eq $State
            }
        }

        if ($PSBoundParameters.ContainsKey("Version")) {
            $instances = $instances | Where-Object {
                $_.Version -eq $Version
            }
        }

        if ($Name.Length -gt 0) {
            $instances = $instances | Where-Object {
                foreach ($pattern in $Name) {
                    if ($_.Name -ilike $pattern) {
                        return $true
                    }
                }

                return $false
            }
            if ($null -eq $instances) {
                throw [UnknownWslInstanceException]::new($Name)
            }
        }

        # The additional registry properties aren't available if running inside WSL.
        $instances | ForEach-Object {
            $_.RetrieveProperties()
        }

        return $instances
    }
}

function Invoke-WslConfigure {
    <#
    .SYNOPSIS
        Configures a WSL instance.

    .DESCRIPTION
        This function runs the configuration script inside the specified WSL instance
        to create a non-root user.

    .PARAMETER Name
        The name(s) of the WSL instance to configure.

    .PARAMETER Instance
        One or more [WSLInstance] objects to configure.

    .PARAMETER Uid
        The user ID to set as the default for the instance.

    .PARAMETER Force
        Whether to force the configuration even if the instance is already configured.

    .OUTPUTS
        WslInstance
        The cmdlet returns objects that represent the instances on the computer.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([WslInstance])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $false, ParameterSetName = "InstanceName", Position = 0)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildCards()]
        [string[]]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Instance")]
        [WslInstance[]]$Instance,
        [Parameter(Position = 1, Mandatory = $false)]
        [int]$Uid = 1000,
        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    process {
        if ($PSCmdlet.ParameterSetName -eq "InstanceName") {
            $Instance = Get-WslInstance $Name
        }

        if ($null -ne $Instance) {
            $Instance | ForEach-Object {
                $existing = $_
                if ($PSCmdlet.ShouldProcess($Name, 'Configure instance')) {
                    $existing.Configure($Force, $Uid)
                }
                $existing
            }
        }
    }
}

function New-WslInstance {
    <#
    .SYNOPSIS
        Creates and configures a minimal WSL instance.

    .DESCRIPTION
        This command performs the following operations:
        - Create an Instance directory
        - Download the Image if needed.
        - Create the WSL instance.
        - Configure the WSL instance if needed.

        The instance is configured as follow:
        - A user named after the name of the instance (arch, alpine or
        ubuntu) is set as the default user.
        - zsh with oh-my-zsh is used as shell.
        - `powerlevel10k` is set as the default oh-my-zsh theme.
        - `zsh-autosuggestions` plugin is installed.

    .PARAMETER Name
        The name of the instance.

    .PARAMETER From
        The identifier of the image to create the instance from. It can be an
        already known name:
        - Arch
        - Alpine
        - Ubuntu
        - Debian

        It also can be the URL (https://...) of an existing filesystem or a
        image name saved through Export-WslInstance.

        It can also be a name in the form:

            incus://<os>#<release> (ex: incus://rockylinux#9)

        In this case, it will fetch the last version the specified image in
        https://images.linuxcontainers.org/images.

    .PARAMETER Image
        The image to use. It can be a WslImage object or a
        string that contains the path to the image.

    .PARAMETER BaseDirectory
        Base directory where to create the instance directory. Equals to
        $env:APPLOCALDATA\Wsl (~\AppData\Local\Wsl) by default.

    .PARAMETER Configure
        Perform Configuration. Runs the configuration script inside the newly created
        instance to create a non root user.

    .PARAMETER Sync
        Perform Synchronization. If the instance is already installed, this will
        ensure that the image is up to date.

    .INPUTS
        None.

    .OUTPUTS
        None.

    .EXAMPLE
        New-WslInstance alpine -From Alpine
        Install an Alpine based WSL instance named alpine.

    .EXAMPLE
        New-WslInstance arch -From Arch
        Install an Arch based WSL instance named arch.

    .EXAMPLE
        New-WslInstance arch -From Arch -Configured
        Install an Arch based WSL instance named arch from the already configured image.

    .EXAMPLE
        New-WslInstance rocky -From incus:rocky:9
        Install a Rocky Linux based WSL instance named rocky.

    .EXAMPLE
        New-WslInstance lunar -From https://cloud-images.ubuntu.com/wsl/lunar/current/ubuntu-lunar-wsl-amd64-wsl.rootfs.tar.gz -SkipConfigure
        Install a Ubuntu 23.04 based WSL instance named lunar from the official Canonical image and skip configuration.

    .EXAMPLE
         Get-WslImage | Where-Object { $_.Type -eq 'Local' } | New-WslInstance -Name test
        Install a WSL instance named test from the image of the first local image.
    .LINK
        Remove-WslInstance
        https://github.com/romkatv/powerlevel10k
        https://github.com/zsh-users/zsh-autosuggestions
        https://github.com/antoinemartin/wsl2-ssh-pageant-oh-my-zsh-plugin

    .NOTES
        The command tries to be idempotent. It means that it will try not to
        do an operation that already has been done before.

    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([WslInstance])]
    param(
        [Parameter(Position = 0, Mandatory = $true)]
        [string]$Name,
        [Parameter(ParameterSetName = 'Name', Mandatory = $true)]
        [string]$From,
        [Parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName = 'Image')]
        [WslImage]$Image,
        [string]$BaseDirectory = $null,
        [Parameter(Mandatory = $false)]
        [switch]$Configure,
        [Parameter(Mandatory = $false)]
        [switch]$Sync
    )

    # Retrieve the instance if it already exists
    $current_distribution = try {
        Get-WslInstance $Name
    } catch {
        # Ignore not found errors
        $null
    }

    if ($null -ne $current_distribution) {
        throw [WslInstanceAlreadyExistsException]::new($Name)
    }

    if (-not $BaseDirectory) {
        $BaseDirectory = [WslInstance]::DistrosRoot.FullName
    }
    # Where to install the instance
    $distribution_dir = Join-Path -Path $BaseDirectory -ChildPath $Name

    # Create the directory
    If (!(test-path $distribution_dir)) {
        Progress "Creating directory [$distribution_dir]..."
        if ($PSCmdlet.ShouldProcess($distribution_dir, 'Create Instance directory')) {
            $null = New-Item -ItemType Directory -Force -Path $distribution_dir
        }
    }
    else {
        Information "Instance directory [$distribution_dir] already exists."
    }

    if ($PSCmdlet.ParameterSetName -eq "Name") {
        $Image = [WslImage]::new($From)
        if (($Sync -eq $true -or -not $Image.IsAvailableLocally) -and $PSCmdlet.ShouldProcess($Image.Url, 'Synchronize locally')) {
            $null = $Image | Sync-WslImage
        }
    } elseif ($PSCmdlet.ParameterSetName -eq "Image") {
        $Image = $Image
    }

    $Image_file = $Image.File.FullName

    Progress "Creating instance [$Name] from [$Image_file]..."
    if ($PSCmdlet.ShouldProcess($Name, 'Create instance')) {
        Wrap-Wsl-Raw -Arguments '--import',$Name,$distribution_dir,$Image_file | Write-Verbose
    }

    $Uid = $Image.Uid
    $wsl = [WslInstance]::new($Name)

    if ($true -eq $Configure) {
        if ($PSCmdlet.ShouldProcess($Name, 'Configure instance')) {
            if (!$Image.Configured) {
                $wsl.Configure($false, $Uid)
            } else {
                Information "Instance [$Name] is already configured, skipping configuration."
            }
        }
    } else {
        if ($Uid -ne 0 -and $PSCmdlet.ShouldProcess($Name, 'Set default UID')) {
            $wsl.SetDefaultUid($Uid)
        }
    }

    Success "Done. Command to enter instance: Invoke-WslInstance -In $Name or wsl -d $Name"
    return $wsl
}

function Remove-WslInstance {
    <#
    .SYNOPSIS
        Removes WSL instance.

    .DESCRIPTION
        This command remove the specified instance. It also deletes the
        instance vhdx file and the directory of the instance. It's the
        equivalent of `wsl --unregister`.

    .PARAMETER Name
        The name of the instance. Wildcards are permitted.

    .PARAMETER Instance
        Specifies WslInstance objects that represent the instances to be removed.

    .PARAMETER KeepDirectory
        If specified, keep the instance directory. This allows recreating
        the instance from a saved image.

    .INPUTS
        WslInstance, System.String

        You can pipe a WslInstance object retrieved by Get-WslInstance,
        or a string that contains the instance name to this cmdlet.

    .OUTPUTS
        None.

    .EXAMPLE
        Remove-WslInstance toto

        Uninstall instance named toto.

    .EXAMPLE
        Remove-WslInstance test*

        Uninstall all instances which names start by test.

    .EXAMPLE
        Get-WslInstance -State Stopped | Sort-Object -Property -Size -Last 1 | Remove-WslInstance

        Uninstall the largest non running instance.

    .LINK
        New-WslInstance
        https://github.com/romkatv/powerlevel10k
        https://github.com/zsh-users/zsh-autosuggestions
        https://github.com/antoinemartin/wsl2-ssh-pageant-oh-my-zsh-plugin

    .NOTES
        The command tries to be idempotent. It means that it will try not to
        do an operation that already has been done before.

    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([WslInstance])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "InstanceName", Position = 0)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildCards()]
        [string[]]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Instance")]
        [WslInstance[]]$Instance,
        [Parameter(Mandatory = $false)]
        [switch]$KeepDirectory
    )

    process {
        if ($PSCmdlet.ParameterSetName -eq "InstanceName") {
            $Instance = Get-WslInstance $Name
        }

        if ($null -ne $Instance) {
            $Instance | ForEach-Object {
                if ($PSCmdlet.ShouldProcess($_.Name, "Unregister")) {
                    $_.Unregister() | Write-Verbose
                    if ($false -eq $KeepDirectory) {
                        $_.BasePath | Remove-Item -Recurse
                    }
                }
            }
        }
    }
}


function Export-WslInstance {
    <#
    .SYNOPSIS
        Exports the file system of a WSL instance.

    .DESCRIPTION
        This command exports the instance and tries to compress it with
        the `gzip` command embedded in the instance. If no destination file
        is given, it creates or replaces an image file named after the instance
        in the images directory ($env:APPLOCALDATA\Wsl\RootFS).

    .PARAMETER Name
        The name of the instance.

    .PARAMETER OutputName
        Name of the output image. By default, uses the name of the
        instance.

    .PARAMETER Destination
        Base directory where to save the root file system. Equals to
        $env:APPLOCALDATA\Wsl\RootFS (~\AppData\Local\Wsl\RootFS) by default.

    .PARAMETER OutputFile
        The name of the output file. If it is not specified, it will overwrite
        the root file system of the instance.

    .INPUTS
        None.

    .OUTPUTS
        None.

    .EXAMPLE
        New-WslInstance toto
        wsl -d toto -u root apk add openrc docker
        Export-WslInstance toto docker

        Remove-WslInstance toto
        New-WslInstance toto -From docker

    .LINK
        New-WslInstance
        https://github.com/romkatv/powerlevel10k
        https://github.com/zsh-users/zsh-autosuggestions
        https://github.com/antoinemartin/wsl2-ssh-pageant-oh-my-zsh-plugin

    .NOTES
        The command tries to be idempotent. It means that it will try not to
        do an operation that already has been done before.

    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '',
        Justification='Ingesting /etc/os-release properties into a hashtable')]
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([WslImage])]
    param(
        [Parameter(Position = 0, Mandatory = $true)]
        [string]$Name,
        [Parameter(Position = 1, Mandatory = $false)]
        [string]$OutputName,
        [string]$Destination = $null,
        [Parameter(Mandatory = $false)]
        [string]$OutputFile
    )

    # Retrieve the instance if it already exists
    [WslInstance]$Instance = Get-WslInstance $Name

    if ($null -ne $Instance) {
        if (-not $Destination) {
            $Destination = [WslImage]::BasePath.FullName
        }
        $Instance | ForEach-Object {


            if ($OutputFile.Length -eq 0) {
                if ($OutputName.Length -eq 0) {
                    $OutputName = $Instance.Name
                }
                $OutputFile =  Join-Path -Path $Destination -ChildPath "$OutputName.rootfs.tar.gz"
                If (!(test-path -PathType container $Destination)) {
                    if ($PSCmdlet.ShouldProcess($Destination, 'Create Wsl base directory')) {
                        $null = New-Item -ItemType Directory -Path $Destination
                    }
                }
            }

            if ($PSCmdlet.ShouldProcess($Instance.Name, 'Export instance')) {

                $export_file = $OutputFile -replace '\.gz$'

                Progress "Exporting WSL instance $Name to $export_file..."
                Wrap-Wsl -Arguments --export,$Instance.Name,"$export_file" | Write-Verbose
                $file_item = Get-Item -Path "$export_file"
                $filepath = $file_item.Directory.FullName
                Progress "Compressing $export_file to $OutputFile..."
                Remove-Item "$OutputFile" -Force -ErrorAction SilentlyContinue
                Wrap-Wsl -Arguments --distribution,$Name,"--cd","$filepath","gzip",$file_item.Name | Write-Verbose

                $props =  Invoke-WslInstance -In $Name cat /etc/os-release | ForEach-Object { $_ -replace '=([^"].*$)','="$1"' } | Out-String | ForEach-Object {"@{`n$_`n}"} | Invoke-Expression

                [PSCustomObject]@{
                    Name              = $OutputName
                    Os                = $props.ID
                    Release           = $props.VERSION_ID
                    Type              = [WslImageType]::Local.ToString()
                    State             = [WslImageState]::Synced.ToString()
                    Url               = $null
                    Configured        = $true
                    Uid               = $Instance.DefaultUid
                } | ConvertTo-Json | Set-Content -Path "$($OutputFile).json"


                Success "Instance $Name saved to $OutputFile."
                return [WslImage]::new([FileInfo]::new($OutputFile))
            }
        }
    }
}

function Invoke-WslInstance {
    <#
    .SYNOPSIS
        Runs a command in one or more WSL instances.
    .DESCRIPTION
        The Invoke-WslInstance cmdlet executes the specified command on the specified instances, and
        then exits.
        This cmdlet will raise an error if executing wsl.exe failed (e.g. there is no instance with
        the specified name) or if the command itself failed.
        This cmdlet wraps the functionality of "wsl.exe <command>".
    .PARAMETER In
        Specifies the instance names of instances to run the command in. Wildcards are permitted.
        By default, the command is executed in the default instance.
    .PARAMETER Instance
        Specifies WslInstance objects that represent the instances to run the command in.
        By default, the command is executed in the default instance.
    .PARAMETER User
        Specifies the name of a user in the instance to run the command as. By default, the
        instance's default user is used.
    .PARAMETER Arguments
        Command and arguments to pass to the
    .INPUTS
        WslInstance, System.String
        You can pipe a WslInstance object retrieved by Get-WslInstance, or a string that contains
        the instance name to this cmdlet.
    .OUTPUTS
        System.String
        This command outputs the result of the command you executed, as text.
    .EXAMPLE
        Invoke-WslInstance ls /etc
        Runs a command in the default instance.
    .EXAMPLE
        Invoke-WslInstance -In Ubuntu* -User root whoami
        Runs a command in all instances whose names start with Ubuntu, as the "root" user.
    .EXAMPLE
        Get-WslInstance -Version 2 | Invoke-WslInstance sh "-c" 'echo distro=$WSL_DISTRO_NAME,default_user=$(whoami),flavor=$(cat /etc/os-release | grep ^PRETTY | cut -d= -f 2)'
        Runs a command in all WSL2 instances.
    #>


    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $false, ParameterSetName = "InstanceName")]
        [ValidateNotNullOrEmpty()]
        [SupportsWildCards()]
        [string[]]$In,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Instance")]
        [WslInstance[]]$Instance,
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$User,
        [Parameter(Mandatory = $false, Position = 0, ValueFromRemainingArguments)]
        [string[]]$Arguments
    )

    process {
        if ($PSCmdlet.ParameterSetName -eq "InstanceName") {
            if ($In) {
                $Instance = Get-WslInstance $In
            }
            else {
                $Instance = Get-WslInstance -Default
            }
        }

        $Instance | ForEach-Object {
            $actualArgs = @("--distribution", $_.Name)
            if ($User) {
                $actualArgs += @("--user", $User)
            }

            # Invoke /bin/bash so the whole command can be passed as a single argument.
            if ($Arguments.Count -ne 0) {
                $actualArgs += $Arguments
            }

            if ($PSCmdlet.ShouldProcess($_.Name, $Arguments -join " ")) {
                Wrap-Wsl-Raw @actualArgs
            }
        }
    }
}


function Rename-WslInstance {
    <#
    .SYNOPSIS
        Renames a WSL instance.
    .DESCRIPTION
        The Rename-WslInstance cmdlet renames a WSL instance to a new name.
    .PARAMETER Name
        Specifies the name of the instance to rename.
    .PARAMETER Instance
        Specifies the WslInstance object representing the instance to rename.
    .PARAMETER NewName
        Specifies the new name for the instance.
    .INPUTS
        WslInstance
        You can pipe a WslInstance object retrieved by Get-WslInstance
    .OUTPUTS
        WslInstance
        This command outputs the renamed WSL instance.
    .EXAMPLE
        Rename-WslInstance alpine alpine321
        Renames the instance named "alpine" to "alpine321".
    .EXAMPLE
        Get-WslInstance -Name alpine | Rename-WslInstance -NewName alpine321
        Renames the instance named "alpine" to "alpine321".
    .LINK
        New-WslInstance
    #>

    [CmdletBinding()]
    [OutputType([WslInstance])]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Name', Position = 0)]
        [string]$Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Instance', ValueFromPipeline = $true)]
        [WslInstance]$Instance,

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

    process {
        if ($PSCmdlet.ParameterSetName -eq "Name") {
            $Instance = Get-WslInstance $Name
        }
        $Instance.Rename($NewName)
        return $Instance
    }
}


function Stop-WslInstance {
    <#
    .SYNOPSIS
        Stops one or more WSL instances.
    .DESCRIPTION
        The Stop-WslInstance cmdlet terminates the specified WSL instances. This cmdlet wraps
        the functionality of "wsl.exe --terminate".
    .PARAMETER Name
        Specifies the instance names of instances to be stopped. Wildcards are permitted.
    .PARAMETER Instance
        Specifies WslInstance objects that represent the instances to be stopped.
    .INPUTS
        WslInstance, System.String
        You can pipe a WslInstance object retrieved by Get-WslInstance, or a string that contains
        the instance name to this cmdlet.
    .OUTPUTS
        None.
    .EXAMPLE
        Stop-WslInstance Ubuntu
        Stops the Ubuntu instance.
    .EXAMPLE
        Stop-WslInstance -Name test*
        Stops all instances whose names start with "test".
    .EXAMPLE
        Get-WslInstance -State Running | Stop-WslInstance
        Stops all running instances.
    .EXAMPLE
        Get-WslInstance Ubuntu,Debian | Stop-WslInstance
        Stops the Ubuntu and Debian instances.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([WslInstance])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "InstanceName", Position = 0)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildCards()]
        [string[]]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Instance")]
        [WslInstance[]]$Instance
    )

    process {
        $instances = if ($PSCmdlet.ParameterSetName -eq "InstanceName") {
            Get-WslInstance -Name $Name
        } else {
            $Instance
        }

        foreach ($distro in $instances) {
            if ($PSCmdlet.ShouldProcess($distro.Name, "Stop")) {
                $distro.Stop()
            }
            $distro
        }
    }
}


function Set-WslDefaultUid {
    <#
    .SYNOPSIS
        Sets the default UID for one or more WSL instances.
    .DESCRIPTION
        The Set-WslDefaultUid cmdlet sets the default user ID (UID) for the specified WSL instances.
        This determines which user account is used when launching the instance without specifying a user.
    .PARAMETER Name
        Specifies the instance names of instances to set the default UID for. Wildcards are permitted.
    .PARAMETER Instance
        Specifies WslInstance objects that represent the instances to set the default UID for.
    .PARAMETER Uid
        Specifies the user ID to set as default. Common values are 0 (root) or 1000 (first regular user).
    .INPUTS
        WslInstance, System.String
        You can pipe a WslInstance object retrieved by Get-WslInstance, or a string that contains
        the instance name to this cmdlet.
    .OUTPUTS
        None.
    .EXAMPLE
        Set-WslDefaultUid -Name Ubuntu -Uid 1000
        Sets the default UID to 1000 for the Ubuntu instance.
    .EXAMPLE
        Set-WslDefaultUid -Name test* -Uid 0
        Sets the default UID to 0 (root) for all instances whose names start with "test".
    .EXAMPLE
        Get-WslInstance -Version 2 | Set-WslDefaultUid -Uid 1000
        Sets the default UID to 1000 for all WSL2 instances.
    .EXAMPLE
        Get-WslInstance Ubuntu,Debian | Set-WslDefaultUid -Uid 1000
        Sets the default UID to 1000 for the Ubuntu and Debian instances.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([WslInstance])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "InstanceName", Position = 0)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildCards()]
        [string[]]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Instance", Position = 0)]
        [WslInstance[]]$Instance,
        [Parameter(Mandatory = $true, Position = 1)]
        [int]$Uid
    )

    process {
        $instances = if ($PSCmdlet.ParameterSetName -eq "InstanceName") {
            Get-WslInstance -Name $Name
        } else {
            $Instance
        }

        foreach ($distro in $instances) {
            if ($PSCmdlet.ShouldProcess($distro.Name, "Set default UID to $Uid")) {
                $distro.SetDefaultUid($Uid)
            }
            $distro
        }
    }
}