Skip to content

Commit c30e50d

Browse files
Merge pull request #8 from PowerShellWeb/servers101
Servers101 0.1
2 parents c0300ec + f9941e1 commit c30e50d

18 files changed

+1193
-2
lines changed

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: [StartAutomating]

.github/workflows/BuildServers101.yml

Lines changed: 491 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
@{
2+
name = 'PublishTestResults'
3+
uses = 'actions/upload-artifact@main'
4+
with = @{
5+
name = 'PesterResults'
6+
path = '**.TestResults.xml'
7+
}
8+
if = '${{always()}}'
9+
}
10+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#requires -Module PSDevOps
2+
Import-BuildStep -SourcePath (
3+
Join-Path $PSScriptRoot 'GitHub'
4+
) -BuildSystem GitHubWorkflow
5+
6+
Push-Location ($PSScriptRoot | Split-Path)
7+
8+
New-GitHubWorkflow -Name "Build Module" -On Push,
9+
PullRequest,
10+
Demand -Job TestPowerShellOnLinux, TagReleaseAndPublish -OutputPath ./.github/workflows/BuildServers101.yml
11+
12+
Pop-Location

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## Servers101 0.1:
2+
3+
* Initial Release of Servers101
4+
* An educational module with small servers in PowerShell (#1)
5+
* Built with a basic build (#2)
6+
* Including a single command: `Get-Servers101` (#3)
7+
* Initial demo servers:
8+
* `Server101` (#4)
9+
* `DebugServer` (#5)
10+
* `EventServer` (#6)
11+
* `DualEventServer` (#7)
12+

Commands/Get-Servers101.ps1

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
function Get-Servers101
2+
{
3+
<#
4+
.SYNOPSIS
5+
Servers101
6+
.DESCRIPTION
7+
Gets the list of example servers included in Servers101.
8+
9+
Each server is a self-contained PowerShell script.
10+
.EXAMPLE
11+
Get-Servers101
12+
.EXAMPLE
13+
Servers101
14+
#>
15+
[Alias('Servers101')]
16+
param(
17+
# The name of the server. If no name is provided, all servers will be returned.
18+
[SupportsWildcards()]
19+
[string]
20+
$Name
21+
)
22+
23+
begin {
24+
$myModuleRoot = $PSScriptRoot | Split-Path
25+
Update-TypeData -TypeName Servers101 -DefaultDisplayPropertySet Name,
26+
Synopsis, Description -Force
27+
}
28+
process {
29+
Get-ChildItem -File -Path $myModuleRoot -Recurse |
30+
Where-Object {
31+
$_.Name -match 'server?[^\.]{0,}\.ps1$' -and
32+
$_.Name -notmatch '-Server' -and (
33+
(-not $Name) -or ($_.Name -like "$name*")
34+
)
35+
} |
36+
ForEach-Object {
37+
$file = $_
38+
$help = Get-Help -Name $file.FullName -ErrorAction Ignore
39+
$file.pstypenames.clear()
40+
$file.pstypenames.insert(0,'Servers101')
41+
$file |
42+
Add-Member NoteProperty Synopsis $help.Synopsis -Force -PassThru |
43+
Add-Member NoteProperty Description (
44+
$help.Description.text -join [Environment]::NewLine
45+
) -Force -PassThru
46+
}
47+
}
48+
}

README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,40 @@
1-
# Servers101
2-
Simple Servers in PowerShell
1+
# Servers 101
2+
3+
Servers are pretty simple.
4+
5+
You listen and then you reply.
6+
7+
That's Servers 101.
8+
9+
## Simple Servers
10+
11+
Frameworks often abstract this away.
12+
13+
This can make the basics of servers harder to learn.
14+
15+
To avoid reliance on the framework flavor of the week, it's good to learn how to build a simple server.
16+
17+
This is a collection of simple servers in PowerShell.
18+
19+
Feel free to [contribute](contributing.md) and add your own.
20+
21+
22+
## Server Samples
23+
24+
* [DebugServer.ps1](/Servers/DebugServer.ps1)
25+
* [DualEventServer.ps1](/Servers/DualEventServer.ps1)
26+
* [EventServer.ps1](/Servers/EventServer.ps1)
27+
* [Server101.ps1](/Servers/Server101.ps1)
28+
* [SwitchServer.ps1](/Servers/SwitchServer.ps1)
29+
30+
## Using this module
31+
32+
This module has only one command, Get-Servers101.
33+
34+
It will return all of the sample servers in the module.
35+
36+
Each server will be self-contained in a single script.
37+
38+
To start the server, simply run the script.
39+
40+
To learn about how each server works, read thru each script.

README.md.ps1

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
@"
2+
# Servers 101
3+
4+
Servers are pretty simple.
5+
6+
You listen and then you reply.
7+
8+
That's Servers 101.
9+
10+
## Simple Servers
11+
12+
Frameworks often abstract this away.
13+
14+
This can make the basics of servers harder to learn.
15+
16+
To avoid reliance on the framework flavor of the week, it's good to learn how to build a simple server.
17+
18+
This is a collection of simple servers in PowerShell.
19+
20+
Feel free to [contribute](contributing.md) and add your own.
21+
22+
23+
## Server Samples
24+
25+
"@
26+
27+
28+
foreach ($serverScript in Get-Servers101) {
29+
"* [$($serverScript.Name)]($($serverScript.FullName.Substring("$pwd".Length)))"
30+
}
31+
32+
33+
34+
@"
35+
36+
## Using this module
37+
38+
This module has only one command, `Get-Servers101`.
39+
40+
It will return all of the sample servers in the module.
41+
42+
Each server will be self-contained in a single script.
43+
44+
To start the server, simply run the script.
45+
46+
To learn about how each server works, read thru each script.
47+
"@

Servers/DebugServer.ps1

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<#
2+
.SYNOPSIS
3+
A debug server.
4+
.DESCRIPTION
5+
A server that runs on the current thread, so you can debug it.
6+
7+
You can run this with -AsJob, but then you cannot debug in PowerShell.
8+
.NOTES
9+
A few notes:
10+
11+
1. This will effectively lock the current thread (CTRL+C works).
12+
2. Because of the way requests are processed, you may need to refresh to hit the breakpoint.
13+
3. Be aware that browsers will request a `favicon.ico` first.
14+
#>
15+
param(
16+
# The rootUrl of the server. By default, a random loopback address.
17+
[string]$RootUrl=
18+
"http://127.0.0.1:$(Get-Random -Minimum 4200 -Maximum 42000)/",
19+
20+
# If set, will run in a background job.
21+
[switch]
22+
$AsJob
23+
)
24+
25+
$httpListener = [Net.HttpListener]::new()
26+
$httpListener.Prefixes.add($RootUrl)
27+
$httpListener.Start()
28+
Write-Warning "Listening on $rootUrl"
29+
30+
$listenScript = {
31+
param([Net.HttpListener]$httpListener)
32+
# Listen for the next request
33+
:nextRequest while ($httpListener.IsListening) {
34+
$getContext = $httpListener.GetContextAsync()
35+
36+
while (-not $getContext.Wait(17)) { }
37+
38+
$context = $getContext.Result
39+
$requestTime = [DateTime]::Now
40+
$request, $reply = $context.Request, $context.Response
41+
$debugObject = $request |
42+
Select-Object HttpMethod, Url, Is* |
43+
Add-Member NoteProperty Headers ([Ordered]@{}) -Force -passThru |
44+
Add-Member NoteProperty Query ([Ordered]@{}) -Force -passThru
45+
46+
foreach ($headerName in $request.Headers) {
47+
$debugObject.headers[$headerName] = $request.Headers[$headerName]
48+
}
49+
if ($request.Url.Query) {
50+
51+
foreach ($chunk in $request.Url.Query -split '&') {
52+
$parsedQuery =
53+
[Web.HttpUtility]::ParseQueryString($chunk)
54+
$key = @($parsedQuery.Keys)[0]
55+
if ($debugObject.Query[$key]) {
56+
$debugObject.Query[$key] = @(
57+
$debugObject.Query[$key]
58+
) + $parsedQuery[$key]
59+
} else {
60+
$debugObject.Query[$key] = $parsedQuery[$key]
61+
}
62+
}
63+
}
64+
$reply.ContentType = 'application/json'
65+
$reply.Close(
66+
$OutputEncoding.GetBytes(
67+
($debugObject | ConvertTo-Json -Depth 5)
68+
), $false)
69+
"Responded to $($Request.Url) in $([DateTime]::Now - $requestTime)"
70+
}
71+
}
72+
73+
if ($AsJob) {
74+
Start-ThreadJob -ScriptBlock $listenerScript -ArgumentList $httpListener
75+
} else {
76+
. $listenScript $httpListener
77+
}

Servers/DualEventServer.ps1

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<#
2+
.SYNOPSIS
3+
Event Server
4+
.DESCRIPTION
5+
A simple event driven server.
6+
7+
Each request will generate an event, which will be responded to by a handler.
8+
#>
9+
param(
10+
# The rootUrl of the server. By default, a random loopback address.
11+
[string]$RootUrl=
12+
"http://127.0.0.1:$(Get-Random -Minimum 4200 -Maximum 42000)/"
13+
)
14+
15+
$httpListener = [Net.HttpListener]::new()
16+
$httpListener.Prefixes.Add($RootUrl)
17+
Write-Warning "Listening on $RootUrl $($httpListener.Start())"
18+
19+
$io = [Ordered]@{ # Pack our job input into an IO dictionary
20+
HttpListener = $httpListener ; ServerRoot = $RootDirectory
21+
MainRunspace = [Runspace]::DefaultRunspace; SourceIdentifier = $RootUrl
22+
TypeMap = $TypeMap
23+
}
24+
25+
# Our server is a thread job
26+
Start-ThreadJob -ScriptBlock {param([Collections.IDictionary]$io)
27+
$psvariable = $ExecutionContext.SessionState.PSVariable
28+
foreach ($key in $io.Keys) { # First, let's unpack.
29+
if ($io[$key] -is [PSVariable]) { $psvariable.set($io[$key]) }
30+
else { $psvariable.set($key, $io[$key]) }
31+
}
32+
33+
$thisRunspace = [Runspace]::DefaultRunspace
34+
35+
# Because we are handling the event locally, the main thread can keep chugging.
36+
Register-EngineEvent -SourceIdentifier $SourceIdentifier -Action {
37+
try {
38+
$request = $event.MessageData.Request
39+
$reply = $event.MessageData.Reply
40+
41+
$timeToRespond = [DateTime]::Now - $event.TimeGenerated
42+
$myReply = "$($request.HttpMethod) $($request.Url) $($timeToRespond)"
43+
$reply.Close($OutputEncoding.GetBytes($myReply), $false)
44+
} catch {
45+
Write-Error $_
46+
}
47+
}
48+
49+
# Listen for the next request
50+
:nextRequest while ($httpListener.IsListening) {
51+
$getContext = $httpListener.GetContextAsync()
52+
while (-not $getContext.Wait(17)) { }
53+
$request, $reply =
54+
$getContext.Result.Request, $getContext.Result.Response
55+
56+
# Generate events for every request
57+
foreach ($runspace in $thisRunspace, $mainRunspace) {
58+
# by broadcasting to multiple runspaces, we can both reply and have a record.
59+
$runspace.Events.GenerateEvent(
60+
$SourceIdentifier, $httpListener, @(
61+
$getContext.Result, $request, $reply
62+
), [Ordered]@{
63+
Method = $Request.HttpMethod; Url = $request.Url
64+
Request = $request; Reply = $reply; Response = $reply
65+
ServerRoot = $ServerRoot; TypeMap = $TypeMap
66+
}
67+
)
68+
}
69+
}
70+
} -ThrottleLimit 100 -ArgumentList $IO -Name "$RootUrl" | # Output our job,
71+
Add-Member -NotePropertyMembers @{ # but attach a few properties first:
72+
HttpListener=$httpListener # * The listener (so we can stop it)
73+
IO=$IO # * The IO (so we can change it)
74+
Url="$RootUrl" # The URL (so we can easily access it).
75+
} -Force -PassThru # Pass all of that thru and return it to you.

0 commit comments

Comments
 (0)