[GH-ISSUE #878] Fix RDP reconnection #2634

Closed
opened 2026-03-01 17:22:07 +03:00 by kerem · 31 comments
Owner

Originally created by @itagagaki on GitHub (Mar 1, 2025).
Original GitHub issue: https://github.com/1Remote/1Remote/issues/878

Originally assigned to: @VShawn, @itagagaki on GitHub.

The session window closes or rarely crashes when the RDP session hangs.

I thought closing the window was a specification, but I think it was originally designed to reconnect automatically or was modified to do so. However, it still does not, and in some cases it crashes, so something is wrong. Could be a local problem, but needs to be investigated.

As for protocols other than RDP, I have not tried them yet.

Originally created by @itagagaki on GitHub (Mar 1, 2025). Original GitHub issue: https://github.com/1Remote/1Remote/issues/878 Originally assigned to: @VShawn, @itagagaki on GitHub. The session window closes or rarely crashes when the RDP session hangs. I thought closing the window was a specification, but I think it was originally designed to reconnect automatically or was modified to do so. However, it still does not, and in some cases it crashes, so something is wrong. Could be a local problem, but needs to be investigated. As for protocols other than RDP, I have not tried them yet.
kerem 2026-03-01 17:22:07 +03:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@itagagaki commented on GitHub (Mar 1, 2025):

@VShawn

github.com/1Remote/1Remote@aa84184cdd/Ui/View/Host/ProtocolHosts/AxMsRdpClient09Host.cs (L143-L152)

When I force my RDP host PC to sleep, e.discReason is 0x208 (no host), but _rdpClient?.ExtendedDisconnectReason is ExtendedDisconnectReasonCode.exDiscReasonNoInfo, so it was not reconnected in this block and disposed in the else block. I think it should be reconnected in this case as well, but can you recall why it dared not do so?

<!-- gh-comment-id:2692200411 --> @itagagaki commented on GitHub (Mar 1, 2025): @VShawn https://github.com/1Remote/1Remote/blob/aa84184cdd40cfa73e745739912f32bce7fb670b/Ui/View/Host/ProtocolHosts/AxMsRdpClient09Host.cs#L143-L152 When I force my RDP host PC to sleep, `e.discReason` is 0x208 (no host), but `_rdpClient?.ExtendedDisconnectReason` is `ExtendedDisconnectReasonCode.exDiscReasonNoInfo`, so it was not reconnected in this block and disposed in the else block. I think it should be reconnected in this case as well, but can you recall why it dared not do so?
Author
Owner

@VShawn commented on GitHub (Mar 1, 2025):

There was no particular reason; at that time, I inferred that reconnect was not necessary based on the enum name.

<!-- gh-comment-id:2692256230 --> @VShawn commented on GitHub (Mar 1, 2025): There was no particular reason; at that time, I inferred that reconnect was not necessary based on the enum name.
Author
Owner

@itagagaki commented on GitHub (Mar 1, 2025):

I got what OnRdpClientDisconnected does.
Here we have three cases. Reconnect, display an error message and reconnect button, or close the window.
The conditions for switching (bifurcation) may need to be reviewed.

I couldn't find 0xb08 as a disconnect reason in the current Microsoft Learn, so the specification may have changed.

exDiscReasonNoInfo may occur when the RDP server goes to sleep, in which case it may be better to display a reconnect button instead of closing or auto-reconnecting.

However, the comment in the code says that win2008 (Windows Server 2008?) will report exDiscReasonNoInfo on logout. If the user has signed off, it would be better to close it, but that would be inconsistent with the above. Hmmm. I don't have the environment to try Windows Server, so I can't to investigate this.

And I think _retryCount should be reset to 0 when a connection is made.

<!-- gh-comment-id:2692302250 --> @itagagaki commented on GitHub (Mar 1, 2025): I got what `OnRdpClientDisconnected` does. Here we have three cases. Reconnect, display an error message and reconnect button, or close the window. The conditions for switching (bifurcation) may need to be reviewed. I couldn't find 0xb08 as a disconnect reason in the current Microsoft Learn, so the specification may have changed. `exDiscReasonNoInfo` may occur when the RDP server goes to sleep, in which case it may be better to display a reconnect button instead of closing or auto-reconnecting. However, the comment in the code says that win2008 (Windows Server 2008?) will report `exDiscReasonNoInfo` on logout. If the user has signed off, it would be better to close it, but that would be inconsistent with the above. Hmmm. I don't have the environment to try Windows Server, so I can't to investigate this. And I think `_retryCount` should be reset to 0 when a connection is made.
Author
Owner

@VShawn commented on GitHub (Mar 3, 2025):

Yes, win2008 = Windows Server 2008. And these are conclusions I reached after repeated testing several years ago.

I no longer have Windows Server 2008 now, so I am unable to test it either.

And I think _retryCount should be reset to 0 when a connection is made.

u are right.

<!-- gh-comment-id:2693455244 --> @VShawn commented on GitHub (Mar 3, 2025): Yes, win2008 = Windows Server 2008. And these are conclusions I reached after repeated testing several years ago. I no longer have Windows Server 2008 now, so I am unable to test it either. > And I think _retryCount should be reset to 0 when a connection is made. u are right.
Author
Owner

@VShawn commented on GitHub (Mar 15, 2025):

here is the reference of e.discReason

https://learn.microsoft.com/en-us/windows/win32/termserv/imstscaxevents-ondisconnected

<!-- gh-comment-id:2726323566 --> @VShawn commented on GitHub (Mar 15, 2025): here is the reference of e.discReason https://learn.microsoft.com/en-us/windows/win32/termserv/imstscaxevents-ondisconnected
Author
Owner

@itagagaki commented on GitHub (Apr 6, 2025):

If the RDP server works differently on Windows Server 2008, 2016, Windows 10, 11, etc., then I have no way to verify it. I'm stuck.

<!-- gh-comment-id:2781494681 --> @itagagaki commented on GitHub (Apr 6, 2025): If the RDP server works differently on Windows Server 2008, 2016, Windows 10, 11, etc., then I have no way to verify it. I'm stuck.
Author
Owner

@VShawn commented on GitHub (Apr 7, 2025):

well You won't be able to complete all the TEST on your owm. recommended to focus on the server versions that you can test, and leave the rest aside :)

<!-- gh-comment-id:2782412475 --> @VShawn commented on GitHub (Apr 7, 2025): well You won't be able to complete all the TEST on your owm. recommended to focus on the server versions that you can test, and leave the rest aside :)
Author
Owner

@github-actions[bot] commented on GitHub (May 8, 2025):

This issue is stale because it has been open for 30 days with no activity.

<!-- gh-comment-id:2861250925 --> @github-actions[bot] commented on GitHub (May 8, 2025): This issue is stale because it has been open for 30 days with no activity.
Author
Owner

@itagagaki commented on GitHub (May 10, 2025):

I have tried several cases with the Windows 10 Pro. I could see the following 4 in e.discReason.

  • 2 (disconnectReasonRemoteByUser)
  • 260 (disconnectReasonDNSLookupFailed)
  • 516 (disconnectReasonSocketConnectFailed)
  • 2308 (disconnectReasonAtClientWinsockFDCLOSE)

When user signed out it was 2.
Same for shutdown (makes sense since shutdown includes a sign-out step).
I think 1Remote should only close the session in this case, and reconnect in other cases.

@VShawn
The current code contains several conditional decisions. The history of this is not clear to me. If you recall, please let me know.
Also, why did you set 1Remote's reconnect process to disabled?

https://github.com/1Remote/1Remote/blame/81d19f150e671597cda2e24659bbdf28ad2e9489/Ui/View/Host/ProtocolHosts/AxMsRdpClient09Host.cs#L167

<!-- gh-comment-id:2868999428 --> @itagagaki commented on GitHub (May 10, 2025): I have tried several cases with the Windows 10 Pro. I could see the following 4 in `e.discReason`. - 2 (`disconnectReasonRemoteByUser`) - 260 (`disconnectReasonDNSLookupFailed`) - 516 (`disconnectReasonSocketConnectFailed`) - 2308 (`disconnectReasonAtClientWinsockFDCLOSE`) When user signed out it was 2. Same for shutdown (makes sense since shutdown includes a sign-out step). I think 1Remote should only close the session in this case, and reconnect in other cases. @VShawn The current code contains several conditional decisions. The history of this is not clear to me. If you recall, please let me know. Also, why did you set 1Remote's reconnect process to disabled? https://github.com/1Remote/1Remote/blame/81d19f150e671597cda2e24659bbdf28ad2e9489/Ui/View/Host/ProtocolHosts/AxMsRdpClient09Host.cs#L167
Author
Owner

@VShawn commented on GitHub (May 11, 2025):

The current code contains several conditional decisions. The history of this is not clear to me. If you recall, please let me know.

At that time, I tested the e.discReason on multiple systems (Windows 8, Windows 10, Windows Server 2000, Windows Server 2008, etc.) and found that they do not seem to return a consistent enumeration value when the user logs out, which led to the conditions as you see. Unfortunately, I no longer have enough servers to retest these systems.

Also, why did you set 1Remote's reconnect process to disabled?

I remember trying to get the auto reconnect feature to work properly but failed. https://github.com/1Remote/1Remote/issues/869 reported errors. I didn't have the time to resolve the problems then, so I disabled the feature.

<!-- gh-comment-id:2869464483 --> @VShawn commented on GitHub (May 11, 2025): > The current code contains several conditional decisions. The history of this is not clear to me. If you recall, please let me know. At that time, I tested the e.discReason on multiple systems (Windows 8, Windows 10, Windows Server 2000, Windows Server 2008, etc.) and found that they do not seem to return a consistent enumeration value when the user logs out, which led to the conditions as you see. Unfortunately, I no longer have enough servers to retest these systems. > Also, why did you set 1Remote's reconnect process to disabled? I remember trying to get the auto reconnect feature to work properly but failed. https://github.com/1Remote/1Remote/issues/869 reported errors. I didn't have the time to resolve the problems then, so I disabled the feature.
Author
Owner

@itagagaki commented on GitHub (May 12, 2025):

Done to fix reconnect logic.
Willing to submit PR after some testing, but it is blocked by #929.

The problem of random occasional hangs and crashes has not been resolved, so we need to investigate further. I will add it as a new issue.

<!-- gh-comment-id:2870479904 --> @itagagaki commented on GitHub (May 12, 2025): Done to fix reconnect logic. Willing to submit PR after some testing, but it is blocked by #929. The problem of random occasional hangs and crashes has not been resolved, so we need to investigate further. I will add it as a new issue.
Author
Owner

@itagagaki commented on GitHub (May 15, 2025):

I pushed the modification of the process after RDP disconnection to my branch.
https://github.com/itagagaki/1Remote/tree/fix-rdp-reconnection

It should now reconnect as expected.
However, it is blocked by #930.

<!-- gh-comment-id:2883725791 --> @itagagaki commented on GitHub (May 15, 2025): I pushed the modification of the process after RDP disconnection to my branch. https://github.com/itagagaki/1Remote/tree/fix-rdp-reconnection It should now reconnect as expected. However, it is blocked by #930.
Author
Owner

@github-actions[bot] commented on GitHub (Jun 15, 2025):

This issue is stale because it has been open for 30 days with no activity.

<!-- gh-comment-id:2973430842 --> @github-actions[bot] commented on GitHub (Jun 15, 2025): This issue is stale because it has been open for 30 days with no activity.
Author
Owner

@itagagaki commented on GitHub (Jun 15, 2025):

Sorry, but I've been busy lately and haven't had a chance to investigate this issue yet.

<!-- gh-comment-id:2973991557 --> @itagagaki commented on GitHub (Jun 15, 2025): Sorry, but I've been busy lately and haven't had a chance to investigate this issue yet.
Author
Owner

@VShawn commented on GitHub (Jun 16, 2025):

it's ok, plz take care of your self friend. I believe our daily live is much more important than working on open-source projects.

<!-- gh-comment-id:2977162469 --> @VShawn commented on GitHub (Jun 16, 2025): it's ok, plz take care of your self friend. I believe our daily live is much more important than working on open-source projects.
Author
Owner

@github-actions[bot] commented on GitHub (Jul 17, 2025):

This issue is stale because it has been open for 30 days with no activity.

<!-- gh-comment-id:3082198391 --> @github-actions[bot] commented on GitHub (Jul 17, 2025): This issue is stale because it has been open for 30 days with no activity.
Author
Owner

@itagagaki commented on GitHub (Jul 19, 2025):

Experimental results on Windows 10

Way to disconnect Reconnection attempt by RdpClient discReason disconnectReason*** ExtendedDisconnectReason exDiscReason***
Other connection Not ran ByServer (3) ReplacedByOtherConnection
Start - Disconnect Not ran RemoteByUser (2) LogoffByUser
Start - Signout Not ran RemoteByUser (2) LogoffByUser
Start - Shutdown Not ran RemoteByUser (2) LogoffByUser
Start - Reboot Not ran RemoteByUser (2) LogoffByUser
Communication breakdown (sleep, unplug, adapter invalidated, etc.) Canceled by user LocalNotError (1) NoInfo
^^ ditto Failed xMaxReconnectAttempts SocketConnectFailed (516) NoInfo
^^ ditto Not ran because EnableAutoReconnect:i:0 AtClientWinsockFDCLOSE (2308) NoInfo
^^ ditto Not ran because MaxReconnectAttempts:i:0 (2824) NoInfo

Based on this result, we can conclude that 1Remote should attempt to reconnect when ExtendedDisconnectReason is exDiscReasonNoInfo. However, according to the current source code, Windows Server 2008 returns exDiscReasonNoInfo when signing out too.

Therefore, it would be better to close the window immediately when discReason is either disconnectReasonRemoteByUser (2) or disconnectReasonByServer (3), and to attempt to reconnect in all other cases.

What would be better in the case of discReason is disconnectReasonLocalNotError (1), in UX?

<!-- gh-comment-id:3092296917 --> @itagagaki commented on GitHub (Jul 19, 2025): ## Experimental results on Windows 10 | Way to disconnect | Reconnection attempt by RdpClient | discReason `disconnectReason***` |ExtendedDisconnectReason `exDiscReason***` | |---|---|---|---| | Other connection | Not ran | `ByServer` (3) | `ReplacedByOtherConnection` | | Start - Disconnect | Not ran | `RemoteByUser` (2) | `LogoffByUser` | | Start - Signout | Not ran | `RemoteByUser` (2) | `LogoffByUser` | | Start - Shutdown | Not ran | `RemoteByUser` (2) | `LogoffByUser` | | Start - Reboot | Not ran | `RemoteByUser` (2) | `LogoffByUser` | | Communication breakdown (sleep, unplug, adapter invalidated, etc.) | Canceled by user | `LocalNotError` (1) | `NoInfo` | |^^ ditto | Failed x`MaxReconnectAttempts` | `SocketConnectFailed` (516) | `NoInfo` | |^^ ditto | Not ran because `EnableAutoReconnect:i:0` | `AtClientWinsockFDCLOSE` (2308) | `NoInfo` | |^^ ditto | Not ran because `MaxReconnectAttempts:i:0` | (2824) | `NoInfo` | Based on this result, we can conclude that 1Remote should attempt to reconnect when ExtendedDisconnectReason is `exDiscReasonNoInfo`. However, according to the current source code, Windows Server 2008 returns `exDiscReasonNoInfo` when signing out too. Therefore, it would be better to close the window immediately when discReason is either `disconnectReasonRemoteByUser` (2) or `disconnectReasonByServer` (3), and to attempt to reconnect in all other cases. What would be better in the case of discReason is `disconnectReasonLocalNotError` (1), in UX?
Author
Owner

@itagagaki commented on GitHub (Jul 26, 2025):

What would be better in the case of discReason is disconnectReasonLocalNotError (1), in UX?

Let's attempt to reconnect in order to switch to an alternate host. The user can dismiss it if he/she wants to.

<!-- gh-comment-id:3122035248 --> @itagagaki commented on GitHub (Jul 26, 2025): > What would be better in the case of discReason is `disconnectReasonLocalNotError` (1), in UX? Let's attempt to reconnect in order to switch to an alternate host. The user can dismiss it if he/she wants to.
Author
Owner

@itagagaki commented on GitHub (Jul 26, 2025):

I found a clue as to why the app freezes after RDP reconnecting.
The call to Shawn.Utils.Wpf.RelayCommand.CanExecute(null) repeats indefinitely.
@VShawn Do you have any advice on how to further investigate the cause?

<!-- gh-comment-id:3122041488 --> @itagagaki commented on GitHub (Jul 26, 2025): I found a clue as to why the app freezes after RDP reconnecting. The call to `Shawn.Utils.Wpf.RelayCommand.CanExecute(null)` repeats indefinitely. @VShawn Do you have any advice on how to further investigate the cause?
Author
Owner

@VShawn commented on GitHub (Jul 27, 2025):

The CanExecute method is called by the WPF UI, e.g. a button refreshes its disabled state through the CanExecuteChanged event of its bound ICommand object. Honestly, I don't think it would cause the UI to freeze.

We can temporarily remove all CanExecute methods to verify if they are the cause of the freeze.

Image

If they are indeed causing the freeze, the most straightforward solution would be to eliminate the CanExecute for the corresponding command.

Then I checked, and there is only one instance of relaycommand used in the RDP, so if relaycommand is the issue, modifying this one instance should suffice.

Image

Annd the CanExecute condition of this command just relate to whether the RDP is connected, and this status will change during reconnection.

Image
<!-- gh-comment-id:3123685427 --> @VShawn commented on GitHub (Jul 27, 2025): The `CanExecute` method is called by the WPF UI, e.g. a button refreshes its disabled state through the CanExecuteChanged event of its bound ICommand object. Honestly, I don't think it would cause the UI to freeze. We can temporarily remove all CanExecute methods to verify if they are the cause of the freeze. <img width="1394" height="228" alt="Image" src="https://github.com/user-attachments/assets/39cf121b-e3e1-4898-a244-1c484a543e36" /> If they are indeed causing the freeze, the most straightforward solution would be to eliminate the CanExecute for the corresponding command. Then I checked, and there is only one instance of relaycommand used in the RDP, so if relaycommand is the issue, modifying this one instance should suffice. <img width="1689" height="1097" alt="Image" src="https://github.com/user-attachments/assets/567ff4d6-3973-438b-b85c-4000eab9a38b" /> Annd the `CanExecute` condition of this command just relate to `whether the RDP is connected`, and this status will change during reconnection. <img width="1313" height="429" alt="Image" src="https://github.com/user-attachments/assets/96343a8a-49c4-46b3-9e50-57bbeb87c1cb" />
Author
Owner

@itagagaki commented on GitHub (Jul 27, 2025):

@VShawn
The problem seems to be in this block. Commenting it out will at least the UI will prevent the UI from freezing.

github.com/1Remote/1Remote@90aad35337/Ui/View/Host/ProtocolHosts/AxMsRdpClient09Host.cs (L63-L68)

Steps to reproduce:

  1. Set Host B as the alternate host for Host A
  2. Physically disconnect Host A and enable Host B to connect
  3. Connect to Host A
  4. The connection will be established on Host B
  5. Physically disconnect the connection
  6. Wait for the 1Remote dialog to appear, then select “Reconnect”
  7. Wait for the dialog to appear again, then select “Dismiss”
  8. The main window (host list) may freeze.
<!-- gh-comment-id:3124049413 --> @itagagaki commented on GitHub (Jul 27, 2025): @VShawn The problem seems to be in this block. Commenting it out will at least the UI will prevent the UI from freezing. https://github.com/1Remote/1Remote/blob/90aad35337addc42cbec4a73d13c54aebe72b394/Ui/View/Host/ProtocolHosts/AxMsRdpClient09Host.cs#L63-L68 Steps to reproduce: 1. Set Host B as the alternate host for Host A 2. Physically disconnect Host A and enable Host B to connect 3. Connect to Host A 4. The connection will be established on Host B 5. Physically disconnect the connection 6. Wait for the 1Remote dialog to appear, then select “Reconnect” 7. Wait for the dialog to appear again, then select “Dismiss” 8. The main window (host list) may freeze.
Author
Owner

@VShawn commented on GitHub (Jul 29, 2025):

I'm glad to hear that you've found the root block. We can start by removing it so that the reconnection feature can work properly.

I'm puzzled as to why it's causing the interface to freeze, as it performs well in another place where it's called.

It is likely that an error occurred in TcpHelper.TestConnectionAsync or FindFirstConnectableAddressAsync functions called within the GetCredential function.

I suspect that the error was caused by UI controls in FindFirstConnectableAddressAsync, so I have created a non-UI version of this function. Could you please test it for me since i failed to find the a net cable in my house now.


        /// <summary>
        /// Find the first connectable address from the given credentials. if return null then no address is connectable.
        /// </summary>
        public static async Task<Credential?> FindFirstConnectableAddressAsyncWithoutUI(IEnumerable<Credential> pingCredentials, string protocolDisplayName)
        {
            var credentials = pingCredentials.Select(x => x.CloneMe()).ToList();
            const int maxWaitSeconds = 5;
            var cts = new CancellationTokenSource();

            var uiPingItems = new List<PingTestItem>();
            foreach (var credential in credentials)
            {
                credential.Address = string.IsNullOrEmpty(credential.Address) ? credentials.First().Address : credential.Address;
                credential.Port = string.IsNullOrEmpty(credential.Port) ? credentials.First().Port : credential.Port;
                uiPingItems.Add(new PingTestItem(credential.Name, credential.Address + ":" + credential.Port)
                {
                    Status = PingStatus.None,
                });
            }

            var tasks = new List<Task<bool?>>();
            // add tasks to ping each credential
            for (int i = 0; i < credentials.Count; i++)
            {
                // give each task a different sleep time to avoid all tasks start at the same time
                var credential = credentials[i];
                var pingTestItem = uiPingItems[i];
                tasks.Add(Task.Factory.StartNew(() =>
                {
                    pingTestItem.Status = PingStatus.Pinging;
                    var startTime = DateTime.UtcNow;
                    var ret = TcpHelper.TestConnectionAsync(credential.Address, credential.Port, cts.Token, maxWaitSeconds * 1000).Result;
                    pingTestItem.Ping = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
                    pingTestItem.Status = ret switch
                    {
                        null => PingStatus.Canceled,
                        true => PingStatus.Success,
                        _ => PingStatus.Failed
                    };
                    Thread.Sleep(200); // To avoid the UI closing too quickly, making it hard to see.
                    return ret;
                }, cts.Token));
            }

            // an extra task to update the message
            var countingTask = Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < maxWaitSeconds; i++)
                {
                    if (cts.Token.IsCancellationRequested)
                    {
                        break;
                    }
                    Thread.Sleep(1000);
                }

                bool? ret = null;
                return ret;
            }, cts.Token);
            tasks.Add(countingTask);

            var delay = Task.Delay(500, cts.Token);

            // wait for the first task with result true or all tasks completed
            int completedTaskIndex = -1;
            var ts = tasks.ToArray();
            while (true)
            {
                if (ts.Any() == false
                    || ts.Length == 1 && ts.FirstOrDefault() == countingTask)
                {
                    break;
                }
                var completedTask = await Task.WhenAny(ts);
                if (completedTask.IsCanceled == false && completedTask.Result == true)
                {
                    completedTaskIndex = tasks.IndexOf(completedTask);
                    SimpleLogHelper.DebugInfo($"Task{completedTaskIndex} completed first. Cancelling remaining tasks.");
                    break;
                }
                ts = ts.Where(t => t != completedTask).ToArray();
            }

            // cancel all tasks
            if (ts.Any())
            {
                try
                {
                    await cts.CancelAsync();
                }
                catch (Exception)
                {
                    // ignored
                }
            }

            await delay;

            // return the first credential when ping success
            if (completedTaskIndex >= 0 && completedTaskIndex < tasks.Count)
            {
                // close the pop window
                return credentials[completedTaskIndex].CloneMe();
            }

            // none of the address is connectable
            return null;
        }
