Every programmer has a certain set of routines that he refers back to and uses over and over again. These utility functions are usually bits of code that are not provided by any particular language or framework. This chapter is a compilation of utility routines that we have gathered during our time with C# and the .NET Framework. The types of things we share in this chapter are:
Power management events
Determining the path for various locations in the operating system
Interacting with services
Inspecting the Global Assembly Cache
Message queuing
It is a grab bag of code that can help to solve a specific need while you are working on a larger set of functionality in your application.
You want to be notified whenever the operating system or a user has initiated an action that requires your application to shut down or be inactive (user logoff, remote session disconnect, system shutdown, hibernate/restore, etc.). This notification will allow you to have your application respond gracefully to the changes.
Use the Microsoft.Win32.SystemEvents
class to get notification of operating system, user session change, and power management events. The RegisterForSystemEvents
method shown next hooks up the five event handlers necessary to capture these events and should be placed in the initialization section for your code:
public static void RegisterForSystemEvents() { // always get the final notification when the event thread is shutting down // so we can unregister SystemEvents.EventsThreadShutdown += new EventHandler(OnEventsThreadShutdown); SystemEvents.PowerModeChanged += new PowerModeChangedEventHandler(OnPowerModeChanged); SystemEvents.SessionSwitch += new SessionSwitchEventHandler(OnSessionSwitch); SystemEvents.SessionEnding += new SessionEndingEventHandler(OnSessionEnding); SystemEvents.SessionEnded += new SessionEndedEventHandler(OnSessionEnded); }
The EventsThreadShutdown
event notifies you of when the thread that is distributing the events from the SystemEvents
class is shutting down so that you can unregister the events on the SystemEvents
class if you have not already done so. The PowerModeChanged
event triggers when the user suspends or resumes the system from a suspended state. The SessionSwitch
event is triggered by a change in the logged-on user. The SessionEnding
event is triggered when the user is trying to log off or shut down the system, and the SessionEnded
event is triggered when the user is actually logging off or shutting down the system.
You can unregister the events using the UnregisterFromSystemEvents
method. UnregisterFromSystemEvents
should be called from the termination code of your Windows Form, user control, or any other class that may come and go, as well as from one other area shown later in the recipe:
private static void UnregisterFromSystemEvents() { SystemEvents.EventsThreadShutdown -= new EventHandler(OnEventsThreadShutdown); SystemEvents.PowerModeChanged -= new PowerModeChangedEventHandler(OnPowerModeChanged); SystemEvents.SessionSwitch -= new SessionSwitchEventHandler(OnSessionSwitch); SystemEvents.SessionEnding -= new SessionEndingEventHandler(OnSessionEnding); SystemEvents.SessionEnded -= new SessionEndedEventHandler(OnSessionEnded); }
Since the events exposed by SystemEvents
are static, if you are using them in a section of code that could be invoked multiple times (secondary Windows Form, user control, monitoring class, etc.), you must unregister your handlers, or you will cause memory leaks in the application.
The SystemEvents
handler methods are the individual event handlers for each event that has been subscribed to in RegisterForSystemEvents
. The first handler to cover is the OnEventsThreadShutdown
handler. It is essential that your handlers are unregistered if this event fires, as the notification thread for the SystemEvents
class is going away, and the class may be gone before your application is. If you haven’t unregistered before that point, you will cause memory leaks, so add a call to UnregisterFromSystemEvents
into this handler as shown here:
private static void OnEventsThreadShutdown(object sender, EventArgs e) { Debug.WriteLine( "System event thread is shutting down, no more notifications."); // Unregister all our events as the notification thread is going away UnregisterFromSystemEvents(); }
The next handler to explore is the OnPowerModeChanged
method. This handler can report the type of power management event through the Mode
property of the PowerModeEventChangedArgs
parameter. The Mode
property has the PowerMode
enumeration type and specifies the event type through the enumeration value contained therein:
private static void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e) { // power mode is changing switch (e.Mode) { case PowerModes.Resume: Debug.WriteLine("PowerMode: OS is resuming from suspended state"); break; case PowerModes.StatusChange: Debug.WriteLine( "PowerMode: There was a change relating to the power" + " supply (weak battery, unplug, etc..)"); break; case PowerModes.Suspend: Debug.WriteLine("PowerMode: OS is about to be suspended"); break; } }
The next three handlers all deal with operating system session states. They are OnSessionSwitch
, OnSessionEnding
, and OnSessionEnded
. Handling all three of these events covers all of the operating system session state transitions that your application may need to worry about. In OnSessionEnding
, there is a SessionEndingEventArgs
parameter, which has a Cancel
member. This Cancel
member allows you to request that the session not end if it is set to false
. Code for the three handlers is shown in Example 13-1.
private static void OnSessionSwitch(object sender, SessionSwitchEventArgs e) { // check reason switch (e.Reason) { case SessionSwitchReason.ConsoleConnect: Debug.WriteLine("Session connected from the console"); break; case SessionSwitchReason.ConsoleDisconnect: Debug.WriteLine("Session disconnected from the console"); break; case SessionSwitchReason.RemoteConnect: Debug.WriteLine("Remote session connected"); break; case SessionSwitchReason.RemoteDisconnect: Debug.WriteLine("Remote session disconnected"); break; case SessionSwitchReason.SessionLock: Debug.WriteLine("Session has been locked"); break; case SessionSwitchReason.SessionLogoff: Debug.WriteLine("User was logged off from a session"); break; case SessionSwitchReason.SessionLogon: Debug.WriteLine("User has logged on to a session"); break; case SessionSwitchReason.SessionRemoteControl: Debug.WriteLine("Session changed to or from remote status"); break; case SessionSwitchReason.SessionUnlock: Debug.WriteLine("Session has been unlocked"); break; } } private static void OnSessionEnding(object sender, SessionEndingEventArgs e) { // true to cancel the user request to end the session, false otherwise e.Cancel = false; // check reason switch(e.Reason) { case SessionEndReasons.Logoff: Debug.WriteLine("Session ending as the user is logging off"); break; case SessionEndReasons.SystemShutdown: Debug.WriteLine("Session ending as the OS is shutting down"); break; } } private static void OnSessionEnded(object sender, SessionEndedEventArgs e) { switch (e.Reason) { case SessionEndReasons.Logoff: Debug.WriteLine("Session ended as the user is logging off"); break; case SessionEndReasons.SystemShutdown: Debug.WriteLine("Session ended as the OS is shutting down"); break; } }
The .NET Framework provides many opportunities to get feedback from the system when there are changes due to user or system interactions. The SystemEvents
class exposes more events than just the ones used in this recipe. For a full listing, see Table 13-1.
Value | Description |
---|---|
DisplaySettingsChanged |
User changed display settings. |
DisplaySettingsChanging |
Display settings are changing. |
EventsThreadShutdown |
Thread listening for system events is terminating. |
InstalledFontsChanged |
User added or removed fonts. |
PaletteChanged |
User switched to an application with a different palette. |
PowerModeChanged |
User suspended or resumed the system. |
SessionEnded |
User shut down the system or logged off. |
SessionEnding |
User is attempting to shut down the system or log off. |
SessionSwitch |
The currently logged-in user changed. |
TimeChanged |
User changed system time. |
TimerElapsed |
A Windows timer interval expired. |
UserPreferenceChanged |
User changed a preference in the system. |
UserPreferenceChanging |
User is trying to change a preference in the system. |
Keep in mind that these are system events. Therefore, the amount of work done in the handlers should be kept to a minimum, so the system can move on to the next task.
The notifications from SystemEvents
come on a dedicated thread for raising these events. In a UI application, you will need to get back onto the correct user interface thread before updating a UI with any of this information, using one of the various methods for doing so (Control.BeginInvoke
, Control.Invoke
, or BackgroundWorker
).
Note that .NET Core (the open source version of .NET for cross-platform coding) does not include a Microsoft.Win32.SystemEvents
class at the time of this writing, so this recipe will not work on .NET Core (until someone adds it!).
The “SystemEvents Class,” “PowerModeChangedEventArgs Class,” “SessionEndedEventArgs Class,” “SessionEndingEventArgs Class,” and “SessionSwitchEventArgs Class” topics in the MSDN documentation.
Use the System.ServiceProcess.ServiceController
class to control the service. ServiceController
allows you to interact with an existing service and to read and change its properties. In the example, it will be used to manipulate the ASP.NET State Service. The name, the service type, and the display name are easily available from the ServiceName
, ServiceType
, and DisplayName
properties:
ServiceController scStateService = new ServiceController("COM+ Event System"); Console.WriteLine($"Service Type: {scStateService.ServiceType.ToString()}"); Console.WriteLine($"Service Name: {scStateService.ServiceName}"); Console.WriteLine($"Display Name: {scStateService.DisplayName}");
The ServiceType
enumeration has a number of values, as shown in Table 13-2.
Value | Description |
---|---|
Adapter |
Service that serves a hardware device |
FileSystemDriver |
Driver for the filesystem (kernel level) |
InteractiveProcess |
Service that communicates with the desktop |
KernelDriver |
Low-level hardware device driver |
RecognizerDriver |
Driver for identifying filesystems on startup |
Win32OwnProcess |
Win32 program that runs as a service in its own process |
Win32ShareProcess |
Win32 program that runs as a service in a shared process such as SvcHost |
One useful task is to determine a service’s dependents. The services that depend on the current service are accessed through the DependentServices
property, an array of ServiceController
instances (one for each dependent service):
foreach (ServiceController sc in scStateService.DependentServices) Console.WriteLine($"{scStateService.DisplayName} is depended on by: " + $" {sc.DisplayName}");
By contrast, the ServicesDependedOn
array contains ServiceController
instances for each of the services the current service depends on:
foreach (ServiceController sc in scStateService.ServicesDependedOn) Console.WriteLine( $"{scStateService.DisplayName} depends on: {sc.DisplayName}");
One of the most important things about services is what state they are in. A service doesn’t do much good if it is supposed to be running and it isn’t—or worse yet, if it is supposed to be disabled (perhaps as a security risk) and isn’t. To find out the current status of the service, check the Status
property. For this example, the original state of the service will be saved, so it can be restored later in the originalState
variable:
Console.WriteLine($"Status: {scStateService.Status}"); // save original state ServiceControllerStatus originalState = scStateService.Status;
Now that we have set up the proper access, we can start to work with the service methods. If a service is stopped, it can be started with the Start
method. First, check if the service is stopped, and then, once Start
has been called on the ServiceController
instance, call the WaitForStatus
method to make sure that the service started. WaitForStatus
can take a timeout value so that the application is not waiting forever for the service to start in the case of a problem:
TimeSpan serviceTimeout = TimeSpan.FromSeconds(60); // if it is stopped, start it if (scStateService.Status == ServiceControllerStatus.Stopped) { scStateService.Start(); // wait up to 60 seconds for start scStateService.WaitForStatus(ServiceControllerStatus.Running, serviceTimeout); } Console.WriteLine($"Status: {scStateService.Status}");
Services can also be paused. If the service is paused, the application needs to determine if it can be continued by checking the CanPauseAndContinue
property. If so, the Continue
method will get the service going again, and the WaitForStatus
method should be called to wait until it does:
// if it is paused, continue if (scStateService.Status == ServiceControllerStatus.Paused) { if (scStateService.CanPauseAndContinue) { scStateService.Continue(); // wait up to 60 seconds for start scStateService.WaitForStatus(ServiceControllerStatus.Running, serviceTimeout); } } Console.WriteLine($"Status: {scStateService.Status}"); // Should be running at this point.
To determine if a service can be stopped, you use the CanStop
property. If it can be stopped, then stopping it is a matter of calling the Stop
method followed by WaitForStatus
:
// can we stop it? if (scStateService.CanStop) { scStateService.Stop(); // wait up to 60 seconds for stop scStateService.WaitForStatus(ServiceControllerStatus.Stopped, serviceTimeout); } Console.WriteLine($"Status: {scStateService.Status}");
Even though CanStop
could have returned true
, if we are not running under an administrative context, we would have gotten this exception when trying to stop the service:
A first chance exception of type 'System.InvalidOperationException' occurred in System.ServiceProcess.dll Additional information: Cannot open EventSystem service on computer '.'.
See the Discussion section for how to set up proper security access for the code.
Now it is time to set the service back to how you found it. The originalState
variable has the original state, and the switch
statement holds actions for taking the service from the current stopped state to its original state:
// set it back to the original state switch (originalState) { case ServiceControllerStatus.Stopped: if (scStateService.CanStop) scStateService.Stop(); break; case ServiceControllerStatus.Running: scStateService.Start(); // wait up to 60 seconds for start scStateService.WaitForStatus(ServiceControllerStatus.Running, serviceTimeout); break; case ServiceControllerStatus.Paused: // if it was paused and is stopped, need to restart so we can pause if (scStateService.Status == ServiceControllerStatus.Stopped) { scStateService.Start(); // wait up to 60 seconds for start scStateService.WaitForStatus(ServiceControllerStatus.Running, serviceTimeout); } // now pause if (scStateService.CanPauseAndContinue) { scStateService.Pause(); // wait up to 60 seconds for paused scStateService.WaitForStatus(ServiceControllerStatus.Paused, serviceTimeout); } break; }
To be sure that the Status
property is correct on the service, the application should call Refresh
to update it before testing the value of the Status
property. Once the application is done with the service, call the Close
method:
scStateService.Refresh(); Console.WriteLine($"Status: {scStateService.Status.ToString()}"); // close it scStateService.Close();
Services run many of the operating system functions today. They usually run under a system account (LocalSystem
, NetworkService
, LocalService
) or a specific user account that has been granted specific permissions and rights. If your application uses a service, this is a good way to determine if everything for the service to run is set up and configured properly before your application attempts to use it. Not all applications depend on services directly. But if your application does, or you have written a service as part of your application, it can be handy to have an easy way to check the status of your service and possibly correct the situation.
When you are manipulating services, the question of access comes into play. While in earlier Microsoft operating systems (pre–Windows 7) you could call the ServiceController
APIs without any special privileges, with the introduction of User Account Control you now have to be in an administrative context to access methods that affect the service operation. You can still inspect the properties of the service without this level of access, but if you want to Start, Stop, and so on, you need to have elevated privileges.
To accomplish this in code, you would add an app.manifest file to your application by right-clicking the project and selecting Add*New Item and selecting the Application Manifest File, as shown in Figure 13-1.
In the asmv1:assembly rustinfosecurity equestedPrivileges section of the file the default requested execution level is to run as the person invoking the code:
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
To allow access to the service methods, we will change this so our code requires an administrative context by setting the level
attribute to requireAdministrator
:
<!-- Necessary for service interaction in Recipe 13.2 Controlling a Service --> <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
This will ensure that when the code is run, it requires the user to have enough rights to perform the actions we are requesting.
The “ServiceController Class” and “ServiceControllerStatus Enumeration” topics in the MSDN documentation.
Use the GetProcessesAssemblyIsLoadedIn
method that we’ve created for this purpose to return a list of processes that contain a given assembly. GetProcessesAssemblyIsLoadedIn
takes the filename of the assembly to look for (such as mscoree.dll) and then gets a list of the currently running processes on the machine by calling Process.GetProcesses
. It then searches the processes to see if the assembly is loaded into any of them. When found in a process, that Process
object is projected into an enumerable set of Process
objects. The iterator for the set of processes found is returned from the query:
public static IEnumerable<Process> GetProcessesAssemblyIsLoadedIn( string assemblyFileName) { // System and Idle are not actually processes, so there are no modules // associated and we skip them. var processes = from process in Process.GetProcesses() where process.ProcessName != "System" && process.ProcessName != "Idle" from ProcessModule processModule in process.SafeGetModules() where processModule.ModuleName.Equals(assemblyFileName, StringComparison.OrdinalIgnoreCase) select process; return processes; }
The Process.GetSafeModules
extension method gets a list of the modules that the caller is authorized to see for the process. If we just accessed the Modules
property directly, we would get a series of different access errors depending on the caller’s security context:
public static ProcessModuleCollection SafeGetModules(this Process process) { List<ProcessModule> listModules = new List<ProcessModule>(); ProcessModuleCollection modules = new ProcessModuleCollection(listModules.ToArray()); try { modules = process.Modules; } catch (InvalidOperationException) { } catch (PlatformNotSupportedException) { } catch (NotSupportedException) { } catch (Win32Exception wex) { Console.WriteLine($"Couldn't get modules for {process.ProcessName}: " + $"{wex.Message}"); } // return either the modules or an empty collection return modules; }
In some circumstances, such as when you are uninstalling software or debugging version conflicts, it is beneficial to know if an assembly is loaded into more than one process. By quickly getting a list of the Process
objects that the assembly is loaded in, you can narrow the scope of your investigation.
The following code uses this routine to look for .NET 4 processes:
string searchAssm = "mscoree.dll"; var processes = GetProcessesAssemblyIsLoadedIn(searchAssm); foreach (Process p in processes) Console.WriteLine($"Found {searchAssm} in {p.MainModule.ModuleName}");
When you’re running the GetProcessesAssemblyIsLoadedIn
method, the user’s security context plays a large role in how much the code can discover. If the caller is a normal Windows user not running in the administrative context (which must be entered into explicitly), you would see a number of processes reported that cannot have their modules examined, as shown in Example 13-2.
Couldn't get modules for dasHost: Access is denied Couldn't get modules for WUDFHost: Access is denied Couldn't get modules for StandardCollector.Service: Access is denied Couldn't get modules for winlogon: Access is denied Couldn't get modules for svchost: Access is denied Couldn't get modules for FcsSas: Access is denied Couldn't get modules for VBCSCompiler: Access is denied Couldn't get modules for svchost: Access is denied Couldn't get modules for coherence: Access is denied Couldn't get modules for coherence: Access is denied Couldn't get modules for svchost: Access is denied Couldn't get modules for MOMService: Access is denied Couldn't get modules for svchost: Access is denied Couldn't get modules for csrss: Access is denied Couldn't get modules for svchost: Access is denied Couldn't get modules for vmms: Access is denied Couldn't get modules for dwm: Access is denied Found mscoree.dll in Microsoft.VsHub.Server.HttpHostx64.exe Couldn't get modules for wininit: Access is denied Couldn't get modules for svchost: Access is denied Couldn't get modules for prl_tools: Access is denied Couldn't get modules for coherence: Access is denied Couldn't get modules for MpCmdRun: Access is denied Couldn't get modules for svchost: Access is denied Couldn't get modules for svchost: Access is denied Couldn't get modules for audiodg: Access is denied Couldn't get modules for mqsvc: Access is denied Couldn't get modules for WmiApSrv: Access is denied Couldn't get modules for conhost: Access is denied Couldn't get modules for sqlwriter: Access is denied Couldn't get modules for svchost: Access is denied Couldn't get modules for svchost: Access is denied Found mscoree.dll in CSharpRecipes.exe Couldn't get modules for WmiPrvSE: Access is denied Couldn't get modules for spoolsv: Access is denied Couldn't get modules for svchost: Access is denied Couldn't get modules for WmiPrvSE: Access is denied Couldn't get modules for svchost: Access is denied Found mscoree.dll in msvsmon.exe Couldn't get modules for csrss: Access is denied Couldn't get modules for dllhost: Access is denied Couldn't get modules for svchost: Access is denied Couldn't get modules for SearchIndexer: Access is denied Couldn't get modules for WmiPrvSE: Access is denied Found mscoree.dll in VBCSCompiler.exe Couldn't get modules for svchost: Access is denied Couldn't get modules for OSPPSVC: Access is denied Couldn't get modules for WmiPrvSE: Access is denied Couldn't get modules for smss: Access is denied Couldn't get modules for IpOverUsbSvc: Access is denied Couldn't get modules for lsass: Access is denied Couldn't get modules for services: Access is denied Couldn't get modules for MsMpEng: Access is denied Couldn't get modules for msdtc: Access is denied Couldn't get modules for prl_tools_service: Access is denied Couldn't get modules for inetinfo: Access is denied Couldn't get modules for sppsvc: Access is denied
When we run the same call to the GetProcessesAssemblyIsLoadedIn
method under an administrative context, we get output similar to Example 13-3.
Found mscoree.dll in VBCSCompiler.exe Found mscoree.dll in Microsoft.VsHub.Server.HttpHostx64.exe Found mscoree.dll in msvsmon.exe Found mscoree.dll in VBCSCompiler.exe Couldn't get modules for audiodg: Access is denied Found mscoree.dll in ElevatedPrivilegeActions.vshost.exe Couldn't get modules for sppsvc: Access is denied
Since this is a diagnostic function, you will need FullTrust
security access to use this method.
Note that the query skips inspection for the System
and Idle
processes:
var processes = from process in Process.GetProcesses() where process.ProcessName != "System" && process.ProcessName != "Idle" from ProcessModule processModule in process.SafeGetModules() where processModule.ModuleName.Equals(assemblyFileName, StringComparison.OrdinalIgnoreCase) select process;
The Modules
collection can’t be used to examine these two processes, so it throws a Win32Exception
. There are two other processes you might see access denied for: audiodg
and sppsvc
:
audiodg
is a DRM-protected process used to host audio drivers so that they can be run in login sessions isolated from locally logged-in users.
sppsvc
is a Microsoft software protection platform service that can be used to prevent the use of software without a license.
Since both of those services are sensitive in the operating system, you can see why they would not be accessible through Process
enumeration.
The “Process Class,” “ProcessModule Class,” and “GetProcesses Method” topics in the MSDN documentation.
Use message queues to separate this work, and use the MQWorker
class shown here in both the first and second components to write and read messages to and from the associated message queue.
Message queues provide an asynchronous communications protocol between parties, meaning that the sender and receiver of the message do not need to interact with the message queue at the same time. Messages placed onto the queue are stored until the recipient retrieves them. They allow you to ensure messages don’t get lost, keep the work segmented, handle inconsistent loads, and scale your application by having multiple workers that can read from a queue.
MQWorker
uses the local message-queuing services to store and retrieve messages. The queue pathname is supplied in the constructor, and the existence of the queue is checked in the SetUpQueue
method:
class MQWorker : IDisposable { private bool _disposed; private string _mqPathName; MessageQueue _queue; public MQWorker(string queuePathName) { if (string.IsNullOrEmpty(queuePathName)) throw new ArgumentNullException(nameof(queuePathName)); _mqPathName = queuePathName; SetUpQueue(); }
SetUpQueue
creates a message queue of the supplied name using the MessageQueue
class if none exists. It accounts for the scenario in which the message-queuing services are running on a workstation computer. In that situation, it makes the queue private, as that is the only type of queue allowed on a workstation:
private void SetUpQueue() { // See if the queue exists, create it if not if (!MessageQueue.Exists(_mqPathName)) { try { _queue = MessageQueue.Create(_mqPathName); } catch (MessageQueueException mqex) { // see if we are running on a workgroup computer if (mqex.MessageQueueErrorCode == MessageQueueErrorCode.UnsupportedOperation) { string origPath = _mqPathName; // must be a private queue in workstation mode int index = _mqPathName.ToLowerInvariant(). IndexOf("private$", StringComparison.OrdinalIgnoreCase); if (index == -1) { // get the first index = _mqPathName.IndexOf(@"", StringComparison.OrdinalIgnoreCase); // insert private$ after server entry _mqPathName = _mqPathName.Insert(index + 1, @"private$"); // try try again try { if (!MessageQueue.Exists(_mqPathName)) _queue = MessageQueue.Create(_mqPathName); else _queue = new MessageQueue(_mqPathName); } catch (Exception) { // set original as inner exception throw new Exception( $"Failed to create message queue with {origPath}" + $" or {_mqPathName}", mqex); } } } } } else { _queue = new MessageQueue(_mqPathName); } }
The SendMessage
method sends a message to the queue to set up in the constructor. The body of the message is supplied in the body
parameter, and then an instance of System.Messaging.Message
is created and populated. The BinaryMessageFormatter
is used to format the message, as it enables larger volumes of messages to be sent with fewer resources than does the default XmlMessageFormatter
. To make messages persistent (so that they stick around until they are processed and are not lost if the machine loses power), it sets the Recoverable
property to true
. Finally, the Body
is set, and the message is sent:
public void SendMessage(string label, string body) { Message msg = new Message(); // label our message msg.Label = label; // override the default XML formatting with binary // as it is faster (at the expense of legibility while debugging) msg.Formatter = new BinaryMessageFormatter(); // make this message persist (causes message to be written // to disk) msg.Recoverable = true; msg.Body = body; _queue?.Send(msg); }
The ReadMessage
method reads messages from the queue set up in the constructor by creating a Message
object and calling its Receive
method. The message formatter is set to the BinaryMessageFormatter
for the Message
, since that is how we write to the queue. Finally, the body of the message is returned from the method:
public string ReadMessage() { Message msg = null; msg = _queue.Receive(); msg.Formatter = new BinaryMessageFormatter(); return (string)msg.Body; } #region IDisposable Members public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (!this._disposed) { if (disposing) _queue.Dispose(); _disposed = true; } } #endregion }
To show how the MQWorker
class is used, the following example creates an MQWorker
. It then sends a message (a small blob of XML) using SendMessage
and retrieves it using ReadMessage
:
// NOTE: Message Queue services must be set up for this to work. // This can be added in Add/Remove Windows Components. using (MQWorker mqw = new MQWorker(@".MQWorkerQ")) { string xml = "<MyXml><InnerXml location="inside"/></MyXml>"; Console.WriteLine("Sending message to message queue: " + xml); mqw.SendMessage("Label for message", xml); string retXml = mqw.ReadMessage(); Console.WriteLine("Read message from message queue: " + retXml); }
Message queues are very useful when you are attempting to distribute the processing load for scalability purposes. Without question, using a message queue adds overhead to the processing because the messages must travel through the infrastructure of MSMQ. One benefit, however, is that MSMQ allows your application to spread out across multiple machines, so there can be a net gain in processing. Another advantage is that message queuing supports reliable asynchronous handling of the messages so that the sending side can be confident that the receiving side will get the message without the sender having to wait for confirmation. The message queue services are not installed by default, but can be installed through the Add/Remove Windows Components applet in Control Panel.
Using a message queue to buffer your processing logic from high volumes of requests (such as in the web service scenario presented earlier) can lead to more stability and ultimately produce more throughput for your application by using multiple reader processes on multiple machines.
The “Message Class” and “MessageQueue Class” topics in the MSDN documentation.
Use the Console.SetOut
method to capture and release the standard output stream. SetOut
sets the standard output stream to whatever System.IO.TextWriter
-based stream it is handed. To capture the output to a file, create a StreamWriter
to write to it, and set that writer using SetOut
. We use Path.GetTempFileName
to get a location to write our log to that is accessible by the identity calling the code.
Now when Console.WriteLine
is called, the output goes to the StreamWriter
, not to stdout
, as shown here:
try { Console.WriteLine("Stealing standard output!"); string logfile = Path.GetTempFileName(); Console.WriteLine($"Logging to: {logfile}"); using (StreamWriter writer = new StreamWriter(logfile)) { // steal stdout for our own purposes... Console.SetOut(writer); Console.WriteLine("Writing to the console... NOT!"); for (int i = 0; i < 10; i++) Console.WriteLine(i); } } catch(IOException e) { Debug.WriteLine(e.ToString()); return ; }
To restore writing to the standard output stream, create another StreamWriter
. This time, call the Console.OpenStandardOutput
method to acquire the standard output stream and use SetOut
to set it once again. Now calls to Console.WriteLine
appear on the console again:
// Recover the standard output stream so that a // completion message can be displayed. StreamWriter standardOutput = new StreamWriter(Console.OpenStandardOutput()); standardOutput.AutoFlush = true; Console.SetOut(standardOutput); Console.WriteLine("Back to standard output!");
The console output from this code looks similar to this:
Stealing standard output! Logging to: C:UsersuserAppDataLocalTemp mpFE7C.tmp Back to standard output!
The logfile we created contains the following after the code is executed:
Writing to the console... NOT! 0 1 2 3 4 5 6 7 8 9
Redirecting the standard output stream inside of the program may seem a bit antiquated. But consider the situation when you’re using another class that writes information to this stream. You don’t want the output to appear in your application, but you have to use the class. This could also be useful if you create a small launcher application to capture output from a console application or if you are using a third-party assembly that insists on outputting lots of verbose messages that would be confusing to your user.
The “Console.SetOut Method,” “Console.OpenStandardOutput Method,” “Path.GetTempFilePath Method,” and “StreamWriter Class” topics in the MSDN documentation.
Use the RedirectStandardOutput
property of the Process.StartInfo
class to capture the output from the process. By redirecting the standard output stream of the process, you read it when the process terminates. UseShellExecute
is a property on the ProcessInfo
class that tells the runtime whether or not to use the Windows shell to start the process. By default, it is turned on (true
) and the shell runs the program, which means that the output cannot be redirected. UseShellExecute
needs to be turned off (set to false
) so the redirection can occur.
In this example, a Process
object for cmd.exe is set up with arguments to perform a directory listing, and then the output is redirected. A logfile is created to hold the resulting output, and the Process.Start
method is called:
Process application = new Process(); // run the command shell application.StartInfo.FileName = @"cmd.exe"; // get a directory listing from the current directory application.StartInfo.Arguments = @"/Cdir " + Environment.CurrentDirectory; Console.WriteLine($"Running cmd.exe with arguments:" + $"{application.StartInfo.Arguments}"); // redirect standard output so we can read it application.StartInfo.RedirectStandardOutput = true; application.StartInfo.UseShellExecute = false; // Create a log file to hold the results in the current EXE directory string logfile = Path.GetTempFileName(); Console.WriteLine($"Logging to: {logfile}"); using (StreamWriter logger = new StreamWriter(logfile)) { // start it up application.Start();
Once the process is started, the StandardOutput
stream can be accessed and a reference to it held. Once the application finishes, the code then reads in the information from the output stream that was written while the application ran and writes it to the logfile that was set up previously. Finally, the logfile is closed and then the Process
object is closed:
application.WaitForExit(); string output = application.StandardOutput.ReadToEnd(); logger.Write(output); } // close the process object application.Close();
The temporary logfile we created using Path.GetTempPathFile
holds information similar to the following output:
Volume in drive C has no label. Volume Serial Number is DDDD-FFFF Directory of C:CS60_CookbookCSCB6CSharpRecipesinDebug 04/11/2015 04:27 PM <DIR> . 04/11/2015 04:27 PM <DIR> .. 02/05/2015 10:06 PM 724 BigSpenders.xml 02/05/2015 10:05 PM 719 Categories.xml 02/05/2015 04:04 PM 64,566 CSCBCover.bmp 12/31/2014 05:23 PM 489,269 CSharpCookbook.zip 04/11/2015 04:27 PM 495,616 CSharpRecipes.exe 04/11/2015 04:27 PM 31,154 CSharpRecipes.exe.CodeAnalysisLog.xml 02/05/2015 09:53 PM 3,075 CSharpRecipes.exe.config 04/11/2015 04:27 PM 0 CSharpRecipes.exe.lastcodeanalysissucceeded 04/11/2015 04:27 PM 775,680 CSharpRecipes.pdb 02/05/2015 04:04 PM 5,190,856 EntityFramework.dll 02/05/2015 04:04 PM 620,232 EntityFramework.SqlServer.dll 02/05/2015 04:04 PM 154,645 EntityFramework.SqlServer.xml 02/05/2015 04:04 PM 3,645,119 EntityFramework.xml 03/09/2015 02:51 PM 6,569 IngredientList.txt 04/04/2015 09:55 AM 513,536 Newtonsoft.Json.dll 04/04/2015 09:55 AM 494,336 Newtonsoft.Json.xml 03/09/2015 02:51 PM 4,390,912 Northwind.mdf 04/06/2015 04:11 PM 51,712 NorthwindLinq2Sql.dll 04/06/2015 04:11 PM 128,512 NorthwindLinq2Sql.pdb 04/11/2015 01:18 PM 573,440 Northwind_log.ldf 03/09/2015 02:51 PM 80 RecipeChapters.txt 04/06/2015 04:11 PM 16,384 SampleClassLibrary.dll 04/06/2015 04:11 PM 1,283 SampleClassLibrary.dll.CodeAnalysisLog.xml 04/06/2015 04:11 PM 0 SampleClassLibrary.dll.lastcodeanalysissucceeded 04/06/2015 04:11 PM 11,776 SampleClassLibrary.pdb 12/02/2014 03:35 PM 387 SampleClassLibraryTests.xml 04/11/2015 03:48 PM 8,704 SharedCode.dll 04/11/2015 03:48 PM 15,872 SharedCode.pdb 28 File(s) 17,685,158 bytes 2 Dir(s) 67,929,718,784 bytes free
Redirecting standard output can be of great use for tasks like automated build scenarios or test harnesses. While not quite as easy as simply placing >
after the command line for a process at the command prompt, this approach is more flexible, as the stream output can be reformatted to XML or HTML for posting to a website. It also provides the opportunity to send the data to multiple locations at once, which the simple command-line redirect function in Windows can’t do.
Waiting to read from the stream until the application has finished ensures that there will be no deadlock issues. If the stream is accessed synchronously before this time, then it’s possible for the parent to block the child. At a minimum, the child will wait until the parent has finished reading from the stream before it continues writing to it. So, by postponing the read until the end, you save the child some performance degradation at the cost of some additional time at the end.
The “ProcessStartInfo.RedirectStandardOutput Property” and “ProcessStartInfo.UseShellExecute Property” topics in the MSDN documentation.
Create a separate AppDomain
to run the code using the AppDomain.CreateDomain
method. CreateDomain
allows the application to control many aspects of the AppDomain
being created, like the security environment, AppDomain
settings, and base paths for the AppDomain
. To demonstrate this, the following code creates an instance of the RunMe
class (shown in full later in this recipe) and calls the PrintCurrentAppDomainName
method. This prints the name of the AppDomain
where the code is running:
AppDomain myOwnAppDomain = AppDomain.CreateDomain("MyOwnAppDomain"); // print out our current AppDomain name RunMe rm = new RunMe(); rm.PrintCurrentAppDomainName();
Now, you create an instance of the RunMe
class in the "MyOwnAppDomain" AppDomain
by calling CreateInstance
on the AppDomain
. We pass CreateInstance
the module and type information necessary for constructing the type, and it returns an ObjectHandle
.
We can then retrieve a proxy to the instance running in the AppDomain
by taking the returned ObjectHandle
and casting it to a RunMe
reference using the Unwrap
method:
// Create our RunMe class in the new appdomain Type adType = typeof(RunMe); ObjectHandle objHdl = myOwnAppDomain.CreateInstance(adType.Module.Assembly.FullName, adType.FullName); // unwrap the reference RunMe adRunMe = (RunMe)objHdl.Unwrap();
The PrintCurrentAppDomainName
method is called on the RunMe
instance in the "MyOwnAppDomain" AppDomain
, and it prints out "Hello from MyOwnAppDomain!"
. The AppDomain
is unloaded via AppDomain.Unload
and the program terminates:
// make a call on the toolbox adRunMe.PrintCurrentAppDomainName(); // now unload the appdomain AppDomain.Unload(myOwnAppDomain);
The RunMe
class is defined here. It inherits from MarshalByRefObject
, as that allows you to retrieve the proxy reference when you call Unwrap
on the ObjectHandle
and have the calls on the class remoted into the new AppDomain
. The PrintCurrentApp-DomainName
method simply accesses the FriendlyName
property on the current AppDomain
and prints out the “Hello from {AppDomain
}!” message:
public class RunMe : MarshalByRefObject { public RunMe() { PrintCurrentAppDomainName(); } public void PrintCurrentAppDomainName() { string name = AppDomain.CurrentDomain.FriendlyName; Console.WriteLine($"Hello from {name}!"); } }
The output from this example is shown here:
Hello from CSharpRecipes.exe! Hello from CSharpRecipes.exe! Hello from MyOwnAppDomain! Hello from MyOwnAppDomain!
Isolating code in a separate AppDomain
is overkill for something as trivial as this example, but it demonstrates that code can be executed remotely in an AppDomain
created by your application. There are six overloads for the CreateDomain
method, and each adds a bit more complexity to the AppDomain
creation. In situations in which the isolation or configuration benefits outweigh the complexities of not only setting up a separate AppDomain
but debugging code in it as well, it is a useful tool. A good real-world example is hosting a separate AppDomain
to run ASP.NET pages outside of the normal ASP.NET environment (though this is truly a nontrivial usage) or loading third-party code into a secondary AppDomain
for isolation.
The “AppDomain Class,” “AppDomain.CreateDomain Method,” and “ObjectHandle Class” topics in the MSDN documentation.
Use the GetOSAndServicePack
method shown in Example 13-4 to get a string representing the current operating system and service pack. GetOSAndServicePack
uses the Environment.OSVersion
property to get the version information for the operating system and checks the registry for the “official” name of the OS. The OperatingSystem
class retrieved from Environment.OSVersion
has a property for the service pack, called ServicePack
. These values are all returned as the operating system name, version, and service pack string.
public static string GetOSAndServicePack() { // Get the current OS info OperatingSystem os = Environment.OSVersion; RegistryKey rk = Registry.LocalMachine.OpenSubKey( @"SOFTWAREMicrosoftWindows NTCurrentVersion"); string osText = (string)rk?.GetValue("ProductName"); if (string.IsNullOrWhiteSpace(osText)) osText = os.VersionString; else osText = ( $"{osText} {os.Version.Major}.{os.Version.Minor}.{os.Version.Build}"); if (!string.IsNullOrWhiteSpace(os.ServicePack)) osText = $"{osText} {os.ServicePack}"; return osText; }
Enabling your application to know the current operating system and service pack allows you to include that information in debugging reports and in the About box (if you have one) for your application. This simple knowledge, transmitted through your support department, can save you hours in debugging time. It is well worth making this information available so your support department can easily direct your clients to it if they cannot otherwise locate it.
The “Environment.OSVersion Property” and “OperatingSystem Class” topics in the MSDN documentation.