Nuværende state af mit custom blogsystem

Jeg har i mange år arbejdet på mit blog setup. Setuppet har taget mange former over årenes løb (fra VMs, over AngularJS til WCF services, Kibana og CouchDB), men jeg synes jeg er endt ud med noget som jeg er tilfreds med.

Dog har jeg i det sidste år perfektioneret hvad jeg det er jeg gerne vil. Jeg har på en måde fundet lige præcis den måde, som virker bedst for mig at blogge på (så må vi se om jeg også får det gjort). Jeg har i mange år ledt efter "the sweet spot", og det er også derfor jeg er blevet ved med at lave systemet om. Min initielle tanke har altid været, at jeg gerne vil kunne blogge fra en (eller flere) editors, som virkelig var godt til en ting (faktisk to):

  1. tekst (skrevet i markdown)
  2. vise kode

Jeg vil IKKE have mine drafts gemt i et eller andet obskurt data format, most ned i en database. Den allerstørste grund hertil er rimelig simpel: adgang. Jeg vil på ingen måde være afhængig af en browser for at skulle editere tekst. Jeg vil på ingen måde være afhængig af en custom made editor, som kan dykke ned i databasen, hente tekst frem osv. Alt dette skal ske med allerede kendte teknologier, så jeg kan vælge den editor på markedet som er bedst, editere teksten på en hvilken som helst platform, gemme teksten, og se teksten dukke op på bloggen. Dette valg har jeg været glad for. Jeg har ikke mange indlæg på min nuværende blog, men på de foregående systemer, har jeg udgivet indlæg skrevet fra Visual Studo, Notepad, Notepad++ etc. Jeg har derved forfinet den måde jeg har fundet mest optimal, og jeg synes selv jeg har endt et rimelig godt sted. Jeg er endt et så godt sted, at jeg for første gang gerne vil blogge omkring systemet, noget jeg aldrig har haft i tankerne før.

Pt bruger jeg Visual Studio Code, og jeg er rimelig tilfreds med valget da:

  1. den kender Markdown. Markdown er en first class citizen i editoren, hvilket er ret lækker, da
  2. den har live preview af markdown, som kan vises side om side, og sidst med ikke mindst,
  3. den har INLINE color coding af kodestykker.

Disse tre ting kombineret gør, at VS Code er den ultimative editor i mine øjne, og da mit nuværende setup tillader at jeg kan bruge editoren, så gør jeg selvfølgelig det. Jeg er glad for at jeg ikke skal skule langt i en eller anden obskur browser baseret editor, som aldrig får nogle updates (da jeg selv skal lave dem), efter VS Code, men rent faktisk kan bruge den.

Nuværende setup

Nuværende blog setup

Hele min blog lever i Azure. Jeg har valgt Azure fordi jeg

  1. altid har været glad for Microsoft teknologi stakken
  2. er imponeret over den åbenhed som Microsoft har vist de seneste år
  3. har en C# certificering som gør at jeg har månedlig credit i Azure til at lege for

Jeg er ydermere blevet meget glad for VSTS. Jeg har arbejdet meget med TeamCity, en del med Octopus deploy og lidt med Jenkins, og jeg må sige at jeg VSTS har alt. Og det er selvfølgelig et kæmpe plus at den integrerer nydeligt med Azure (andet ville være mærkeligt).

Dette setup er IKKE en oneclick wonderland

Jeg har i sinde at udgive hele baduljen som open source. Jeg har undgået at sovse kildekoden ind med adgangskoder, brugernavne og andet privat (som jeg iøvrigt aldrig kunne finde på medmindre jeg for explicit besked på det). Dog vil der gå noget tid før alt kan ske fuldautomatisk. Jeg har i sinde at udgive koden, sammen med en readme, som udførligt fortæller opsætning.

Den native editor

En af de ting jeg har haft fokus på igennem hele forløbet er understøttelse for lige præcis den editor jeg finder præcis. Formatet som indlæggene skrives i er markdown, så finder man en editor, som understøtter dette format, så er man good to go. Editoren skal også kunne læse fra og skrive til en fil som er opsat som et netværksdrev (dog skulle dette helst virke på alle operativ systemer da det er en egenskab som udstilles gennem styresystemet).

Mit nuværende valg er Visual Studio Code, som har nativ undersøttelse for markdown, og kan vise markdown side-by-side

VSC med side-by-side load af markdown

En virkelig fed feature. Visual Studio Code undersøtter også inline highlight af kode, hvilket er rimlig sejt, når man som mig stort set kun skriver indlæg som indeholder kodestumper

highlight af kode i VSC

