Skip to content

Instantly share code, notes, and snippets.

@JustinGrote
Last active April 20, 2024 05:30
Show Gist options
  • Save JustinGrote/448e2cf774c06112f3b4d61906b1f578 to your computer and use it in GitHub Desktop.
Save JustinGrote/448e2cf774c06112f3b4d61906b1f578 to your computer and use it in GitHub Desktop.
Visual Studio Code Tunnel bootstrap via PowerShell (works in Azure Cloud Shell)

Visual Studio Code Server Tunnel Bootstrap

THIS IS NOT OFFICIALLY SUPPORTED

This is a PowerShell script that will automate the process of:

  1. Identify the version of code cli your platform requires
  2. Download and install
  3. Initiate the tunneling process using an autogenerated name based on the environment context

I designed this for Azure Cloud Shell but it can eventually work for all platforms that can run code-server. Azure Cloud Shell is just an ephemeral version of CBL-Mariner Linux so most everything works, even PowerShell debugging!

Known limitation is that your cloud shell will time out with 20 mins of inactivity and kill the tunnel, I'm working on how Azure Cloud Shell "detects" activity and try to fake it with the script...

VSCode Demo

Quick Start

iwr bit.ly/codetunnel | iex

Custom Name

& ([scriptblock]::Create((iwr bit.ly/codetunnel))) -Name myServer
#require -version 5.1
#Usage: iwr https://tinyurl.com/VSCodeServer | iex
#Parameterized usage: & ([ScriptBlock]::Create((iwr https://tinyurl.com/VSCodeServer))) -Your -Options
param(
#Path to install the vscode binary to. This defaults to a folder in your Local AppData path. Must be writable by your current user without sudo
[ValidateNotNullOrEmpty()]
[string]$InstallPath = $(Join-Path -Path ([System.Environment]::GetFolderPath('LocalApplicationData')) -ChildPath 'vscode-cli'),
#Installation architecture. This is normally autodetected.
$Arch,
$OS,
[ValidateSet('stable', 'insider')]
[ValidateNotNullOrEmpty()]
$Build = 'stable',
#Path to find the vscode server launcher. You normally dont need to change.
[ValidateNotNullOrEmpty()]
$InstallUri = 'https://code.visualstudio.com/sha/download',
#Normally this script will use the existing detected code server or vscode instance. Specify to force a new install.
[Switch]$Force,
#This script typically starts code-server automatically upon run. Specify to prevent that behavior
[switch]$InstallOnly,
#This script updates the path to include code-server so it can be specified again. Specify to prevent that behavior
[switch]$NoUpdatePath,
#Additional arguments to pass to code --tunnel, such as if you want to customize the name of the tunnel
[string[]]$ArgumentList,
#By default this uses GitHub for auth, specify this to use MS
[switch]$UseMicrosoftLogin,
#Customize the name of the tunnel. 20 characters max
$Name
)
$ErrorActionPreference = 'Stop'
function Get-CodeTunnelName {
#Just use the machine name if this is not cloudshell
if (-not $env:ACC_TID) {
return [environment]::MachineName
}
[string]$tenant = Get-AzTenantDomain | Get-SubString 10
[string]$user = $ENV:USER | Get-SubString (15 - $tenant.length)
return "csh-{0}-{1}" -f $tenant, $user
}
function Get-AzTenantDomain {
#Get the tenant ID name for the cloudshell
#This is wrapped in a scriptblock as Get-AzTenant may not be present in the environment and will cause a parse error
[string]((Get-AzTenant -TenantId $env:ACC_TID).Domains | Select-String '^([^\.]+?).onmicrosoft.com$').Matches[0].Groups[1]
}
filter Get-SubString ($length) {
$PSItem.Substring(0, [Math]::Min($length, $PSItem.Length))
}
if (-not $Name) {$Name = Get-CodeTunnelName}
if (-not $OS) {
#Polyfill for 5.1. This will be $false and not $null in non-Windows versions and won't apply.
if ($null -eq $IsWindows) {
$IsWindows = $true
}
$OS = switch ($true) {
$IsWindows { 'win32' }
$IsLinux { 'alpine' }
$IsMacOS { 'darwin' }
default { throw [NotSupportedException]'This platform is not supported by this script.' }
}
}
if (-not $Arch) {
if ($OS -eq 'win32') {
if (-not $Arch) {
$Arch = switch ($ENV:PROCESSOR_ARCHITECTURE) {
'AMD64' { 'x64' }
'ARM64' { 'arm64' }
default { throw [NotSupportedException]'Unknown Windows Architecture, this is either a bug or you are trying to install on an x86 machine' }
}
}
} elseif ($isLinux -or $isMacOS) {
$arch = switch (uname -m) {
'x86_64' { 'x64' }
'aarch64' { 'arm64' }
'armv7l' { 'arm32' }
default { throw [NotSupportedException]'Unknown Linux Architecture, please specify x64, arm32, or arm64 for the Arch parameter of this command' }
}
} else {
throw [NotSupportedException]'This platform is not supported by this script.'
}
}
$Body = @{
'build' = $Build
'os' = "cli-$OS-$Arch"
}
$OutBin = if ($IsWindows) { 'code-cli.exe' } else { 'code-cli' }
$CodeCliPath = Join-Path -Path $InstallPath -ChildPath $OutBin
if ((Test-Path $CodeCliPath) -and -not $Force) {
Write-Verbose "Found existing vscode CLI at $CodeCliPath"
$LocalInstallFound = $true
$OutBinPath = $CodeCliPath
} else {
Write-Verbose 'No vscode CLI install found or -Force specified, checking for existing vscode installations'
if ($Force) {
Write-Verbose '-Force Specified, skipping vscode binary detection'
} else {
$VSCodePath = foreach ($command in 'code-insiders', 'code') {
$candidatePath = (Get-Command $command -CommandType Application -ErrorAction Ignore) |
Select-Object -First 1 |
ForEach-Object Source
if ($candidatePath) {
#This is just a shim in cloud shell so we ignore it
if ($env:ACC_CLOUD -and $candidatePath -eq '/usr/bin/code') {
continue
}
Write-Verbose "Found existing vscode binary at $candidatePath"
$candidatePath
break
}
}
if ($VSCodePath) {
Write-Verbose "VSCode detected at $VSCodePath"
$OutBinPath = $VSCodePath
$LocalInstallFound = $true
} else {
Write-Verbose 'No existing vscode installation found'
}
}
if (-not $LocalInstallFound) {
$OutBinPath = $CodeCliPath
Write-Verbose "Downloading vscode $OS $Arch from $InstallUri"
#Prep the target folder if it doesn't exist and download the binary
New-Item -ItemType Directory -Force -Path $InstallPath | Out-Null
if ($IsWindows) {
#Improves download performance on 5.1
$lastProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
}
$outExtension = if ($IsWindows) {'.zip'} else {'.tgz'}
$archiveTempPath = [System.IO.Path]::GetTempPath() + [System.IO.Path]::GetRandomFileName() + $outExtension
try {
Invoke-WebRequest -Method GET -UseBasicParsing -Body $Body -Uri $InstallUri -OutFile $archiveTempPath
if (-not (Test-Path $archiveTempPath)) {
throw "Archive did not download successfully to $archiveTempPath from $InstallUri"
}
$codeFileName = if ($isWindows) {
#5.1 Windows won't tell us the outfile so we have to guess
Expand-Archive -Path $archiveTempPath -DestinationPath $InstallPath -Force
'code.exe'
} else {
& tar -xzvf $archiveTempPath -C $InstallPath
}
$codeFilePath = Join-Path $InstallPath $CodeFileName
if (-not (Test-Path $codeFilePath)) {throw 'Was not able to extract the code file correctly'}
Move-Item $codeFilePath $OutBinPath -Force
} finally {
Remove-Item $archiveTempPath -ErrorAction Ignore
}
if ($IsWindows) {
$ProgressPreference = $lastProgressPreference
}
Write-Verbose "Downloaded code server to $OutBinPath"
if (-not $NoUpdatePath) {
$Paths = [Environment]::GetEnvironmentVariable('Path', 'User') -split [io.path]::PathSeparator
if ($InstallPath -notin $Paths) {
Write-Verbose "PATH variable already contains $InstallPath, skipping..."
} else {
Write-Verbose "Updating PATH to include $InstallPath"
}
[string]$newPathVar = "$InstallPath" + [io.path]::PathSeparator + [Environment]::GetEnvironmentVariable('Path', 'User')
[Environment]::SetEnvironmentVariable('Path', $newPathVar, 'User')
}
}
if (-not (Test-Path $OutBinPath)) {
throw [InvalidOperationException]'Failed to find or install vscode binary'
}
}
if (-not $InstallOnly) {
Write-Verbose "Starting $OutBinPath in current path $PWD"
if ($UseMicrosoftLogin) {
Write-Verbose "-UseMicrosoftLogin specified, starting $OutBinPath with --auth-provider=Microsoft"
$codeCliParams = @(
'tunnel'
'login'
'--provider'
'microsoft'
'--name'
)
& $OutBinPath @codeCliParams
if ((& $OutBinPath tunnel user show) -ne 'logged in') {
throw [InvalidOperationException]'Failed to log in to Microsoft'
}
}
$codeCliParams = @(
'tunnel'
'--accept-server-license-terms'
'--name'
$Name
)
& $OutBinPath @codeCliParams @ArgumentList
}
@stevenjudd
Copy link

Super cool! Question: is there a way to return control to the Cloud Shell prompt or does it need to stay at the "...info [rpc.0] Server started" message to stay connected?

@JustinGrote
Copy link
Author

JustinGrote commented Apr 20, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment