Password Cracking RAR Archives With Perl |
Written by Nikos Vaggalis | ||||
Monday, 13 March 2017 | ||||
Page 3 of 3
This approach is workable but can hide dangers if used improperly as described in 'Is DoEvents Evil?' on the Coding Horror website. Avoiding those dangers was the main reason (apart from the HCI principles) that all buttons except 'Pause' and 'Resume' are disabled while file processing is in effect. An additional issue is that update() is not nice to the rest of the threads running through the OS. I've already mentioned that update() had to be called in a loop inside the module. I could have hardcoded the update() call inside the module but that would hinder its universal intent; the module/library has to be independent because it can be called from client applications be it GUI driven ones or not. There is one programming trick that can help here which is very effective and goes back to the C programming days; make the library call your own user-defined function. This is done with the so called 'callback' which is a function pointer to a user-defined function. The pointer is then passed to the module's subroutine as a parameter. In this case the callback is a wrapper around the update() Tk function and is called at intervals from within the loop that does the file processing. Thus we keep the library independence and overcome the GUI 'freezing', at least to a degree. (gui.pl) my $callback=sub {$gui::top->update()}; (Unrar.pm) An additional advantage of single threading and tight coupling of the processing and GUI code is that pausing the module's operation was very easy. When the user presses the 'Pause' button, waitforvariable is called, pausing everything except the GUI which can still process events. (gui.pl) while (1) { Multi-threadedAlthough this improved the situation, it was not a complete remedy. I needed to use threads to separate concerns and also to decouple the GUI code from the actual processing code - the GUI had to be able to do its job at all times while a worker thread had to be doing all the file processing. Note that one worker thread would suffice in this case because we are dealing with the hard drive seeking files and we wouldn't want to stress the head by moving it relentlessly around; that would be the case if we kept on spawning threads that did file processing on their own. Thus, we just have to spawn one worker thread which we reuse. Of course the boss (GUI thread) and the worker thread had to be coordinated. In view of the thread safety issues prevailing in Tk, the initial approach was to use user-defined windows WM_ messages. The worker thread would send a custom made windows message to the GUI thread which would retrieve it from its Message Loop and fire an event upon it. This in effect would resemble a raw form of the Background worker .NET component, where a callback is fired on a worker thread and upon completion it fires an event on the GUI thread. This did not work because Tk uses its own window manager, not a native Win32 one, plus it does not provide low level access to the Tk window Windows Procedure which would allow mapping the custom message to an event handler (acting like a C++ MFC MESSAGE_MAP). This could be bypassed by implementing a custom low level hook to intercept all messages to the Tk window and handle them myself, but the complexity made the effort unworthwhile. Instead, a much cleaner way, in the form of two threadsafe queues was chosen; the queues would be responsible of coordinating the threads and carry the messaging between them. (Unrar_Extract_and_Recover.pl) The boss/GUI thread passes the information needed to start processing to the worker thread by using the $boss_to_worker_queue : (gui.pl) while the worker thread processes the file and communicates the result of the processing back to the boss thread through the $worker_to_boss_queue: (Unrar_Extract_and_Recover.pl) The boss thread then reads the message and acts upon it: (gui.pl) given ($message) { } when ("update") { when ("end") { } But how does the boss thread know when there is a message inside the queue waiting to be read while not blocking at the same time? We certainly cannot have the checking done in a loop since that would make the GUI freeze. For that reason normally polling is employed. The boss thread polls at a given internal, and check the queues. This is done by using an alarm: my $alarm = $mw->repeat( 200 => sub { But I have opted for another approach. Message loopThe heart of the GUI is its message loop. (gui.pl) sub init_gui { gui::userinit() if defined &gui::userinit When Tk::MainLoop() is reached , the GUI gets in a state of continuously looking for events; when there is one, it dispatches it to the appropriate event handler. However Tk::MainLoop() can be expanded into : while (Tk::MainWindow->Count) { So if I have access to a global while loop that continuously checks for events, why not use that to my advantage? Hence I hook into the main loop itself it and check for any thread queued messages with non-blocking dequeue_nb instead of polling (gui.pl)
When the worker thread finishes processing a file (successfully or unsuccessfully that is not the issue) it not only has to communicate the result back to the main thread, but also has to update the progress bar. Because of thread-affinity, all visual controls belong to the GUI thread and cannot be touched by our worker thread. Therefore when the main thread receives a message from the worker thread it also uses it to update the progress bar: $gui::percent_done += 1 Pausing operationThe callback proved handy again. Now there is no need to wrap the update() method anymore but we use it for pausing the worker thread. The callback now wraps the pause method of a Win32 Event. (Unrar_Extract_and_Recover.pl) When the user clicks 'Pause', which is in the main GUI thread's responsibility, a Win32 event is signalled which indicates that the worker should take a break from what it is doing. The worker thread checks inside the same loop as before for the signalling of the event and pauses the operation if it does. (gui.pl) (Unrar.pm) On the other hand when the user presses 'Resume' the event is switched to non-signalled state and the worker threads wakes up and resumes processing. A clean solution. (gui.pl) The good thing about a Win32 event is that it is a kernel object which means that it is visible by all threads (it is also visible by other processes hence it has to have a unique identifying name to avoid name collision) and is immediately visible by the worker thread with no need to coordinate anything All fine but there is a fundamental issue with Perl threading; it uses TLS (thread local storage) and when a thread is spawn it actually forks the whole interpreter which means that all state is duplicated into the new thread. All global variables and contents of the heap are not shared but are cloned seamlessly into the new thread (to actually share a variable or structure you have to explicitly share). This in effect means that (expect of the increased memory consumption) if the thread is spawned after the Tk window is active, then also the Tk window structure will be cloned into the new thread. However the handle of the original Tk window is a kernel object and thus not cloned; we now have one invisible orphan window. Hence it is common practice to spawn a thread before you create something that you do not want to be cloned. That is also the reason that we spawn the worker thread before we create the Tk window. Another issue with Tk is that its controls do not exchange windows messages (WM_) with native windows MFC controls correctly, which poses issues with drawing/painting the GUI such as this:
Here a button is clicked on the Tk main window which fires the MFC windows control for browsing directories (ShBrowseForFolder). Everything seems fine until you drag the control around which does not refresh the background. The way to handle it would be, again, to hook into the Tk Windows Procedure, capture the messages (especially WM_PAINT, WM_ERASEBKGND) and handle them manually.
SummarySo at the end of the day, what have we achieved by utilizing threading? We not only have a fully responsive GUI, no freezing, no waiting and more user satisfaction as an outcome, but also cleaner more maintainable code.
To be informed about new articles on I Programmer, sign up for our weekly newsletter, subscribe to the RSS feed and follow us on Twitter, Facebook or Linkedin.
Comments
or email your comment to: comments@i-programmer.info
|
||||
Last Updated ( Monday, 13 March 2017 ) |