Vel at mærke med automatisk indryk mens man skriver. Det gør faktisk VSC mere eller mindre de facto hvis man vil have en god editor til blogindlæg indtil der kommer en bedre forbi. Kommer der en bedre editor forbi, så kan man med mit blog system bare skifte. Man er ikke bundet, og det er lige præcis det som er kernen i det hele. Det at man lader de andre folk, som er ekstrem gode til at lave editors, om at lave gode editors, så laver man selv resten. En god editor tager lang tid at lave, og den tid er det de færreste som har til rådighed.

Det specielle format

Det ene af de to ting som er lidt kluntet ved systemet er det format som man bruger når man skal poste et indlæg. Formatet (om man vil) består af to filer:

  1. selve indlægget - som er skrevet i markdown
  2. en xml fil som indeholder metadata omkring indlægget

De to filer skal:

  1. ligge i samme mappe
  2. have præcis samme navn (på nær endelsen selvfølgelig)

Som vist her

highlight af kode i VSC

Selve indlægget er plain markdown, så det gider jeg ikke vise, dog er meta dataformatet lidt sjovt:

<?xml version="1.0" encoding="utf-8"?>
<!--state can be Draft/Publish/RePublish-->
<!--date times is on following format 2008-09-22T14:01:54.9571247Z year-month-day-->
<information    state="Draft"
                isSticky="false"
                publishtime="2018-04-28T14:10:01.9571247Z" 
                headline="Nuværende state af mit custom blogsystem" 
                author="Martin Slot" 
                tags="blog,custom,csharp,azure" 
                creationtime="2018-04-28T14:10:01.9571247Z" 
                id=""
                rev=""/>

Metadata filen beskriver følgende:

  1. state - hvad state indlægget er i (valide states er Publish, Published, RePublish og Draft)
  2. isSticky - fortæller om indlægget skal have et menupunkt (og derved optræde som en side)
  3. publishtime - er tidspunktet hvor indlægget skal publiseres
  4. headline - er overskriften for indlægget
  5. author - er indlæggets forfatter
  6. creationtime - er oprettelsestidspunktet for indlægget
  7. id - er artiklens id, som bliver givet af databasen
  8. rev - er databasens revisionsnummer (hvis en sådan findes)

Disse metadata skal sættes manuelt, dog er det let at gøre. Når en artikel oprettes første gang sker dette via en import (mere om importen i sektionen efter denne). Når importen er sket, sættes state til Published samt så sættes id og revisionsnummeret. Jeg har endnu ikke lavet logik som kan give en forfatter besked om at der findes en nyere revision. Jeg har udelukkede haft fokus på publish, og republish (samt draft). Rent teoretisk ved jeg godt at jeg nok aldrig får brug for denne logik selv, da jeg er ene forfatter på bloggen, dog lægger jeg op til at dette kan laves.

Kluntet importering (men også lidt fedt)

