Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(windows): handle a hard windows reset occurring while downloading updated keyman files #13128

Open
wants to merge 4 commits into
base: feat/windows/13167/add-release-kmutex
Choose a base branch
from

Conversation

rc-swag
Copy link
Contributor

@rc-swag rc-swag commented Feb 5, 2025

Fixes: #13119

When kmshell.exe is started to run configuration on any other number of switches, is in the downloading state, the state machine would just let the execution continue. This PR adds a check to ensure the Download process hasn't crashed in such away that let the state 'stuck' in the downloading state. It know resets the StateMachine if it finds that is the State Machine is in the downloading state but there is no active download process.

I used KeymanMutex because it was quite simple. If however it turns out that the mutex is not removed when the process crashes. Then it maybe better to use something like this https://stackoverflow.com/questions/876224/how-to-check-if-a-process-is-running-using-delphi and check for the -bd command line argument.

User Testing

TEST_DOWNLOAD_INTERUPTED_TASK_MANAGER

  1. Install Keyman attached the PR
  2. Start Keyman
  3. Open Regedit WinR type regedit
  4. Go to the current user key update state found at (Computer\HKEY_CURRENT_USER\SOFTWARE\Keyman\Keyman Engine).
  5. If required set the state back to usIdle
  6. Open Windows explorer at "C:\Users"yourusername"\AppData\Local\Keyman\UpdateCache" folder.
  7. Delete cache.json, any *.kmp and *.exe files.
  8. Go to the update tab and click check for updates
  9. Open a command prompt (kbd>WinR type cmd)
  10. Change to the directory where kmshell is installed
    cd "c:\Program Files (x86)\Keyman\Keyman Desktop"
  11. Type kmshell.exe -buc
  12. In the Regedit window verify the update state has advanced to either usUpdateAvailable or usDownloading. Press F5 to refresh the view.
  13. Press ctrl + shift + esc to open the task manager
    Screenshot 2025-02-07 152127
  14. In task manager find the command prompt that is running Keyman Configuration right click and choose End Task from the pop-menu
  15. Observe that the update state still says usDownloading.
  16. This test now wants to see recovery from this state and not stuck in usDownloading.
  17. Type kmshell.exe -c
  18. In the registry editor press F5 to observe the state changes. In my testing sometimes I missed seeing all the updates as it quickly resets to usIdle -> usUpdateAvailable -> usDownloading -> usWaitingRestart
  19. Expected Result The state should go to usWaitingRestart.

TEST_DOWNLOAD_INTERUPTED_HARD_POWER_OFF

I am not sure if the Test team can complete this test but I tested it twice.

  1. Install Keyman attached the PR
  2. Open Keyman Configuration and turn off start with Windows
  3. Open Regedit WinR type regedit
  4. Go to the current user key update state found at (Computer\HKEY_CURRENT_USER\SOFTWARE\Keyman\Keyman Engine).
  5. If required set the state back to usIdle
  6. Open Windows explorer at "C:\Users"yourusername"\AppData\Local\Keyman\UpdateCache" folder.
  7. Delete cache.json, any *.kmp and *.exe files.
  8. Go to the update tab and click check for updates
  9. Open a command prompt (kbd>WinR type cmd)
  10. Change to the directory where kmshell is installed
    cd "c:\Program Files (x86)\Keyman\Keyman Desktop"
  11. Type kmshell.exe -buc
  12. In the Regedit window verify the update state has advanced to either usUpdateAvailable or usDownloading. Press F5 to refresh the view.
  13. Power off the machine (not a clean shutdown but a power-off)
  14. Restart the Machine
  15. Observe that the update state still says usDownloading using the registry editor
  16. This test now wants to see recovery from this state and not stuck in usDownloading.
  17. Type kmshell.exe -c
  18. In the registry editor press F5 to observe the state changes. In my testing sometimes I missed seeing all the updates as it quickly resets to usIdle -> usUpdateAvailable -> usDownloading -> usWaitingRestart
  19. Expected Result The state should go to usWaitingRestart.

Using the KeymanMutex wrapper to check if a download process is occuring
if it isn't and we are in the downloading state this means the download
process exited early. We can then clean up any downloaded files and
reset the statemachine and check for updates again.
@keymanapp-test-bot keymanapp-test-bot bot added the user-test-missing User tests have not yet been defined for the PR label Feb 5, 2025
@keymanapp-test-bot
Copy link