<!-- gh-comment-id:3130365613 --> @VShawn commented on GitHub (Jul 29, 2025): I'm glad to hear that you've found the root block. We can start by removing it so that the reconnection feature can work properly. I'm puzzled as to why it's causing the interface to freeze, as it performs well in another place where it's called. It is likely that an error occurred in TcpHelper.TestConnectionAsync or FindFirstConnectableAddressAsync functions called within the GetCredential function. I suspect that the error was caused by UI controls in FindFirstConnectableAddressAsync, so I have created a non-UI version of this function. Could you please test it for me since i failed to find the a net cable in my house now. ``` C# /// <summary> /// Find the first connectable address from the given credentials. if return null then no address is connectable. /// </summary> public static async Task<Credential?> FindFirstConnectableAddressAsyncWithoutUI(IEnumerable<Credential> pingCredentials, string protocolDisplayName) { var credentials = pingCredentials.Select(x => x.CloneMe()).ToList(); const int maxWaitSeconds = 5; var cts = new CancellationTokenSource(); var uiPingItems = new List<PingTestItem>(); foreach (var credential in credentials) { credential.Address = string.IsNullOrEmpty(credential.Address) ? credentials.First().Address : credential.Address; credential.Port = string.IsNullOrEmpty(credential.Port) ? credentials.First().Port : credential.Port; uiPingItems.Add(new PingTestItem(credential.Name, credential.Address + ":" + credential.Port) { Status = PingStatus.None, }); } var tasks = new List<Task<bool?>>(); // add tasks to ping each credential for (int i = 0; i < credentials.Count; i++) { // give each task a different sleep time to avoid all tasks start at the same time var credential = credentials[i]; var pingTestItem = uiPingItems[i]; tasks.Add(Task.Factory.StartNew(() => { pingTestItem.Status = PingStatus.Pinging; var startTime = DateTime.UtcNow; var ret = TcpHelper.TestConnectionAsync(credential.Address, credential.Port, cts.Token, maxWaitSeconds * 1000).Result; pingTestItem.Ping = (int)(DateTime.UtcNow - startTime).TotalMilliseconds; pingTestItem.Status = ret switch { null => PingStatus.Canceled, true => PingStatus.Success, _ => PingStatus.Failed }; Thread.Sleep(200); // To avoid the UI closing too quickly, making it hard to see. return ret; }, cts.Token)); } // an extra task to update the message var countingTask = Task.Factory.StartNew(() => { for (int i = 0; i < maxWaitSeconds; i++) { if (cts.Token.IsCancellationRequested) { break; } Thread.Sleep(1000); } bool? ret = null; return ret; }, cts.Token); tasks.Add(countingTask); var delay = Task.Delay(500, cts.Token); // wait for the first task with result true or all tasks completed int completedTaskIndex = -1; var ts = tasks.ToArray(); while (true) { if (ts.Any() == false || ts.Length == 1 && ts.FirstOrDefault() == countingTask) { break; } var completedTask = await Task.WhenAny(ts); if (completedTask.IsCanceled == false && completedTask.Result == true) { completedTaskIndex = tasks.IndexOf(completedTask); SimpleLogHelper.DebugInfo($"Task{completedTaskIndex} completed first. Cancelling remaining tasks."); break; } ts = ts.Where(t => t != completedTask).ToArray(); } // cancel all tasks if (ts.Any()) { try { await cts.CancelAsync(); } catch (Exception) { // ignored } } await delay; // return the first credential when ping success if (completedTaskIndex >= 0 && completedTaskIndex < tasks.Count) { // close the pop window return credentials[completedTaskIndex].CloneMe(); } // none of the address is connectable return null; } ```
Author
Owner