Den del som står for selve importen (Min oversigtstegning indeholder en "Importer"), er en TimeTrigger Azure funktion. Koden som bliver kørt her har levet i alle mulige afskygninger i alle de år som jeg har udviklet bloggen (og koden bærer præg af mange delvise omskrivninger, og ikke særlig meget refaktorering). Fx findes denne kodeklump:

       private static Automation.Model.UpsertState WriteToDb(CloudFile file)
        {
            Automation.Model.UpsertState upsertState = null;
            string fileName = file.Name.Replace(".md", String.Empty);
            string metadataFileName = String.Format("{0}.{1}", fileName, "xml");
            var metaDataFile = _articlesDirectory.GetFileReference(metadataFileName);

            var fileExistsTask = metaDataFile.ExistsAsync();
            fileExistsTask.Wait();
            if (fileExistsTask.Result)
            {
                var downloadTask = metaDataFile.DownloadTextAsync();
                downloadTask.Wait();
                string metadataContents = downloadTask.Result;
                var metadataContentsBytes = System.Text.Encoding.UTF8.GetBytes(metadataContents);
                System.Xml.Linq.XDocument document = System.Xml.Linq.XDocument.Load(new MemoryStream(metadataContentsBytes));
                System.Xml.Linq.XElement stateXml = document.Descendants("information").FirstOrDefault();
                string author = stateXml.Attribute("author").Value;
                string tags = stateXml.Attribute("tags").Value;
                var idAttribute = stateXml.Attribute("id");
                var revAttribute = stateXml.Attribute("rev");
                var isStickyAttribute = stateXml.Attribute("isSticky");

                bool isSticky = false;
                string id = String.Empty;
                string rev = String.Empty;

                if (isStickyAttribute != null)
                {
                    isSticky = bool.Parse(isStickyAttribute.Value);
                }

                if (idAttribute != null)
                    id = idAttribute.Value;

                if (revAttribute != null)
                    rev = revAttribute.Value;

                DateTime creationDateTime = DateTime.Parse(stateXml.Attribute("creationtime").Value);
                string headline = stateXml.Attribute("headline").Value;
                DateTime publishTime = DateTime.Parse(stateXml.Attribute("publishtime").Value);
                var downloadTextTask = file.DownloadTextAsync();
                downloadTextTask.Wait();
                string text = downloadTextTask.Result;
                bool isDraft = false;

                FileStates state = (FileStates)Enum.Parse(typeof(FileStates), stateXml.Attribute("state").Value);

                if (state == FileStates.Draft)
                    isDraft = true;

                string connectionstring = GetEnvironmentVariable("blogConnectionstring");
                string primaryKey = GetEnvironmentVariable("blogPrimarykey");

                Repository.Blog.ArticleRepository.Interface.IArticleRepository articleRepository = new Repository.Blog.ArticleRepository.CosmosDb.ArticleRepository(connectionstring, "Blog", "Article", primaryKey);
                Service.Blog.BlogService.Interface.IBlogService blogService = new BlogService(articleRepository);

                Automation.Model.Article article = new Automation.Model.CosmosDb.Article();
                article.IsSticky = isSticky;
                article.Author = author;
                article.CreationDateTime = creationDateTime;
                article.Document.Headline["da"] = headline;
                article.Document.Text["da"] = text;
                article.IsDraft = isDraft;
                article.PublishTime = publishTime;
                article.AddTagsString(tags);
                article.Id = id;
                article.Revision = rev;

                var saveTask = blogService.UpsertArticleAsAsync(article);
                Task.WaitAll(saveTask);

                if (saveTask.IsCompleted)
                {
                    upsertState = saveTask.Result;
                    Console.WriteLine($"Upserted: {upsertState.Upserted}");
                    Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(article, Newtonsoft.Json.Formatting.Indented));

                }
                else
                {
                    Console.WriteLine("fault when upserting");
                }
            }

            return upsertState;
        }

Ja. Du har læst rigtigt. En funktion på næsten 100 linjer. Dette er slet ikke normalen for hvordan jeg udvikler på mit job. På jobbet holder jeg mine metoder små, og de vigtige metoder er dækket af enten en, eller flere unit test, og/eller en eller flere integrationstests. Dog forbeholder jeg mig rettet til at programmere i min fritid (og ikke udvikle). Mit fokus har aldrig været lave kode, som er dokumenteret og testet i form af noget automatiseret test. Jeg har altid haft fokus på at prøve nye ting i min fritid, og det bærer importeren også præg af. Kan man se gennem fingre med den dårlige kodekvalitet, så læg mærke til at funktionen faktisk:

  1. Gemmer artiklen in en database (her CosmosDB)
  2. Henter filen fra Azure Storage (jeg gifer en CloudFile med som parameter til funktionen)

Hele to nye teknologier, som jeg aldrig har leget med før I EN FUNKTION. Fedt! Jeg omskrev metoden, fra at læse fra en folder i filsystemet på min PC, til at læse fra et mounted drev fra en docker container, som kørte på en RaspberryPI, til at lægge i en Azure funktion. Pyyyh. Men min kode overlevede næsten (jeg skulle kun lave små omskrivninger). Og jeg er rimlig tilfreds som det virker i dag.

Min Importer kører hver time, og læser alle filerne som ligger i roden af en folder, som jeg peger på. Herefter opretter den indlægget, eller ændre dem, alt afhængig af hvad state som står i metadata filen. Rimlig simpel, og lige til at forstå. Og ja, til den skarpe læser, så kan man ikke slette endnu. Denne feature ligger i støbeskeen:

Og Importer'en skal derefter kunne reagere på disse. Jeg har aldrig haft brug for at slette endnu, og når jeg har, jamen så skriver jeg koden. Det er nu en gang det smukke ved selv at eje koden. Ship it when you need it.

Den dyre database

Helt i starten gemte jeg alle mine artikler i Couchdb. En fin dokument database. Det oprindelige system bestod af tre VMs, som kørte i Azure:

  1. en database VM med Couchdb og Elastic
  2. en VM som kørte en WCF service som man kunne bruge til at hente indlæg, gemme indlæg, søge efter indlæg osv
  3. en VM som stod for at hoste min hjemmeside

Opsætning virkede faktisk fint (faktisk kørte det hele i et virtuelt netværk med en frontend og backend subnet - meget fancy hvis man er til den slags).

