0

I have some code that I would like to use for device discovery on a network. It simply pings any ip address on said network and if it gets an answer, adds the address in a list.

Code looks like this:

static void Main(string[] args){
    //do stuff to get subnetmask and local address
    //succession of for loop to increment ip address
    {
        PingAsync(ip_address);
    }

    //here I display the list of addresses that answered the ping
    //first Readline() is to manually wait for threads to end so the list isn't empty
    Console.Readline()
    Console.WriteLine("List of all devices found :\n");
    ipList.ForEach(delegate (string str)
        {
            Console.WriteLine($"\t=> {str}");
        });
    Console.ReadLine();
}
private static void PingAsync(IPAddress address)
{
    AutoResetEvent waiter = new AutoResetEvent(false);

    Ping pingSender = new Ping();
    //items are added to ipList in PingCompletedCallback()
    pingSender.PingCompleted += new PingCompletedEventHandler(PingCompletedCallback);

    string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    byte[] buffer = Encoding.ASCII.GetBytes(data);

    int timeout = 1200;

    PingOptions options = new PingOptions(64, true);

    pingSender.SendAsync(address, timeout, buffer, options, waiter);
}

In the PingCompletedCallback method, I add items to a list that will be displayed in the main

private static void PingCompletedCallback(object sender, PingCompletedEventArgs e)
{
    Program prog = new Program();

    if (e.Cancelled)
    {
        Console.WriteLine("Ping failed !");
        Console.WriteLine(e.Error.ToString());
        ((AutoResetEvent)e.UserState).Set();
    }

    if(e.Error != null)
    {
        Console.WriteLine("Ping failed !");
        Console.WriteLine(e.Error.ToString());

        ((AutoResetEvent)e.UserState).Set();
    }

    PingReply reply = e.Reply;

    if(reply.Status.ToString().Equals("Success", StringComparison.OrdinalIgnoreCase))
    {
    //If the ping succeeds add the address to a list
        ipList.Add(reply.Address.ToString());
    }

    ((AutoResetEvent)e.UserState).Set();
}

The code works fine at discovering devices, only issue is with the list of addresses. If I don't manually wait for all threads to end it will appear empty.

According these posts (1) (2) and some other, using Thread.join or Task.Waitall are the way to go. However, unlike them I am not creating threads myself but letting SendAsync() create its own thread.

Also, I can't change PingAsync() to make it await SendAsync() since it returns void.

I would like to know you would go about waiting for threads to end before printing/using the ipList.

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
  • 2
    There are a lot of issues here. However you are probably better off using the more modern `SendPingAsync ` then awaiting it. – TheGeneral Sep 06 '21 at 09:17

1 Answers1

2

Most of this code can be replaced with Ping.SendPingAsync :

static async Task Main(string[] args)
{

   using var pingSender = new Ping();
   var options = new PingOptions(64, true);
   var reply=await pingSender.SendPingAsync(address,1200,options)
   if(reply.Status == IPStatus.Success)
   {
      ...
   }
}

Concurrent Requests - One sender per request

SendPingAsync can't be called multiple times to send multiple requests from a single sender. Doing so throws an InvalidOperationException.

One solution is to create and use a separate sender per call :

async Task<Reply> PingAsync(IPAddress address)
{
    using var pingSender = new Ping();
   var options = new PingOptions(64, true);
   var reply=await pingSender.SendPingAsync(address,1200,options)
}

Combined with LINQ, it's possible to start multiple pings concurrently and collect the results:

var addresses=new List<IPAddress>();
...
//Create the tasks with LINQ
var tasks=addresses.Select(address=>PingAsync(address))
                   .ToArray();
//And wait for all of them to complete
var replies=await Task.WhenAll(tasks);

//Get the successful pings:
var successes=replies.Where(reply=>reply.Status==IPStatus.Success)
                     .Select(reply=>reply.Address.ToString())
                     .ToArray();

The first LINQ query will call PingAsync for every address and returns the Task<Reply> object returned by each call:

var tasks=addresses.Select(address=>PingAsync(address))
                   .ToArray();

The query is actually evaluated when ToArray() is called. All ping requests will start at the same time.

The next call awaits all of them to complete :

var replies=await Task.WhenAll(tasks);

And finally, the last LINQ query retrieves the IPs of the successful pings:

var successes=replies.Where(reply=>reply.Status==IPStatus.Success)
                     .Select(reply=>reply.Address.ToString())
                     .ToArray();
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Thank you for your anwser. However, the first LINQ query throws an InvalidOperationException : "asynchronous call already running" due to pingSender.SendPingAsync(). It says I should wait for the process to end or terminate it before starting another one. – FrougeurPro Sep 06 '21 at 12:14
  • @FrougeurPro duh, that's what I get for not reading the small print. It's still possible to make concurrent requests, but that would require using multiple `PingSender` instances – Panagiotis Kanavos Sep 06 '21 at 12:22
  • @FrougeurPro I modified the code to use a different sender per request – Panagiotis Kanavos Sep 06 '21 at 12:29
  • It works perfectly now, thank you again for your work. – FrougeurPro Sep 06 '21 at 12:44