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 2:19:00 PM


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 2:01:54 PM


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 12:12:00 PM


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 12:00:00 PM