Jeg byggede alt fra en fjerde VM, som kørte TeamCity og Octopus. Opsætningen var før der fandtes VSTS. Da VSTS kom besluttede jeg mig for at nedlægge setuppet, og lige så stille steppe væk fra de virtuelle maskiner. Jeg ville holde alt i Azure, og ikke kalde op til tredjeparts database (beliggende et helt andet sted i en helt anden sky i en helt anden region et fjerde sted i verden), derfor faldt valget på CosmosDB. Den native svejtserkniv, som bare kan altid ... næsten. Og så er den semi dyr, selv hvis man ikke bruger den.

Vælger man som jeg, at køre med den SQL motor, så skulle man tro at der var en 1-til-1 mellem MSSQL servers SQL og CosmosDB SQL. Det er det som Microsoft gerne vil have til at tro, men kradser man lidt i overfladen, så er verdenen en anden. Fx er groupings ikke understøttet (ikke pr. dagsdato, men det ligger vist nok i deres roadmap). Mange af deres aggregate funktioner var heller ikke implementeret fra starten af, selvom de sagde at SQL var understøttet. De er nogle snydere dem fra Microsoft. Heldigvis har jeg aldrig skulle gøre brug heraf, så CosmosDB passede fint til min usecase.

Hvad prisen angår, så koster det ca 300 kr om måneden for den "mindste" CosmosDB, og vi snakker altid bare for at have den tændt, og med nogle få 100 opslag om måneden. CosmosDB er dyr, men hvis man skal bruge en dokument dabase, som kan skalere mellem regioner og klare et meget stort pres, uden selv at stå for maintance, så tror jeg roligt man kan tage CosmosDB med i sine overvejelser. Teknologien fejler intet.

X-Cache: HIT vs cf-cache-status: HIT

Den nyeste teknologi jeg har rodet med er CDN. Jeg har på mit job beskæftiget mig med Akamai, men aldrig gennem Azure. Da min CosmosDB alligevel koster lidt, og da jeg ikke skriver særlig mange indlæg, så besluttede jeg mig for at sætte en CDN ind foran. Først prøvede jeg Azures to CDN tjenester:

  1. Akamai
  2. Verizon

Dog følte jeg ikke rigtig jeg fik et værktøj som var brugbart. De hostede ikke selv DNS, og min egen DNS leverandør understøttede ikke redirects fra http(s)://sigsys.dk og til http(s):/www.sigsys.dk. Administrationsværktøjet for dem begge var som hevet ud af halvfemserne. Nøj noget lort. Jeg gad godt jeg havde et screenshot (heldigvis har jeg ikke), så I kunne se det. Da jeg manglede et eller andet besluttede jeg mig for at se på Cloudflare. OG BOOM. Jeg var solgt. Cloudflare er en klar anbefaling. De overtager min DNS (undskyld unoeuro, I har været gode, men ikke så gode som Cloudflare). De hacker egentlig lidt CNAME direktivet i DNS protokollen, så man kan lave redirects fra non-www til www, og derefter spreder de magi udover dit website. Jeg har valgt den gratis løsning ved dem, hvor man får tre page rules, som lige er nok til det jeg gerne vil have. Jeg har slået caching til både på edge noden, som leverer siden og i browseren som modtager siden. Edge noden cacher i en dag, og browseren cacher i et par timer ad gangen. Det tager meget load fra mit website, og det load af min CosmosDB. Nu har jeg ikke meget load endnu, men det er altid godt at tage sorgerne på forskud. Specielt når det handler om Azure credits. De har det med at slippe op.

Jeg fik desuden bundlet alt min css og javascript til en fil, site.css og site.js bliver bygget og kopieret til en blog storage, som derefter serverer filerne. Dette sker også med billeder. Derved fjerner jeg presset fra mit website. Igen. Der er ikke meget pres på det som det er nu, men det er altid godt at tage sorgerne på forskud når det handler om Azure credits. De har det med at slippe op.

Automagisk deploy med VSTS (Næsten)

Min Importer og mit website bliver deployet via VSTS. I VSTS hoster jeg mit kode. Hver gang jeg laver et commit, bygges koden, og der laves et release. Jeg behøver ikke gøre noget, og jeg får en mail når der er deployet. Jeg er meget overrasket over VSTS. VSTS tager det bedste (synes jeg) fra TeamCity, Jira (tildels) og Octopus deploy, og giver et fedt administrations interface, hvor man kan planlægge, hoste og release software. Da VSTS er firstclass citizen i Azure, så integrerer den perfekt med Azure (alt andet ville være mærkelig).

Jeg sætter ikke mine resorucer op via ARM templates endnu. Det er noget jeg har lidt erfaring med, men det kommer også i nærmeste fremtid, da det skal laves, hvis jeg på et tidspunkt frigiver koden til fri afbenyttelse.

