Skip to content

Commit 13997b9

Browse files
committed
#21: Encapsulate UI Automation element traversal retries in Unfucked.Windows, kind of like how CasperJS and Playwright do it
1 parent ea0fdfc commit 13997b9

3 files changed

Lines changed: 28 additions & 32 deletions

File tree

AuthenticatorChooser/AuthenticatorChooser.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
88
<ImplicitUsings>enable</ImplicitUsings>
99
<Nullable>enable</Nullable>
10-
<Version>0.3.0</Version>
10+
<Version>0.3.1</Version>
1111
<Authors>Ben Hutchison</Authors>
1212
<Copyright>© 2025 $(Authors)</Copyright>
1313
<Company>$(Authors)</Company>
@@ -30,10 +30,10 @@
3030
<ItemGroup>
3131
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
3232
<PackageReference Include="mwinapi" Version="0.3.0.5" />
33-
<PackageReference Include="NLog" Version="5.4.0" />
33+
<PackageReference Include="NLog" Version="5.5.0" />
3434
<PackageReference Include="System.Management" Version="9.0.5" />
35-
<PackageReference Include="ThrottleDebounce" Version="3.0.0-beta2" />
36-
<PackageReference Include="Unfucked.Windows" Version="0.0.1-beta.1" />
35+
<PackageReference Include="ThrottleDebounce" Version="3.0.0-beta3" />
36+
<PackageReference Include="Unfucked.Windows" Version="0.0.1-beta.2" />
3737
<PackageReference Include="Workshell.PE.Resources" Version="4.0.0.147" />
3838
</ItemGroup>
3939

AuthenticatorChooser/WindowsSecurityKeyChooser.cs

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Runtime.InteropServices;
55
using System.Windows.Automation;
66
using System.Windows.Input;
7-
using ThrottleDebounce;
87
using Unfucked;
98

109
namespace AuthenticatorChooser;
@@ -46,31 +45,29 @@ public override void chooseUsbSecurityKey(SystemWindow fidoPrompt) {
4645
}
4746