@itagagaki commented on GitHub (Aug 1, 2025):

The following log was obtained using the conventional FindFirstConnectableAddressAsync. Since the host, crow, is offline, it connects to the alternative, aero.

[T:009][21:26:19.795]   Debug   TcpHelper: timeoutTask is completed, cancel connectTask.
[T:009][21:26:19.843]   Debug   TcpHelper: Error connecting to crow:3389: A task may only be disposed if it is in a completion state (RanToCompletion, Faulted or Canceled).
[T:009][21:26:19.847]   Debug   FindFirstConnectableAddressAsync in 2 address, showing dlg...
[T:001][21:26:19.855]   Debug   AlternateAddressSwitchingView init
[T:009][21:26:19.929]   Debug   TcpHelper: Connected to aero:3389
[T:016][21:26:20.131]   Info    Task1 completed first. Cancelling remaining tasks.
[T:009][21:26:20.132]   Debug   TcpHelper: Connection cancelled.
[T:009][21:26:20.165]   Debug   TcpHelper: Error connecting to crow:3389: A task may only be disposed if it is in a completion state (RanToCompletion, Faulted or Canceled).
[T:001][21:26:20.476]   Debug   TabWindowView init
[T:001][21:26:20.589]   Debug   AxMsRdpClient09Host: Status => Initializing
[T:001][21:26:20.591]   Debug   RDP Host: _rdpClient.Dispose()
[T:001][21:26:20.597]   Debug   RDP Host: init new AxMsRdpClient9NotSafeForScriptingEx()
[T:001][21:26:20.599]   Debug   RDP Host: init CreateControl();
[T:001][21:26:20.683]   Debug   RDP Host: init Static
[T:001][21:26:20.687]   Debug   RDP Host: init conn bar
[T:001][21:26:20.690]   Debug   RDP Host: init Redirect
[T:001][21:26:20.694]   Debug   RDP Host: init Display with ScaleFactor = 150, W = 796, H = 566, isReconnecting = False
[T:001][21:26:20.698]   Info    RDP Host: init Display set DesktopWidth = 796 * 1.500 = 1194,  DesktopHeight = 566 * 1.500 = 849,     RdpControl.Width = 288, RdpControl.Height = 288
[T:001][21:26:20.699]   Info    RDP Host: init Display set DesktopWidth = 1194,  DesktopHeight = 848
[T:001][21:26:20.699]   Debug   RDP Host: Display init end: RDP.DesktopWidth = 1194, RDP.DesktopHeight = 848,
[T:001][21:26:20.700]   Debug   RDP Host: init Performance
[T:001][21:26:20.701]   Debug   RdpInit: DisplayPerformance = Auto, flag = 0
[T:001][21:26:20.703]   Debug   RDP Host: init Gateway
[T:001][21:26:20.714]   Debug   AxMsRdpClient09Host: Status => Initialized
[T:001][21:26:20.729]   Debug   AxMsRdpClient09Host: Status => Connecting
[T:001][21:26:21.104]   Debug   AxMsRdpClient09Host: Status => Connected
[T:001][21:26:21.501]   Debug   RDP Host:  RdpOnOnConnected
[T:009][21:26:21.809]   Warning [AxMsRdpClient09Host.xaml.cs(<.ctor>b__18_0:110)]       _loginResizeTimer stop
[T:001][21:26:25.221]   Debug   RDP Host:  OnRdpClientLoginComplete