Hvad mangler der (der er altid småting)

Der mangler altid noget når man laver software, og specielt når man som mig ikke har særlig meget fritid, og selv er kunden:

  1. webpack til js og css - bundling af js og css
  2. ARM template - til automatisk deploy (så man hurtigt kan spinde test eller feature miljøer op uden at røre en finger)
  3. Deploy kræver stadig at MIME types bliver sat korrekt på js og css - et hurtigt fiks, men det ligner at dette ikke sker første gang man lægger js og css filer op på blob storage
  4. Ny template til bloggen - min nuværende template er ... ja. Skod
  5. Udskiftning af bower - bower anbefaler selv man kigger på Yarn eller NPM
  6. Som I kan se så skalere billederne ikke særlig godt (det ser faktisk grimt ud) - så det skal fikses

Det med småt

Jeg kan kun anbefale ghost platformen (selvom jeg aldrig selv har prøvet kræfter med den direkte), hvis man ikke selv vil til at lave et system som jeg har gjort. Lav kun et system som dette hvis du vil eksperimentere med forskellige ting. Dog må jeg indrømme at jeg bruger mit blogsystem som grundlag for at rode med nye teknologier:

  1. Docker
  2. VSTS
  3. Storage
  4. Azure functions
  5. Akamai, Verizon og Cloudflare CDN

Bare for at nævne nogle stykker. Jeg kan altid anbefale at udfordre ens egen teknologiske indsigt.

Det var et længere indlæg fra min side. Stavefejl og skøre sætninger bliver rettet, som jeg finder dem. Skriv endelig på keybase.io, hvis i har nogle spørgsmål (mit username er martinslot).

Skrevet af Martin Slot den 5/28/2018


Deseralisering af json

En måde er at man kan sørge for at ens objekter bliver skrevet med $type property der giver klassenavnet + assembly navnet, altså et semi fuldt navn, uden versionering. Dette kan Newtonsoft nemt gøre ved at angive en TypeNameHandling setting når man serialisere:

var messageString = JsonConvert.SerializeObject(sensor, new JsonSerializerSettings()
         	{
	            TypeNameHandling = TypeNameHandling.All
            });

Derefter kan man parse strengen, fremsøge $type property'en og tjekke om den matcher et givent mønster

var parsedObject = Newtonsoft.Json.Linq.JObject.Parse(document);
Newtonsoft.Json.Linq.JProperty type = parsedObject.Children<Newtonsoft.Json.Linq.JProperty>().FirstOrDefault(p => p.Name.Equals("$type"));
string value = type.Value.ToString();

var sensorType = typeof(Model.HomeInformation.Sensor);
var lightType = typeof(Model.HomeAutomation.Light);
string fullLight = $"{lightType.FullName}, {lightType.Assembly.GetName().Name}";
string fullSensor = $"{sensorType.FullName}, {sensorType.Assembly.GetName().Name}";

var obj = Newtonsoft.Json.JsonConvert.DeserializeObject(document, new Newtonsoft.Json.JsonSerializerSettings() { TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Auto });

if (value.Contains(fullLight))
{
	Newtonsoft.Json.JsonSerializerSettings settings = new Newtonsoft.Json.JsonSerializerSettings();
	var light = Newtonsoft.Json.JsonConvert.DeserializeObject<Model.HomeAutomation.Light>(document, settings);
}
else if (value.Contains(fullSensor))
{
	var sensor = Newtonsoft.Json.JsonConvert.DeserializeObject<Model.HomeInformation.Sensor>(document);
}

Dette var et eksempel, jeg følte var halvt. Den smagte ikke godt. Læg mærke til at en bedre måde at gøre det på er ikke at kalde DeserializeObject efter tjek på $type, men loope igennem parsedObject efterfølgende, dog ville jeg først som det overhovedet var muligt, og hvordan koden egentlig så ud. Det er ikke letlæseligt, svært at vedligeholde og det føles mest af alt som et hack. Dog slog det mig om Newtonsoft selv kan udnytte at $type bliver hældt med i det serialiserede data, og dette kan Newtonsoft nemt, man skal bare huske at sætte en setting, TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Auto når man deserialisere sit objekt:

var obj = Newtonsoft.Json.JsonConvert.DeserializeObject(document, 
	new Newtonsoft.Json.JsonSerializerSettings()
				{
					TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Auto
				});

obj er nu instanitieret som det objekt man hældte i $type. Koden er letlæselig, nem at vedligeholde og føles mere robust.

Skrevet af Martin Slot den 10/7/2017


Udfør XUnit fra PowerShell

