0

What is the context : Set DNS IPv6 addresses via netsh Processes

What I Did :

    public static bool StartProcessAsync(
        string filename,
        string arguments,            
        string verb = null,
        bool useShell = false,
        bool noWindow = false,
        bool redirectOut = true,
        bool redirectErr = true,
        string workingDir = null,
        int? timeoutInMs = null,
        EventHandler startHandler = null,
        EventHandler exitHandler = null
        )
    {
        if (string.IsNullOrWhiteSpace(filename))
            return false;
        Process newProcess = CreateProcess(filename, arguments, verb, useShell, noWindow, redirectOut, redirectErr, workingDir);
        if (newProcess == null)
            return false;

        newProcess.EnableRaisingEvents = true;
        newProcess.Exited += new EventHandler((s, e) => 
        {
            Debug.WriteLine(filename + " Exited with " + newProcess.ExitCode.ToString() + " exit code.");
            string errors = newProcess.StandardError.ReadToEnd();
            string output = newProcess.StandardOutput.ReadToEnd();
            if (newProcess.ExitCode != 0)
                Debug.WriteLine(filename + " exit code: " + newProcess.ExitCode.ToString() + " " + (!string.IsNullOrEmpty(errors) ? " " + errors : "") + " " + (!string.IsNullOrEmpty(output) ? " " + output : ""));
        }
        );

        if (exitHandler != null)
            newProcess.Exited += exitHandler;            

        if (startHandler != null)
            startHandler.Invoke(null, null);

        return newProcess.Start();            
    }

How I am calling it :

        //set new IPv6 DNS addresses
        if (ipv6 != null && ipv6.Length > 0)
        {
            
            //primary
            address = ipv6[0];
            try
            {                    
                bool res = NetshDNSManaging(ni.Name, true, true, address, startHandler, exitHandler);
            }
            catch (Exception e)
            { Debug.WriteLine(e.Message); }

            if (ipv6.Length > 1)
            {
                //secondary
                address = ipv6[1];
                try
                {                        
                    bool res = NetshDNSManaging(ni.Name, true, false, address, startHandler, exitHandler);
                }
                catch (Exception e)
                { Debug.WriteLine(e.Message); }
            }
        }

with those intermediate convenient method :

    private static bool NetshDNSManaging(
        string interfaceName,
        bool isIPv6,
        bool isPrimary = true,
        string address = null,
        EventHandler startHandler = null,
        EventHandler exittHandler =null
        )
    {
        if (string.IsNullOrWhiteSpace(interfaceName))
            return false;
        bool noAddress = (string.IsNullOrWhiteSpace(address));
        string args = string.Format("interface {0} {1} dnsservers \"{2}\"{3} {4} {5}",
                                   isIPv6 ? "ipv6" : "ipv4",//{0}
                                   noAddress?"delete":(isPrimary ? "set" : "add"),//{1}
                                   interfaceName,//{2}
                                   (isPrimary && !noAddress) ? " static" : "",//{3}
                                   noAddress?"all":address,//{4}
                                   noAddress?"":(isPrimary ? "primary" : "index=2")//{5}
                                   );
        return StartProcessAsAdminAsync("netsh", args, startHandler, exittHandler);
    }

and :

    private static bool StartProcessAsAdminAsync(
        string filename,
        string arguments,
        EventHandler startHandler = null,
        EventHandler exitHandler = null,
        string verb = "runas",
        bool useShell = false,
        bool noWindow = true,
        bool redirectOut = true,
        bool redirectErr = true,
        string workingDir = null,
        int? timeoutInMs = null
        )
    {
        return ProcessUtils.StartProcessAsync(filename, arguments, verb, useShell, noWindow, redirectOut, redirectErr, workingDir, timeoutInMs, startHandler, exitHandler);
    }

What is my issue :

It happen from time to time that due to non sequential Process code execution, that the second call NetshDNSManaging(ni.Name, true, false, address, startHandler, exitHandler); (witch execute for example : netsh interface ipv6 add dnsservers "Ethernet" 2001:4860:4860::8844 index=2) finish before the first one NetshDNSManaging(ni.Name, true, true, address, startHandler, exitHandler); (e.g. netsh interface ipv6 set dnsservers "Ethernet" static 2001:4860:4860::8888 primary).

It result of having only the primary DNS set, as the secondary one is overwritten (witch was meanwhile transformed as a primary, by netsh I guess, since it was the first to finish)

What I end up :

EventHandler startHandler = new EventHandler(ProcessStarted);
EventHandler exitHandler = new EventHandler(ProcessExited);

