Add PowerShell 7-preview to Microsoft Windows Terminal (0.6+)

The new Windows Terminal continues to be developed and it has a dwindling list of deal-breakers (why won’t my mousewheel scroll??)

Along the way, the format of the settings file has changed, and my previous method for adding a profile for PowerShell 7 no longer works.

Below is a new block of code that’ll do the job. As before, just copy-n-paste into a PowerShell window and you’re good to go.

PowerShell 7-preview x64 needs to be installed and you need to have run Windows Terminal at least once.

I’m now doing horrible things with strings – because it’s easier than dealing with a comments and a JSON object – so this code is officially 3x more likely to blow up.

# Get Windows Terminal settings
$terminalFolderPath = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState"
$settingsFilePath = Join-Path $terminalFolderPath 'profiles.json'
[System.Collections.ArrayList]$settings = Get-Content $settingsFilePath
# Download icon
$pwsh7IconPath = Join-Path $terminalFolderPath 'pwsh7.ico'
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/weebsnore/Add-PS7ToWindowsTerminal/master/pwsh7.ico' -OutFile $pwsh7IconPath
# Generate PS7 profile JSON
$ps7profile = @{
    'guid' = '{' + (New-Guid).ToString() + '}'
    'name' = 'PowerShell 7-preview (x64)'
    'commandline' = 'C:\Program Files\PowerShell\7-preview\pwsh.exe'
    'icon' = $pwsh7IconPath
} | ConvertTo-Json
# Append comma to profile JSON
$ps7profile = $ps7profile + ','
# Find "profiles" line number
$profilesLine = ($settings | Select-String '"profiles":').LineNumber
# Add new profile to JSON and write to disk
,$settings.Insert($profilesLine+1,$ps7profile)
$settings | Out-File $settingsFilePath

Code here on GitHub.

Tutorial: Use a CSV file to make a graphical menu of PowerShell scripts

I’ve recently been working on a PowerShell module that uses a CSV file to create a custom menu of scripts.

It looks like this:

It hopefully helps to bridge the gap between engineers and automators, who write scripts useful to others, and service desk people and technicians, who may not be confident with the command line. PSScriptMenuGui allows PowerShell coders to put their scripts in a simple menu, usable by anyone.

It’s loosely inspired by (criminally similar to) something I made for a previous employer.

(The main difference is that this version starts instantly. The old version was so slow that I made an entertaining loading screen to fill the void. A good subject for a future blog post…)

If you’d like to dive straight in, please:

The rest of this blog post acts as a tutorial.

If you’d like to be guided through making your own menu, keep reading…

Step 0: System requirements

The module works on Windows only – sorry, rest of world!

Apart from that, it should run pretty much anywhere. It works on:

  • PowerShell for Windows 5.1 which comes with Windows 10.
  • PowerShell 7, currently available as a preview and due to be finished at the start of 2020.

It does not work on PowerShell Core 6. If you have this version, the easiest solution is to use PowerShell 5.1 as it is already on your PC.

Step 1: Install the module and make an example menu

Open a PowerShell prompt and:

# Navigate to where you want to work on your menu - in my case OneDrive:
cd $env:OneDrive
# Install the module:
Install-Module PSScriptMenuGui -Scope CurrentUser
# You may need read and agree to messages about updates and trust
# Make an example menu to get you going:
New-ScriptMenuGuiExample

You should see this:

VERBOSE: Copying example files to PSScriptMenuGui_example...

Step 2: Explore the example

Navigate to your PSScriptMenuGui_example folder and open PSScriptMenuGui.ps1. You should see a bit of boilerplate to ensure that the module is loaded, followed by this line which displays the menu:

Show-ScriptMenuGui -csvPath '.\example_data.csv' -Verbose

Try running the line in your PowerShell window. You should see the example menu from the GIF at the top of this post.

Now open example_data.csv. A text editor is fine but Excel is easier. You can see that every row in the CSV represents an item in the menu.

Step 3: Make it your own

Experiment with editing the CSV and running the Show-ScriptMenuGui command again to see your changes.

