############################################################################## # Copyright (C) 2024 Sebastian Oehms <seb.oehms@gmail.com> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # http://www.gnu.org/licenses/ ############################################################################## # ToDo # 1. Implement the *More* functionality in the *tags* window # 2. Add possiblility to stop a container # 3. Add possiblility to rename a container # 4. Extract project specific data from devcontainer.json files # 5. Move $this._container to argument list of methods # 6. Open separate terminal for selection windows on Linux, too ############################################################################################### # To show verbose messages set $VerbosePreference = 2 (Continue) in your Powershell session. # To revert this set $VerbosePreference = 0 (SilentlyContinue) ############################################################################################### ############################################################################################### # Static declarations ############################################################################################### param( [string] $option = "no option" ) enum TagFilterValues { all stable pre } $env:WSL_UTF8 = 1 # needed to pass variables to WSL commands ############################################################################################### # Access to global PS variables in class methods needs the following functions ############################################################################################### function os_is_linux { return $PSVersionTable.Platform -eq 'Unix' } function get_matches { # get result of previous regular expresssion match via -match option Write-Verbose "get_matches ${Matches}" return $Matches } function reset_matches { # reset result of previous regular expresssion match via -match option if ($Matches -eq $null) {return} Write-Verbose "reset_matches before ${Matches}" $Matches.Clear() Write-Verbose "reset_matches after ${Matches}" } function journal_message($text) { # add a line to journal file Write-Verbose "Write to journal $PSScriptRoot" $today = Get-Date -Format "dddd MM\/dd\/yyyy HH:mm:ss K" if (os_is_linux) { $journal = "$PSScriptRoot/journal.log" $disk_space = (df -B1 /var/lib/docker)[1].Split() $drive = $disk_space[0] $disk_size = $disk_space[1] / 1GB | % {$_.ToString("####.##")} $disk_free = $disk_space[3] / 1GB | % {$_.ToString("####.##")} } else { $journal = "$PSScriptRoot\journal.log" $drive = "C:" $disk_space = @(foreach ($d in Get-WmiObject -Class Win32_LogicalDisk) {if ($d.DeviceID -eq $drive) {$d.Size, $d.FreeSpace}}) $disk_size = $disk_space[0] / 1GB | % {$_.ToString("####.##")} $disk_free = $disk_space[1] / 1GB | % {$_.ToString("####.##")} } if (-not (Test-Path $journal)) { Write-Verbose "Init journal ${journal}" "-------------------------------------------" > $journal "Journal file of your projects docker guide " >> $journal "-------------------------------------------" >> $journal } $message = "${today}: Size of drive ${drive}: ${disk_size} GB, free: ${disk_free} GB ($text)" Write-Verbose "Write ${message} to journal ${journal}" $message >> $journal } ############################################################################################### # Classes for configuration purpose ############################################################################################### class DockerGuideMenue { [string] $text [int] $dialog DockerGuideMenue([string] $text, [int] $dialog) { $this.text = $text $this.dialog = $dialog } } class DockerGuideApp { [string] $Name [string] $Prefix [int] $port [string] $option [int] $rank DockerGuideApp([string] $name, [string] $prefix, [int] $port, [string] $option, [int] $rank) { $this.Name = $name $this.Prefix = $prefix $this.port = $port $this.option = $option $this.rank = $rank } } class DockerGuideRepo { [string] $user [string] $name [string] $description [TagFilterValues] $filter [System.Collections.ArrayList] $apps DockerGuideRepo([string] $user, [string] $name, [string] $description, [TagFilterValues] $filter, [System.Collections.ArrayList] $apps) { $this.user = $user $this.name = $name $this.description = $description $this.filter = $filter $this.apps = $apps } } ############################################################################################### # Base class containing declarations of static constants and methods common to # ProjectsDockerGuide and DockerInstallAssistance ############################################################################################### class DockerGuideBase { [string] $_proj_name [string] $_version [string] $_default_distro [pscustomobject] $image_keys [pscustomobject] $container_keys [pscustomobject] $tag_keys [pscustomobject] $dialogs [pscustomobject] $return_values [pscustomobject] $select_mode [pscustomobject] $buttons [pscustomobject] $icons [pscustomobject] $button_pressed DockerGuideBase() { # dictionaries for keys used in https://registry.hub.docker.com/v2/repositories/ $this._default_distro = 'DockerForPowershell-0.2' $this.image_keys = [ordered]@{ repo = 'Repository'; tag = 'Tag'; id = 'ID'; created = 'CreatedAt'; size = 'Size'; } $this.container_keys = [ordered]@{ name = 'Names'; id = 'ID'; image = 'Image'; created = 'CreatedAt' status = 'Status'; size = 'Size'; } $this.tag_keys = [ordered]@{ name = 'name'; full_size = 'full_size'; id = 'id'; user = 'last_updater_username'; updated = 'last_updated'; last_pulled = 'tag_last_pulled'; last_pushed = 'tag_last_pushed'; } # dictionaries for dialog handling $this.dialogs = [ordered]@{ distros = 1; repos = 2; tags = 3; images = 4; containers = 5; apps = 6; } $this.return_values = @{ created = 1; pulled = 2; } $this.select_mode = @{ single = 1; multiple = 2; } # dictionaries for popup-messages $this.buttons = @{ ok = 0; ok_cancel = 1; abort_ignore_retry = 2; yes_no_cancel = 3; yes_no = 4; retry_cancel = 5; } $this.icons = @{ critical = 16; question = 32; exclamtion = 48; information = 64; } $this.button_pressed = @{ ok = 1; cancel = 2; abort = 3; retry = 4; ignore = 5; yes = 6; no = 7; timeout = -1; } } [pscustomobject] popup_message([string] $text, [int] $button, [int] $icon) { $wshell = New-Object -ComObject Wscript.Shell $answer = $wshell.Popup($text, 0, $this._proj_name + ' Docker Guide', $button + $icon) Write-Verbose "Popup ${answer} ${text}" return $answer } } ############################################################################################### ############################################################################################### # Main Class ############################################################################################### ############################################################################################### class ProjectsDockerGuide : DockerGuideBase { [DockerInstallAssistent] $_install_assist [boolean] $_linux [boolean] $_ready [string] $_path [string] $_path_prefix [string] $_wsl_distro [pscustomobject] $_container [pscustomobject] $_tag_lists [pscustomobject] $_menues [System.Collections.Hashtable] $_port_maps [System.Collections.ArrayList] $_proj_repositories [System.Collections.ArrayList] $_images [System.Collections.ArrayList] $_containers ProjectsDockerGuide([string] $project_name, [System.Collections.ArrayList] $project_repositories) { # Instatiating the class $this._proj_name = $project_name $this._version = '0.2' $this._install_assist = [DockerInstallAssistent]::new($project_name) $this._linux = $false $this._port_maps = @{} $this._path = "$PWD" if (os_is_linux) { $this._linux = $true $this._path_prefix = $this._path if ($this._path_prefix.StartsWith('/')) { $this._path_prefix = $this._path_prefix.Substring(1) } $this._path_prefix = $this._path_prefix.Replace('/', '-') } else { $this._path_prefix = $this._path.Replace(':\', '_').Replace('\', '-') } Write-Verbose "_path_prefix: $($this._path_prefix)" $this._proj_name = $project_name $menues = @{ install = [DockerGuideMenue]::new("Install $($this._default_distro)", $this.dialogs.distros); backr = [DockerGuideMenue]::new("Back", $this.dialogs.repos); backt = [DockerGuideMenue]::new("Back", $this.dialogs.tags); more = [DockerGuideMenue]::new("More", $this.dialogs.tags); download = [DockerGuideMenue]::new("Download other software version", $this.dialogs.images); del_image = [DockerGuideMenue]::new("Delete a software version", $this.dialogs.images); create = [DockerGuideMenue]::new("Create new session", $this.dialogs.containers); del_container = [DockerGuideMenue]::new("Delete a session", $this.dialogs.containers); bash = [DockerGuideMenue]::new("Work in a bash terminal (advanced)", $this.dialogs.apps); lab = [DockerGuideMenue]::new("Work in a Jupyter lab", $this.dialogs.apps); notebook = [DockerGuideMenue]::new("Work in a Jupyter notebook", $this.dialogs.apps); ipython = [DockerGuideMenue]::new("Work in a IPython terminal (default)", $this.dialogs.apps); } $this._menues = $menues $this._proj_repositories = $project_repositories $this.banner() $this._ready = $this.start_docker() } #################################################################### # Generic helper methods #################################################################### [void] banner() { $projname = $this._proj_name $version = $this._version @( "" "-----------------------------------------------------" "| Welcome to the ${projname} Docker Guide version ${version}! |" "-----------------------------------------------------" "" ) | Write-Host -BackgroundColor White -ForegroundColor DarkBlue } [pscustomobject] show_dialog([System.Collections.ArrayList] $view, [string] $description, [int] $mode) { Write-Verbose "show_dialog view $($view | Out-String)" Write-Host "Please have a look at the window: ${description}" $ans = $null try { $ans = $view | Out-GridView -Title $description -OutputMode $mode } catch [System.Management.Automation.CommandNotFoundException] { $ans = $view | Out-ConsoleGridView -Title $description -OutputMode $mode } Write-Verbose "show_dialog_view answer $($ans | Out-String)" return $ans } [boolean] check_proj_image([string] $img) { $proj_reps = @(foreach ($r in $this._proj_repositories) { "$($r.user)/$($r.name)" }) if ($img -in $proj_reps) { return $true } return $false } [System.Collections.ArrayList] dialog_menue([int] $dialog) { return @(foreach ($m in $this._menues.values) {if ($m.dialog -eq $dialog) {$m.text}}) } [DockerGuideRepo] image_to_repo([pscustomobject] $image) { Write-Verbose "Entering image_to_repo for $($image | Out-String)" $repo = $image.($this.image_keys.repo) $rep = $repo.Split("/") $user = $rep[0] $name = $rep[1] foreach ($r in $this._proj_repositories) { if ($r.user -eq $user -and $r.name -eq $name) { Write-Verbose "Leaving image_to_repo for $($image | Out-String) with $($r | Out-String)" return $r } } Write-Verbose "Leaving image_to_repo for $($image | Out-String) without result" return $null } [System.Collections.ArrayList] image_to_apps([pscustomobject] $image) { Write-Verbose "Entering image_to_apps for $($image | Out-String)" $repo = $this.image_to_repo($image) $apps = $repo.apps if ($apps.Count -eq 0) { $apps = @([DockerGuideApp]::new("Default", "X-", 0, "", 0)) } Write-Verbose "Leaving image_to_apps for $($image | Out-String) with $($apps | Out-String)" return $apps } [pscustomobject] container_to_image([pscustomobject] $container) { Write-Verbose "Entering container_to_image for $($container | Out-String)" $img = $container.($this.container_keys.image) $imgs = $this.find_images() foreach ($i in $imgs) { $repo = $i.($this.image_keys.repo) $tag = $i.($this.image_keys.tag) if ($img -eq "${repo}:${tag}") { Write-Verbose "Leaving container_to_image for $($container | Out-String) with $($i | Out-String)" return $i } } Write-Verbose "Leaving container_to_image for $($container | Out-String) without result" return $null } [DockerGuideApp] container_to_app([pscustomobject] $container) { Write-Verbose "Entering container_to_app for $($container | Out-String)" $img = $this.container_to_image($container) if (-not $img) {return $null} $apps = $this.image_to_apps($img) if ($apps.Count -gt 1) { $name = $container.($this.container_keys.name) foreach ($app in $apps) { if ($name.StartsWith($app.Prefix)) { Write-Verbose "Leaving container_to_image for $($container | Out-String) with $($app | Out-String)" return $app } } } $app = $apps[0] Write-Verbose "Leaving container_to_image for $($container | Out-String) with default $($app | Out-String)" return $app } [pscustomobject] select_from_list([System.Collections.ArrayList] $items, [System.Collections.ArrayList] $cols, [System.Collections.ArrayList] $menue, [string] $title ) { # Return one image from the list according to user choice $proj_name = $this._proj_name Write-Verbose "Entering: select_from_list $title" $description = "${proj_name} Docker Guide: $title Please select the line of your choice!" $view = @(echo $items | select $cols) $mkeys = @() foreach ($m in $menue) { if ($cols.Count -eq 0) { $view += $m continue } $mline = echo @($items[0]) | select $cols # copy of first line foreach ($key in $cols) { if ($cols.IndexOf($key) -eq 0) { $mline.$key = $m $mkeys += $key } else { $mline.$key = "" } } $view += $mline } Write-Verbose "mkeys $($mkeys | Out-String)" $ans = $this.show_dialog($view, $description, $this.select_mode.single) if (-not $ans) { Write-Verbose "select_from_list $title canceled" return $null } else { foreach ($m in $menue) { $ind = $menue.IndexOf($m) $col = $mkeys[$ind] if ($ans.$col -eq $m) { Write-Verbose "Leaving: select_from_list $title with $m" return $m } } } Write-Verbose "Leaving: select_from_list $title with $ans" return $ans } [System.Collections.ArrayList] search_tags([pscustomobject] $repo) { # search tags for the given repository on Docker Hub $uri = "https://registry.hub.docker.com/v2/repositories/$($repo.user)/$($repo.name)/tags/?page_size=250" Write-Verbose "Invoke-WebRequest ${uri}" $result = Invoke-WebRequest -UseBasicParsing -Uri $uri $JsonObject = ConvertFrom-Json -InputObject $result.Content $res = $JsonObject.results # filter the result $new_res = @() foreach ($t in $res) { if ($t.name.Contains('latest') -or $t.name.Contains('develop')) { continue } if ($repo.filter -eq [TagFilterValues]::pre) { if ($t.name.Contains('beta') -or $t.name.Contains('rc')) { $new_res += $t } } elseif ($repo.filter -eq [TagFilterValues]::stable) { if (-not ($t.name.Contains('beta') -or $t.name.Contains('rc'))) { $new_res += $t } } else { $new_res += $t } } return $new_res } [DockerGuideApp] select_app($apps) { # Return one tag from the repository $title = "List of different applications to work with $($this._proj_name)." $app_names = $apps[0].psobject.Properties.Name $cols = @($app_names[0], $app_names[1]) $view = @(echo $apps | select $cols) $description = "$($this._proj_name) Docker Guide: $title Please select the line of your choice!" $ans = $this.show_dialog($view, $description, $this.select_mode.single) foreach ($app in $apps) { if ($app.Name -eq $ans.Name) { Write-Verbose "Selected app: $($app | Out-String)" return $app } } Write-Verbose "No Selection $($ans | Out-String)" return $null } [pscustomobject] select_wsl_distro($distros) { # Return wsl distribution $menue = $this.dialog_menue($this.dialogs.distros) $title = "List of WSL distros that have Docker available." return $this.select_from_list($distros, @(), $menue, $title) } [pscustomobject] select_repo() { # Return a repository $repos = $this._proj_repositories $cols = $repos[0].psobject.properties.name $menue = $this.dialog_menue($this.dialogs.repos) $title = "List of software that can be downloaded." return $this.select_from_list($repos, $cols, $menue, $title) } [pscustomobject] select_tag([pscustomobject] $repo) { # Return one tag from a repository $tags = $this.search_tags($repo) $cols = $($this.tag_keys.Values) $menue = $this.dialog_menue($this.dialogs.tags) $title = "List of software versions that can be downloaded." return $this.select_from_list($tags, $cols, $menue, $title) } #################################################################### # Methods to choose an image #################################################################### [System.Collections.ArrayList] find_images() { # Find all images return $this.find_images($false) } [System.Collections.ArrayList] find_images([bool] $refresh) { # Find all images if ($this._images -and -not $refresh) { return $this._images } $lines = $this.docker("images --format json") Write-Verbose "docker images: $($lines | Out-String)" $this._images = [System.Collections.ArrayList]@(foreach ($line in $lines) { $json = $line | ConvertFrom-Json $json }) Write-Verbose "Images found: $($this._images | Out-String)" return $this._images } [System.Collections.ArrayList] find_proj_images() { # Filter the list of containers for the project $proj_images = [System.Collections.ArrayList]@() if (-not $this._images) { $this.find_images() } foreach ($i in $this._images) { $irep = $i.($this.image_keys.repo) if ($this.check_proj_image($irep)) { $proj_images.Add($i) | Out-Null } } return $proj_images } [pscustomobject] choose_image() { # Return the Image for which a new container should be started Write-Verbose "Entering choose image" do { $images = $this.find_proj_images() $i = $images.Count if ($i -eq 0) { $ans = $this.pull_images() if ($ans -eq $this.return_values.pulled) { continue } elseif ($this._menues.backr.text -eq $ans) { continue } else { return $ans } } $ans = $this.select_image($images) if ($this._menues.download.text -eq $ans) { $ans = $this.pull_images() if ($ans -eq $this.return_values.pulled) { continue } elseif ($this._menues.backr.text -eq $ans) { continue } else { return $ans } } elseif ($this._menues.del_image.text -eq $ans) { $this.delete_images($images) continue } else { return $ans } } while ($true) return $null } [pscustomobject] select_image([System.Collections.ArrayList] $images) { # Return one image from the list according to user choice $cols = $($this.image_keys.Values) $menue = $this.dialog_menue($this.dialogs.images) $title = "List of software versions that have been downloaded." return $this.select_from_list($images, $cols, $menue, $title) } [pscustomobject] pull_images() { # Return the Image for which a new container should be started Write-Verbose "Entering: pull images" do { $repo = $this.select_repo() if ($repo -eq $null) { return $null } if ($this._menues.backr.text -eq $repo) { Write-Verbose "Repo cmd $($repo | Out-String)" return $repo } $tag = $this.select_tag($repo) if ($tag -eq $null) { return $null } if ($this._menues.backt.text -eq $tag) { Write-Verbose "Tag cmd $($tag | Out-String)" continue } $this.pull_image($repo, $tag) $this._images = $null return $this.return_values.pulled } while ($true) return $null } [void] delete_images([System.Collections.ArrayList] $images) { # Return a list of images that should be deleted $proj_name = $this._proj_name $description = "${proj_name} Docker Guide: List of software that have been used formerly. Please select the lines you want to remove!" $cols = $($this.image_keys.Values) $view = @(echo $images | select $cols) $list = $this.show_dialog($view, $description, $this.select_mode.multiple) if (-not $list) { return } foreach ($i in $list) { Write-Verbose "Delete $($i | Out-String)" $this.delete_image($i) } return } #################################################################### # Methods to choose a container #################################################################### [System.Collections.ArrayList] find_containers() { # Find all containers return $this.find_containers($false) } [System.Collections.ArrayList] find_containers([bool] $refresh) { # Find all containers if ($this._containers -and -not $refresh) { return $this._containers } $lines = $this.docker("ps -a --format json") Write-Verbose "docker ps: $($lines | Out-String)" $this._containers = [System.Collections.ArrayList]@(foreach ($line in $lines) { $json = $line | ConvertFrom-Json $json }) Write-Verbose "Containers found: $($this._containers | Out-String)" return $this._containers } [System.Collections.ArrayList] find_proj_containers() { # Filter the list of containers for the project return $this.find_proj_containers($false) } [System.Collections.ArrayList] find_proj_containers([bool] $matching) { # Filter the list of containers for the project $proj_containers = [System.Collections.ArrayList]@() if (-not $this._containers) { $this.find_containers() } foreach ($c in $this._containers) { $this.get_container_port($c) $cim = $c.($this.container_keys.image) $ci = $cim -split ":" # cut off the tag $img = $ci[0] if ($this.check_proj_image($img)) { $cn = $c.($this.container_keys.name) if ($matching -and -not $cn.Contains($this._path_prefix)) { Write-Verbose "Container ${cim} does not match $($this._path_prefix)" continue } $proj_containers.Add($c) | Out-Null } } return $proj_containers } [int] get_container_port([pscustomobject] $c) { $ports = $c.Ports if ($ports -eq "") { return 0 } Write-Verbose "Ports ${ports}" if ($ports -match ":([0-9]*)-" -eq $true) { $m = get_matches Write-Verbose "match $($m | Out-String), $($m.Count)" $p = [int] $m[1] $cn = $c.($this.container_keys.name) Write-Verbose "port for ${cn}: $($p)" $this._port_maps[$cn] = $p return $p } return 0 } [int] find_free_port([string] $start_port) { $s = [int] $start_port $p = $s foreach ($c in $this._containers) { $app = $this.container_to_app($c) if ($app -and $app.port -eq $s) { $p += 1 } } Write-Verbose "Free port ${p} found for $($start_port)" return $p } [pscustomobject] choose_container() { # Return the Container that should be started Write-Verbose "Entering: choose container" $matching_containers = $this.find_proj_containers($true) $l = $matching_containers.Count if ($l -eq 0) { $containers = $this.find_proj_containers() $k = $containers.Count if (-not $k) { $img = $this.choose_image() if ($img -eq $null) { return $null } $this.create_container($img) return $this.return_values.created } else { Write-Host "No session belongs to the current directory $($this._path)!" return $this.select_container($containers) } } else { return $this.select_container($matching_containers) } } [pscustomobject] select_container([System.Collections.ArrayList] $containers) { # Return one container from the list according to user choice $cols = $($this.container_keys.Values) $menue = $this.dialog_menue($this.dialogs.containers) $title = "List of sessions that have been used or created formerly." return $this.select_from_list($containers, $cols, $menue, $title) } [void] delete_containers([System.Collections.ArrayList] $containers, [string] $purpose) { # Return a list of containers that should be deleted $proj_name = $this._proj_name $description = "${proj_name} Docker Guide: ${purpose}Please select the lines you want to remove!" $cols = $($this.container_keys.Values) $view = @(echo $containers | select $cols) $list = $this.show_dialog($view, $description, $this.select_mode.multiple) if (-not $list) { return } foreach ($c in $list) { Write-Verbose "Delete $($c | Out-String)" $this.delete_container($c) } return } #################################################################### # Methods that directly access Docker #################################################################### [string] docker_str([string] $arguments) { $d = $this._wsl_distro if ($d) { $cmd = "wsl -d $d -e docker $arguments" } else { $cmd = "docker $arguments" } Write-Verbose "Command to call Docker: $cmd" return $cmd } [pscustomobject] docker([string] $arguments) { $cmd = $this.docker_str($arguments) Write-Verbose "Invoke Docker with: $cmd" return Invoke-Expression $cmd } [void] start_daemon() { Write-Host "The Docker daemon is not running but will be started now" $d = $this._wsl_distro $def = $this._default_distro.Split('-')[0] # version independent part if (-not $d) { if ($this._linux) { Write-Host "Your admin authentication is needed to start the Docker daemon!" sudo service docker start Write-Verbose "Starting Docker daemon" return } # share using Docker Desktop $cmd = 'C:\Program Files\Docker\Docker\resources\dockerd.exe' # must be started with 'Start-Process -Verb runAs' and than only listens to admin $cmd = 'C:\Program Files\Docker\Docker\Docker Desktop.exe' Start-Process $cmd Write-Verbose "Starting Docker Desktop with $cmd" } elseif ($d.StartsWith($def)) { # default distro wsl -d $d -e sh /root/start_dockerd Write-Verbose "Starting Docker daemon in default WSL-distro $d" } else { # others with fingers crossed Write-Host "Your admin authentication is needed to start the Docker daemon!" wsl -d $d -e sudo service docker start Write-Verbose "Starting Docker daemon in WSL-distro $d" } } [boolean] daemon_down() { $test = $this.docker("ps") -join ' ' | Out-String Write-Verbose "Test if Docker daemon s up with ps gives '$test'" if (-not $test) { return $true } return -not $test.Contains('CONTAINER ID') } [boolean] set_wsl_distro() { if ($this._linux) {return $true} if ($this._install_assist.reboot_needed()) { if ($this._install_assist.run()) { $this._wsl_distro = $this._default_distro return $true } return $false } $def = $this._default_distro.Split('-')[0] # version independent part $distributions = wsl -l -q | Sort-Object -Descending $docker_desktop = $null $default = $null $docker_distros = @() foreach ($d in $distributions) { if ($d -eq "") { continue } if ($d -eq "docker-desktop") { Write-Verbose "Docker Desktop is present" $docker_desktop = $d $docker_distros += $d continue } elseif ($d.StartsWith($def)) { Write-Verbose "$def is present: $d" $default = $d $docker_distros += $d continue } else { $test_distro = wsl -d $d -e docker info Write-Verbose "Test WSL distro $d gives '$test_distro'" if ($test_distro.StartsWith('Client')) { $distro = $d Write-Verbose "Use WSL distro: $distro" $docker_distros += $d } } } $distro = $null if ($docker_distros.Count -eq 1) { if ($docker_distros[0] -eq $default) { $distro = $default } elseif ($docker_distros[0] -ne $docker_desktop) { $distro = $docker_distros[0] $text = "Docker is available on your system in the WSL-distribution $distro. Shall we share its usage?" $answer = $this.popup_message($text, $this.buttons.yes_no, $this.icons.information) if ($answer -ne $this.button_pressed.yes) { if ($this._install_assist.run()) { $distro = $this._default_distro } else { return $false } } } } elseif ($docker_distros.Count -gt 1) { if ($default) { $distro = $default } else { $answer = $this.select_wsl_distro($docker_distros) if (-not $answer) {return $false} if ($answer -eq $this._menues.install.text) { if ($this._install_assist.run()) { $distro = $this._default_distro } else { return $false } } $distro = $answer } } if ($distro -eq $null) { try { docker if ($docker_desktop) { $text = "Docker Desktop is available on your system. Shall we share its usage?" $answer = $this.popup_message($text, $this.buttons.yes_no, $this.icons.information) if ($answer -ne $this.button_pressed.yes) { if ($this._install_assist.run()) { $distro = $this._default_distro } else { return $false } } } } catch [System.Management.Automation.CommandNotFoundException] { if ($this._install_assist.run()) { $distro = $this._default_distro } else { return $false } } } $this._wsl_distro = $distro Write-Verbose "Set WSL-Distro: $distro" return $true } [boolean] start_docker() { if (-not $this.set_wsl_distro()) { return $false } if ($this.daemon_down()) { $this.start_daemon() } while ($this.daemon_down()) { $text = 'Please wait until the Docker engine is running!' if (-not $this._wsl_distro) { $text = $text + 'As soon as this message does not appear any more, you may close the Docker Desktop app.' } $answer = $this.popup_message($text, $this.buttons.retry_cancel, $this.icons.information) if ($answer -ne $this.button_pressed.retry) { return $false } Write-Verbose "Start Docker $($answer | Out-String)" } return $true } [void] pull_image([pscustomobject] $repo, [pscustomobject] $tag) { # pull image $ruser = $repo.user $rname = $repo.name $tname = $tag.($this.tag_keys.name) $name = "${ruser}/${rname}:${tname}" Write-Host "Download of ${name} starts!" $cmd = $this.docker_str("pull $($name)") $argu = "/c " + $cmd Write-Verbose "docker pull: ${argu}" journal_message "before $cmd" Start-Process -FilePath cmd -ArgumentList $argu -Wait journal_message "after $cmd" } [void] delete_image([pscustomobject] $i) { # delete an image $rep = $i.($this.image_keys.repo) $tag = $i.($this.image_keys.tag) $name = "${rep}:${tag}" Write-Host "Try to delete ${name}" $cont_to_delete = @() foreach ($c in $this._containers) { if ($c.($this.container_keys.image) -eq $name) { $cont_to_delete += $c } } Write-Verbose "cont_to_delete $($cont_to_delete | Out-String)" if ($cont_to_delete.Count -eq 0) { journal_message "before deleting ${name}" $this.docker("rmi ${name}") journal_message "after deleting ${name}" Write-Host "The software ${name} has been deleted!" # remove it from the list $this._images = @($this._images | Where-Object {$i.($this.image_keys.id) -ne $_.($this.image_keys.id)}) Write-Verbose "Images Count $($this._images.Count)" } else { Write-Host "There are containers for ${name} which must be deleted first!" $this.delete_containers($cont_to_delete, "List of sessions that must be deleted, to delete ${name}. ") } } [string] find_new_container_name([DockerGuideApp] $app) { # Rename Container to path Write-Verbose "Entering: find new container name" $base = $($app.Prefix) + $this._path_prefix $occupied = @() foreach ($c in $this._containers) { $cname = $c.($this.container_keys.name) if ($cname.Contains($base)) { $occupied += $cname } } Write-Verbose "occupied $($occupied | Out-String)" $i = 0 do { if ($i -eq 0) { $name = ${base} } else { $name = "${base}_${i}" } $i += 1 } while ($name -in $occupied) Write-Verbose "occupied $($occupied | Out-String)" Write-Verbose "new name ${name}" return $name } [void] create_container([pscustomobject] $image) { # create a new container $repo = $image.Repository $tag = $image.Tag Write-Host "Create a new container for ${repo}:${tag}" $port = "" $option = "" $apps = $this.image_to_apps($image) $app = $apps[0] if ($apps.Count -gt 1) { $app = $this.select_app($apps) if ($app -eq $null) { return } } $p = $this.find_free_port($app.port) if ($p -eq 0) { $port = "" } else { $port = " -p $($p):8888" } $option = " $($app.option)" $name = $this.find_new_container_name($app) $spwd = "$PWD" if ($this._wsl_distro) { $spwd = $spwd.Replace('C:', '/mnt/c').Replace('\', '/') } $tpwd = "/home/" + $name journal_message "before creating ${name}" $this.docker("create -it --mount `"type=bind,src=$($spwd),target=$($tpwd)`" --name $($name) -w $($tpwd)$($port) $($repo):$($tag)$($option)") journal_message "after creating ${name}" $this._port_maps[$name] = $p $this._containers = $null # to refresh the list } [void] delete_container([pscustomobject] $c) { # delete a container $id = $c.($this.container_keys.id) $name = $c.($this.container_keys.name) Write-Host "Delete container ${name} (id ${id})" journal_message "before deleting ${name} (id ${id})" $this.docker("stop ${id}") $this.docker("rm ${id}") journal_message "after deleting ${name} (id ${id})" # remove it from the list $this._containers = @($this._containers | Where-Object {$id -ne $_.($this.container_keys.id)}) Write-Verbose "Containers Count $($this._containers.Count)" } [void] attach_container() { # attach to an existing container $c = $this._container $id = $c.($this.container_keys.id) $name = $c.($this.container_keys.name) $app = $this.container_to_app($c) $ports = $this._port_maps $port_a = $app.port $port_m = $ports[$name] Write-Host "Attach container ${name} (id ${id})" $this.docker("start $($id)") if ($port_a -gt 0) { Write-Verbose "Use port $($port_m | Out-String) from mapping list $($ports | Out-String)" $cmd = $this.docker_str("logs $($id)") reset_matches $url = "" $port = "" $tok = "" $match_str = "http\://127\.0\.0\.1\:([8-9][0-9]{3})\/.*\?token=([a-z,0-9]*)" Write-Verbose "match_str ${match_str}" do { $log_lines = Invoke-Expression $cmd if ($log_lines.Count -eq 0) { continue } [array]::Reverse($log_lines) # Revert the log-lines to not use older token $logs = $log_lines | Out-String Write-Verbose "Logs ${logs}" if ($logs -match $match_str) { $m = get_matches Write-Verbose "match $($m | Out-String), $($m.Count)" $url = $m[0] $port = $m[1] $tok = $m[2] } } while ($tok -eq "") $url = $url.Replace(":$($port_a)", ":$($port_m)") Write-Host "Please have a look at your browser at a tab with URL: ${url}" Start-Process $url } else { $cmd = $this.docker_str("attach $($id)") if ($this._linux) { $argu = "-x " + $cmd } else { $argu = "/c " + $cmd } Write-Verbose "docker attach: ${argu}" Start-Process -FilePath cmd -ArgumentList $argu -Wait } } #################################################################### # Entrypoint to app #################################################################### [void] run() { if (-not $this._ready) { $text = 'Docker is not available on your system, yet! Try again later on.' $this.popup_message($text, $this.buttons.ok, $this.icons.information) return } do { $this._container = $this.choose_container() Write-Verbose "Container Choice: $($this._container | Out-String)" if ($this._container -eq $this.return_values.created) { Write-Verbose "Just created a conrainer, now search it." continue } elseif ($this._menues.create.text -eq $this._container) { $image = $this.choose_image() if ($image -ne $null) { $this.create_container($image) } Write-Verbose "Just created a container here, now search it." continue } elseif ($this._menues.del_container.text -eq $this._container) { $this.delete_containers($this._containers, "List of sessions that have been used formerly. ") Write-Verbose "After deletion of containsers, now search again." continue } elseif ($this._container -eq $null) { Write-Verbose "No container chosen, now finish." Write-Host "Good bye!" return } elseif (-not $($this._container.($this.container_keys.name)).Contains($this._path_prefix)) { $c = $this._container Write-Verbose "$($c.Names) does not contain $($this._path_prefix)" $text = "The choosen session does not belong to the current folder $($this._path)! Start in anyway?" $answer = $this.popup_message($text, $this.buttons.yes_no, $this.icons.information) if ($answer -ne $this.button_pressed.yes) { continue } } $this.attach_container() } while ($true) } } ############################################################################################### # Helper class to assist the Installation of Docker Desktop ############################################################################################### class DockerInstallAssistent : DockerGuideBase { DockerInstallAssistent([string] $project_name) { $this._proj_name = $project_name } [void] banner() { $app_name = $this._proj_name + " Docker Guide" @( " " " Docker For Powershell will now be installed! " " " " This also needs the Windows Subsystem for Linux (WSL) " " " " If you didn't use this before on your computer it might " " be necessary to install this too. " " " " In this case you will be asked to reboot your computer. " " Afterwards, to finish the instalation start " " ${app_name} again. " ) | Write-Host -BackgroundColor White -ForegroundColor Red } [void] reboot() { Write-Verbose "Restart Computer" $app_name = $this._proj_name + " Docker Guide" $text = "Your Computer must be restarted to complete the current installation step. After this is finished start ${app_name} again to continue." $answer = $this.popup_message($text, $this.buttons.ok_cancel, $this.icons.information) if ($answer -ne $this.button_pressed.ok) { return } Restart-Computer } [void] install() { $distro = $this._default_distro Write-Verbose "${distro} will now be installed!" $this.banner() $reboot_info ="" if ($this.reboot_needed()) { $reboot_info = " Maybe your computer must be rebooted during the process." } $text = "${distro} installation starts now.${reboot_info} Continue?" $answer = $this.popup_message($text, $this.buttons.yes_no, $this.icons.information) if ($answer -ne $this.button_pressed.yes) { return } $version = $distro.split('-')[1] $url_templ = "https://github.com/soehms/docker_for_powershell/releases/download/VER/docker_for_powershell-VER-installer.ps1" $url = $url_templ.Replace('VER', $version) Write-Verbose "URL: ${url}" $file = "docker_for_powershell-installer.ps1" Write-Verbose "File: ${file}" $web_client = New-Object System.Net.WebClient Write-Host "Download the ${distro} installer" $web_client.DownloadFile($url, $file) Write-Host "Install ${distro}" $argu = "-ExecutionPolicy Bypass .\docker_for_powershell-installer.ps1" Start-Process -Wait powershell -ArgumentList $argu del $file } [boolean] reboot_needed() { $app_name = $this._proj_name + " Docker Guide" $test_wsl = wsl --status if ($test_wsl -eq $null) { Write-Host "It seems that you don't have Docker on your system! ${app_name} does not run without it!" return $true } $test_str = $test_wsl -join ' ' | Out-String if ($test_str.Contains('WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED')) { Write-Host "It seems that you need to reboot your system!" return $true } if ($test_str.Contains('enablevirtualization')) { Write-Host "It seems that virtualization is not enabled in your BIOS settings! ${app_name} does not run without it!" Write-Host "For help have a look at https://support.microsoft.com/en-us/windows/enable-virtualization-on-windows-c5578302-6e43-4b4b-a449-8ced115f58e1" return $true } return $false } [boolean] run() { $distro = $this._default_distro if ((wsl -l -q) -contains $distro) { return $true } $text = "${distro} is not available on your system, but needed for this software! Shall we install it?" $answer = $this.popup_message($text, $this.buttons.yes_no, $this.icons.information) if ($answer -eq $this.button_pressed.yes) { if ((wsl --status) -eq $null) { $this.install() return $false } elseif ($this.reboot_needed()) { $this.reboot() return $false } $this.install() return $true } return $false } }