0

I have an IP discover method where I test every IP address with a TcpClient, when any IP has a successful connection I set the WebServerIp in the memory, and that's when in my world the task is completed successfully. Is there a way to implement it here in my code? Or some kind of cancelation token when a task is "successful".

Here is my code:

public async Task<bool> DiscoverDeviceAsync()
{
    var ips = GeneralService.GetIPAddress();
    PhoneIp = string.Join(",", ips.Select(x => x.ToString()));

    await Task.Delay(1000);

    var tasks = new List<Task>();

    foreach (IPAddress ip in ips)
    {
        byte[] ipBytes = ip.GetAddressBytes();

        for (int i = 0; i <= 254; i++)
        {
            ipBytes[3] = (byte)i;
            IPAddress currentIPAddress = new(ipBytes);
            tasks.Add(ScanIPAddress(currentIPAddress, 9431));
        }
    }

    await Task.WhenAny(tasks);

    return WebServerIp != "";
}

async Task ScanIPAddress(IPAddress ipAddress, int port)
{
    try
    {
        using TcpClient tcpClient = new()
        {
            SendTimeout = 100,
            ReceiveTimeout = 100,
        };

        await tcpClient.ConnectAsync(ipAddress, port);

        WebServerIp = ipAddress.ToString();
    }
    catch
    {
        throw new Exception($"No device found at address: {ipAddress}");
    }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Hickori
  • 55
  • 6
  • Stop setting `WebServerIp` from `ScanIPAddress`, start setting it after `Task.WhenAny(tasks)`, using the `Task` that was returned by `WhenAny`. – GSerg Aug 20 '23 at 11:27
  • @GSerg The OP's problem is that the first Task to complete could be in a faulted state, while the first that succeeds is required. So maybe passing a CancellationToken to the Tasks is not a bad idea (if Tasks that fail don't actually *end on their own*). I assume the code presented here is more pseudo-code than a real implementation – Jimi Aug 20 '23 at 11:38
  • WhenAny seem to return even if it throws an error, so if I was to set it from the whenAny task it would just be wrong, @Jimi this is a real implementation :) – Hickori Aug 20 '23 at 11:44
  • Yes, `Task.WhenAny()` returns when the first Task returns, no matter its state, it's enough that it has *run to completion* -- No, it's not real code :) You cannot have `PhoneIp = ...`, `return WebServerIp ...` in a real method, just to say a couple, not to mention the non-async code that is run there (what's `Task.Delay()` for ?) -- Your only problem is to determine when a connection is actually *faulty* (no problem if nothing else can answer that request, but if it's possible... i.e., it's not *generic usage*) – Jimi Aug 20 '23 at 11:56
  • Out of curiosity, isn't the [`Ping`](https://learn.microsoft.com/en-us/dotnet/api/system.net.networkinformation.ping) class better at checking the validity of an IP than the [`TcpClient`](https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.tcpclient) class? – Theodor Zoulias Aug 20 '23 at 13:27
  • Related: [Is there default way to get first task that finished successfully?](https://stackoverflow.com/questions/37528738/is-there-default-way-to-get-first-task-that-finished-successfully) – Theodor Zoulias Aug 20 '23 at 19:23
  • 1
    @TheodorZoulias: Using the `Ping` class would be more practical but I don't think the `Ping` class can be used to check ports besides machine/IP addresses. It does not have a member related with ports. The OP checks ports in the `ScanIPAddress()` method. – Mustafa Özçetin Aug 22 '23 at 08:35

2 Answers2

2

If you're happy to use Microsoft's Reactive Framework, then you could do this:

public async Task<string> DiscoverDeviceAsync() =>
    await
    (
        from ip in GeneralService.GetIPAddress().ToObservable()
        let ipPrefix = String.Concat(ip.GetAddressBytes().Take(3).Select(x => $"{x}."))
        from i in Observable.Range(0, 255)
        let currentIPAddress = IPAddress.Parse($"{ipPrefix}{i}")
        from scan in Observable.FromAsync(ct => ScanIPAddress(currentIPAddress, 9431, ct))
        where scan != null
        select scan
    ).FirstAsync();

async Task<string> ScanIPAddress(IPAddress ipAddress, int port, CancellationToken ct)
{
    try
    {
        using TcpClient tcpClient = new()
        {
            SendTimeout = 100,
            ReceiveTimeout = 100,
        };
        await tcpClient.ConnectAsync(ipAddress, port, ct);
        return ipAddress.ToString();
    }
    catch
    {
        return null;
    }
}

Of course, this is possible to use method syntax:

public async Task<string> DiscoverDeviceAsync() =>
    await
        GeneralService
            .GetIPAddress()
            .ToObservable()
            .Select(ip => String.Concat(ip.GetAddressBytes().Take(3).Select(x => $"{x}.")))
            .SelectMany(prefix => Observable.Range(0, 255), (prefix, i) => IPAddress.Parse($"{prefix}{i}"))
            .SelectMany(ip => Observable.FromAsync(ct => ScanIPAddress(ip, 9431, ct)))
            .Where(scan => scan != null)
            .FirstAsync();

The query syntax is only ever syntactic-sugar.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Is it possible to do this with the LINQ labmda procedural way? :) – Hickori Aug 22 '23 at 06:45
  • @Hickori - Yes, it's always possible. – Enigmativity Aug 22 '23 at 07:29
  • My understanding is that the OP want's to cancel all pending operations when a valid IP is found. This answer just lets all pending operations to become fire-and-forget. – Theodor Zoulias Aug 22 '23 at 08:08
  • @TheodorZoulias - I've tried not to modify `ScanIPAddress` too much, so didn't add a cancellation token. I'll put one in tomorrow. Otherwise, the `await` and `FirstAsync` will automatically call `Dispose()` and shut down the subscription stopping it running. – Enigmativity Aug 22 '23 at 08:54
  • You might have to use the `Observable.FromAsync` overload that has a `functionAsync` parameter of type `Func>`. – Theodor Zoulias Aug 22 '23 at 11:22
  • @TheodorZoulias - Yes, agreed. That will cancel on `Dispose()`. – Enigmativity Aug 22 '23 at 11:28
  • 1
    @TheodorZoulias - I've made it fully cancelable. – Enigmativity Aug 23 '23 at 02:20
  • I don't know how important is, but I think that this answer returns the first IP that succeeds the test chronologically, without taking the order into account. For example if both 1.0.0.1 and 1.0.0.2 are valid IP addresses, this answer might return either the one or the other, depending on which test completed faster. It won't return consistently the 1.0.0.1 IP address. – Theodor Zoulias Aug 23 '23 at 02:43
  • @TheodorZoulias - Isn't that the same as the OP's use of `Task.WhenAny`? – Enigmativity Aug 23 '23 at 03:20
  • Yes, it's the same. I wonder if the OP is aware of this lack of determinism. – Theodor Zoulias Aug 23 '23 at 04:27
  • 1
    The main purpose of this method is that I have an IoT device which I want to find on any network I plugg it in as fast as possible. And I noticed that some routers my testers have dont work with mDNS, hostnames without having to configure the router to do so. Therefore I want a method that scans it on a certain port so that the users never have to touch the router and everything will work at any time anywhere. There will always just be 1 device on the network so the first ip to respond on that port will probably be the IoT :) – Hickori Aug 28 '23 at 05:59