A few ideas:

  • Put one of your scripts in the folder and add it to the menu using Method powershell_file and Command .\filename_of_script.ps1.
  • Try including PowerShell commands in the CSV file. Use Method powershell_inline and Command Get-ComputerInfo. Run Show-ScriptMenuGui with -NoExit to stop the PowerShell window from closing.
  • Add a link to an external application. Use Method cmd and enter the path of the program in Command.

Step 4: Next steps

Step 5: Make a shortcut

When you’re happy with your menu, why not make a shortcut to it in File Explorer?

  1. Right click a blank area in a folder window or on your desktop.
  2. Select New → Shortcut
  3. Enter something like this as the location:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File "C:\full\path\PSScriptMenuGui_example\PSScriptMenuGui.ps1"

Add PowerShell-7 preview to Windows Terminal in 30 seconds

Edit: The stuff below doesn’t work any more. But this new stuff works.

Fun times in Windows land over the last few days with the release of new previews of Windows Terminal (v0.4) and PowerShell 7 (Preview 3).

To celebrate, here’s a simple script that you can copy-and-paste into a PowerShell window to add PowerShell 7 to Windows Terminal. (You can even use the Windows Terminal PowerShell terminal. Wow!)

The whole thing might take 30 seconds, which is a significant improvement on my last effort of 2 minutes. Why not take 1 minutes 30 seconds to make yourself a drink?

Pre-requisites:

  • PowerShell 7-preview x64 is installed
  • Windows Terminal v0.4 is installed and has been run

Here’s what it looks like:

Here’s the code:

$terminalFolderPath = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState"
# Get Windows Terminal settings file
$settingsFilePath = Join-Path $terminalFolderPath 'profiles.json'
$json = Get-Content $settingsFilePath | ConvertFrom-Json
# Get profiles
$profiles = $json.profiles
# Make a copy of first profile and configure for PS7 x64
$ps7 = $profiles[0].psobject.Copy()
$ps7.name = 'PowerShell 7-preview (x64)'
$ps7.commandline = 'C:\Program Files\PowerShell\7-preview\pwsh.exe'
$ps7.guid = '{' + (New-Guid).ToString() + '}'
# Download and set icon
$pwsh7IconPath = Join-Path $terminalFolderPath 'pwsh7.ico'
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/weebsnore/Add-PS7ToWindowsTerminal/master/pwsh7.ico' -OutFile $pwsh7IconPath
$ps7.icon = $pwsh7IconPath
# Write updated settings file to disk
$json.profiles = $profiles + $ps7
$json | ConvertTo-Json | Out-File $settingsFilePath

And here it is on GitHub.

Get Task Manager list of Apps with PowerShell

Over the past couple of years I’ve been impressed by a series of small improvements to the Task Manager which have made it pretty great to use.

I recently noticed that you if you right click the column titles in the Processes tab and tick all the boxes, you rarely have to venture to the Details tab. (The most valuable column to add, in my opinion, is Command line.)

The Processes tab also attempts to lump items into a few categories: Apps, Background processes, Windows processes.

How does it do this?

Luckily, Raymond Chen briefly explains what’s going on in a blog post from 2017.

To take Apps as an example: If the process has a visible window, then Task Manager calls it an “App”

Can we do something similar with PowerShell?

Probably. Kinda.

Here’s my attempt:

Get-Process | Where-Object {$_.MainWindowTitle} | Select-Object Description

And the result:

You can see that I get processes that have a MainWindowTitle and display the process Description.

The results are similar but not identical: PowerShell shows some bits of Windows internals that are displayed elsewhere in Task Manager.

Can you get any closer?

Tip: avoid Wait-Debugger gotcha on Azure Functions

My biggest gotcha with Azure Functions is that you need to put Wait-Debugger in your script for local debugging, and it’s easy to forget to remove it when you deploy to Azure.

My last post talked about exploring Azure Functions’ environment, and I mentioned that you could compare the cloud version with its locally-run approximation.

Well, when you’re running your function locally, the variable $env:AZURE_FUNCTIONS_ENVIRONMENT is set to Development.

This means you can ensure that you never leave a function hanging at Wait-Debugger in the cloud by wrapping it like this:

if ($env:AZURE_FUNCTIONS_ENVIRONMENT -eq 'Development') {
    Wait-Debugger
}

Another solution is to wrap your debug command like this:

if ($Request.Query.Debug -eq 'True') {
    Wait-Debugger
}

And then invoke your function with &Debug=True when you want to debug it.

How do you handle this problem?

How to make your free Azure Linux VM actually free

Azure gives you a good amount of stuff free for 12 months when you sign up.

Quite tantalizing among these is a free Linux VM:

But if you make a B1S VM with the Ubuntu Server 18.04 LTS image, you start getting charged a trickle of cash for the disk. I amassed a bill of $1.26 before I noticed – quite shocking!

What’s up?

It turns out the 6 in P6 represents the size of the disk: 64 GB.

And the Ubuntu disk is only 30 GB. So you’re being charged for using too small a disk!

Luckily the fix is simple.

Start by creating your VM as normal:

Once it’s been provisioned, Stop (Deallocate) the VM:

Then open your VM in the Azure portal and navigate to Disks -> click disk name -> Configuration.

Set Size (GiB) to 64 and click Save.

Then start your VM back up. It should now be free of charge – and you’ll have a bit more space to play with.

Explore Azure Function’s PowerShell environment

Want to know more about the environment that your Azure Functions script is running in?

Here’s a little script I put together for this month’s Minneapolis PowerShell User Group:

# d_ExploreAzureEnvironment

# Script to explore the Azure Functions PowerShell environment
# by running commands from a menu and returning their output
# as a string.

using namespace System.Net

param($Request, $TriggerMetadata)

$itemToRun = $Request.Query.Run

$runResult = switch ($itemToRun) {
    0 { Get-ChildItem env: }
    1 { $PSVersionTable }
    2 { Get-Variable }
    3 { Get-Process }
}
if ($runResult) {
    $status = [HttpStatusCode]::OK
    $body   = $runResult | Out-String
}
else {
    $status = [HttpStatusCode]::BadRequest
    $body   = 'Supply a parameter of Run with a value between 0 and 3'
}

Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = $status
    Body       = $body
})

(code is also on GitHub)

As you can see, the script contains a menu of commands that you can select by supplying a parameter of Run with a value between 0 and 3.

The selected command is executed, its output is stored in $runResult, and $runResult is converted to a string, then returned in the HTTP response body.

You can control the whole thing in your web browser like so:

I’ve included a few commands to examine system and PowerShell variables and processes. Adding a command is as simple as this:

You can also use this script to compare the local debug environment provided by Azure Functions Core Tools with Microsoft’s hosted version (a bit more on this soon…)

It would be trivial to extend this concept and make a script that executes arbitrary commands. But it feels like a bad idea, so I restrained myself.

What other commands would you like to try out? How’s your exploration of Azure Functions going? The race to be first commenter continues!

PowerShell Azure Functions to PowerApps “Hello World” (Part 2)

Last time, we deployed a function to Azure.

We now have a PowerShell script sitting in the cloud which we can talk to via a HTTP REST call.

This time, we’ll create a Custom Connector in PowerApps so that we can hook our script into an app.

Part 2: Create a Custom Connector

You should first make sure that you can talk to your function with PowerShell:

  1. Open your Function in the Azure console and click Get function URL.
  2. Leave default (Function key) selected and copy the URL. You should get something like this:
    https://dan-function.azurewebsites.net/api/a_HelloWorld?code=PA1Twlk/anVrchlbKSZSvZcWQCawE5MjY2JcQ3s0/kMYqpnvI2WEMA==
  3. Append &Name=myName to the end and run with Invoke-RestMethod:
Invoke-RestMethod "https://dan-function.azurewebsites.net/api/a_HelloWorld?code=PA1Twlk/anVrchlbKSZSvZcWQCawE5MjY2JcQ3s0/kMYqpnvI2WEMA==&Name=Dan"

Once you’re happy that your function is working, switch over to Power Apps.

Here’s a video demo:

And here are step-by-step instructions:

Start with Custom Connectors → New custom connector → Create from blank