with

 void ProcessExited(object? sender, EventArgs e)
    {
        _procNumber = Math.Max(_procNumber - 1, 0);
        if (_procNumber == 0)
        {
            IsWaiting = false;   
        }
    }

    void ProcessStarted(object? sender, EventArgs e)
    {
        while (_procNumber > 0) ; //Kinda ugly synchronization !!!
        _procNumber++;            
        IsWaiting = true;
        
    }

and IsWaiting Property notifying UI to display or not the waiting wheel animation.

So that work but I'm unhappy with what I think isn't a good practice here.

What I want : Make the second call starting only when the first one finish (so sequential call to the 2 created Process), but without blocking main WPF thread (to have my UI display a waiting animated wheel).

So something like : Main UI thread having a List<Process> (my example have only 2 process, but could be generalized to several more), and when needed, it should start the Process in the list one after one, in order, the second one waiting for the first one to finish before starting, and so on until end of the List. But without blocking the main WPF UI thread !

Parallelism is one of my nemesis, I struggle with this.

TRex
  • 127
  • 1
  • 8
  • 2
    Read up on async/await. That will allow you to call methods on the UI thread (aka Dispatcher) asynchronously. I find it confusing that you have several methods called "Async" but none of them are actually async...they're all synchronous. – Tam Bui Sep 09 '21 at 22:33
  • @TamBui Well it is my issue here, I have part synchronous as all process are synchronized to start after the previous one finishes AND also asynchronous toward the main WPF UI thread. Yeah I'll read again about async/await, maybe it will tilt this time. – TRex Sep 10 '21 at 13:59
  • As a good coding practice, if the method by itself is purely synchronous, do not append it with "Async", even if it will be running asynchronously higher up the call stack. However, if the method is currently synchronous, and you KNOW that it should be asynchronous becuz it may potentially perform a long-timed operation, then append the name w/ "Async" along with placing an `await` operation inside the method where you believe the long-timed op will occur. And don't forget to label the method appropriately like `private static async Task MyMethodAsync()`. Good luck on your journey! – Tam Bui Sep 10 '21 at 16:06

1 Answers1

1

Thanks to @TamBui, I found the solution with await/async as he suggested.

First I'd like to share the YouTube tutorial that I found extremely pedagogic in this way : https://www.youtube.com/watch?v=2moh18sh5p4