keymanapp-test-bot bot commented Feb 5, 2025

User Test Results

Test specification and instructions

  • TEST_DOWNLOAD_INTERUPTED_TASK_MANAGER (PASSED) (notes)
  • TEST_DOWNLOAD_INTERUPTED_HARD_POWER_OFF (PASSED): notes developer tested

Test Artifacts

@keymanapp-test-bot keymanapp-test-bot bot added this to the B18S1 milestone Feb 5, 2025
@rc-swag rc-swag changed the base branch from master to feat/windows/13114/merge-update-reg-key February 5, 2025 05:43
Base automatically changed from feat/windows/13114/merge-update-reg-key to master February 6, 2025 11:46
@rc-swag rc-swag changed the title feat(windows): handle reset downloading feat(windows): handle a hard windows reset occurring while downloading updated keyman files Feb 7, 2025
@rc-swag rc-swag marked this pull request as ready for review February 7, 2025 05:19
@rc-swag rc-swag requested a review from ermshiperete as a code owner February 7, 2025 05:19
@keymanapp-test-bot keymanapp-test-bot bot added user-test-required User tests have not been completed and removed user-test-missing User tests have not yet been defined for the PR labels Feb 7, 2025
@rc-swag
Copy link
Contributor Author

rc-swag commented Feb 7, 2025

  • TEST_DOWNLOAD_INTERUPTED_HARD_POWER_OFF (PASS): notes developer tested