The result of replacing it with WithoutUI version is as follows. No connection was made, no window opened, and no error message appeared.

[T:005][21:30:21.380]   Debug   TcpHelper: timeoutTask is completed, cancel connectTask.
[T:005][21:30:21.410]   Debug   TcpHelper: Error connecting to crow:3389: A task may only be disposed if it is in a completion state (RanToCompletion, Faulted or Canceled).
[T:018][21:30:21.445]   Debug   TcpHelper: Connected to aero:3389
[T:003][21:30:21.648]   Info    Task1 completed first. Cancelling remaining tasks.
[T:003][21:30:21.650]   Debug   TcpHelper: Connection cancelled.
[T:003][21:30:21.843]   Debug   TcpHelper: Error connecting to crow:3389: A task may only be disposed if it is in a completion state (RanToCompletion, Faulted or Canceled).
<!-- gh-comment-id:3144442050 --> @itagagaki commented on GitHub (Aug 1, 2025): The following log was obtained using the conventional `FindFirstConnectableAddressAsync`. Since the host, crow, is offline, it connects to the alternative, aero. ``` [T:009][21:26:19.795] Debug TcpHelper: timeoutTask is completed, cancel connectTask. [T:009][21:26:19.843] Debug TcpHelper: Error connecting to crow:3389: A task may only be disposed if it is in a completion state (RanToCompletion, Faulted or Canceled). [T:009][21:26:19.847] Debug FindFirstConnectableAddressAsync in 2 address, showing dlg... [T:001][21:26:19.855] Debug AlternateAddressSwitchingView init [T:009][21:26:19.929] Debug TcpHelper: Connected to aero:3389 [T:016][21:26:20.131] Info Task1 completed first. Cancelling remaining tasks. [T:009][21:26:20.132] Debug TcpHelper: Connection cancelled. [T:009][21:26:20.165] Debug TcpHelper: Error connecting to crow:3389: A task may only be disposed if it is in a completion state (RanToCompletion, Faulted or Canceled). [T:001][21:26:20.476] Debug TabWindowView init [T:001][21:26:20.589] Debug AxMsRdpClient09Host: Status => Initializing [T:001][21:26:20.591] Debug RDP Host: _rdpClient.Dispose() [T:001][21:26:20.597] Debug RDP Host: init new AxMsRdpClient9NotSafeForScriptingEx() [T:001][21:26:20.599] Debug RDP Host: init CreateControl(); [T:001][21:26:20.683] Debug RDP Host: init Static [T:001][21:26:20.687] Debug RDP Host: init conn bar [T:001][21:26:20.690] Debug RDP Host: init Redirect [T:001][21:26:20.694] Debug RDP Host: init Display with ScaleFactor = 150, W = 796, H = 566, isReconnecting = False [T:001][21:26:20.698] Info RDP Host: init Display set DesktopWidth = 796 * 1.500 = 1194, DesktopHeight = 566 * 1.500 = 849, RdpControl.Width = 288, RdpControl.Height = 288 [T:001][21:26:20.699] Info RDP Host: init Display set DesktopWidth = 1194, DesktopHeight = 848 [T:001][21:26:20.699] Debug RDP Host: Display init end: RDP.DesktopWidth = 1194, RDP.DesktopHeight = 848, [T:001][21:26:20.700] Debug RDP Host: init Performance [T:001][21:26:20.701] Debug RdpInit: DisplayPerformance = Auto, flag = 0 [T:001][21:26:20.703] Debug RDP Host: init Gateway [T:001][21:26:20.714] Debug AxMsRdpClient09Host: Status => Initialized [T:001][21:26:20.729] Debug AxMsRdpClient09Host: Status => Connecting [T:001][21:26:21.104] Debug AxMsRdpClient09Host: Status => Connected [T:001][21:26:21.501] Debug RDP Host: RdpOnOnConnected [T:009][21:26:21.809] Warning [AxMsRdpClient09Host.xaml.cs(<.ctor>b__18_0:110)] _loginResizeTimer stop [T:001][21:26:25.221] Debug RDP Host: OnRdpClientLoginComplete ``` The result of replacing it with `WithoutUI` version is as follows. No connection was made, no window opened, and no error message appeared. ``` [T:005][21:30:21.380] Debug TcpHelper: timeoutTask is completed, cancel connectTask. [T:005][21:30:21.410] Debug TcpHelper: Error connecting to crow:3389: A task may only be disposed if it is in a completion state (RanToCompletion, Faulted or Canceled). [T:018][21:30:21.445] Debug TcpHelper: Connected to aero:3389 [T:003][21:30:21.648] Info Task1 completed first. Cancelling remaining tasks. [T:003][21:30:21.650] Debug TcpHelper: Connection cancelled. [T:003][21:30:21.843] Debug TcpHelper: Error connecting to crow:3389: A task may only be disposed if it is in a completion state (RanToCompletion, Faulted or Canceled). ```
Author
Owner