Here is what I modified :

  1. My convenient methods :

    private static Process NetshDNSManaging(
         string interfaceName,
         bool isIPv6,
         bool isPrimary = true,
         string address = null,
         EventHandler startHandler = null,
         EventHandler exittHandler =null
         )
     {
         if (string.IsNullOrWhiteSpace(interfaceName))
             return null;
         bool noAddress = (string.IsNullOrWhiteSpace(address));
         string args = string.Format("interface {0} {1} dnsservers \"{2}\"{3} {4} {5}",
                                    isIPv6 ? "ipv6" : "ipv4",//{0}
                                    noAddress?"delete":(isPrimary ? "set" : "add"),//{1}
                                    interfaceName,//{2}
                                    (isPrimary && !noAddress) ? " static" : "",//{3}
                                    noAddress?"all":address,//{4}
                                    noAddress?"":(isPrimary ? "primary" : "index=2")//{5}
                                    );
         return GetProcessAsAdmin("netsh", args, startHandler, exittHandler);
     }
    
     private static Process GetProcessAsAdmin(
         string filename,
         string arguments,
         EventHandler startHandler = null,
         EventHandler exitHandler = null,
         string verb = "runas",
         bool useShell = false,
         bool noWindow = true,
         bool redirectOut = true,
         bool redirectErr = true,
         string workingDir = null,
         int? timeoutInMs = null
         )
     {
         return ProcessUtils.CreateProcess(filename, arguments, startHandler, exitHandler, verb, useShell, noWindow, redirectOut, redirectErr, workingDir);
     }
    
  2. The Utilities for Process managing :

     public static class ProcessUtils
     {
    
     public static async Task<bool> StartChainedProcessAsync(List<Process> processes)
     {
         if (processes == null || !processes.Any())
             return false;
         bool cumulativeExitStatus = true;
         foreach (var p in processes)
         {
             cumulativeExitStatus &= await StartProcessAsync(p);
         }
         return cumulativeExitStatus;
     }
    
     public static async Task<bool> StartProcessAsync(Process p)
     {
         if (p == null)
             return false;
         p.Start();
         await p.WaitForExitAsync();
         return p.ExitCode == 0;
     }
     public static Process CreateProcess(
         string filename,
         string arguments,
         EventHandler startHandler = null,
         EventHandler exitHandler = null,
         string verb = null,
         bool useShell = false,
         bool noWindow = false,
         bool redirectOut = true,
         bool redirectErr = true,
         string workingDir = null 
         //int? timeoutInMs = null,
         )
     {
         /// set <see cref="ProcessStartInfo"/> of the <see cref="Process"/> to return;
         ProcessStartInfo psi = new ProcessStartInfo()
         {
             FileName = filename,
             Arguments = arguments,
             UseShellExecute = useShell,
             CreateNoWindow = noWindow,
             RedirectStandardError = redirectErr,
             RedirectStandardOutput = redirectOut
         };
         if (verb != null)
             psi.Verb = verb;
         if (workingDir != null)
             psi.WorkingDirectory = workingDir;                            
    
         Process p = new Process();
         p.StartInfo = psi;
    
         p.EnableRaisingEvents = true;
         p.Exited += new EventHandler((s, e) =>
         {
             Debug.WriteLine(filename + " Exited with " + p.ExitCode.ToString() + " exit code.");
             string errors = p.StandardError.ReadToEnd();
             string output = p.StandardOutput.ReadToEnd();
             if (p.ExitCode != 0)
                 Debug.WriteLine("Errors : " + (!string.IsNullOrEmpty(errors) ? " " + errors : "") + "\nOutputs : " + (!string.IsNullOrEmpty(output) ? " " + output : ""));
         }
         );
    
         if (exitHandler != null)
             p.Exited += exitHandler;
    
         if (startHandler != null)
             startHandler.Invoke(null, null);
    
         return p;
     }
     }
    
  3. Calling them :

     public static async Task SetDNS(this NetworkInterface ni, EventHandler startHandler, EventHandler exitHandler, string[] ipv4, string[] ipv6)
     {
         if (!(ipv4.Any(add => IsIP(add, RGXV4)) || ipv6.Any(add => IsIP(add, RGXV6))))
         {
             Debug.WriteLine("None of the suplied addresses are IPv4/6.\nCan not update DNS IP addresses.");
             return;
         }
         List<Process> chainedProcess = new List<Process>();
    
         // delete current IPv4 DNS            
         chainedProcess.Add(NetshDNSManaging(ni.Name, false));
         // delete current IPv6 DNS
         chainedProcess.Add(NetshDNSManaging(ni.Name, true));
         string address;            
    
         //set new IPv6 DNS addresses
         if (ipv6 != null && ipv6.Length > 0)
         {                
             //primary
             address = ipv6[0];
             try
             {                    
                 chainedProcess.Add(NetshDNSManaging(ni.Name, true, true, address/*, startHandler, exitHandler*/));
             }
             catch (Exception e)
             { Debug.WriteLine(e.Message); }
    
             if (ipv6.Length > 1)
             {
                 //secondary
                 address = ipv6[1];
                 try
                 {                        
                     chainedProcess.Add(NetshDNSManaging(ni.Name, true, false, address/*, startHandler, exitHandler*/));
                 }
                 catch (Exception e)
                 { Debug.WriteLine(e.Message); }
             }
         }
         startHandler.Invoke(null, null);
         await ProcessUtils.StartChainedProcessAsync(chainedProcess);
         exitHandler.Invoke(null, null);
    
         //set new IPv4 DNS addresses
         if (ipv4 != null && ipv4.Length > 0)
         {
             //Use legacy (WMI win32) version for better performance on some ipv4 DNS addresses (observed especially on FDN ipv4 addresses)
             SetLegacyDNS(ni, ipv4);
    
         }
     }
    
  4. Finally starting call from my main ViewModel :

     EventHandler startHandler = new EventHandler(ProcessStarted);
     EventHandler exitHandler = new EventHandler(ProcessExited); 
     SelectedNI.Ni.SetDNS(startHandler, exitHandler, ipv4, ipv6);
    
     void ProcessExited(object? sender, EventArgs e)
     {
         IsWaiting = false;       
     }
    
     void ProcessStarted(object? sender, EventArgs e)
     {            
         IsWaiting = true;            
     }
    

Maybe I should get rid of this last 2 callback Event handler now ?

TRex
  • 127
  • 1
  • 8
  • Maybe this well help with your question. Take a look at the highest voted answer. And glad to hear you got something working! https://stackoverflow.com/questions/10788982/is-there-any-async-equivalent-of-process-start – Tam Bui Sep 11 '21 at 02:14