Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save aeveltstra/94806a1230b8165f43e9b4e4dec9bacc to your computer and use it in GitHub Desktop.
Save aeveltstra/94806a1230b8165f43e9b4e4dec9bacc to your computer and use it in GitHub Desktop.
A Powershell GUI using DotNet Windows.Forms to start AWS Lambda Functions and display their output
<#
# To start known AWS Lambda Functions.
#
# You MUST have the AWS CLI installed. Run this command to install:
# PS$> Install-Module -Name AWS.Tools.Lambda
#
# You MUST have it configured with an account that has access
# to AWS Lambda Functions.
#
# @author A.E.Veltstra
# @since 2.24.523.1422
# @version 2.24.614.1946
#>
# "Using" works like an import or require command. It makes it
# so Powershell 3 and up know we need additional functionality.
# That would be the Microsoft Windows Forms and Drawing API. The
# statements don't appear needed in version 5 of Powershell when
# running this script from the Powershell ISE, but are needed
# when running it from a command line or desktop shortcut, if
# the script doesn't use the Add-Type statement later.
using assembly System.Windows.Forms;
using assembly System.Drawing;
using namespace System.Windows.Forms;
# Add-Type works like an import or require command. It makes it
# so Powershell 3 and up know we need additional functionality.
# That would be the Microsoft Windows Forms and Drawing API. The
# statements don't appear needed in version 5 of Powershell when
# running this script from the Powershell ISE, and it also isn't
# needed when running it from a command line or desktop shortcut,
# if you start the script with the using statements.
# Add-Type -AssemblyName System.Windows.Forms;
# Add-Type -AssemblyName System.Drawing;
# Enable visual styles. Makes a difference on my Windows 10 Pro,
# if running a high-contrast visual theme. This helps accessibility.
[System.Windows.Forms.Application]::EnableVisualStyles();
#################################################################
# Let's create a window. Microsoft calls a window, a form.
#################################################################
$win1 = [Windows.Forms.Form]::new();
# Let's figure out what font the user wants.
# This one changes sizes when the user does in Windows.
$win1.Font = [System.Drawing.SystemFonts]::IconTitleFont;
# Set the window title.
$win1.Text = "Start AWS Lambda Functions";
# Choose an icon for the top left, in the title bar.
$win1.Icon = [System.Drawing.SystemIcons]::Application;
# Give the window a size.
$win1.ClientSize = [System.Drawing.Size]::new(720,480);
# We cannot allow it to size itself automatically, because doing that
# makes it impossible to make it smaller after making it bigger, if
# controls get anchored (which they will, later in this script).
$win1.AutoSize = $false;
$win1.AutoSizeMode = [System.Windows.Forms.AutoSizeMode]::GrowAndShrink;
$win1.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::FONT;
# Determine a default padding. This will be the distance between
# the window border and the things shown on the window, but also
# between those things.
$padding = [int]15;
$default_txt_width = [int]($win1.ClientSize.width - (2 * $padding));
$default_multiline_text_height = [int]256;
$win1.Margin = 0;
$win1.Padding = 0;
# Let's make it a little translucent.
$win1.Opacity = 0.9;
# Add the ability to show tool tips on form controls:
$tooltip = [System.Windows.Forms.Tooltip]::new();
#################################################################
# Let's add a container to hold the tabs we'll add later.
#################################################################
$tc1 = [System.Windows.Forms.TabControl]::new();
# Let's position it on the window.
$tc1.Left = 0;
$tc1.Top = 0;
$tc1.Margin = 0;
# We don't want this docked: we set the position explicitly.
$tc1.Dock = [System.Windows.Forms.DockStyle]::None;
#################################################################
# Let's add a tab that allows for choosing and starting a function.
#################################################################
$tp_start = [System.Windows.Forms.TabPage]::new();
# Prefixing the L with an & won't turn it into a shortcut key, to
# be used by pressing ALT+L. Instead it will be displayed as &L,
# and Forms won't create the key event listener.
$tp_start.Text = "List";
# Setting this appears to have no impact.
$tp_start.Padding = $padding;
# Let's make a button to load the functions;
$btn_load_functions = [System.Windows.Forms.Button]::new();
# Placing the & turns the L into a shortcut key, that can be used
# by pressing ALT+L. It won't show, visually, until the ALT key is
# pressed. Forms automatically creates the event listener.
$btn_load_functions.Text = "&Load Functions";
# And add it to the tab page so it becomes visible.
$tp_start.Controls.Add($btn_load_functions);
$btn_load_functions.Top = $padding;
$btn_load_functions.Left = $padding;
$btn_load_functions.AutoSize = $true;
$btn_load_functions.AccessibleDescription = `
"Fetches Lambda functions in your default account";
$tooltip.SetToolTip(
$btn_load_functions,
$btn_load_functions.AccessibleDescription
);
# Add a progress bar. It needs to show that the functions
# are being loaded.
$progress_bar = [System.Windows.Forms.ProgressBar]::new();
$progress_bar.Top = $padding;
$progress_bar.Left = $padding;
$progress_bar.Width = $win1.ClientSize.Width - (2 * $padding);
# We don't set a style just yet, as we need to set the Marquee
# style. Once we do that, the progress bar will start animating.
# We want to delay that until needed.
$progress_bar.Hide();
$tp_start.Controls.Add($progress_bar);
$tooltip.SetToolTip($progress_bar, "Loading...");
$progress_bar.AccessibleDescription = `
"Shows that the program continues to run";
# Let's add a panel for a function chooser and input parameters.
# It will be invisible until the AWS Lambda Functions get loaded.
$panel_functions = [System.Windows.Forms.Panel]::new();
$panel_functions.Top = $btn_load_functions.ClientSize.Bottom;
$panel_functions.Left = 0;
$panel_functions.Padding = 0;
$panel_functions.Margin = 0;
$panel_functions.Hide();
$tp_start.Controls.Add($panel_functions);
# Let's add a label to explain what the next addition does
$lbl_loaded_functions = [System.Windows.Forms.Label]::new();
# Placing the & turns the c into a shortcut key, that can be used
# by pressing ALT+C. It won't show, visually, until the ALT key is
# pressed. Forms automatically creates the event listener. Labels
# don't activate themselves - they activate the next control.
$lbl_loaded_functions.Text = "Fun&ctions for your AWS account:";
$lbl_loaded_functions.Left = $padding;
$lbl_loaded_functions.Top = $btn_load_functions.Bottom + $padding;
$lbl_loaded_functions.Width = $default_txt_width;
$lbl_loaded_functions.AutoSize = $true;
$panel_functions.Controls.Add($lbl_loaded_functions);
# Let's add a combobox with choices to tab page 1.
$pick_loaded_functions = [System.Windows.Forms.ComboBox]::new();
$pick_loaded_functions.Items.Add("Load functions first");
$pick_loaded_functions.Left = $padding;
$pick_loaded_functions.Top = $lbl_loaded_functions.Bottom + $padding;
$pick_loaded_functions.Width = $default_txt_width;
$panel_functions.Controls.Add($pick_loaded_functions);
# Let's add a label to explain what the next addition does
$lbl_params_input = [System.Windows.Forms.Label]::new();
# Placing the & turns the P into a shortcut key, that can be used
# by pressing ALT+P. It won't show, visually, until the ALT key is
# pressed. Forms automatically creates the event listener. Labels
# don't activate themselves - they activate the next control.
$lbl_params_input.Text = "Input &Parameters:";
$lbl_params_input.Left = $padding;
$lbl_params_input.Top = $pick_loaded_functions.Bottom + $padding;
$lbl_params_input.Width = $default_txt_width;
$panel_functions.Controls.Add($lbl_params_input);
# We want an input textbox to send parameters to the function
$txt_params_input = [System.Windows.Forms.TextBox]::new();
$txt_params_input.Height = $default_multiline_text_height;
$txt_params_input.Top = $lbl_params_input.Bottom;
$txt_params_input.Left = $padding;
$txt_params_input.Width = $default_txt_width;
$txt_params_input.AccessibleDescription = "Input Parameters";
$txt_params_input.Multiline = $true;
#$txt_params_input.AcceptsReturn = $true;
$txt_params_input.AcceptsTab = $true;
$txt_params_input.ScrollBars = [System.Windows.Forms.ScrollBars]::Vertical;
$txt_params_input.Text = '{"Records": [
{"s3":{
"bucket":{
"name": "plmapp"
},
"object":{
"key": "that.csv"
}
}}
]}';
$txt_params_input.SelectedText = $null;
$panel_functions.Controls.Add($txt_params_input);
# Let's make a button to start the functions;
$btn_start_function = [System.Windows.Forms.Button]::new();
$btn_start_function.Text = "Sta&rt";
$btn_start_function.AutoSize = $true;
# And add it to the tab page so it becomes visible.
$panel_functions.Controls.Add($btn_start_function);
$btn_start_function.AccessibleDescription = `
"Run the function and fetch its results.";
$tooltip.SetToolTip(
$btn_start_function,
$btn_start_function.AccessibleDescription
);
# We position the button under the input box.
$btn_start_function.Top = $txt_params_input.Bounds.Bottom + $padding;
$btn_start_function.Left = $padding;
# We need the entire panel to be invisible until the time comes to
# allow use, which happens after the AWS Lambda Functions get loaded.
$panel_functions.Visible = $false;
# Let's give the panel a size.
$panel_functions.MinimumSize = New-Object System.Drawing.Size(
($win1.ClientSize.Width),
($padding + $btn_start_function.Bounds.Bottom)
);
# Let's calculate how high the tab page should be.
$tp_start_height = (3 * $padding) `
+ $btn_start_function.Bounds.Bottom;
$tp_start_width = $win1.ClientSize.Width;
# Set the minimum size of the tab page.
# Note: it is imperative to use a new Size object, otherwise
# the control refuses to accept any width and height settings.
$tp_start.MinimumSize = New-Object System.Drawing.Size(
($tp_start_width),
($tp_start_height)
);
#################################################################
# Let's add a tab page to display the results
#################################################################
$tp_results = [System.Windows.Forms.TabPage]::new();
$tp_results.Text = "Results";
$tp_results.Padding = $padding;
# Let's add a label to explain what the next addition does
$lbl_results_statusCode = [System.Windows.Forms.Label]::new();
$lbl_results_statusCode.Text = "Execution Status:";
$tp_results.Controls.Add($lbl_results_statusCode);
$lbl_results_statusCode.Left = $padding;
$lbl_results_statusCode.Top = $padding;
$lbl_results_statusCode.Width = $default_txt_width;
# We want a results statusCode textbox
$txt_results_statusCode = [System.Windows.Forms.TextBox]::new();
$txt_results_statusCode.Height = $default_multiline_text_height;
$txt_results_statusCode.Top = $lbl_results_statusCode.Bounds.Bottom;
$txt_results_statusCode.Left = $padding;
$txt_results_statusCode.Width = $default_txt_width;
$txt_results_statusCode.Multiline = $false;
$txt_results_statusCode.AcceptsReturn = $false;
$txt_results_statusCode.AcceptsTab = $false;
$txt_results_statusCode.ScrollBars = [System.Windows.Forms.ScrollBars]::None;
$txt_results_statusCode.Text = "";
$txt_results_statusCode.SelectedText = "";
$tp_results.Controls.Add($txt_results_statusCode);
# Let's add a label to explain what the next addition does
$lbl_results_body = [System.Windows.Forms.Label]::new();
$lbl_results_body.Text = "Function Output:";
$tp_results.Controls.Add($lbl_results_body);
$lbl_results_body.Left = $padding;
$lbl_results_body.Top = $txt_results_statusCode.Bounds.Bottom + $padding;
$lbl_results_body.Width = $default_txt_width;
# We also want a results body textbox
$txt_results_body = [System.Windows.Forms.TextBox]::new();
$txt_results_body.Height = $default_multiline_text_height;
$txt_results_body.Top = $lbl_results_body.Bounds.Bottom;
$txt_results_body.Left = $padding;
$txt_results_body.Width = $default_txt_width;
$txt_results_body.Multiline = $true;
$txt_results_body.AcceptsReturn = $true;
$txt_results_body.AcceptsTab = $true;
$txt_results_body.ScrollBars = [System.Windows.Forms.ScrollBars]::Vertical;
$txt_results_body.Text = "";
$txt_results_body.SelectedText = "";
$tp_results.Controls.Add($txt_results_body);
# And place the container on the window.
$win1.Controls.add($tc1);
# Let's calculate how high and wide the results tab page should be.
$tp_results_height = (2 * $padding) `
+ $txt_results_body.Bounds.Bottom;
$tp_results_width = (2 * $padding) + $txt_results_body.ClientSize.Width;
$tp_results.MinimumSize = New-Object System.Drawing.Size(
($tp_results_width),
($tp_results_height)
);
# Recalculate sizes. We capture it in variables to display later.
$tc1_new_width = [int][Math]::max(
$tp_start.ClientSize.Width,
$tp_results.ClientSize.Width
);
$tc1_new_height = [int][Math]::max(
$tp_start.ClientSize.Height,
$tp_results.ClientSize.Height
);
# Note: it is imperative to use a new Size object, otherwise
# the control refuses to accept any width and height settings.
$tc1.MinimumSize = New-Object System.Drawing.Size(
($tc1_new_width),
($tc1_new_height)
);
#################################################################
# And place the tabs into the container.
#################################################################
$tc1.TabPages.Clear();
$tc1.TabPages.AddRange(@($tp_start, $tp_results));
# We want to allow the tabs to be shown in multiple rows,
# if the window width requires it.
$tc1.Multiline = $true;
#################################################################
# Recalculate the size of the window.
#################################################################
$win1_height = $tc1.ClientSize.Height;
$win1_width = $tc1.ClientSize.Width;
# Note: it is imperative to use a new Size object, otherwise
# the control refuses to accept any width and height settings.
$win1.MinimumSize = New-Object System.Drawing.Size( `
$win1_width, `
$win1_height `
);
$win1.ClientSize = New-Object System.Drawing.Size( `
$win1_width, `
$win1_height `
);
# And then we want to make sure that the controls resize with the window:
$anchor_all = @(
$tc1,
$tp_start,
$panel_functions,
$tp_results,
$txt_params_input
);
$anchor_all | % {
$_.Anchor = [System.Windows.Forms.AnchorStyles]::Top `
-bor [System.Windows.Forms.AnchorStyles]::Right `
-bor [System.Windows.Forms.AnchorStyles]::Bottom `
-bor [System.Windows.Forms.AnchorStyles]::Left;
}
$anchor_trl = @(
$btn_load_functions,
$progress_bar,
$lbl_loaded_functions,
$pick_loaded_functions,
$btn_start_function,
$lbl_results_statusCode,
$txt_results_statusCode,
$lbl_results_body,
$txt_results_body
);
$anchor_trl | % {
$_.Anchor = [System.Windows.Forms.AnchorStyles]::Top `
-bor [System.Windows.Forms.AnchorStyles]::Right `
-bor [System.Windows.Forms.AnchorStyles]::Left;
}
$anchor_tl = @(
$btn_load_functions
);
$anchor_tl | % {
$_.Anchor = [System.Windows.Forms.AnchorStyles]::Top `
-bor [System.Windows.Forms.AnchorStyles]::Left;
}
$anchor_bl = @(
$btn_start_function
);
$anchor_bl | % {
$_.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom `
-bor [System.Windows.Forms.AnchorStyles]::Left;
}
#################################################################
# Add the functionality to the buttons
#################################################################
$btn_load_functions.Add_Click({
<#
# Reaches out to AWS to fetch the Lambda Function ARNs,
# and lists them in the picker.
#>
try {
$btn_start_function.Hide();
$panel_functions.Hide();
$btn_load_functions.Hide();
# We don't actually know when the load function will complete.
# So we cannot use a traditional progress bar, but have to use
# a marquee, which just moves from left to right and back.
$progress_bar.Style = [System.Windows.Forms.ProgressBarStyle]::Marquee;
$progress_bar.Show();
$pick_loaded_functions.Items.Clear();
# We need this to ensure GUI responsiveness.
[System.Windows.Forms.Application]::DoEvents();
if ((Test-Connection www.google.com -Quiet) -ne $true) {
throw "Error: no internet connection.";
}
[System.Windows.Forms.Application]::DoEvents();
$job = Start-Job -ScriptBlock {
$_xs = @((Get-LMFunctionList -Select Functions).FunctionName);
$_xs | Sort-Object;
};
# Cannot use Wait-Job $job, because that blocks the GUI.
while ($job.State -ne "Completed") {
# We need this to ensure GUI responsiveness.
[System.Windows.Forms.Application]::DoEvents();
Start-Sleep -Milliseconds 200;
}
$xs = Receive-Job $job;
$pick_loaded_functions.Items.Clear();
$xs | % {
$pick_loaded_functions.Items.Add($_);
# We need this to ensure GUI responsiveness.
[System.Windows.Forms.Application]::DoEvents();
};
$progress_bar.Hide();
# We set the style to continuous to make it stop animating.
$progress_bar.Style = [System.Windows.Forms.ProgressBarStyle]::Continuous;
$panel_functions.Show();
$btn_load_functions.Show();
} catch {
$txt_params_input.Text = $_;
$progress_bar.Hide();
# We set the style to continuous to make it stop animating.
$progress_bar.Style = [System.Windows.Forms.ProgressBarStyle]::Continuous;
$btn_load_functions.Show();
$panel_functions.Show();
}
});
$pick_loaded_functions.Add_SelectedIndexChanged({
$btn_start_function.Show();
});
$btn_start_function.Add_Click({
<#
# Switches the focus to the results tab and shows the results of
# the started AWS Lambda Function.
#>
$tc1.SelectedIndex = $tp_results.TabIndex;
$txt_results_statusCode.Text = "Running...";
$txt_results_body.Text = "";
# We need this to ensure the GUI will update before starting the
# lambda function.
[System.Windows.Forms.Application]::DoEvents();
$globals = @(
$pick_loaded_functions.SelectedItem,
$txt_params_input.Text
);
try {
$job = Start-Job -InputObject $globals -ScriptBlock {
# The script block runs in its own memory scope:
# it cannot reach the form we just set up. Instead,
# we give it the information it needs as text,
# via the -InputObject argument.
# The $input variable is generated automatically
# and will hold a pipeline of the parameter passed
# in as the -InputObject argument.
# We read the $input pipeline to an array.
$_argv = $input | %{$_};
$_function_name = 0;
$_function_parameters = 1;
$_result = (Invoke-LMFunction `
-FunctionName $_argv[$_function_name] `
-InvocationType RequestResponse `
-Payload $_argv[$_function_parameters]);
# AWS sends the payload in a System.IO.MemoryStream.
# That is great for huge payloads if we want to show
# small amounts at a time. We choose to read all and
# show it all at once.
$_rdr = [System.IO.StreamReader]::new($_result.Payload);
$_payload = $_rdr.ReadToEnd();
$_out = [pscustomobject]@{
StatusCode = $_result.StatusCode
Payload = $_payload
};
$_out;
};
# Cannot use Wait-Job $job, because that blocks the GUI.
while ($job.State -ne "Completed") {
$txt_results_statusCode.Text += ".";
[System.Windows.Forms.Application]::DoEvents();
Start-Sleep -Milliseconds 900;
}
$result = Receive-Job $job;
$result.StatusCode | % { $txt_results_statusCode.Text = $_ };
$result.Payload | % { $txt_results_body.Text = $_ };
} catch {
$txt_results_statusCode.Text = 500;
$txt_results_body.Text = $_;
}
});
#################################################################
# Show the window
#################################################################
# If the application starts single-threaded, Windows Forms refuses
# to show a window using [Application]::run($win1). Instead, we
# must use Form::ShowDialog.
[void]$win1.ShowDialog();
@aeveltstra
Copy link
Author

This showcase provides an example of what is possible for DevOps engineers who may not have a full-fledged MS Visual Studio IDE at their disposal. It is well commented in the hopes of making it easy to follow and modify.

@aeveltstra
Copy link
Author

The import statements at the top (Add-Type) appear not needed when running from the ISE, but they are needed when running from a command line: without them, Powershell doesn't understand the Windows Forms.

@aeveltstra
Copy link
Author

Now added the ability for the font to adapt to the user's font theme and size changes in Windows. It's silly, really: Windows.Forms should do that by itself. But it doesn't, so every programmer has to be aware and make it happen.

@aeveltstra
Copy link
Author

The script is updated to run long-running tasks without freezing the GUI, showing that the application still runs.

@aeveltstra
Copy link
Author

Added a network connection check, so the program won't be trying to read functions from AWS if the computer isn't connected.

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