@rc-swag rc-swag self-assigned this Feb 7, 2025
ChangeState(IdleState);
bucStateContext.CurrentState.HandleCheck;
end;
FreeAndNil(FMutex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it matter that we call FreeAndNil twice if FMutex.MutexOwned? (same below)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good catch, I should exit early to avoid the redundant call. I have also added a comment to make clear to future me or other developers that the mutex needs to be freed before changing state and triggering a force check. Updated to use a try .. finally handling.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, FreeAndNil is safe to call on an already-nil variable, but it's definitely a code smell in any case 😁

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use a try .. finally handling.

Note that exiting early still runs the finally section

@dinakaranr
Copy link

Test Results

I tested this issue with the attached Keyman"18.0.184-alpha-test-13128" build(08/02/2025) on Windows 10. Here I am sharing my observation.

  • TEST_DOWNLOAD_INTERUPTED_TASK_MANAGER (Passed):
  1. Install Keyman-18.0.184.exe file
  2. Start Keyman
  3. Open Regedit WinR type regedit
  4. Go to the current user key update state found at (Computer\HKEY_CURRENT_USER\SOFTWARE\Keyman\Keyman Engine).
  5. If required set the state back to usIdle
  6. Open Windows Explorer at "C:\Users"yourusername"\AppData\Local\Keyman\UpdateCache" folder.
  7. Delete cache.json, any *.kmp and *.exe files.
  8. Go to the update tab and click the "check for new updates" button.
  9. Verified that the state is changing usUpdateAvailable -> usDownloading -> usWaitingRestart
  10. Open a command prompt (kbd>WinR type cmd)
  11. Change to the directory where kmshell is installed (cd "c:\Program Files (x86)\Keyman\Keyman Desktop")
  12. Type kmshell.exe -buc
  13. In the Regedit window verify the update state has advanced to either usUpdateAvailable --> usDownloading. Press F5 to refresh the view.
  14. In the usDownloading state: Press ctrl + shift + esc to open the task manager
  15. In the task manager find the command prompt that is running Keyman Configuration right click and choose End Task from the pop-menu
  16. Verified that the update state still says "usDownloading".
  17. This test now wants to see recovery from this state and not be stuck in usDownloading.
  18. Type kmshell.exe -c
  19. In the registry editor press F5 to observe the state changes.
  20. Verified that the "Update State" changing usIdle -> usUpdateAvailable -> usDownloading -> usWaitingRestart (It happens in very short duration so, refresh and see the changes)
  21. Verified that the "usWaitingRestart" state for "Update state"
    It works well. Thank you.

@keymanapp-test-bot keymanapp-test-bot bot removed the user-test-required User tests have not been completed label Feb 8, 2025
@rc-swag rc-swag requested a review from ermshiperete February 10, 2025 00:09
Copy link
Member

@mcdurdin mcdurdin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asking for some changes to this PR...

So, TKeymanMutex is my fault and has the terribly named MutexOwned function which takes ownership of the mutex. Let's fix that -- rename it to TakeOwnership and add a ReleaseOwnership function. That means a handful of one-line changes elsewhere but clarifies usage dramatically IMHO!

Comment on lines 766 to 767
try
FMutex := TKeymanMutex.Create('KeymanDownloading');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be outside the try-finally:

Suggested change
try
FMutex := TKeymanMutex.Create('KeymanDownloading');
FMutex := TKeymanMutex.Create('KeymanDownloading');
try

Also, given we use the same name in multiple calls to the constructor, can we make that a constant at top of implementation section?

const
  KeymanDownloadMutexName = 'KeymanDownloading';

begin
// Enter DownloadingState
bucStateContext.SetRegistryState(usDownloading);

RetryCount := 0;
DownloadResult := False;
FMutex := nil;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
FMutex := nil;

With the construction of FMutex outside try/finally, this is not needed

Comment on lines 775 to 780
while (not DownloadResult) and (RetryCount < 3) do
begin
DownloadResult := DownloadUpdatesBackground;
if not DownloadResult then
Inc(RetryCount);
end;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a repeat/until loop which is a little cleaner.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
while (not DownloadResult) and (RetryCount < 3) do
begin
DownloadResult := DownloadUpdatesBackground;
if not DownloadResult then
Inc(RetryCount);
end;
RetryCount := 0;
repeat
DownloadResult := DownloadUpdatesBackground;
Inc(RetryCount);
until DownloadResult or (RetryCount = 3);

Comment on lines 835 to 836
try
FMutex := TKeymanMutex.Create('KeymanDownloading');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
try
FMutex := TKeymanMutex.Create('KeymanDownloading');
FMutex := TKeymanMutex.Create('KeymanDownloading');
try

ditto

begin
// Downloading state, in other process, so continue
FMutex := nil;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
FMutex := nil;

if FMutex.MutexOwned then
begin
bucStateContext.RemoveCachedFiles;
FreeAndNil(FMutex); // Mutex must be freed before changing state
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would it be cleaner to relinquish ownership without freeing here. Add TKeymanMutex.Release function which calls ReleaseMutex()

Comment on lines 854 to 857
FMutex := nil;
// If downloading process is not running clean files and return to idle
try
FMutex := TKeymanMutex.Create('KeymanDownloading');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
FMutex := nil;
// If downloading process is not running clean files and return to idle
try
FMutex := TKeymanMutex.Create('KeymanDownloading');
// If downloading process is not running clean files and return to idle
FMutex := TKeymanMutex.Create('KeymanDownloading');
try

if FMutex.MutexOwned then
begin
bucStateContext.RemoveCachedFiles;
FreeAndNil(FMutex); // Mutex must be freed before changing state
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto .Release

FreeAndNil(FMutex); // Mutex must be freed before changing state
ChangeState(IdleState);
bucStateContext.CurrentState.HandleCheck;
Exit;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto not required

Suggested change
Exit;

Comment on lines 762 to 763
RetryCount := 0;
DownloadResult := False;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these should be immediately before their first use in the repeat/until loop, and then DownloadResult does not need to be pre-initialized either

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
RetryCount := 0;
DownloadResult := False;

…into feat/windows/13119/handle-reset-downloading

# Keyman Conventional Commit suggestions:
#
# - Link to a Sentry issue with git trailer:
#     Fixes: _MODULE_-_ID_
# - Give credit to co-authors:
#     Co-authored-by: _Name_ <_email_>
# - Use imperative, present tense ('attach' not 'attaches', 'attached' etc)
# - Don't include a period at the end of the title
# - Always include a blank line before trailers
# - More: https://github.com/keymanapp/keyman/wiki/Pull-Request-and-Commit-workflow-notes
@rc-swag rc-swag changed the base branch from master to feat/windows/13167/add-release-kmutex February 10, 2025 04:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

feat(windows): handle hard reboot while downloading update
4 participants