(Create from Azure Service (Preview) doesn’t work with the Azure Functions 2.x runtime – a to-be-expected example of two preview features not working together.)

  1. General screen
    Host is your Azure Functions domain, in my case:
    dan-function.azurewebsites.net
  2. Security
    Authentication type: API key
    Parameter code: code
    Parameter label: code
    Parameter location: Query
  3. Definition
    Action New action
    1. General
      Summary and Operation ID: hello_world
    2. Request
      click Import from sample
      Verb = POST
      URL = an example of a full query, including Name=myName
      Click Import, then delete the block named code, as it is already handled in the Security section.
    3. Response
      Click Add default response, then run a chunk of PowerShell like this to get an example response that you can paste into the Body field:
$response = Invoke-WebRequest "https://dan-function.azurewebsites.net/api/a_HelloWorld?code=PA1Twlk/anVrchlbKSZSvZcWQCawE5MjY2JcQ3s0/kMYqpnvI2WEMA==&Name=Dan"
# Show response in console
$response
# Copy response body to clipboard
$response.Content | clip

Now save your connector and go to the Test tab.

Under Connections, click New connection. Enter your function key when prompted (the long bunch of characters that appear after code= in a request URL).

When your connection has been created, return to the Test screen, and try out your function. You should hopefully see a familiar and reassuring block of JSON – greetings!

That’s it for now. Next time, we complete the package by adding our PowerShell function to a real life actual PowerApp.

Resize browser window with PowerShell

I like to make little videos to go with my blog posts, and today I’ve been looking for a way to resize my browser window to a consistent size for recording.

Google immediately turned up this little beauty in the TechNet Script Center:

But when I tried it out:

Set-Window -ProcessName msedge -Width 1024 -Height 768

…I got a scary error:

Cannot convert argument "hWnd", with value: "System.Object[]", for "GetWindowRect" to type "System.IntPtr": "Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.IntPtr"."
At C:\Git\PowerShell\Set-Window.ps1:91 char:9
+         $Return = [Window]::GetWindowRect($Handle,[ref]$Rectangle)
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

Here are the relevant lines from the function:

$Handle = (Get-Process -Name $ProcessName).MainWindowHandle
$Return = [Window]::GetWindowRect($Handle,[ref]$Rectangle)

A bit of exploring shows that one of the msedge processes is very different from the others:

$msedge = Get-Process msedge

$msedge

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     10     1.77       2.64       0.03     904   1 msedge
     19    17.69      30.85       0.34    4044   1 msedge
     57   128.71     150.07      57.81    5000   1 msedge
     83   185.82     182.69      38.77    8840   1 msedge
     29    47.08      75.16       9.78    9548   1 msedge
     83    82.12     128.55     171.92   11304   1 msedge
     22    27.96      41.45       0.41   11596   1 msedge
     20    21.31      38.14       2.47   12664   1 msedge
     49   303.89     156.82     116.14   12828   1 msedge
     24    29.71      48.62       1.52   14788   1 msedge
     26    21.57      31.14      25.20   15904   1 msedge
     56   132.80     157.91      76.39   15976   1 msedge
     20    19.50      33.82       0.27   16360   1 msedge
     16    11.88      19.46       0.05   16416   1 msedge
     34    59.82      89.91       3.55   17300   1 msedge
     29    48.37      76.49       3.34   17884   1 msedge
     22    25.07      42.07       0.41   18408   1 msedge
     27    51.70      78.73       5.69   18492   1 msedge
     26    39.59      60.88       0.81   18520   1 msedge
     28    54.87      73.89      18.56   19080   1 msedge
     47   108.27     135.33      26.95   19888   1 msedge

$msedge.MainWindowHandle

0
0
0
0
0
4326398
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0

This was my first solution, which works fine:

# Only get non-zero handles
$Handle = (Get-Process -Name $ProcessName).MainWindowHandle | Where-Object {$_.ToInt32() -gt 0}
# The Handles have type IntPtr. Without .ToInt32() you get this error:
# Cannot compare "0" because it is not IComparable

But after a bit of poking around, I think this is a little better:

# Only get process whose parent is explorer
$Handle = (Get-Process -Name $ProcessName | Where-Object {$_.Parent.ProcessName -eq 'explorer'}).MainWindowHandle