Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: [StartAutomating]
491 changes: 491 additions & 0 deletions .github/workflows/BuildServers101.yml

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions Build/GitHub/Steps/PublishTestResults.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@{
name = 'PublishTestResults'
uses = 'actions/upload-artifact@main'
with = @{
name = 'PesterResults'
path = '**.TestResults.xml'
}
if = '${{always()}}'
}

12 changes: 12 additions & 0 deletions Build/Servers101.GitHubWorkflow.PSDevOps.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#requires -Module PSDevOps
Import-BuildStep -SourcePath (
Join-Path $PSScriptRoot 'GitHub'
) -BuildSystem GitHubWorkflow

Push-Location ($PSScriptRoot | Split-Path)

New-GitHubWorkflow -Name "Build Module" -On Push,
PullRequest,
Demand -Job TestPowerShellOnLinux, TagReleaseAndPublish -OutputPath ./.github/workflows/BuildServers101.yml

Pop-Location
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Servers101 0.1:

* Initial Release of Servers101
* An educational module with small servers in PowerShell (#1)
* Built with a basic build (#2)
* Including a single command: `Get-Servers101` (#3)
* Initial demo servers:
* `Server101` (#4)
* `DebugServer` (#5)
* `EventServer` (#6)
* `DualEventServer` (#7)

48 changes: 48 additions & 0 deletions Commands/Get-Servers101.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
function Get-Servers101
{
<#
.SYNOPSIS
Servers101
.DESCRIPTION
Gets the list of example servers included in Servers101.

Each server is a self-contained PowerShell script.
.EXAMPLE
Get-Servers101
.EXAMPLE
Servers101
#>
[Alias('Servers101')]
param(
# The name of the server. If no name is provided, all servers will be returned.
[SupportsWildcards()]
[string]
$Name
)

begin {
$myModuleRoot = $PSScriptRoot | Split-Path
Update-TypeData -TypeName Servers101 -DefaultDisplayPropertySet Name,
Synopsis, Description -Force
}
process {
Get-ChildItem -File -Path $myModuleRoot -Recurse |
Where-Object {
$_.Name -match 'server?[^\.]{0,}\.ps1$' -and
$_.Name -notmatch '-Server' -and (
(-not $Name) -or ($_.Name -like "$name*")
)
} |
ForEach-Object {
$file = $_
$help = Get-Help -Name $file.FullName -ErrorAction Ignore
$file.pstypenames.clear()
$file.pstypenames.insert(0,'Servers101')
$file |
Add-Member NoteProperty Synopsis $help.Synopsis -Force -PassThru |
Add-Member NoteProperty Description (
$help.Description.text -join [Environment]::NewLine
) -Force -PassThru
}
}
}
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,40 @@
# Servers101
Simple Servers in PowerShell
# Servers 101

Servers are pretty simple.

You listen and then you reply.

That's Servers 101.

## Simple Servers

Frameworks often abstract this away.

This can make the basics of servers harder to learn.

To avoid reliance on the framework flavor of the week, it's good to learn how to build a simple server.

This is a collection of simple servers in PowerShell.

Feel free to [contribute](contributing.md) and add your own.


## Server Samples

* [DebugServer.ps1](/Servers/DebugServer.ps1)
* [DualEventServer.ps1](/Servers/DualEventServer.ps1)
* [EventServer.ps1](/Servers/EventServer.ps1)
* [Server101.ps1](/Servers/Server101.ps1)
* [SwitchServer.ps1](/Servers/SwitchServer.ps1)

## Using this module

This module has only one command, Get-Servers101.

It will return all of the sample servers in the module.

Each server will be self-contained in a single script.

To start the server, simply run the script.

To learn about how each server works, read thru each script.
47 changes: 47 additions & 0 deletions README.md.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@"
# Servers 101

Servers are pretty simple.

You listen and then you reply.

That's Servers 101.

## Simple Servers

Frameworks often abstract this away.

This can make the basics of servers harder to learn.

To avoid reliance on the framework flavor of the week, it's good to learn how to build a simple server.

This is a collection of simple servers in PowerShell.

Feel free to [contribute](contributing.md) and add your own.


## Server Samples

"@


foreach ($serverScript in Get-Servers101) {
"* [$($serverScript.Name)]($($serverScript.FullName.Substring("$pwd".Length)))"
}



@"

## Using this module

This module has only one command, `Get-Servers101`.

It will return all of the sample servers in the module.

Each server will be self-contained in a single script.

To start the server, simply run the script.

To learn about how each server works, read thru each script.
"@
77 changes: 77 additions & 0 deletions Servers/DebugServer.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<#
.SYNOPSIS
A debug server.
.DESCRIPTION
A server that runs on the current thread, so you can debug it.

You can run this with -AsJob, but then you cannot debug in PowerShell.
.NOTES
A few notes:

1. This will effectively lock the current thread (CTRL+C works).
2. Because of the way requests are processed, you may need to refresh to hit the breakpoint.
3. Be aware that browsers will request a `favicon.ico` first.
#>
param(
# The rootUrl of the server. By default, a random loopback address.
[string]$RootUrl=
"http://127.0.0.1:$(Get-Random -Minimum 4200 -Maximum 42000)/",

# If set, will run in a background job.
[switch]
$AsJob
)

$httpListener = [Net.HttpListener]::new()
$httpListener.Prefixes.add($RootUrl)
$httpListener.Start()
Write-Warning "Listening on $rootUrl"

$listenScript = {
param([Net.HttpListener]$httpListener)
# Listen for the next request
:nextRequest while ($httpListener.IsListening) {
$getContext = $httpListener.GetContextAsync()

while (-not $getContext.Wait(17)) { }

$context = $getContext.Result
$requestTime = [DateTime]::Now
$request, $reply = $context.Request, $context.Response
$debugObject = $request |
Select-Object HttpMethod, Url, Is* |
Add-Member NoteProperty Headers ([Ordered]@{}) -Force -passThru |
Add-Member NoteProperty Query ([Ordered]@{}) -Force -passThru

foreach ($headerName in $request.Headers) {
$debugObject.headers[$headerName] = $request.Headers[$headerName]
}
if ($request.Url.Query) {

foreach ($chunk in $request.Url.Query -split '&') {
$parsedQuery =
[Web.HttpUtility]::ParseQueryString($chunk)
$key = @($parsedQuery.Keys)[0]
if ($debugObject.Query[$key]) {
$debugObject.Query[$key] = @(
$debugObject.Query[$key]
) + $parsedQuery[$key]
} else {
$debugObject.Query[$key] = $parsedQuery[$key]
}
}
}
$reply.ContentType = 'application/json'
$reply.Close(
$OutputEncoding.GetBytes(
($debugObject | ConvertTo-Json -Depth 5)
), $false)
"Responded to $($Request.Url) in $([DateTime]::Now - $requestTime)"
}
}

if ($AsJob) {
Start-ThreadJob -ScriptBlock $listenerScript -ArgumentList $httpListener
} else {
. $listenScript $httpListener
}
75 changes: 75 additions & 0 deletions Servers/DualEventServer.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<#
.SYNOPSIS
Event Server
.DESCRIPTION
A simple event driven server.

Each request will generate an event, which will be responded to by a handler.
#>
param(
# The rootUrl of the server. By default, a random loopback address.
[string]$RootUrl=
"http://127.0.0.1:$(Get-Random -Minimum 4200 -Maximum 42000)/"
)

$httpListener = [Net.HttpListener]::new()
$httpListener.Prefixes.Add($RootUrl)
Write-Warning "Listening on $RootUrl $($httpListener.Start())"

$io = [Ordered]@{ # Pack our job input into an IO dictionary
HttpListener = $httpListener ; ServerRoot = $RootDirectory
MainRunspace = [Runspace]::DefaultRunspace; SourceIdentifier = $RootUrl
TypeMap = $TypeMap
}

# Our server is a thread job
Start-ThreadJob -ScriptBlock {param([Collections.IDictionary]$io)
$psvariable = $ExecutionContext.SessionState.PSVariable
foreach ($key in $io.Keys) { # First, let's unpack.
if ($io[$key] -is [PSVariable]) { $psvariable.set($io[$key]) }
else { $psvariable.set($key, $io[$key]) }
}

$thisRunspace = [Runspace]::DefaultRunspace

# Because we are handling the event locally, the main thread can keep chugging.
Register-EngineEvent -SourceIdentifier $SourceIdentifier -Action {
try {
$request = $event.MessageData.Request
$reply = $event.MessageData.Reply

$timeToRespond = [DateTime]::Now - $event.TimeGenerated
$myReply = "$($request.HttpMethod) $($request.Url) $($timeToRespond)"
$reply.Close($OutputEncoding.GetBytes($myReply), $false)
} catch {
Write-Error $_
}
}

# Listen for the next request
:nextRequest while ($httpListener.IsListening) {
$getContext = $httpListener.GetContextAsync()
while (-not $getContext.Wait(17)) { }
$request, $reply =
$getContext.Result.Request, $getContext.Result.Response

# Generate events for every request
foreach ($runspace in $thisRunspace, $mainRunspace) {
# by broadcasting to multiple runspaces, we can both reply and have a record.
$runspace.Events.GenerateEvent(
$SourceIdentifier, $httpListener, @(
$getContext.Result, $request, $reply
), [Ordered]@{
Method = $Request.HttpMethod; Url = $request.Url
Request = $request; Reply = $reply; Response = $reply
ServerRoot = $ServerRoot; TypeMap = $TypeMap
}
)
}
}
} -ThrottleLimit 100 -ArgumentList $IO -Name "$RootUrl" | # Output our job,
Add-Member -NotePropertyMembers @{ # but attach a few properties first:
HttpListener=$httpListener # * The listener (so we can stop it)
IO=$IO # * The IO (so we can change it)
Url="$RootUrl" # The URL (so we can easily access it).
} -Force -PassThru # Pass all of that thru and return it to you.
Loading
Loading