In the past, the Google Hangouts desktop app on Windows would integrate with the system “tray” (actually the notification area) and show you missed chats and calls.
The [Wayback/Archive] Google Chat desktop app does not. It shows missed messages only as a number on the taskbar icon. Even worse: when you close the Window, the taskbar application icon does not show that number any more.
The odd thing is that the Google Duo desktop app does stay active and shows a notification popup on incoming calls. The Google Chat desktop app does not.
So I wanted to restart the Google Chat desktop app automatically when the Window was closed. But there is a catch:
The Google Chat desktop app is not a process by itself: it is ran by the main chrome.exe
process at the root of the Chrome process tree (which also hosts all other Chrome processes that render the various browser windows and tabs). Which means you cannot detect the Google Chat desktop app window being closed by the main chrome.exe
process disappearing.
This renders traditional “restart after failure” programs useless.
Not wanting to re-invent wheels, I asked around if anyone had a ready to run script on Twitter, got one response, which meant I had to at least partially re-invent some portions of the wheels.
Let’s first get to the tweets:
- [Wayback/Archive] Jeroen Wiert Pluimers on Twitter: “Who of the fellow tweeps has a script or tool that automatically restarts an application that has stopped? It is about Google Chat:
"C:Program FilesGoogleChromeApplicationchrome_proxy.exe" --profile-directory=Default --app-id=mdpkiolbdkhdjpekfbkbmhigcaggjagi
“ - [Wayback/Archive] Niel
on Twitter: “@jpluimers for example this
w-shadow.com/blog/2009/03/04/restart-on-crash
“ - [Wayback/Archive] Jeroen Wiert Pluimers on Twitter: “@nieldk Thanks. Fails because it monitors the process which is chrome.exe. So I need to find a solution that can monitor a certain Window. I might need to build a custom thing like I did here for @ScanSnap_US But thanks for the link, as it likely will be useful another day.”
- [Wayback/Archive] Jeroen Wiert Pluimers on Twitter: “@nieldk @ScanSnap_US That was this little Delphi tool. … Hopefully I can get something like this running from PowerShell.”
- [Wayback/Archive] Jeroen Wiert Pluimers on Twitter: “@nieldk How cool, I just found out how you can add a bit of C# as a Type in PowerShell: …”
-
- [Wayback/Archive] Get all windows of a process in powershell – Stack Overflow (thanks [Wayback/Archive] Jack_of_no_trades and [Wayback/Archive] Bacon Bits)
Thanks to Bacon Bits suggestion I managed to find a solution, but if you have any less cumbersome than this, please share:
…
Related SO questions:
- [Wayback/Archive] Get all windows of a process in powershell – Stack Overflow (thanks [Wayback/Archive] Jack_of_no_trades and [Wayback/Archive] Bacon Bits)
Later I found out it was not their code, but from (a now defunct and non-archived site powershell-knowhow.de) by [Wayback/Archive] Peter Monadjemi (@pemo09) who published [Wayback/Archive] PowerShell für die Windows-Administration: … – Peter Monadjemi – Google Books. but I could not find any current version of it via [Wayback/Archive] In Windows, how can I view a list of all window titles ? – Super User (thanks [Wayback/Archive] barlop and [Wayback/Archive] Oliver Salzburg)
Not even the homepage was successfully archived in the Wayback Machine, just a different domain on which the site apparently moved to in 2015: [Wayback/Archive] Posh-Admin | Per PowerShelll Abläufe in der IT automatisieren – das PowerShell-Blog von Peter Monadjemi.
-
The cool thing about their code is that it shows how to integrate a C# class into a PowerShell script. I didn’t know that is possible, but it basically boils down to Add-Type -TypeDefinition 'your code here' -Language CSharp
.
And it finds Windows too; sometimes too many, so I have to refine the search regular? expression:
PS C:\Temp> [Api.Apidef]::GetWindows() | Where-Object { $_.WinTitle -like "*Chat*" } | Sort-Object -Property WinTitle | Select-Object WinTitle,@{Name="Handle"; Expression={"{0:X0}" -f $_.WinHwnd}} WinTitle -------- Edit Post “Writing a tool that restarts the Google Chat desktop app Window (and hopefully the Google Duo desktop app... Google Chat - Chat PS C:\Temp> [Api.Apidef]::GetWindows() | Where-Object { $_.WinTitle -like "*Chat*" } | Sort-Object -Property WinTitle | Select-Object WinTitle,@{Name="Handle"; Expression={"{0:X0}" -f $_.WinHwnd}} WinTitle -------- Edit Post “Writing a tool that restarts the Google Chat desktop app Window (and hopefully the Google Duo desktop app... Google Chat - My Brother - Chat
Their code is so similar to my Delphi code (just compare the gists with code below the signature), so that in a future blog post (or series) I will rewrite my code to C#, integrate it in PowerShell and make the query argument more flexible.
Then I need to find out how to run the PowerShell script hidden and start it every minute or so. The latter should be easy through the Windows Task Scheduler (which in the past was called “Scheduled Tasks”) because the at
command got deprecated:
The AT command has been deprecated. Please use schtasks.exe instead.
Before the tweets: searching
Before starting the first Tweet I already had searched for these app-id values:
- [Wayback/Archive] “
mdpkiolbdkhdjpekfbkbmhigcaggjagi
” – Recherche Google - [Wayback/Archive] “
imgohncinckhbblnlmaedahepnnpmdma
” – Google Search
The first ID of course is from the Google Chat shortcut on my system mentioned in the first tweet above.
The second ID is which is from the Google Duo
shortcut on my system pointing to "C:Program FilesGoogleChromeApplicationchrome_proxy.exe" --profile-directory=Default --app-id=imgohncinckhbblnlmaedahepnnpmdma
.
Both queries didn’t return many useful results, so I did more digging:
- [Wayback/Archive] restart mdpkiolbdkhdjpekfbkbmhigcaggjagi – Google Search
- [Wayback/Archive] mynotes.cz | Welcome to my notes (regrettably the archived Google Cache is empty and the page itself is not accessible any more)
- [Wayback/Archive] Google Chat launcher via AutoHotkey (with hardcoded paths, and a regular expression that likely is too limited for my purpose)
- [Wayback/Archive] Google Chat version 1.0 by Google Chat – How to uninstall it
- [Wayback/Archive] “Google Chat” “Tray” – Recherche Google
- [Wayback/Archive] Minimize to tray? · Issue #37 · squalou/google-chat-linux
- [Wayback/Archive] Tray | Electron (if I could find the local source code of the Google Chat desktop app and modify it….)
- [Wayback/Archive] Google (Hangouts) Chat Windows client minimize to tray : gsuite zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
- [Wayback/Archive] Google Chat main.js modified to minimize to tray – Pastebin.com
- [Wayback/Archive] Re: Google Chat Background – Google Cloud Community
- [Wayback/Archive] Google Chat main.js modified to minimize to tray
Back to the code
Integrating C# into PowerShell. Given the Add-Type -TypeDefinition 'your code here' -Language CSharp
above, I did some searches to see if there were some immediate caveats:
- [Wayback/Archive] integrate C# in powershell – Recherche Google
- [Wayback/Archive] How to use .Net code in PowerShell (C# and DLL integration)
- [Wayback/Archive] Executing C# code using PowerShell script – Random IT Utensils (which demonstrates adding a random ID to classes in order to prevent adding duplicate types, how to add referencing assemblies, load native DLLs and how to show compilation errors)
- [Wayback/Archive] “Add-Type -TypeDefinition” “-Language CSharp” – Recherche Google
- [Wayback/Archive] Powershell add-type update my first script – Microsoft Q&A (bumping into the duplicate type name)
But when I change the “
$code
” variable script and “Add-Type
” it second time it shows me an error.Add-Type : Cannot add type. The type name 'HelloWorld.Program' already exists. At line:1 char:1 + Add-Type -TypeDefinition $code -Language CSharp + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (HelloWorld.Program:String) [Add-Type], Exception + FullyQualifiedErrorId : TYPE_ALREADY_EXISTS,Microsoft.PowerShell.Commands.AddTypeCommand
- [Wayback/Archive] c# – PowerShell Add-Type : Cannot add type. already exist – Stack Overflow (thanks [Wayback/Archive] zingwing for asking and [Wayback/Archive] Marc, [Wayback/Archive] Carsten, [Wayback/Archive] Jeff D and [Wayback/Archive] Sam Tran for answering as they all come up with various solutions to the problem: checking if the type exists; ensuring the type gets added to a specific namespace (for instance
System
instead of defaultMicrosoft.PowerShell.Commands.AddType.AutoGeneratedTypes
), give each class a unique ID, or execute theAdd-Type
and calling code as a Background Job so it runs in a separate process and thus App Domain). - [Wayback/Archive] Execute C# Code with Parameters Using PowerShell – Sam Tran ( 陈 偉明) (showing how to pass multiple parameters as string arrays. WARNING: THE LINK SEEMS TO OPEN THROUGH SOME PHISHY INTERMEDIATE LINKS WHICK UBLOCK BLOCKS FOR ME; the Wayback Machine and Archive.is links are fine).
- [Wayback/Archive] powershell – How do I conditionally add a class with Add-Type -TypeDefinition if it isn’t added already? – Stack Overflow (thanks [Wayback/Archive] Justin Dearing, [Wayback/Archive] skataben, [Wayback/Archive] x0n and [Wayback/Archive] LeeHolmes; mainly about checking for existing types, the namespaces they can be in and the kind of exceptions involved)
- [Wayback/Archive] dll – Can you remove an Add-ed Type in PowerShell again? – Stack Overflow (thanks [Wayback/Archive] Joey for asking, [Wayback/Archive] Start-Automating and [Wayback/Archive] x0n for answering; no you cannot remove the type, but you can load the type in a Background Job that runs in a separate process)
- [Wayback/Archive] Add-Type (Microsoft.PowerShell.Utility) – PowerShell | Microsoft Docs
…
You can specify the type by specifying an existing assembly or source code files, or you can specify the source code inline or saved in a variable. You can even specify only a method and
Add-Type
defines and generates the class. On Windows, you can use this feature to make Platform Invoke (P/Invoke) calls to unmanaged functions in PowerShell. If you specify source code,Add-Type
compiles the specified source code and generates an in-memory assembly that contains the new .NET Core types.You can use the parameters ofAdd-Type
to specify an alternate language and compiler, C# is the default, compiler options, assembly dependencies, the class namespace, the names of the type, and the resulting assembly.Beginning in PowerShell 7,Add-Type
does not compile a type if a type with the same name already exists. Also,Add-Type
looks for assemblies in aref
folder under the folder that containspwsh.dll
.…
- [Wayback/Archive] about Jobs – PowerShell | Microsoft Docs
- [Wayback/Archive] Powershell add-type update my first script – Microsoft Q&A (bumping into the duplicate type name)
A few notes here:
- I have yet to see a code editor that syntax-highlights both the PowerShell and the C# code.
- You cannot add the same type name twice in the same App Domain (Application Domain or for that matter: PowerShell session as there is one App Domain per PowerShell session), so be aware of any errors or try to prevent them (see the StackOverflow and Random IT examples above).
- Note I have seen in PowerShell 5.1 on Windows 10 that this only holds when the type names are identical but the types themselves are not. In this example if you run the code multiple types it shill executes fine (as the type name is still the same, but also the type itself is identical). So probably some optimisation took place:
$code = @" using System; namespace HelloWorld { public class Program { public static void Main(){ Console.WriteLine("Hello world!"); } } } "@ Add-Type -TypeDefinition $code -Language CSharp Invoke-Expression "[HelloWorld.Program]::Main()"
- Note I have seen in PowerShell 5.1 on Windows 10 that this only holds when the type names are identical but the types themselves are not. In this example if you run the code multiple types it shill executes fine (as the type name is still the same, but also the type itself is identical). So probably some optimisation took place:
- There is both a speed penalty on using PowerShell and one for including C# in that so be aware if you need performant code.
- I’m not sure if the
Add-Type
really needs to be at the beginning of the script; my impression is that you need to include it before using it (just like any other PowerShell code).
The PowerShell/C# code for finding Word windows
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.Synopsis | |
Enumerieren der vorhandenen Fenster | |
#> | |
$TypeDef = @" | |
using System; | |
using System.Text; | |
using System.Collections.Generic; | |
using System.Runtime.InteropServices; | |
namespace Api | |
{ | |
public class WinStruct | |
{ | |
public string WinTitle {get; set; } | |
public int WinHwnd { get; set; } | |
} | |
public class ApiDef | |
{ | |
private delegate bool CallBackPtr(int hwnd, int lParam); | |
private static CallBackPtr callBackPtr = Callback; | |
private static List<WinStruct> _WinStructList = new List<WinStruct>(); | |
[DllImport("User32.dll")] | |
[return: MarshalAs(UnmanagedType.Bool)] | |
private static extern bool EnumWindows(CallBackPtr lpEnumFunc, IntPtr lParam); | |
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] | |
static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); | |
private static bool Callback(int hWnd, int lparam) | |
{ | |
StringBuilder sb = new StringBuilder(256); | |
int res = GetWindowText((IntPtr)hWnd, sb, 256); | |
_WinStructList.Add(new WinStruct { WinHwnd = hWnd, WinTitle = sb.ToString() }); | |
return true; | |
} | |
public static List<WinStruct> GetWindows() | |
{ | |
_WinStructList = new List<WinStruct>(); | |
EnumWindows(callBackPtr, IntPtr.Zero); | |
return _WinStructList; | |
} | |
} | |
} | |
"@ | |
Add-Type -TypeDefinition $TypeDef -Language CSharpVersion3 | |
[Api.Apidef]::GetWindows() | Where-Object { $_.WinTitle -like "*Word" } | Sort-Object -Property WinTitle | Select-Object WinTitle,@{Name="Handle"; Expression={"{0:X0}" -f $_.WinHwnd}} |
My Delphi code for ScanAnap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
program AbbyyFineReader_CreativeCloud_AutoCloseConsoleProject; | |
{$APPTYPE CONSOLE} | |
{$R *.res} | |
uses | |
Winapi.Messages, | |
Winapi.Windows, | |
System.SysUtils; | |
const | |
CreativeCloud = 'Creative Cloud'; | |
AbbyyFineReaderForScanSnap50 = 'ABBYY FineReader for ScanSnap 5.0'; | |
type | |
TWindowDump = record | |
public | |
HWnd: HWnd; | |
IsAppWindow: Boolean; | |
IsOwned: Boolean; | |
IsVisible: Boolean; | |
WindowText: string; | |
WindowTextLength: Integer; | |
ParentHWnd: lParam; | |
Prefix: string; | |
constructor Create(const APrefix: string; const AHWnd: HWnd; const AParentHWnd: lParam); | |
function ToString: string; | |
end; | |
constructor TWindowDump.Create(const APrefix: string; const AHWnd: HWnd; const AParentHWnd: lParam); | |
begin | |
Prefix := APrefix; | |
HWnd := AHWnd; | |
ParentHWnd := AParentHWnd; | |
IsAppWindow := GetWindowLongPtr(HWnd, GWL_STYLE) and WS_EX_APPWINDOW <> 0; | |
IsOwned := GetWindow(HWnd, GW_OWNER) <> 0; | |
IsVisible := IsWindowVisible(HWnd); | |
WindowTextLength := GetWindowTextLength(HWnd); | |
if WindowTextLength <> 0 then | |
begin // without Length check you can get an access violation | |
SetLength(WindowText, WindowTextLength); | |
GetWindowText(HWnd, PChar(WindowText), Length(WindowText) + 1); | |
end; | |
end; | |
function TWindowDump.ToString: string; | |
begin | |
Result := Format('%sParentHWnd=$%8.8x;HWnd=$%8.8x;IsVisible=%s;IsOwned=%s;IsAppWindow=%s;WindowTextLength=%d;WindowText="%s"', [ // | |
Prefix, ParentHWnd, HWnd, BoolToStr(IsVisible), BoolToStr(IsOwned), BoolToStr(IsAppWindow), WindowTextLength, WindowText]); | |
end; | |
function DumpWindow(const Prefix: string; const HWnd: HWnd; const ParentHWnd: lParam): TWindowDump; | |
begin | |
Result := TWindowDump.Create(Prefix, HWnd, ParentHWnd); | |
Writeln(Result.ToString()); | |
end; | |
procedure DumpWindowStack(const Prefix: string; const HWnd: HWnd; const ParentHWnd: lParam); | |
var | |
OwnerHWnd: lParam; | |
begin | |
if ParentHWnd <> 0 then | |
begin | |
OwnerHWnd := GetWindow(ParentHWnd, GW_OWNER); | |
DumpWindow(Prefix, ParentHWnd, OwnerHWnd); | |
DumpWindowStack(' ' + Prefix, ParentHWnd, OwnerHWnd); | |
end; | |
end; | |
function EnumChildWindowsProc(HWnd: HWnd; ParentHWnd: lParam): Bool; stdcall; | |
var | |
IsVisible: Boolean; | |
ParentWindowText: string; | |
ParentWindowTextLength: Integer; | |
WindowDump: TWindowDump; | |
begin | |
Result := True; // carry on enumerating | |
IsVisible := IsWindowVisible(HWnd); | |
if IsVisible then | |
begin | |
WindowDump := DumpWindow(' ', HWnd, ParentHWnd); | |
ParentWindowTextLength := GetWindowTextLength(ParentHWnd); | |
if ParentWindowTextLength <> 0 then | |
begin | |
SetLength(ParentWindowText, ParentWindowTextLength); | |
GetWindowText(ParentHWnd, PChar(ParentWindowText), Length(ParentWindowText) + 1); | |
end; | |
if ParentWindowText = AbbyyFineReaderForScanSnap50 then | |
begin | |
if WindowDump.WindowText = '&Close' then | |
begin | |
Writeln(' > Child is Close button: clicking.'); | |
DumpWindowStack(' < ', HWnd, ParentHWnd); | |
SendMessage(HWnd, BM_CLICK, 0, 0); | |
end; | |
end; | |
if ParentWindowText = CreativeCloud then | |
begin | |
if WindowDump.WindowText = 'Sign in – Adobe ID' then | |
begin | |
Writeln(' > Child is Signin button: closing parent.'); | |
DumpWindowStack(' < ', HWnd, ParentHWnd); | |
SendMessage(ParentHWnd, WM_CLOSE, 0, 0); | |
end; | |
end; | |
end; | |
end; | |
function EnumWindowsProc(HWnd: HWnd; lParam: lParam): Bool; stdcall; | |
var | |
WindowDump: TWindowDump; | |
begin | |
Result := True; // carry on enumerating | |
WindowDump := TWindowDump.Create('', HWnd, lParam); | |
if WindowDump.IsVisible then | |
begin | |
Writeln(WindowDump.ToString()); | |
(* | |
ParentHWnd=$00000000;HWnd=$00030602;IsVisible=-1;IsOwned=0;IsAppWindow=-1;WindowTextLength=33;WindowText="ABBYY FineReader for ScanSnap 5.0" | |
> Recursive child windows for ABBYY | |
ParentHWnd=$00030602;HWnd=$000205E2;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=0;WindowText="" | |
ParentHWnd=$00030602;HWnd=$000205E0;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=0;WindowText="" | |
ParentHWnd=$00030602;HWnd=$000205EC;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=0;WindowText="" | |
ParentHWnd=$00030602;HWnd=$000205EA;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=74;WindowText="Register your copy of ABBYY FineReader and receive the following benefits:" | |
ParentHWnd=$00030602;HWnd=$000205E8;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=25;WindowText="- Free technical support;" | |
ParentHWnd=$00030602;HWnd=$000205E6;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=51;WindowText="- Information about new versions of ABBYY products." | |
ParentHWnd=$00030602;HWnd=$000205E4;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=12;WindowText="Registration" | |
ParentHWnd=$00030602;HWnd=$000205FC;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=0;WindowText="" | |
ParentHWnd=$00030602;HWnd=$000205FA;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=6;WindowText="&Close" | |
> Child is Close button: clicking. | |
< ParentHWnd=$00000000;HWnd=$00030602;IsVisible=-1;IsOwned=0;IsAppWindow=-1;WindowTextLength=33;WindowText="ABBYY FineReader for ScanSnap 5.0" | |
ParentHWnd=$00030602;HWnd=$000205F6;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=34;WindowText="Processing finished (warnings: 1)." | |
ParentHWnd=$00030602;HWnd=$000205F4;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=31;WindowText="Converting to searchable PDF…" | |
ParentHWnd=$00030602;HWnd=$000205F0;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=0;WindowText="" | |
ParentHWnd=$00030602;HWnd=$000205EE;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=0;WindowText="" | |
ParentHWnd=$00030602;HWnd=$000205D2;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=63;WindowText="Page 1. Make sure the correct recognition language is selected." | |
ParentHWnd=$00000000;HWnd=$00010248;IsVisible=-1;IsOwned=-1;IsAppWindow=0;WindowTextLength=14;WindowText="Creative Cloud" | |
> Recursive child windows for Creative Cloud | |
ParentHWnd=$00010248;HWnd=$0001024A;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=28;WindowText="Main Container Client Dialog" | |
ParentHWnd=$00010248;HWnd=$0002034A;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=3;WindowText="IMS" | |
ParentHWnd=$00010248;HWnd=$0001035A;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=0;WindowText="" | |
ParentHWnd=$00010248;HWnd=$00020350;IsVisible=-1;IsOwned=0;IsAppWindow=0;WindowTextLength=18;WindowText="Sign in – Adobe ID" | |
> Child is Signin button: closing parent. | |
< ParentHWnd=$0003011A;HWnd=$00010248;IsVisible=-1;IsOwned=-1;IsAppWindow=0;WindowTextLength=14;WindowText="Creative Cloud" | |
< ParentHWnd=$00000000;HWnd=$0003011A;IsVisible=0;IsOwned=0;IsAppWindow=0;WindowTextLength=4;WindowText="Core" | |
*) | |
if WindowDump.IsVisible and WindowDump.IsAppWindow then | |
begin | |
if WindowDump.WindowText = AbbyyFineReaderForScanSnap50 then | |
begin | |
Writeln('> Recursive child windows for ABBYY'); | |
EnumChildWindows(HWnd, @EnumChildWindowsProc, HWnd); | |
end; | |
end; | |
if WindowDump.IsVisible and WindowDump.IsOwned then | |
if WindowDump.WindowText = CreativeCloud then | |
begin | |
Writeln('> Recursive child windows for Creative Cloud'); | |
EnumChildWindows(HWnd, @EnumChildWindowsProc, HWnd); | |
end; | |
end; | |
end; | |
begin | |
try | |
repeat | |
EnumWindows(@EnumWindowsProc, 0); | |
Writeln('.'); | |
Sleep(10 * 1000); | |
until False; | |
except | |
on E: Exception do | |
Writeln(E.ClassName, ': ', E.Message); | |
end; | |
end. |