4847
IEnumerable<string> expectedTitles = I18N.getStrings(I18N.Key.SIGN_IN_WITH_YOUR_PASSKEY).Concat(skipAllNonSecurityKeyOptions ? I18N.getStrings(I18N.Key.MAKING_SURE_ITS_YOU) : []).ToList();
49-
try {
50-
// #21: title not rendered immediately
51-
AutomationElement titleLabel = retry(() => outerScrollViewer.FindFirst(TreeScope.Children, TITLE_CONDITION) ?? throw new NullReferenceException(), 18); // #4, #15
52-
string? actualTitle = titleLabel.GetCurrentPropertyValue(AutomationElement.NameProperty) as string;
48+
// #21: title not rendered immediately
49+
if (outerScrollViewer.WaitForFirst(TreeScope.Children, TITLE_CONDITION, TimeSpan.FromSeconds(5)) is { } titleLabel) {
50+
string? actualTitle = titleLabel.GetCurrentPropertyValue(AutomationElement.NameProperty) as string;
5351
if (expectedTitles.Any(expected => expected.Equals(actualTitle, StringComparison.CurrentCulture))) {
5452
LOGGER.Trace("Found dialog title {0:N3} sec after dialog opened", overallStopwatch.Elapsed.TotalSeconds);
5553
} else {
5654
LOGGER.Debug("Window is not a passkey choice prompt because the first TextBlock child of the ScrollViewer has the name {actual} instead of {expected}", actualTitle,
5755
string.Join(" or ", expectedTitles));
5856
return;
5957
}
60-
} catch (NullReferenceException) {
58+
} else {
6159
LOGGER.Debug("Window is not a passkey choice prompt because there is no TextBlock child of the ScrollViewer after retrying for {0:N3}", overallStopwatch.Elapsed.TotalSeconds);
6260
return;
6361
}
6462

6563
LOGGER.Trace("Window 0x{hwnd:x} is a Windows Security window", fidoPrompt.HWnd);
6664

67-
Stopwatch authenticatorChoicesStopwatch = Stopwatch.StartNew();
68-
ICollection<AutomationElement> authenticatorChoices;
69-
try {
70-
authenticatorChoices = retry(() => outerScrollViewer.FindFirst(TreeScope.Children, CREDENTIALS_LIST_ID_CONDITION).Children().ToList(), 127);
65+
Stopwatch authenticatorChoicesStopwatch = Stopwatch.StartNew();
66+
// #5, #11: power series backoff, max=500 ms per attempt, ~1 minute total
67+
if (outerScrollViewer.WaitForFirst(TreeScope.Children, CREDENTIALS_LIST_ID_CONDITION, el => el.Children().ToList(), TimeSpan.FromMinutes(1)) is { } authenticatorChoices) {
7168
LOGGER.Trace("Found authenticator choices after retrying for {0:N3} sec", authenticatorChoicesStopwatch.Elapsed.TotalSeconds);
72-
} catch (Exception e) when (e is not OutOfMemoryException) {
73-
LOGGER.Warn(e, "Could not find authenticator choices after retrying for {0:N3} sec due to the following exception. Giving up and not automatically selecting Security Key.",
69+
} else {
70+
LOGGER.Warn("Could not find authenticator choices after retrying for {0:N3} sec due to the following exception. Giving up and not automatically selecting Security Key.",
7471
authenticatorChoicesStopwatch.Elapsed.TotalSeconds);
7572
return;
7673
}
@@ -113,17 +110,15 @@ public override void chooseUsbSecurityKey(SystemWindow fidoPrompt) {
113110
((InvokePattern) nextButton.GetCurrentPattern(InvokePattern.Pattern)).Invoke();
114111
LOGGER.Info("Next button pressed {0:N3} sec after dialog appeared", overallStopwatch.Elapsed.TotalSeconds);
115112
}
113+
} catch (ElementNotAvailableException e) {
114+
LOGGER.Error(e, "Element in Windows Security dialog box disappeared before this program could interact with it, skipping this dialog box instance");
116115
} catch (COMException e) {
117116
LOGGER.Error(e, "UI Automation error while selecting security key, skipping this dialog box instance");
118117
} catch (Exception e) when (e is not OutOfMemoryException) {
119118
LOGGER.Error(e, "Uncaught exception while handling Windows Security dialog box, skipping it");
120119
}
121120
}
122121

123-
// #5, #11: power series backoff, max=500 ms per attempt, ~1 minute total
124-
private static T retry<T>(Func<T> attempt, int maxAttempts) =>
125-
Retrier.Attempt(_ => attempt(), maxAttempts, Retrier.Delays.Power(TimeSpan.FromMilliseconds(1), max: TimeSpan.FromMilliseconds(500)));
126-
127122
// Window name and title are localized, so don't match against those
128123
public override bool isFidoPromptWindow(SystemWindow window) => window.ClassName == WINDOW_CLASS_NAME;
129124

AuthenticatorChooser/packages.lock.json

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
},
2323
"NLog": {
2424
"type": "Direct",
25-
"requested": "[5.4.0, )",
26-
"resolved": "5.4.0",
27-
"contentHash": "LwMcGSW3soF3/SL68rlJN3Eh3ktrAPycC3zZR/07OYBPraZUu0bygEC7kIN10lUQgMXT4s84Fi1chglGdGrQEg=="
25+
"requested": "[5.5.0, )",
26+
"resolved": "5.5.0",
27+
"contentHash": "FCH8s7GWlonH5JXV9/EpeNJ8pRZQMVZOSWX3JrHPU8rzdHJhS5+lUGGvJIUOtzkGV1clYBFR0WXOI5FnUwVCMA=="
2828
},
2929
"System.Management": {
3030
"type": "Direct",
@@ -37,17 +37,18 @@
3737
},
3838
"ThrottleDebounce": {
3939
"type": "Direct",
40-
"requested": "[3.0.0-beta2, )",
41-
"resolved": "3.0.0-beta2",
42-
"contentHash": "VHkd+bDkUGBP9+PIARIgKAiczlrItAq2x/zmLMnuIbwI8QDXPUvIYoOQbyGmKVdgODC6V37a0FT4eIhwESPWjA=="
40+
"requested": "[3.0.0-beta3, )",
41+
"resolved": "3.0.0-beta3",
42+
"contentHash": "pZTefK4xZEvc5Q+M63lIAKVl8ZDsPWiUvuFk1+d9q3Hmgme76P3qC07Ho40ccaUiUKQYnO3a/dnu/SD8CsPZqQ=="
4343
},
4444
"Unfucked.Windows": {
4545
"type": "Direct",
46-
"requested": "[0.0.1-beta.1, )",
47-
"resolved": "0.0.1-beta.1",
48-
"contentHash": "w/+ZPkg+HhnFsH5rYUnciaO+WZjFqWgbJX7QtR5TmNzw9EmWjVuVBOnruwZJRMgrAS/hNqyQulEPdXpoDZgyjw==",
46+
"requested": "[0.0.1-beta.2, )",
47+
"resolved": "0.0.1-beta.2",
48+
"contentHash": "Q1Yu/PGfDa/MUfgSq2Tb8zrWK8mraF49sOg+fDkNg8+bCFho98+l64u7n973Gei+1bIcfmXP/OA9cn5Yv378TA==",
4949
"dependencies": {
50-
"Unfucked": "0.0.1-beta.1",
50+
"ThrottleDebounce": "3.0.0-beta3",
51+
"Unfucked": "0.0.1-beta.5",
5152
"mwinapi": "0.3.0.5"
5253
}
5354
},
@@ -86,8 +87,8 @@
8687
},
8788
"Unfucked": {
8889
"type": "Transitive",
89-
"resolved": "0.0.1-beta.1",
90-
"contentHash": "K20GUaGtfJ0uPLaWzQSFkM8zjWdNl6jTWicSwxb0EV7w0vJ433TUVgWB6zO9gYXwNLrlbWK4GJHZMo+NLA4nOw=="
90+
"resolved": "0.0.1-beta.5",
91+
"contentHash": "2vpz1u3knWf7F7QCbns1lbEps0wWWeJa1d5lGHiSQ9JTK3YBvd6Nn1jh3AzNTxSLppb9SAPUijLKP63kI3DdvA=="
9192
},
9293
"Workshell.PE": {
9394
"type": "Transitive",

0 commit comments

Comments
 (0)