0

What you need is a way to signal to the main and other threads when the work is done.

For that you can use an EventWaitHandle in combination with a CancellationToken

First we create the handle and cancellation token:

 var waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
 var cts = new CancellationTokenSource();

Pass them to your method:

tasks.Add(ScanIPAddress(currentIPAddress, 9431, waitHandle, cts.Token));

Then inside the method set WaitHandle if connection is succesfull, also check the cancellation token:

async Task ScanIPAddress(IPAddress ipAddress, int port, EventWaitHandle waitHandle, CancellationToken cancellationToken)
{
    try
    {
        if (cancellationToken.IsCancellationRequested)
        {
            return;
        }
        
        using TcpClient tcpClient = new()
        {
            SendTimeout = 100,
            ReceiveTimeout = 100,
        };

        await tcpClient.ConnectAsync(ipAddress, port, cancellationToken);

        waitHandle.Set();
        WebServerIp = ipAddress.ToString();
    }
    catch
    {
        throw new Exception($"No device found at address: {ipAddress}");
    }
}

After this you can use WaitOne method of EventHandle to wait until the flag is set. This can mean one of two things, either one of the operations were successful, or all have failed:

// Set wait handle after all tasks are finished, so it doesn't wait forever if none of them are successful
// no await here is intentional as we don't wanna wait for all tasks to complete here.
Task.WhenAll(tasks).ContinueWith(_ => waitHandle.Set());
    
waitHandle.WaitOne();

cts.Cancel();

return WebServerIp != "";
Selman Genç
  • 100,147
  • 13
  • 119
  • 184
  • Works perfecly! Thank you so much! – Hickori Aug 20 '23 at 12:01
  • @Hickori you are welcome, I actually missed something, you need to call cts.Cancel after WaitOne returns so that other tasks will stop working as the work is already completed, added it now. – Selman Genç Aug 20 '23 at 12:03
  • I think there might be a (small) race condition here. `waitHandle.Set()` is being called before setting the `WebServerIp` variable. I think those lines should be switched. – dana Aug 20 '23 at 12:21
  • yes it's possible @dana if you only want the first successful result and not override the value of WebServerIp, you can check if it's empty or not before setting it again, although that also doesn't eliminate the race condition %100, you might wanna consider adding a lock for a more guaranteed approach but that might be an overkill – Selman Genç Aug 20 '23 at 12:53