@VShawn commented on GitHub (Aug 3, 2025):

Image

okey it did works when i test it. but I test it by on/off my wifi..

I need to check why it failed

log said: Task1 completed first. Cancelling remaining tasks.

it means completedTaskIndex == 1, and the func has found that aero is connectable, then the func should return return credentials[1].CloneMe();

The logic seems to be correct....

Image

I don't have any leads for now, willl think about it again tomorrow.

<!-- gh-comment-id:3148420368 --> @VShawn commented on GitHub (Aug 3, 2025): <img width="163" height="163" alt="Image" src="https://blog.codinghorror.com/content/images/2025/05/works-on-my-machine-v2-2025-jon-galloway-2-1.png" /> okey it did works when i test it. but I test it by on/off my wifi.. I need to check why it failed log said: `Task1 completed first. Cancelling remaining tasks. ` it means completedTaskIndex == 1, and the func has found that aero is connectable, then the func should return `return credentials[1].CloneMe();` The logic seems to be correct.... <img width="163" height="109" alt="Image" src="https://github.com/user-attachments/assets/ec203bc2-d1f3-44c7-9d49-8e6ee42d1822" /> I don't have any leads for now, willl think about it again tomorrow.
Author
Owner

@itagagaki commented on GitHub (Aug 3, 2025):