Nogen gange mangler jeg en måde hvorpå jeg kan køre mine unit tests fra powershell, hvor jeg angiver hvilke filer som skal inkluderes i testen. Jeg er sikker på at der findes andre måder, hvorpå dette kan lade sig gøre (måske endda indbygget i console runneren), dog har jeg altid haft success med denne snippet:

$includeFilters = "*.Tests.dll","*.Testing.dll"
$consoleRunner =   Get-ChildItem -Path $PSScriptRoot\..\ -Recurse  -Filter "xunit.console.exe"
$testFiles = Get-ChildItem -Path $PSScriptRoot\..\src -Include $includeFilters -Exclude $excludeFilters -Recurse | ?{ $_.fullname -notmatch "\\obj\\?" }

Write-Host "Root dir for testing"  $PSScriptRoot
Write-Host "Include test filters pattern"  $includeFilters
Write-Host "Found xunit console runner at" $consoleRunner.FullName
Write-Host "Found" $testFiles.Count "to test"

foreach($testFile in $testFiles)
{
   $path = $testFile.FullName
   $runner = $consoleRunner.FullName
   $command = $runner +" " + $path

   $path

   iex $command
}

Man skal selv fortælle to ting i starten:

  1. Hvad skal inkluderes - dette gøres med $includeFilters variablen
  2. Hvor ligger console runneren - dette gøres med $consoleRunner variablen

Resten af scriptet gør brug af disse to variabler. Der kunne sikkert godt laves noget if checks som fortæller om alt kan findes, men jeg bruger altid scriptet steder hvor jeg ved at alt findes.

Scriptet er til fri afbenyttelse.

Skrevet af Martin Slot den 9/23/2016


Om

Mit navn er Martin Slot, jeg er 32 år gammel, er far til en lille pige, Nicoline og er gift med verdens dejligste, Trine Slot. Dette site har eksisteret i mange forskellige versioner, på mange forskellige platforme, men jeg er ved at finde mig tilrette nu med den korrekte setup i Azure. Setuppet før nuværende var alt for kompliceret, og var bygget op over flere år i de tidlige dage hvor Azure som platform så væsentlig anderledes ud. Pt eksisterer hele mit univers som

Setuppet før nuværende, var egentlig samme stack (dog med lidt andre teknologier):

Alt blev administreret (og opgraderet løbende) af mig selv. Det er (og var) der egentlig ikke noget galt i, dog fik jeg fornyet blod på tanden, da jeg blev introduceret til Team Services, på at genopfriske setuppet, og gentænke det hele, hvilket jeg har gjort. Mit næste projekt er jeg gået igang med, og projektet består af følgende:

Da jeg har fundet ud af at sensorerne rent faktisk indeholder en temperturmåler! (samt at jeg vil prøve at lave en Android app som kan fortælle mig hvis sensorerne bliver aktiveret når jeg ikke er hjemme). Jeg skal nok fortælle mere om projektet, når jeg får lavet de indledende loops.

Skrevet af Martin Slot den 3/17/2018


Ny samlet måde at håndtere langtidskørende processer på i .NET

Siden .NET 4 har der været en ny forenet måde at håndtere standsningen af processer, som skal køre i lang tid. Førhen (man ser det faktisk stadig i utallige af artikler og tutorials rundt omkring), har foreslaget altid været en while(true), med en sleep, eller en Console.ReadKey, dog er dette ikke tilfældet mere, hvilket man kan læse meget mere om her.

For .NET 4 var rådet altid at man brugte en Console.ReadKey for at holde processen fra at terminere

using System;
using System.IO;

namespace Programming
{
    public class Program
    {
        //...
        //kode som skal køre lang tid
        //...

        Console.WriteLine("press key to stop");
        Console.Readkey();
    }
}

En notice til læseren: Jeg startede først med at bruge .NET lige efter .NET 4 udkom, så teoretisk set ved jeg faktisk ikke om der vitterlig har været en hel anden måde at gøre det på, dog studser jeg over, hver gang jeg enten læser

at man altid nævner while(true), med en sleep, eller en Console.ReadKey som løsningen på, ikke at få processen til at stoppe, og jeg er egentlig ligeglad med om det "bare" er vis-og-smid-væk kode, eller, at selve det at stoppe termineringen ikke erhoved fokus, for det som afsenderen vil vise. Man kan lige så godt vende læseren (dig og mig), til at gøre det korrekte, og det er på grænsen til det pinlige, at samtlige afsendere endnu ikke ved hvordan man gør (for det antager jeg de ikke ved). Den korrekte måde, hvorved man undgår en process i at terminere, er at lytte på der bliver sendt en Cancel til processen, og derefter terminere korrekt:


using System;

namespace Programming
{
    class Program
    {
        private static System.Threading.Timer _timer;

        static void Main(string[] args)
        {
            var cts = new System.Threading.CancellationTokenSource();
            _timer = new System.Threading.Timer((arg) =>
            {
                Console.WriteLine($"Tick {DateTime.Now}");

            }, null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(10));

            System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += (ctx) => cts.Cancel();
            Console.CancelKeyPress += (sender, cpe) => cts.Cancel();
            WhenCancelled(cts.Token).Wait();
            Console.WriteLine("Console terminating");
        }

        public static System.Threading.Tasks.Task WhenCancelled(System.Threading.CancellationToken cancellationToken)
        {
            var tcs = new System.Threading.Tasks.TaskCompletionSource<bool>();
            cancellationToken.Register(s => ((System.Threading.Tasks.TaskCompletionSource<bool>)s).SetResult(true), tcs);
            return tcs.Task;
        }
    }
}

Her lytter jeg efter om der

Hvis der gør, fortæl System.Threading.CancellationTokenSource at den skal stoppe. Dette er den korrekte måde at gøre det jvf Microsofts dokumentation, som jeg linkede til før

Starting with the .NET Framework 4, the .NET Framework uses a unified model for cooperative cancellation of asynchronous or long-running synchronous operations that involves two objects:

A CancellationTokenSource object, which provides a cancellation token through its Token property and sends a cancellation message by calling its Cancel or CancelAfter method.

A CancellationToken object, which indicates whether cancellation is requested.

Fra Remarks sektionen på følgende link https://msdn.microsoft.com/en-us/library/system.threading.cancellationtokensource%28v=vs.110%29.aspx.

Den opmærksomme læser vil se at jeg har brugt en timer funktion, OG IKKE EN while(true)-og-en-sleep for at køre noget kode igen. Smart, og let at læse. Læg mærke til at jeg gemmer timeren i en private property, så timeren ikke bliver samlet op af GC'eren, hvis timeren er sat til at have en period (sidste variabel i constructoren), som er længere end 10 eller 20 sekunder. Jeg har været ude for det på en period som var sat til to minutter.

Skrevet af Martin Slot den 1/29/2018


Et simpelt eksempel på brug af NamedPipeServerStream

Hvis man vil lave en server og en klient som snakker sammen via pipes, har jeg her sammensat et lille eksempel, som viser hvad man skal bruge, for at komme igang.

Server

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Main
{
    class Program
    {
        static System.Threading.ManualResetEvent _quitEvent = new System.Threading.ManualResetEvent(false);
        static object state = new object();
        static void Main(string[] args)
        {
            Console.CancelKeyPress += (sender, eArgs) => {
                _quitEvent.Set();
                eArgs.Cancel = true;
            };
            var server = new System.IO.Pipes.NamedPipeServerStream("server", System.IO.Pipes.PipeDirection.InOut, 
                1,
                System.IO.Pipes.PipeTransmissionMode.Message,
                System.IO.Pipes.PipeOptions.Asynchronous);


           var result=  server.BeginWaitForConnection((asyncResult) => 
            {
                server.EndWaitForConnection(asyncResult);
                var reader = new System.IO.StreamReader(server);
                Console.WriteLine(reader.ReadLine());

            }, state);
			
            _quitEvent.WaitOne();
            server.Close();
        }
    }
}

Det man skal huske på serversiden, hvis man laver wait kaldet asynkront via server.BeginWaitForConnection er at man skal huske at kalde EndWaitForConnection så hurtigt man kan efter man modtager noget data. Vil man lave det synkront kan man nøjes med at kalde WaitForConnection. Dette kald er et blocking kald, som frigiver tråden, og venter på at der kommer en connection, før tråden bliver vækket til live igen.

Pga. at NamedPipeServerStream er en wrapper omkring en system pipe, skal man huske på at man ikke bare kan lave en while(true), og modtage flere connections over samme begin, som her

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Main
{
    class Program
    {
        static System.Threading.ManualResetEvent _quitEvent = new System.Threading.ManualResetEvent(false);
        static object state = new object();
        static void Main(string[] args)
        {
            Console.CancelKeyPress += (sender, eArgs) => {
                _quitEvent.Set();
                eArgs.Cancel = true;
            };

            var server = new System.IO.Pipes.NamedPipeServerStream("server", System.IO.Pipes.PipeDirection.InOut, 
                1,
                System.IO.Pipes.PipeTransmissionMode.Message,
                System.IO.Pipes.PipeOptions.Asynchronous);

			while(true)
			{
				var result=  server.WaitForConnection();
				
				server.EndWaitForConnection(asyncResult);
				var reader = new System.IO.StreamReader(server);
				Console.WriteLine(reader.ReadLine());
			}
            _quitEvent.WaitOne();

            server.Close();
        }
    }
}

