| Index: installer/src/custom-action/close_application.cpp |
| =================================================================== |
| --- a/installer/src/custom-action/close_application.cpp |
| +++ b/installer/src/custom-action/close_application.cpp |
| @@ -2,48 +2,441 @@ |
| * \file close_application.cpp |
| */ |
| +#include <algorithm> |
| + |
| #include "session.h" |
| #include "property.h" |
| #include "database.h" |
| +#include "process.h" |
| +#include "interaction.h" |
| +#include "custom-i18n.h" |
| +//------------------------------------------------------- |
| +//------------------------------------------------------- |
| +class IE_Closer |
|
Oleksandr
2014/03/12 19:26:04
I am not a fan of a name IE_Closer here. Seeing th
Eric
2014/03/17 12:26:46
Well, a proper name would indicate that we're clos
|
| +{ |
| + Snapshot snapshot ; |
|
Oleksandr
2014/03/12 19:26:04
Cosmetic note here and further. We don't usually h
Eric
2014/03/17 12:26:46
I seem to always use more whitespace than most. I'
|
| -#include <TlHelp32.h> |
| + Process_Closer ie_closer ; |
| + Process_Closer engine_closer ; |
| + |
| + static const wchar_t * ie_names[] ; |
| + static const wchar_t * engine_names[] ; |
| + //Process_Closer iec( snapshot, IE_names, 2 ) ; |
|
Oleksandr
2014/03/12 19:26:04
This should probably be deleted?
Eric
2014/03/17 12:26:46
Yep. That's the previous algorithm I was using to
|
| + |
| +public: |
| + IE_Closer() |
| + : snapshot(), ie_closer( snapshot, ie_names, 1 ), engine_closer( snapshot, engine_names, 1 ) |
|
Oleksandr
2014/03/12 19:26:04
One more cosmetic comment - we don't usually have
|
| + {} |
| + |
| + void refresh() |
| + { |
| + snapshot.refresh() ; |
| + ie_closer.refresh() ; |
| + engine_closer.refresh() ; |
| + } |
| + |
| + bool is_running() |
| + { |
| + return ie_closer.is_running() || engine_closer.is_running() ; |
| + } |
| + |
| + bool shut_down() |
| + { |
| + if ( ie_closer.is_running() && ! ie_closer.shut_down() ) |
| + { |
| + // Assert IE is still running |
| + // This is after we've tried to shut it down, so we fail |
| + return false ; |
| + } |
| + if ( engine_closer.is_running() && ! engine_closer.shut_down() ) |
| + { |
| + // Assert the engine is still running |
| + // This is after IE has shut down itself and after we've tried to shut down the engine. Whatever. |
| + return false ; |
| + } |
| + return true ; |
| + } |
| +} ; |
| + |
| +const wchar_t * IE_Closer::ie_names[] = { L"IExplore.exe" } ; |
| +const wchar_t * IE_Closer::engine_names[] = { L"AdblockPlusEngine.exe" } ; |
| + |
| +//------------------------------------------------------- |
| +// abp_close_ie |
| +//------------------------------------------------------- |
| /** |
| * Exposed DLL entry point for custom action. |
| * The function signature matches the calling convention used by Windows Installer. |
| * |
| + * This function supports four policy stances with respect to a running IE process. |
| + * |
| + * - Allow reboot. |
| + * IE is running, so what? I'm willing to reboot after installation. |
| + * - Avoid reboot passively. |
| + * I don't want to affect any other running processes, but I don't want to reboot. I'll abort the installation. |
| + * - Avoid reboot actively. |
| + * I want to shut down IE in order to avoid a reboot. |
| + * I'll do it manually when the time is right. |
| + * - Avoid reboot automatically. |
| + * I want to shut down IE automatically in order to avoid a reboot. |
| + * |
| + * In a silent installation, the default stance is "allow reboot", which is to say, to act like most installers. |
| + * In an interactive installation, the stance is gathered from the user through dialog boxes. |
| + * If the MSI property AVOIDREBOOT is set to one of the values NO, PASSIVE, ACTIVE, or AUTOMATIC, the policy is set accordingly. |
| + * In a silent installation, this is the only way getting a stance other than the default. |
| + * In an interactive installation, AVOIDREBOOT skips the dialogs. |
| + * |
| * \param[in] session_handle |
| * Windows installer session handle |
| + * |
| + * \return |
| + * An integer interpreted as a custom action return value. |
| + * |
| + * \post |
| + * + The return value is one of the following: |
| + * - ERROR_INSTALL_USEREXIT if the user cancelled installation. |
| + * - ERROR_INSTALL_FAILURE if something unexpected happened, usually if the top level try-block caught an exception. |
| + * - ERROR_SUCCESS otherwise. |
| + * + The function performed at least one check that Internet Explorer was running. |
| + * + If ERROR_SUCCESS, the MSI property RUNNINGBROWSER is set and has one of the following values: |
| + * - 1 if Internet Explorer was running upon the last check. |
| + * - 0 otherwise. |
| + * \post |
| + * Note that this function cannot provide any assurance that Internet Explorer stays either stays running or stays not running. |
| + * |
| + * \sa |
| + * - MSDN [Custom Action Return Values](http://msdn.microsoft.com/en-us/library/aa368072%28v=vs.85%29.aspx) |
| */ |
| extern "C" UINT __stdcall |
| -abp_close_applications( MSIHANDLE session_handle ) |
| +abp_close_ie( MSIHANDLE session_handle ) |
| { |
| - // Always supply an externally-exposed function with a catch-all block |
| + // Utility typedef to shorten the class name. |
| + typedef Installer_Message_Box IMB ; |
| + |
| + /* |
| + * Immediate_Session cannot throw, so it can go outside the try-block. |
| + * It's needed in the catch-all block to write an error message to the log. |
| + */ |
| + Immediate_Session session( session_handle, "abp_close_ie" ) ; |
| + |
| + // The body of an entry point function must have a catch-all. |
| try { |
| - Immediate_Session session( session_handle, L"abp_close_applications" ) ; |
| - session.log( L"Have session object" ) ; |
| + // MSI property BROWSERRUNNING is one of the return values of this function. |
| + Property browser_running( session, L"BROWSERRUNNING" ) ; |
| + Property browser_closed( session, L"BROWSERCLOSED" ) ; |
| + |
| + // Instantiation of Process_Closer takes a snapshot. |
| + IE_Closer iec ; |
| + |
| + /* |
| + * We take the short path through this function if IE is not running at the outset. |
|
Oleksandr
2014/03/12 19:26:04
IE >or Engine<, actually
Eric
2014/03/17 12:26:46
Done.
|
| + */ |
| + if ( ! iec.is_running() ) |
| + { |
| + browser_running = L"0" ; // The browser is not running. |
| + browser_closed = L"0" ; // We didn't close the browser (and we couldn't have). |
| + session.log( "IE not running. No issue with reboot policy." ) ; |
| + return ERROR_SUCCESS ; |
| + } |
| + |
| + /* |
| + * As a (potentially) user-driven function, a state machine manages control flow. |
| + * The states are organized around the policy stances. |
| + */ |
| + enum Policy_State { |
| + // Non-terminal states |
| + not_known, // We don't know the user's stance at all |
| + part_known, // The user has indicated either ACTIVE or AUTOMATIC |
| + active, // Actively avoid reboot |
| + automatic, // Automatically avoid reboot |
| + // Terminal states |
| + success, |
| + abort, |
| + // Aliases for what would ordinarily be non-terminal states. |
| + // They're terminal because of implementation details. |
| + allow = success, // Allow reboot. |
| + passive = abort, // Passively avoid reboot, that is, don't try to close IE. |
| + }; |
| + Policy_State state = not_known; |
|
Oleksandr
2014/03/12 19:26:04
Redundant initialization here
Eric
2014/03/17 12:26:46
Done.
|
| + |
| + /* |
| + * Use the AVOIDREBOOT property, if present, to set an initial state. |
| + */ |
| + std::wstring avoid_reboot = Property( session, L"AVOIDREBOOT" ) ; |
| + std::transform( avoid_reboot.begin(), avoid_reboot.end(), avoid_reboot.begin(), ::towupper ) ; |
| + if ( avoid_reboot == L"" ) |
| + { |
| + state = not_known ; |
| + } |
| + else if ( avoid_reboot == L"NO" ) |
| + { |
| + state = allow ; |
| + session.log( "Reboot allowed on command line." ) ; |
| + } |
| + else if ( avoid_reboot == L"PASSIVE" ) |
| + { |
| + state = passive ; |
| + session.log( "Reboot avoided on command line." ) ; |
| + } |
| + else if ( avoid_reboot == L"ACTIVE" ) |
| + { |
| + state = active ; |
| + } |
| + else if ( avoid_reboot == L"AUTOMATIC" ) |
| + { |
| + state = automatic ; |
| + } |
| + else |
| + { |
| + // It's an error to specify an unrecognized value for AVOIDREBOOT. |
| + throw std::runtime_error( "unrecognized value for AVOIDREBOOT" ) ; |
| + } |
| + |
| + /* |
| + * When running as an update (see Updater.cpp), this installer receives the command line option "/qb", |
| + * which sets the UI level to "Basic UI". |
| + * When running as an initial install, we cannot expect what command line options this installer receives. |
| + */ |
| + /* |
| + * The UILevel property indicates whether we have the ability to put dialog boxes up. |
| + * Levels 2 (silent) and 3 (basic) do not have this ability. |
| + * Levels 4 (reduced) and 5 (full) do. |
| + * |
| + * MSDN [UILevel property](http://msdn.microsoft.com/en-us/library/windows/desktop/aa372096%28v=vs.85%29.aspx) |
| + */ |
| + std::wstring uilevel = Property( session, L"UILevel" ) ; |
| + bool interactive ; |
| + if ( uilevel == L"5" || uilevel == L"4" ) |
| + { |
| + interactive = true ; |
| + // Assert state is one of { not_known, allow, passive, active, automatic } |
| + } |
| + else if ( uilevel == L"3" || uilevel == L"2" ) |
| + { |
| + // Assert installer is running without user interaction. |
| + interactive = false ; |
| + if ( state == not_known ) |
| + { |
| + // Assert AVOIDREBOOT was not specified |
| + /* |
| + * This is where we specify default behavior for non-interactive operation. |
| + * The choice of "allow" makes it act like other installers, which is to make no effort to avoid a reboot after installation. |
| + */ |
| + state = allow ; |
| + session.log( "Reboot allowed by default in non-interactive session." ) ; |
| + } |
| + else if ( state == active ) |
| + { |
| + throw std::runtime_error( "AVOIDREBOOT=ACTIVE in non-interative session is not consistent" ) ; |
| + } |
| + // Assert state is one of { allow, passive, automatic } |
| + } |
| + else |
| + { |
| + throw std::runtime_error( "unrecognized value for UILevel" ) ; |
| + } |
| + |
| + /* |
| + * Now that preliminaries are over, we set up the accessors for UI text. |
| + * We only use the object 'message_text' for interactive sessions, but it's cheap to set up and a hassle to conditionalize. |
| + * |
| + * The key "close_ie" is the component name within the file "close_ie.wxi" that defines rows in the localization table. |
| + * The identifiers for the message_text.text() function are defined within that file. |
| + */ |
| Installation_Database db( session ) ; |
| - session.log( L"Have database object" ) ; |
| + custom_message_text message_text( db, L"close_ie" ) ; |
| - session.log( L"Still with new Property operator+ implementations!" ) ; |
| - session.log( L"VersionMsi = " + Property( session, L"VersionMsi" ) ) ; |
| + /* |
| + * State machine: Loop through non-terminal states. |
| + * |
| + * Loop invariant: IE was running at last check, that is, iec.is_running() would return true. |
| + */ |
| + while ( state <= automatic ) // "automatic" is the non-terminal state with the highest value |
| + { |
| + switch ( state ) |
| + { |
| + case not_known: |
| + /* |
| + * Precondition: interactive session |
| + * |
| + * Ask the user "Would you like to close IE and avoid reboot?" |
| + * Yes -> Close IE somehow. Goto part_known. |
| + * No -> Install with reboot. Goto allow. |
| + * Cancel -> terminate installation. Goto abort. |
| + */ |
| + { |
| + int x = session.write_message( IMB( message_text.text( L"dialog_unknown" ), IMB::warning_box, IMB::yes_no_cancel, IMB::default_button_three ) ) ; |
| + switch ( x ) |
| + { |
| + case IDYES: |
| + state = part_known ; |
| + break ; |
| + case IDNO: |
| + state = allow ; |
| + session.log( "User chose to allow reboot" ) ; |
| + break ; |
| + case IDCANCEL: |
| + state = abort ; |
| + session.log( "User cancelled installation" ) ; |
| + break ; |
| + default: |
| + throw unexpected_return_value_from_message_box() ; |
| + } |
| + } |
| + break ; |
| - Property tv( session, L"TESTVARIABLE" ) ; |
| - session.log( L"TESTVARIABLE = " + tv ) ; |
| - session.log( L"Setting TESTVARIABLE to 'testvalue'" ) ; |
| - tv = L"testvalue" ; |
| - session.log( L"TESTVARIABLE = " + tv ) ; |
| + case part_known: |
| + /* |
| + * Precondition: interactive session |
| + * |
| + * Ask the user "Would you like the installer to close IE for you?" |
| + * Yes -> Goto automatic |
| + * No -> Goto active |
| + * Cancel -> Goto not_known |
| + */ |
| + { |
| + int x = session.write_message( IMB( message_text.text( L"dialog_part_known" ), IMB::warning_box, IMB::yes_no_cancel, IMB::default_button_three ) ) ; |
| + switch ( x ) |
| + { |
| + case IDYES: |
| + state = automatic ; |
| + break ; |
| + case IDNO: |
| + state = active ; |
| + break ; |
| + case IDCANCEL: |
| + state = not_known ; |
| + break ; |
| + default: |
| + throw unexpected_return_value_from_message_box() ; |
| + } |
| + } |
| + break ; |
| + |
| + case active: |
| + /* |
| + * Precondition: interactive session |
| + * |
| + * IE is no longer running -> Goto success |
| + * IE is still running -> |
| + * Ask the user to close IE manually |
| + * OK -> re-enter this state |
| + * Cancel -> Goto not_known |
| + */ |
| + { |
| + int x = session.write_message( IMB( message_text.text( L"dialog_active_retry" ), IMB::warning_box, IMB::ok_cancel, IMB::default_button_one ) ) ; |
| + switch ( x ) |
| + { |
| + case IDOK: |
| + /* |
| + * Refresh our knowledge of whether IE is running. |
| + * If it is, we display the dialog again. The state doesn't change, so we just iterate again. |
| + * If it's not, then the user has closed IE and we're done. |
| + */ |
| + iec.refresh() ; |
| + if ( ! iec.is_running() ) |
| + { |
| + state = success ; |
| + session.log( "User shut down IE manually." ) ; |
| + } |
| + break ; |
| + case IDCANCEL: |
| + state = not_known ; |
| + break ; |
| + default: |
| + throw unexpected_return_value_from_message_box() ; |
| + } |
| + } |
| + break ; |
| + |
| + case automatic: |
| + /* |
| + * Close all known IE instances. |
| + * Unlike other cases, this state starts with an action and not a user query. |
| + * We first shut down IE, or at least attempt to. |
| + * |
| + * Succeeded -> Goto success |
| + * Failed && interactive -> |
| + * Ask user if they would like to try again |
| + * Retry -> re-enter this state |
| + * Cancel -> Goto not_known |
| + * Failed && not interactive -> Goto abort |
| + */ |
| + { |
| + bool IE_was_closed = iec.shut_down() ; |
| + if ( iec.is_running() ) |
| + { |
| + session.log( "Attempt to shut down IE automatically failed." ) ; |
| + if ( interactive ) |
| + { |
| + // Assert Interactive session and IE did not shut down. |
| + int x = session.write_message( IMB( message_text.text( L"dialog_automatic_retry" ), IMB::warning_box, IMB::retry_cancel, IMB::default_button_one ) ) ; |
| + switch ( x ) |
| + { |
| + case IDRETRY: |
| + // Don't change the state. Iterate again. |
| + break ; |
| + case IDCANCEL: |
| + state = not_known ; |
| + break ; |
| + default: |
| + throw unexpected_return_value_from_message_box() ; |
| + } |
| + } |
| + else |
| + { |
| + // Assert Non-interactive session and IE did not shut down. |
| + state = abort ; |
| + session.log( "Failed to shut down IE automatically." ) ; |
| + } |
| + } |
| + else |
| + { |
| + // Assert IE is not running, so shut_down() succeeded. |
| + state = success ; |
| + session.log( "Automatically shut down IE." ) ; |
| + } |
| + } |
| + break; |
| + } |
| + } |
| + /* |
| + * State machine: Actions for terminal states. |
| + */ |
| + switch ( state ) |
| + { |
| + case success: |
| + if ( iec.is_running() ) |
| + { |
| + browser_running = L"1" ; |
| + browser_closed = L"0" ; |
| + } |
| + else |
| + { |
| + browser_running = L"0" ; |
| + browser_closed = L"1" ; |
| + } |
| + return ERROR_SUCCESS ; |
| + break; |
| + case abort: |
| + return ERROR_INSTALL_USEREXIT ; |
| + break; |
| + } |
| + } |
| + catch( std::exception & e ) |
| + { |
| + session.log_noexcept( "terminated by exception: " + std::string( e.what() ) ) ; |
| + return ERROR_INSTALL_FAILURE ; |
| } |
| catch( ... ) |
| { |
| + session.log_noexcept( "terminated by unknown exception" ) ; |
| return ERROR_INSTALL_FAILURE ; |
| } |
| - |
| - /* |
| - * While we're working on infrastructure (and not the CA itself), fail the action. |
| - */ |
| + // Should be unreachable. |
| return ERROR_INSTALL_FAILURE ; |
| } |
| @@ -61,8 +454,6 @@ |
| /** |
| * Windows_List |
| - * |
| - * |
| */ |
| class Window_List { |
| public: |