{"id":688,"date":"2022-06-27T11:14:30","date_gmt":"2022-06-27T18:14:30","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/powershell-community\/?p=688"},"modified":"2022-06-27T11:14:30","modified_gmt":"2022-06-27T18:14:30","slug":"reading-configuration-manager-status-messages-with-powershell","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/powershell-community\/reading-configuration-manager-status-messages-with-powershell\/","title":{"rendered":"Reading Configuration Manager Status Messages With PowerShell"},"content":{"rendered":"<p><strong>Q:<\/strong> I can read Configuration Manager status messages using the <em>Monitoring<\/em> tab. Can I do it using PowerShell?<\/p>\n<p><strong>A:<\/strong> Yes you can! We can accomplish this using SQL\/WQL queries, plus the Win32 function FormatMessage.<\/p>\n<h2>Better understanding Status Messages<\/h2>\n<p>Before we get our hands dirty we need to understand how the Configuration Manager assembles these messages and why we can&#8217;t just query it from some table, view, or WMI class.<\/p>\n<p>To avoid storage or performance issues and to provide better standardization, the Config Manager stores only message&#8217;s key information (and the ones who change from message to message), and uses a Win32 function called <strong>FormatMessage<\/strong> together with a DLL to assembly and display the full message.<\/p>\n<p>At first, it seems intimidating, specially with the whole Win32 function thing, but it&#8217;s actually pretty simple. Let&#8217;s take a look on one of these messages, so we can visualize what we want to accomplish.<\/p>\n<pre><code>Distribution Manager failed to connect to the distribution point [\"Display=\\\\CMGRDP1.contoso.com\\\"]MSWNET:[\"SMS_SITE=PS1\"]\\\\CMGRDP1.contoso.com\\. Check your network and firewall settings.\n<\/code><\/pre>\n<p>This message states a failed content distribution to a Distribution Point. If we remove the part of the message containing the DP information:<\/p>\n<pre><code>[\"Display=\\\\CMGRDP1.contoso.com\\\"]MSWNET:[\"SMS_SITE=PS1\"]\\\\CMGRDP1.contoso.com\\\n<\/code><\/pre>\n<p>we end up with a standard message that can be used every time this problem occurs.<\/p>\n<h2>Querying useful information<\/h2>\n<p>Now that we have an overview of the Status Message structure, let&#8217;s gather the information available on the Config Manager database. For the purpose of this post, we will use failed distribution messages, like the one we saw above.<\/p>\n<ul>\n<li>The WMI classes that store Status Message information interesting for us are <strong>SMS_StatusMessage<\/strong> and <strong>SMS_StatMsgModuleNames<\/strong>.<\/li>\n<li>For content distribution status we will use the <strong>SMS_DistributionDPStatus<\/strong> class.<\/li>\n<li>The SQL views for these classes are <strong>v_StatusMessage<\/strong>, <strong>v_StatMsgModuleNames<\/strong> and <strong>vSMS_distributionDPStatus<\/strong> respectively.<\/li>\n<li>For performance sake and the SQL language accepting more complex queries we are going to use it for our exercise. This SQL query should return all packages from our Distribution Point which the status is not <em>Success<\/em> or <em>InProgress<\/em><\/li>\n<\/ul>\n<pre><code class=\"language-sql\">SELECT  *\nFROM    vSMS_DistributionDPStatus\nWHERE   [Name] = 'CMGRDP1.contoso.com'\n        AND MessageState NOT IN (1,2)<\/code><\/pre>\n<p>On the result, we are interested on some key columns: <strong>MessageID<\/strong>, <strong>LastStatusID<\/strong>, <strong>MessageSeverity<\/strong> and the <strong>InsString(n)<\/strong>.<\/p>\n<ul>\n<li>The <strong>MessageID<\/strong> and <strong>MessageSeverity<\/strong> we will use with the <strong>FormatMessage<\/strong> function.<\/li>\n<li>The <strong>LastStatusID<\/strong> we will use to join with the other views, who name this column <strong>RecordID<\/strong>.<\/li>\n<li>And perhaps the more interesting ones, the <strong>InsString(n)<\/strong> columns.<\/li>\n<\/ul>\n<p>These columns, <strong>InsString1<\/strong>, <strong>InsString2<\/strong>, <strong>InsString3<\/strong>, &#8230;, <strong>InsString10<\/strong> contain the custom part of the message. Let&#8217;s look at one row of the above query shall we?<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left\">\n        ID<sup>1<\/sup>\n      <\/th>\n<th style=\"text-align: left\">\n        MessageID\n      <\/th>\n<th style=\"text-align: left\">\n        LastStatusID\n      <\/th>\n<th style=\"text-align: left\">\n        MessageSeverity\n      <\/th>\n<th style=\"text-align: left\">\n        InsString1<sup>2<\/sup>\n      <\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left\">\n        47365\n      <\/td>\n<td style=\"text-align: left\">\n        2391\n      <\/td>\n<td style=\"text-align: left\">\n        216172782348300122\n      <\/td>\n<td style=\"text-align: left\">\n        -1073741824\n      <\/td>\n<td style=\"text-align: left\">\n        [&#8220;Display=\\CMGRDP1.contoso.com&#8221;]MSWNET:[&#8220;SMS_SITE=PS1&#8221;]\\CMGRDP1.contoso.com\n      <\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<ul>\n<li><sup>1<\/sup> The <strong>ID<\/strong> column is to help us to identify this specific message later.<\/li>\n<li><sup>2<\/sup> The other <strong>InsString<\/strong> columns are null<\/li>\n<\/ul>\n<p>Won&#8217;t you look at that! The info on <strong>InsString1<\/strong> is exactly the custom part of our message! Let&#8217;s join the other views, and we will have all the information needed to proceed. We are also including information from <strong>v_Package<\/strong>, or <strong>SMS_Package<\/strong> on WMI, to make the end result more meaningful.<\/p>\n<pre><code class=\"language-sql\">SELECT\n        pkg.Name\n        ,pkg.PackageID\n        ,dps.LastUpdateDate\n        ,stm.ModuleName\n        ,smn.MsgDLLName\n        ,dps.MessageID\n        ,CASE\n            WHEN dps.MessageSeverity = '1073741824' THEN '1073741824' --Informational\n            WHEN dps.MessageSeverity = '-2147483648' THEN '2147483648' --Warning\n            WHEN dps.MessageSeverity = '-1073741824' THEN '3221225472' --Error\n        END AS 'SeverityCode'\n        ,dps.InsString1\n        ,dps.InsString2\n        ,dps.InsString3\n        ,dps.InsString4\n        ,dps.InsString5\n        ,dps.InsString6\n        ,dps.InsString7\n        ,dps.InsString8\n        ,dps.InsString9\n        ,dps.InsString10\nFROM    vSMS_distributionDPStatus AS dps\nLEFT JOIN    v_StatusMessage AS stm ON stm.RecordID = dps.LastStatusID\nLEFT JOIN    v_StatMsgModuleNames AS smn ON smn.ModuleName = stm.ModuleName\nLEFT JOIN    v_Package AS pkg ON pkg.PackageID = dps.PackageID\nWHERE   dps.MessageState NOT IN (1,2)\n        AND dps.ID = '47365'<\/code><\/pre>\n<p>We are using the <strong>ID<\/strong> from the previous query to stick to our result. Removing this condition should bring all package distribution failure for that site.<\/p>\n<p>The <em>Case<\/em> statement is necessary because the Message Severity is actually hexadecimal, thus:<\/p>\n<pre><code class=\"language-powershell-console\">PS C:\\&gt; '{0:X}' -f -1073741824\nC0000000\nPS C:\\&gt; '{0:X}' -f 3221225472\nC0000000\nPS C:\\&gt;\nPS C:\\&gt; '{0:X}' -f -2147483648\n80000000\nPS C:\\&gt; '{0:X}' -f 2147483648\n80000000\nPS C:\\&gt;<\/code><\/pre>\n<p>Let&#8217;s see what the result of this query looks like.<\/p>\n<ul>\n<li>Name : Visual Studio 2019 Professional<\/li>\n<li>PackageID : PS100095<\/li>\n<li>LastUpdateDate : 6\/16\/2022 3:49:26 AM<\/li>\n<li>ModuleName : SMS Server<\/li>\n<li>MsgDLLName : srvmsgs.dll<\/li>\n<li>MessageID : 2391<\/li>\n<li>SeverityCode : 3221225472<\/li>\n<li>InsString1 : [&#8220;Display=\\CMGRDP1.contoso.com&#8221;]MSWNET:[&#8220;SMS_SITE=PS1&#8221;]\\CMGRDP1.contoso.com<\/li>\n<li>InsString2 :<\/li>\n<li>InsString3 :<\/li>\n<li>InsString4 :<\/li>\n<li>InsString5 :<\/li>\n<li>InsString6 :<\/li>\n<li>InsString7 :<\/li>\n<li>InsString8 :<\/li>\n<li>InsString9 :<\/li>\n<li>InsString10 :<\/li>\n<\/ul>\n<p>As you can see, we have additional information here, especially <strong>ModuleName<\/strong> and <strong>MsgDLLName<\/strong>. This DLL is the one we are going to use to format the message.<\/p>\n<h2>Formatting the message. Finally!<\/h2>\n<p>To format our message to a readable format we will use the Configuration Manager SDK documentation, which instruct us to use the Win32 API function <em>FormatMessage<\/em> together with the information we just got. From the documentation:<\/p>\n<pre><code class=\"language-cpp\">\/\/ Get the module handle for the component's message DLL. This assumes the\n\/\/ message DLL is loaded. If the DLL is not loaded, then load the DLL by using\n\/\/ the Win32 API LoadLibrary.\nhmodMessageDLL = GetModuleHandle(MsgDLLName);\n\n\/\/ The flags tell FormatMessage to allocate the memory needed for the message,\n\/\/ to get the message text from a message DLL, and that the insertion strings are\n\/\/ stored in an array, instead of a variable length argument list. The last\n\/\/ parameter, apInsertStrings, is the array of insertion strings returned by the\n\/\/ query.\ndwMsgLen = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |\n                         FORMAT_MESSAGE_FROM_HMODULE |\n                         FORMAT_MESSAGE_ARGUMENT_ARRAY,\n                         hmodMessageDLL,\n                         Severity | MessageID,\n                         0,\n                         lpBuffer,\n                         nSize,\n                         apInsertStrings);\n\n\/\/ Free the memory after you use the message text.\nLocalFree(lpBuffer);<\/code><\/pre>\n<p>Wait a second&#8230; this is&#8230; C++? How am I supposed to call this function with PowerShell?<\/p>\n<p>We will borrow a platform from .NET called <strong>PlatformInvoke<\/strong> or <strong><em>Pinvoke<\/em><\/strong> for short. Combining this through the namespace <strong>System.Runtime.InteropServices<\/strong> and importing as a type in PowerShell using <code>Add-Type<\/code> will do the trick.<\/p>\n<blockquote>\n<p>Disclaimer: Using Pinvoke to invoke unmanaged code is another beast in on itself and is beyond the scope of this post, however is lot&#8217;s of fun! I&#8217;ll leave a couple of links at the end to get you started.<\/p>\n<\/blockquote>\n<p>The first thing to do is to translate this C++ to C# so we can import into PowerShell.<\/p>\n<pre><code class=\"language-csharp\">namespace Win32Api\n{\n    using System;\n    using System.Text;\n    using System.Runtime.InteropServices;\n\n    public class kernel32\n    {\n\n        [DllImport(\"kernel32.dll\", CharSet=CharSet.Unicode, SetLastError=true)]\n        public static extern IntPtr GetModuleHandle(\n            string lpModuleName\n        );\n\n        [DllImport(\"kernel32.dll\", CharSet=CharSet.Unicode, SetLastError=true)]\n        public static extern int FormatMessage(\n            uint dwFlags,\n            IntPtr lpSource,\n            uint dwMessageId,\n            uint dwLanguageId,\n            StringBuilder msgOut,\n            uint nSize,\n            string[] Arguments\n        );\n\n                [DllImport(\"kernel32\", SetLastError=true, CharSet = CharSet.Unicode)]\n        public static extern IntPtr LoadLibrary(\n            string lpFileName\n        );\n\n    }\n\n}<\/code><\/pre>\n<p>Using <code>Add-Type<\/code> to import this namespace:<\/p>\n<pre><code class=\"language-powershell\">Add-Type -TypeDefinition @\"\nnamespace Win32Api\n{\n    using System;\n    using System.Text;\n    using System.Runtime.InteropServices;\n\n    public class kernel32\n    {\n\n        [DllImport(\"kernel32.dll\", CharSet=CharSet.Unicode, SetLastError=true)]\n        public static extern IntPtr GetModuleHandle(\n            string lpModuleName\n        );\n\n        [DllImport(\"kernel32.dll\", CharSet=CharSet.Unicode, SetLastError=true)]\n        public static extern int FormatMessage(\n            uint dwFlags,\n            IntPtr lpSource,\n            uint dwMessageId,\n            uint dwLanguageId,\n            StringBuilder msgOut,\n            uint nSize,\n            string[] Arguments\n        );\n\n                [DllImport(\"kernel32\", SetLastError=true, CharSet = CharSet.Unicode)]\n        public static extern IntPtr LoadLibrary(\n            string lpFileName\n        );\n\n    }\n\n}\n\"@<\/code><\/pre>\n<p>The SDK documentation lists 4 steps:<\/p>\n<ol>\n<li>Load the DLL with LoadLibrary.<\/li>\n<li>Get a handle to this library with GetModuleHandle.<\/li>\n<li>Call the FormatMessage function.<\/li>\n<li>Free the memory after using the text with LocalFree<\/li>\n<\/ol>\n<p>Since we&#8217;re calling this from PowerShell and the text will be loaded into a <strong>StringBuilder<\/strong> object, the last step isn&#8217;t necessary. The session will take care of the cleaning once we finish.<\/p>\n<p>So let&#8217;s give it a go!<\/p>\n<pre><code class=\"language-powershell\">## Initializing the message and last error variables. Useful when processing lots of messages.\n$lastError = $null\n$message = $null\n\n## All modules location on the CM installation folder.\n$smsMsgsPath = \"$env:SystemDrive\\Program Files\\Microsoft Configuration Manager\\bin\\X64\\system32\\smsmsgs\"\n$moduleHandle = [Win32Api.kernel32]::GetModuleHandle(\"$smsMsgsPath\\srvmsgs.dll\") ## The DLL From our query.\n\n## If the handle is zero, the module is not loaded. Checking to avoid loading the same DLL twice.\nif ($moduleHandle -eq 0) {\n        [void][Win32Api.kernel32]::LoadLibrary(\"$smsMsgsPath\\srvmsgs.dll\")\n        $moduleHandle = [Win32Api.kernel32]::GetModuleHandle(\"$smsMsgsPath\\srvmsgs.dll\")\n}\n\n$bufferSize = [int]16384 ## Buffer size for our output message.\n## The StringBuilder object who will hold our message.\n$bufferOutput = New-Object 'System.Text.StringBuilder' -ArgumentList $bufferSize\n\n$result = [Win32Api.kernel32]::FormatMessage(\n        0x00000800 -bor 0x00000200 ## FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS\n        ,$moduleHandle\n        ,3221225472 -bor 2391 ## SeverityCode | MessageID\n        ,0 ## languageID. 0 = Default.\n        ,$bufferOutput\n        ,$bufferSize\n        ,$null ## Used to inject the InsStrings into the function. We'll process it later to avoid issues.\n)\n\n## If the function returns zero, means a failure. Setting our $lastError variable to troubleshoot further.\nif ($result -eq 0) { $lastError = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() }<\/code><\/pre>\n<p>At this point, if we did everything right the message should be stored on our <strong>StringBuilder<\/strong> object.<\/p>\n<pre><code class=\"language-powershell-console\">PS C:\\&gt; $result = [Win32Api.kernel32]::FormatMessage(\n&gt;&gt;         0x00000800 -bor 0x00000200 ## FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS\n&gt;&gt;         ,$moduleHandle\n&gt;&gt;         ,3221225472 -bor 2391 ## SeverityCode | MessageID\n&gt;&gt;         ,0 ## languageID. 0 = Default.\n&gt;&gt;         ,$bufferOutput\n&gt;&gt;         ,$bufferSize\n&gt;&gt;         ,$null ## Used to inject the InsStrings into the function. We'll process it later to avoid issues.\n&gt;&gt; )\nPS C:\\&gt; $result\n113\nPS C:\\&gt; $bufferOutput.ToString()\n%11Distribution Manager failed to connect to the distribution point %1. Check your network and firewall settings.\nPS C:\\&gt;<\/code><\/pre>\n<p>Eureka! We did it!<\/p>\n<p>And I bet you already know what that <em>%1<\/em> means. ;).<\/p>\n<p>It&#8217;s the location of our <strong>InsString1<\/strong>.<\/p>\n<p>So doing a little cleaning&#8230;<\/p>\n<p><em>Assuming the result from our SQL query is stored on the variable <code>$fail<\/code><\/em>:<\/p>\n<pre><code class=\"language-powershell-console\">PS C:\\&gt; $message = $bufferOutput.ToString().Replace(\"%11\",\"\").Replace(\"%12\",\"\").Replace(\"%3%4%5%6%7%8%9%10\",\"\").Replace(\"%1\",$fail.InsString1).Replace(\"%2\",$fail.InsString2).Replace(\"%3\",$fail.InsString3).Replace(\"%4\",$fail.InsString4).Replace(\"%5\",$fail.InsString5).Replace(\"%6\",$fail.InsString6).Replace(\"%7\",$fail.InsString7).Replace(\"%8\",$fail.InsString8).Replace(\"%9\",$fail.InsString9).Replace(\"%10\",$fail.InsString10)\nPS C:\\&gt;\nPS C:\\&gt; $message\nDistribution Manager failed to connect to the distribution point [\"Display=\\\\CMGRDP1.contoso.com\\\"]MSWNET:[\"SMS_SITE=PS1\"]\\\\CMGRDP1.contoso.com\\. Check your network and firewall settings.\nPS C:\\&gt;<\/code><\/pre>\n<p>Now with the results of the query plus a beautifully formatted message you can store this into a database or create your own reports and automations. Your imagination is the limit!<\/p>\n<h2>Conclusion<\/h2>\n<p>Congratulations! You not only automated Configuration Manager Status Messages, but also called a Win32 Native API function!<\/p>\n<p>I hope you had as much fun trying this as I had writing it.<\/p>\n<p>Thank you very much, and I&#8217;ll see you on the next trip!<\/p>\n<h2>Useful links<\/h2>\n<ul>\n<li><a href=\"https:\/\/docs.microsoft.com\/mem\/configmgr\/develop\/reference\/configuration-manager-reference\">Configuration Manager API Reference<\/a><\/li>\n<li><a href=\"https:\/\/docs.microsoft.com\/mem\/configmgr\/develop\/core\/servers\/manage\/about-configuration-manager-component-status-messages\">About Component Status Messages<\/a><\/li>\n<li><a href=\"https:\/\/docs.microsoft.com\/windows\/win32\/api\/winbase\/nf-winbase-formatmessage\">FormatMessage Function winbase.h<\/a><\/li>\n<li><a href=\"https:\/\/docs.microsoft.com\/windows\/win32\/api\/libloaderapi\/nf-libloaderapi-loadlibrarya\">LoadLibrary Function libloaderapi.h<\/a><\/li>\n<li><a href=\"https:\/\/docs.microsoft.com\/windows\/win32\/api\/libloaderapi\/nf-libloaderapi-getmodulehandlea\">GetModuleHandle Function libloaderapi.h<\/a><\/li>\n<li><a href=\"https:\/\/docs.microsoft.com\/dotnet\/standard\/native-interop\/pinvoke\">Platform Invoke (P\/Invoke)<\/a><\/li>\n<li><a href=\"https:\/\/www.pinvoke.net\/default.aspx\/kernel32.formatmessage\">FormatMessage on pinvoke.net (With examples!)<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>This post&#8217;s intent is to show how to read Configuration Manager status messages using WMI and Win32 API function FormatMessage.<\/p>\n","protected":false},"author":62334,"featured_media":77,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[13],"tags":[72,70,69,71],"class_list":["post-688","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-powershell","tag-config-manager","tag-mecm","tag-sccm","tag-status-message"],"acf":[],"blog_post_summary":"<p>This post&#8217;s intent is to show how to read Configuration Manager status messages using WMI and Win32 API function FormatMessage.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/posts\/688","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/users\/62334"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/comments?post=688"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/posts\/688\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/media\/77"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/media?parent=688"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/categories?post=688"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/tags?post=688"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}