I found that a new RDP tab opens successfully when I insert the delay code into the WithoutUI function as follows.

                      SimpleLogHelper.DebugInfo($"Task{completedTaskIndex} completed first. Cancelling remaining tasks.");
                      break;
                  }
                  ts = ts.Where(t => t != completedTask).ToArray();
              }

+            Thread.Sleep(1000);
              // cancel all tasks
              if (ts.Any())
              {
                  try
                  {
                      await cts.CancelAsync();
<!-- gh-comment-id:3148485545 --> @itagagaki commented on GitHub (Aug 3, 2025): I found that a new RDP tab opens successfully when I insert the delay code into the `WithoutUI` function as follows. ```diff SimpleLogHelper.DebugInfo($"Task{completedTaskIndex} completed first. Cancelling remaining tasks."); break; } ts = ts.Where(t => t != completedTask).ToArray(); } + Thread.Sleep(1000); // cancel all tasks if (ts.Any()) { try { await cts.CancelAsync(); ```
Author
Owner

@VShawn commented on GitHub (Aug 4, 2025):

Today I went through the code again and found an out-of-bounds bug:

    // return the first credential when ping success
    if (completedTaskIndex >= 0 && completedTaskIndex < tasks.Count)
    {
        // close the pop window
        return credentials[completedTaskIndex].CloneMe();
    }

Here, tasks is ping tasks + timeout tasks. When completedTaskIndex = tasks.Count - 1, it causes credentials[completedTaskIndex] to go out of bounds.

For example, when credentials.Count = 2, then tasks = [ping credential 1, ping credential 2, countingTask].

So when completedTaskIndex = tasks.Count - 1 = 2, the if condition is true, and return credentials[2] goes out of bounds.

This might be the reason for the freezing issue, but it doesn't explain why I couldn't reproduce the freezing problem and why it frequently occurs in your environment.

<!-- gh-comment-id:3150373586 --> @VShawn commented on GitHub (Aug 4, 2025): Today I went through the code again and found an out-of-bounds bug: ``` // return the first credential when ping success if (completedTaskIndex >= 0 && completedTaskIndex < tasks.Count) { // close the pop window return credentials[completedTaskIndex].CloneMe(); } ``` Here, `tasks` is ping tasks + timeout tasks. When `completedTaskIndex = tasks.Count - 1`, it causes `credentials[completedTaskIndex]` to go out of bounds. For example, when `credentials.Count = 2`, then `tasks = [ping credential 1, ping credential 2, countingTask]`. So when `completedTaskIndex = tasks.Count - 1 = 2`, the `if` condition is true, and `return credentials[2]` goes out of bounds. This might be the reason for the freezing issue, but it doesn't explain why I couldn't reproduce the freezing problem and why it frequently occurs in your environment.
Author
Owner

@VShawn commented on GitHub (Aug 6, 2025):

I forgot to mention that I rewrote the function FindFirstConnectableAddressAsync and FindFirstConnectableAddressAsyncWithoutUI. I've tested it, and so far, so good. You can give it a try.

https://github.com/1Remote/1Remote/blob/main/Ui/Service/SessionControlService_AlternateCredential.cs#L234-L313

<!-- gh-comment-id:3157003061 --> @VShawn commented on GitHub (Aug 6, 2025): I forgot to mention that I rewrote the function `FindFirstConnectableAddressAsync` and `FindFirstConnectableAddressAsyncWithoutUI`. I've tested it, and so far, so good. You can give it a try. https://github.com/1Remote/1Remote/blob/main/Ui/Service/SessionControlService_AlternateCredential.cs#L234-L313
Author
Owner

@itagagaki commented on GitHub (Aug 9, 2025):

When I rebased and built for the first time in a while, the build failed because putty.exe was not found. Did you switch from KiTTY to PuTTY? What should I do?

<!-- gh-comment-id:3169725053 --> @itagagaki commented on GitHub (Aug 9, 2025): When I rebased and built for the first time in a while, the build failed because putty.exe was not found. Did you switch from KiTTY to PuTTY? What should I do?
Author
Owner

@VShawn commented on GitHub (Aug 9, 2025):

oh lucky me, I opened GitHub and saw the notification just 7 minutes after you spoke.

Yes i move to PuTTY in PR https://github.com/1Remote/1Remote/pull/936 because KiTTY hasn't been updated in years.

Have you pulled this submodule? I put putty.exe inside.

Image
<!-- gh-comment-id:3169787806 --> @VShawn commented on GitHub (Aug 9, 2025): > oh lucky me, I opened GitHub and saw the notification just 7 minutes after you spoke. Yes i move to PuTTY in PR https://github.com/1Remote/1Remote/pull/936 because KiTTY hasn't been updated in years. Have you pulled this submodule? I put putty.exe inside. <img width="676" height="864" alt="Image" src="https://github.com/user-attachments/assets/68c3186b-9925-4018-9089-476ce9544126" />
Author
Owner

@itagagaki commented on GitHub (Aug 9, 2025):

Thanks, I managed to build it. However, it still freezes or crashes. I will continue to investigate.

<!-- gh-comment-id:3169897236 --> @itagagaki commented on GitHub (Aug 9, 2025): Thanks, I managed to build it. However, it still freezes or crashes. I will continue to investigate.
Author
Owner

@VShawn commented on GitHub (Aug 9, 2025):

Thanks, I managed to build it. However, it still freezes or crashes. I will continue to investigate.

💔 really bad news.

<!-- gh-comment-id:3170484850 --> @VShawn commented on GitHub (Aug 9, 2025): > Thanks, I managed to build it. However, it still freezes or crashes. I will continue to investigate. 💔 really bad news.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/1Remote#2634
No description provided.