Ian Branch 127 Posted September 28, 2022 Hi Team, D11.2. Apps residing on a Win 2012 Server, running on the User's PC. Most of the Users are using Win 7 but several are on Win 10. My Apps write to a log file when they open and when they close. Auditable. I have been trying to find out how/why some 'sessions' stay open in the Log overnight. The Apps have an Idle Timeout that closes them after x minutes of no mouse or keyboard activity in the App. I have discovered that some Users are not closing the App at the end of the day and are simply shutting down their PCs, while the App(s) are still open. 😞 Is there some way to detect that the PC has been set to shutdown and stall it while the App(s) close gracefully? Regards & TIA, Ian Share this post Link to post
Angus Robertson 574 Posted September 28, 2022 Windows broadcasts power event messages before close down, WM_POWERBROADCAST or WM_POWER, I have a component for checking them. In theory you can suspend close down, but this can be ignored by Windows. If someone turns the PC power off, no events will occur, so if you really need to know it's best to write the date and time or something to a file every few seconds and on normal close down, and check it on starting up, so you know there was a clean close down and for how long. Angus Share this post Link to post
Stano 143 Posted September 28, 2022 OT: I rather think that they don't turn it off, but put it to sleep or hibernate. I also use hibernation. I am writing this just to make you aware. 2 Share this post Link to post
Ian Branch 127 Posted September 28, 2022 Hi Team, I am advised, although I take it with a grain of salt, that they shut down the PC rather than just turn it off. Angus, what is the component you use please? Ian Share this post Link to post
PeterBelow 238 Posted September 28, 2022 8 hours ago, Ian Branch said: Hi Team, D11.2. Apps residing on a Win 2012 Server, running on the User's PC. Most of the Users are using Win 7 but several are on Win 10. My Apps write to a log file when they open and when they close. Auditable. I have been trying to find out how/why some 'sessions' stay open in the Log overnight. The Apps have an Idle Timeout that closes them after x minutes of no mouse or keyboard activity in the App. I have discovered that some Users are not closing the App at the end of the day and are simply shutting down their PCs, while the App(s) are still open. 😞 Is there some way to detect that the PC has been set to shutdown and stall it while the App(s) close gracefully? Regards & TIA, Ian On normal shutdown running apps get a WM_QUERYENDSESSION message, followed eventually by WM_ENDSESSION. The VCL handles WM_QUERYENDSESSION by firing the main form's OnCloseQuery event but OnClose or OnDestroy may not fire on system shutdown. So OnCloseQuery is the best place for detecting app closing. Share this post Link to post
Angus Robertson 574 Posted September 28, 2022 Magenta Hardware Events Component is part of https://www.magsys.co.uk/delphi/maghardware.asp . It warns if a PC is power sispending or restoring, or closing down, nothing complicated, this is just simple windows messages. As Peter says, OnCloseQuery should fire on normal forms, but belt and braces is often better is you really need a clean close down. And no events fire on forced termination. Angus Share this post Link to post
Ian Branch 127 Posted September 28, 2022 Thanks Guys, Angus - I will have a look at the component. Peter - In FormCloseQuery, how can I tell if it was triggered by WM_QUERYENDSESSION ? Regards, Ian Share this post Link to post
Sherlock 663 Posted September 28, 2022 Nowadays power switches on PCs are no longer switches, they are buttons that send the users desire to power off (or on) to the ACPI. Only if the button is held longer than 5(?) seconds, it will really cut off the power. And @Ian Branch I suggest consulting your thread from two years ago: 😉 Share this post Link to post
Ian Branch 127 Posted September 28, 2022 Hi Sherlock, Yes I did go back to that and I am just as confused now as I was then. I have the closing actions on the OnCloseQuery event. Under normal circumstances it works fine, but when the User shuts down their PC while the App is running/open it doesn't. The following is my OnCloseQuery.. procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean); var TS: TTimeSpan; StartDateTime, FinishDateTime: TDateTime; lUsersLogWritten: Boolean; begin // if GetSystemMetrics (SM_SHUTTINGDOWN) > 0 then iTerminateAction := 3; // app is being closed by a system shutdown // {$IF Defined(ELogging) or Defined(Codesite)} LogMessage('iTerminateAction = '+IntToStr(iTerminateAction)); {$ENDIF} // lUsersLogWritten := False; // LTI1.ShowBalloonHint('Closing Workflow Database...', 'Any Open Edits and/or Inserts are being cancelled and the Database closed.'); // if dmC.DBC1.Connected then begin // {$IF Defined(ELogging) or Defined(Codesite)} LogMessage('DBC1 is connected att.'); {$ENDIF} // CancelEditsInsertsInDatamodules; CancelEditsInsertsInOpenForms; end; // CloseOpenForms; // dmC.DBC1.CloseDataSets; // dmC.UsersLog.IndexName := 'SessionKey'; dmC.UsersLog.Open; // if dmC.UsersLog.FindKey([sSessionKey]) then begin dmC.UsersLog.Edit; // case iTerminateAction of 0: dmC.UsersLog.FieldByName('Action').AsString := 'Logged Out'; 1: dmC.UsersLog.FieldByName('Action').AsString := 'Timed Out'; 2: dmC.UsersLog.FieldByName('Action').AsString := 'Browser Closed'; 3: dmC.UsersLog.FieldByName('Action').AsString := 'System Shutdown'; end; // dmC.UsersLog.FieldByName('FinishDateTime').AsDateTime := now; StartDateTime := dmC.UsersLog.FieldByName('StartDateTime').AsDateTime; FinishDateTime := dmC.UsersLog.FieldByName('FinishDateTime').AsDateTime; TS := TTimeSpan.Subtract(FinishDateTime, StartDateTime); dmC.UsersLog.FieldByName('Duration').AsString := Format('%.3d:%.2d:%.2d:%.2d', [TS.Days, TS.Hours, TS.Minutes, TS.Seconds]); dmC.UsersLog.Post; // lUsersLogWritten := True; // end; // dmC.UsersLog.Close; // DBWReg.WriteString('Session Key', 'DBiWorkflow', ''); // dmC.DBC1.Close; dmC.DBS1.Close; dmC.DBE1.Close; // if iTerminateAction = 0 then begin // if not lUsersLogWritten then TaskMessageDlg('Workflow Closing error!', 'Note:- The Users Log was NOT updated. Workflow will now close..', mtError, [mbOK], 0); // end; // end; It doesn't work on my Win 11 PC. Happy to have the error of my ways pointed out. Regards, Ian Share this post Link to post
Sherlock 663 Posted September 28, 2022 Well, OnCloseQuery is simply not enough. You need to answer the OSes "Will shut down now" Message with a "Wait for me" and clear that "Wait for me" after you are done all the while keeping your main thread responsive to more OS messages that will come because Windows may not just take your word that you'll say "carry on" once you are ready, it will check on you... So what it boils down to is the following (if your application does not take longer than 30 seconds to do what needs to be done your golden, otherwise you'll need to be threading): type TForm1 = class(TForm) procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean); private procedure WMQueryEndSession(var Msg: TMessage); message WM_QUERYENDSESSION; procedure WMEndsession(var Msg: TMessage); message WM_ENDSESSION; public { Public-Deklarationen } end; var Form1: TForm1; implementation {$R *.dfm} var _ShutdownBlockReasonCreate: function(WindowHandle: HWND; Reason: LPCWSTR): UINT; stdcall; _ShutdownBlockReasonDestroy: procedure(WindowHandle: HWND); stdcall; function ShutdownBlockReasonCreate(WindowHandle: HWND; Reason: LPCWSTR): UINT; var UserLib: THandle; begin if @_ShutdownBlockReasonCreate = nil then begin UserLib := GetModuleHandle(Windows.User32); if UserLib <> 0 then @_ShutdownBlockReasonCreate := GetProcAddress(UserLib, 'ShutdownBlockReasonCreate'); end; if @_ShutdownBlockReasonCreate <> nil then Result := _ShutdownBlockReasonCreate(WindowHandle, Reason) else Result := 1; end; procedure ShutdownBlockReasonDestroy(WindowHandle: HWND); var UserLib: THandle; begin if @_ShutdownBlockReasonDestroy = nil then begin UserLib := GetModuleHandle(Windows.User32); if UserLib <> 0 then @_ShutdownBlockReasonDestroy := GetProcAddress(UserLib, 'ShutdownBlockReasonDestroy'); end; if @_ShutdownBlockReasonDestroy <> nil then _ShutdownBlockReasonDestroy(WindowHandle); end; { TForm1 } procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin // Whatever needs to be done... end; procedure TForm1.WMEndsession(var Msg: TMessage); begin // Windows now informs you it will end your session // Free up your BlockReason ShutdownBlockReasonDestroy(Handle); end; procedure TForm1.WMQueryEndSession(var Msg: TMessage); const ENDSESSION_CLOSEAPP = $00000001; // lParam - WM_QUERYENDSESSION ENDSESSION_CRITICAL = $40000000; ENDSESSION_LOGOFF = $80000000; var CanClose: Boolean; begin // Windows asks if it may end your session, your response is // "No, and here's what you can tell the user why" if ((Message.Unused and ENDSESSION_CRITICAL) = 0) then begin if ShutdownBlockReasonCreate(Handle, 'Saving application data...') = 0 then begin SaveLog(SysErrorMessage(GetLastError)); //Assuming you have some kind of logging end; // Now do your thing i.e. call FormCloseQuery FormCloseQuery(Self, CanClose); If CanClose then Close; // Woohoo end else begin // Forcefully shutdown...no time to wait end; end; I have something like this in an application and it has worked for me since Win7. Not tested on Win11 yet though. Share this post Link to post
Pat Foley 51 Posted September 28, 2022 const DBcommandstrs = ('Workstation shut down', 'Auto Close'); // i := IndexText('Workstation shut down', DBcommandstrs); var enlightenedCanClose: Boolean = False; // called by timer procedure autocloseWorkStationSession(Sender: Tobject); begin if Time > 17/24 then begin CloseDB('Auto Close'); //Close; end; end; procedure CloseDB(const ditto: string); var TS: TTimeSpan; StartDateTime, FinishDateTime: TDateTime; lUsersLogWritten: Boolean; if dmC.DBC1.Connected then begin {$IF Defined(ELogging) or Defined(Codesite)} LogMessage(IndexText[0]); LogMessage('DBC1 is connected att.'); {$ENDIF} // ////' check savepoints or cache status? CancelEditsInsertsInDatamodules; CancelEditsInsertsInOpenForms; //end; // ///CloseOpenForms; ///Pat says Boo! // dmC.DBC1.CloseDataSets; // dmC.UsersLog.IndexName := 'SessionKey'; dmC.UsersLog.Open; // if dmC.UsersLog.FindKey([sSessionKey]) then begin dmC.UsersLog.Edit; // //case iTerminateAction of dmC.UsersLog.FieldByName('Action').AsString := ditto; dmC.UsersLog.FieldByName('FinishDateTime').AsDateTime := now; StartDateTime := dmC.UsersLog.FieldByName('StartDateTime').AsDateTime; FinishDateTime := dmC.UsersLog.FieldByName('FinishDateTime').AsDateTime; TS := TTimeSpan.Subtract(FinishDateTime, StartDateTime); dmC.UsersLog.FieldByName('Duration').AsString := Format('%.3d:%.2d:%.2d:%.2d', [TS.Days, TS.Hours, TS.Minutes, TS.Seconds]); dmC.UsersLog.Post; // lUsersLogWritten := True; // end; // dmC.UsersLog.Close; // DBWReg.WriteString('Session Key', 'DBiWorkflow', ''); // dmC.DBC1.Close; dmC.DBS1.Close; dmC.DBE1.Close; // if iTerminateAction = 0 then begin // if not lUsersLogWritten then TaskMessageDlg('Workflow Closing error!', 'Note:- The Users Log was NOT updated. Workflow will now close..', mtError, [mbOK], 0); // end; enlightenedCanClose := True; close; end; procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin // CanClose := enlightenedCanClose; SendMessage(Handle, EM_SETMODIFY, WPARAM(True), 0); //or have a MS product open that needs saved // if not CanClose then begin if (GetSystemMetrics (SM_SHUTTINGDOWN) > 0) and not dmC.DBC1.Connected then closeDB('Workstation shut down'); // app is being closed by workstation shutdown enlightenedCanClose := True; Close; end; end; That should work in Win11. (In fair weather 🙂 ) Pat Share this post Link to post
Ian Branch 127 Posted September 28, 2022 Gentlemen, Thank you for your patience and contributions. Very much appreciated and educational. As it was then and still is now, this is not an area I have any expertise in. Does it show? 😉 I will work with your suggestions. Regards & Tks again, Ian Share this post Link to post
Ian Branch 127 Posted September 28, 2022 Hi Guys, I am going to try each and see which is my best option. @Sherlock Where is this from - "Message.Unused", Delphi tells me "Message" is an undeclared identifier. Regards, Ian Share this post Link to post
Ian Branch 127 Posted September 28, 2022 @Pat Foley I have implemented this att.. procedure TMainForm.CloseDB(const ditto: string); var TS: TTimeSpan; StartDateTime, FinishDateTime: TDateTime; lUsersLogWritten: Boolean; begin // {$IF Defined(ELogging) or Defined(Codesite)} LogMessage('Enter CloseDB.'); {$ENDIF} // if dmC.DBC1.Connected then begin {$IF Defined(ELogging) or Defined(Codesite)} //LogMessage(IndexText[0]); LogMessage('DBC1 is connected att.'); {$ENDIF} // ////' check savepoints or cache status? CancelEditsInsertsInDatamodules; CancelEditsInsertsInOpenForms; //end; // ///CloseOpenForms; ///Pat says Boo! // dmC.DBC1.CloseDataSets; // dmC.UsersLog.IndexName := 'SessionKey'; dmC.UsersLog.Open; // if dmC.UsersLog.FindKey([sSessionKey]) then begin dmC.UsersLog.Edit; // //case iTerminateAction of dmC.UsersLog.FieldByName('Action').AsString := ditto; dmC.UsersLog.FieldByName('FinishDateTime').AsDateTime := now; StartDateTime := dmC.UsersLog.FieldByName('StartDateTime').AsDateTime; FinishDateTime := dmC.UsersLog.FieldByName('FinishDateTime').AsDateTime; TS := TTimeSpan.Subtract(FinishDateTime, StartDateTime); dmC.UsersLog.FieldByName('Duration').AsString := Format('%.3d:%.2d:%.2d:%.2d', [TS.Days, TS.Hours, TS.Minutes, TS.Seconds]); dmC.UsersLog.Post; // lUsersLogWritten := True; // end; // dmC.UsersLog.Close; // DBWReg.WriteString('Session Key', 'DBiWorkflow', ''); // dmC.DBC1.Close; dmC.DBS1.Close; dmC.DBE1.Close; // if iTerminateAction = 0 then begin // if not lUsersLogWritten then TaskMessageDlg('Workflow Closing error!', 'Note:- The Users Log was NOT updated. Workflow will now close..', mtError, [mbOK], 0); // end; enlightenedCanClose := True; close; end; end; procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin // {$IF Defined(ELogging) or Defined(Codesite)} LogMessage('Enter FormCloseQuery.'); {$ENDIF} // CanClose := enlightenedCanClose; // {$IF Defined(ELogging) or Defined(Codesite)} if CanClose then LogMessage('Can Close.') else LogMessage('Can Not Close.'); {$ENDIF} // SendMessage(Handle, EM_SETMODIFY, WPARAM(True), 0); //or have a MS product open that needs saved // if not CanClose then begin if (GetSystemMetrics(SM_SHUTTINGDOWN) > 0) and not dmC.DBC1.Connected then closeDB('Workstation shut down'); // app is being closed by workstation shutdown // enlightenedCanClose := True; Close; end; end; From Codesite logging, neither CloseDB or FormCloseQuery are entered when I shut down the PC with the App running. 😞 May be a Win 11 thing. Ian Share this post Link to post
Ian Branch 127 Posted September 28, 2022 Interesting. I tried again and got this .. but no further. Share this post Link to post
Pat Foley 51 Posted September 29, 2022 Sorry about that I had a showmessage('') in my test letting the code call itself. Plus split out the if tests to reflect the condition and the !. procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin // CanClose := enlightenedCanClose; SendMessage(Handle, EM_SETMODIFY, WPARAM(True), 0); //or have a MS product open that needs saved // if not CanClose then begin if (GetSystemMetrics (SM_SHUTTINGDOWN) > 0) and {not} dmC.DBC1.Connected then closeDB('Workstation shut down'); // app is being closed by workstation shutdown if dmC.DBC1.Connected then closeDB('DB still Open?'); enlightenedCanClose := True; //Close; end; end; Share this post Link to post
Ian Branch 127 Posted September 29, 2022 Hi Pat, I replaced FormCloseQuery per above. I found an end; in the wrong place but commented out that section att anyway, and added some additional logging to CloseDB. procedure TMainForm.CloseDB(const ditto: string); var TS: TTimeSpan; StartDateTime, FinishDateTime: TDateTime; lUsersLogWritten: Boolean; begin // {$IF Defined(ELogging) or Defined(Codesite)} LogMessage('Enter CloseDB.'); {$ENDIF} // // if dmC.DBC1.Connected then // begin // // //{$IF Defined(ELogging) or Defined(Codesite)} // LogMessage('DBC1 is connected att.'); //{$ENDIF} // // // ////' check savepoints or cache status? // CancelEditsInsertsInDatamodules; // CancelEditsInsertsInOpenForms; // //end; // // // ///CloseOpenForms; ///Pat says Boo! // // // dmC.DBC1.CloseDataSets; // // // end; // {$IF Defined(ELogging) or Defined(Codesite)} LogMessage('dmC.UsersLog.IndexName := SessionKey'); {$ENDIF} // dmC.UsersLog.IndexName := 'SessionKey'; dmC.UsersLog.Open; // {$IF Defined(ELogging) or Defined(Codesite)} LogMessage('if dmC.UsersLog.FindKey([sSessionKey]) then'); {$ENDIF} // if dmC.UsersLog.FindKey([sSessionKey]) then begin // {$IF Defined(ELogging) or Defined(Codesite)} LogMessage('dmC.UsersLog.Edit;'); {$ENDIF} // dmC.UsersLog.Edit; // //case iTerminateAction of dmC.UsersLog.FieldByName('Action').AsString := ditto; dmC.UsersLog.FieldByName('FinishDateTime').AsDateTime := now; StartDateTime := dmC.UsersLog.FieldByName('StartDateTime').AsDateTime; FinishDateTime := dmC.UsersLog.FieldByName('FinishDateTime').AsDateTime; TS := TTimeSpan.Subtract(FinishDateTime, StartDateTime); dmC.UsersLog.FieldByName('Duration').AsString := Format('%.3d:%.2d:%.2d:%.2d', [TS.Days, TS.Hours, TS.Minutes, TS.Seconds]); dmC.UsersLog.Post; // lUsersLogWritten := True; // end; // dmC.UsersLog.Close; // DBWReg.WriteString('Session Key', 'DBiWorkflow', ''); // dmC.DBC1.Close; dmC.DBS1.Close; dmC.DBE1.Close; // if iTerminateAction = 0 then begin // if not lUsersLogWritten then TaskMessageDlg('Workflow Closing error!', 'Note:- The Users Log was NOT updated. Workflow will now close..', mtError, [mbOK], 0); // end; // enlightenedCanClose := True; // close; // // end; end; It is only logging/showing "Enter CloseDB", no further. I am going to turn off the logging and see if that is affecting it in some way. Will advise. Share this post Link to post
Ian Branch 127 Posted September 29, 2022 Hi Pat, Well I'm surprised! It seems the logging, Codesite, was influencing/affecting it and preventing the flow of the program continuing... 😞 Some more experimenting to happen. Regards, Ian Share this post Link to post
Ian Branch 127 Posted September 29, 2022 Hi Angus, Pat, I have a working solution the minimizes the amount of changes I have to make to other Apps. I made some changes to Pat's code to facilitate this. procedure TMainForm.CloseDB; var TS: TTimeSpan; StartDateTime, FinishDateTime: TDateTime; lUsersLogWritten: Boolean; sAction: string; begin // if dmC.DBC1.Connected then begin // // ' check savepoints or cache status? CancelEditsInsertsInDatamodules; CancelEditsInsertsInOpenForms; // CloseOpenForms; // Pat says Boo! // dmC.DBC1.CloseDataSets; // end; // case iTerminateAction of 0: sAction := 'Logged Out'; 1: sAction := 'Timed Out'; 2: sAction := 'Browser Closed'; 3: sAction := 'PC Shut Down'; end; // dmC.UsersLog.IndexName := 'SessionKey'; dmC.UsersLog.Open; // if dmC.UsersLog.FindKey([sSessionKey]) then begin // dmC.UsersLog.Edit; // dmC.UsersLog.FieldByName('Action').AsString := sAction; dmC.UsersLog.FieldByName('FinishDateTime').AsDateTime := now; StartDateTime := dmC.UsersLog.FieldByName('StartDateTime').AsDateTime; FinishDateTime := dmC.UsersLog.FieldByName('FinishDateTime').AsDateTime; TS := TTimeSpan.Subtract(FinishDateTime, StartDateTime); dmC.UsersLog.FieldByName('Duration').AsString := Format('%.3d:%.2d:%.2d:%.2d', [TS.Days, TS.Hours, TS.Minutes, TS.Seconds]); dmC.UsersLog.Post; // lUsersLogWritten := True; // end; // dmC.UsersLog.Close; // DBWReg.WriteString('Session Key', 'DBiWorkflow', ''); // dmC.DBC1.Close; dmC.DBS1.Close; dmC.DBE1.Close; // if iTerminateAction = 0 then begin // if not lUsersLogWritten then TaskMessageDlg('Workflow Closing error!', 'Note:- The Users Log was NOT updated. Workflow will now close..', mtError, [mbOK], 0); // end; // enlightenedCanClose := True; // Close; // end; procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin // CanClose := enlightenedCanClose; // SendMessage(Handle, EM_SETMODIFY, WPARAM(True), 0); // or have a MS product open that needs saved // if not CanClose then begin // if (GetSystemMetrics(SM_SHUTTINGDOWN) > 0) then begin // iTerminateAction := 3; CloseDB; // app is being closed by workstation shutdown // end; // enlightenedCanClose := True; // end; // end; My thanks to both of you for your input/education. Regards, Ian Share this post Link to post
Sherlock 663 Posted September 29, 2022 12 hours ago, Ian Branch said: Hi Guys, I am going to try each and see which is my best option. @Sherlock Where is this from - "Message.Unused", Delphi tells me "Message" is an undeclared identifier. Regards, Ian Sorry, turns you do have to be careful when you copy & paste, edit and post code all willy-nilly. That "Message" is just the parameter of the message handler so in this case the abbreviated "msg". Share this post Link to post