Dette vil give en fejl! Man skal have var server = new ... med ind i løkken.

Klient

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            var client = new System.IO.Pipes.NamedPipeClientStream("server");
            client.Connect();
            string message = "hello world";
            client.Write(Encoding.UTF8.GetBytes(message), 0, message.Length);
            client.Close();
        }
    }
}

Klienten er lige ud af landevejen. Husk at kalde Connect() før man laver et Write!!

Skrevet af Martin Slot den 9/22/2016


Powershell remoting

Det er forholdsvis simpel at lave en remote session i PowerShell, man skal dog først lave noget opsætning:

Start af PSRemoting

For at kunne starte en remote session skal der startes en service. Fyr op for en powershell konsol, som administrator og kør følgende i prompten:

Enable-PSRemoting -Force

Da denne kommando tilføjer en firewall regel, til indgående trafik anbefales der at man sætter noget IP restriction på. Dette kan gøres fra prompten:

Set-Item wsman:\localhost\client\trustedhosts *

* gør at alle kan remote ind, hvilket jeg ikke vil anbefale. Man kan udskifte * med en liste af kommasepareret ip adresser for at opnå en form for restriktion.

Test af forbindelse

I mit tilfælde vil jeg gerne kunne styre min farm af Azure servere (Jeg har tre til at køre denne blog). For at kunne gøre dette skal man huske at åbne i den ydre firewall også! Ellers åbner man kun i den interne, hvilket gør at serverne kan nå hinanden på den interne VPN (hvis man har sat dette op). Udefra er der lukket, så husk at åbne her!

Man kan nu teste forbindelsen med

Test-WsMan [IP eller host]

Oprettelse af forbindelse

Meldes der ok, er man klar til at fyre op for en session:

Enter-PSSession -ComputerName [IP eller host] -Credential [brugernavn]

Ønsker man kun at fyre script blokke af, er det også muligt via Invoke-Command

Invoke-Command -ComputerName [IP eller host] -ScriptBlock { COMMAND } -credential [brugernavn]

Brug af SSL

Pga en manglende wildcard certifikat til cloudapp.net kan det godt være lidt svært, hvis man vil bruge -UseSSL når man laver en Enter-PSSession. For at få dette til at virke, har jeg været nød til at følge denne https://blogs.endjin.com/2014/03/a-step-by-step-guide-to-connecting-to-an-azure-virtual-machine-with-powershell-remoting. Man skal lave nogle krumspring, dog virker det i sidste ende.

Skrevet af Martin Slot den 8/12/2016


Custom extensions til Powershell

I mit daglige arbejde bruger jeg powershell en del pga git. Jeg surfer derfor en del rundt i et givent projekt og,

En gang imellem har jeg dog brug for at starte en explorer i det directory jeg står i, da den visuelle repræsentation nogen gange er mere effektiv end hvad en ls kan give mig. Jeg fik derfor stykket denne "oneliner" sammen

explorer.exe (Get-Item -Path ".\" -Verbose).FullName

"onelineren" er dog en del at skulle skrive hver gang jeg gerne vil åbne en explorer.exe i nuværende directory, derfor har jeg lavet en funktion, EC, og tilføjet den til min profile. Har man ikke en profile for den indloggede bruger, kan man hurtigt oprette en ved at køre følgende kommando

New-Item -path $profile -type file –force

Accepter prompten, som springer frem på skærmen, og man vil derefter have en ny fil, Microsoft.Powershell_profile.ps1 i folderen WindowsPowerShell, som ligger under My Documents. Den fulde sti til profile filen, vil være C:\Users\[Username]\Documents\WindowsPowerShell\Microsoft.Powershell_profile.ps1.

Heri har jeg smidt følgende funktion

function EC {
    explorer.exe (Get-Item -Path ".\" -Verbose).FullName
}

Hvis jeg lukker alle min powershells ned, og åbner powershell igen, vil jeg, ved at skrive EC i prompten, få åbnet en explorer i den directory jeg står i. simpelt.

Denne funktion tager højest sandsynlig ikke højde for alt (funktionen tager faktisk ikke højde for noget), så man kan sikkert nemt få den til at fejle. Dog er jeg stadig en rookie når det kommer til Powershell scripting, så funktionen kan nok let laves mere robust. Finder jeg fejl, vil jeg selvfølgelig lave et opfølgende indlæg, hvor jeg kommer med en opdateret funktion.

Skrevet af Martin Slot den 8/11/2016