Saturday, October 3, 2009

Doing Some Stuff on Another Computer

It is fairly easy to restart a service running on a remote computer. You only need to know two things - the name of a remote computer and the name of the service itself. No surprises.

private void RestartService(string MachineName, string ServiceName)
{
using (ServiceController controller = new ServiceController())
{
controller.MachineName = MachineName;
controller.ServiceName = ServiceName;
controller.Stop();
controller.Start();
}
}

Almost as easy is to monitor the printers on the remote computer using WMI. This time, only the remote computer name is required. Here's a small function that returns the list of CustomPrinterObjects. CustomPrinterObject can be defined like this, for example:

public class CustomPrinterObject
{
private string _name;

public string Name
{
get { return _name; }
set { _name = value; }
}

//many other properties
....

private string _status;

public string Status
{
get { return _status; }
set { _status = value; }
}
}

Here's how I get the information about the printers on the remote computer:

public List GetLocalPrinters(string serverName)
{
string[] pStatus = {"Other","Unknown","Idle","Printing","WarmUp","Stopped Printing",
"Offline"};

string[] pState = {"Paused","Error","Pending Deletion","Paper Jam","Paper Out",
"Manual Feed","Paper Problem", "Offline","IO Active","Busy",
"Printing","Output Bin Full","Not Available","Waiting",
"Processing","Initialization","Warming Up","Toner Low",
"No Toner","Page Punt", "User Intervention Required",
"Out of Memory","Door Open","Server_Unknown","Power Save"};

List printers = new List();

string query = string.Format("SELECT * from Win32_Printer");
ManagementObjectSearcher searcher = new ManagementObjectSearcher(query);
searcher.Scope = new ManagementScope("\\\\" + serverName + "\\root\\CIMV2");
ManagementObjectCollection coll = searcher.Get();

System.Windows.Forms.MessageBox.Show(coll.Count.ToString());

foreach (ManagementObject printer in coll)
{
CustomPrinterObject prn = new CustomPrinterObject();

foreach (PropertyData property in printer.Properties)
{
if (property.Value != null)
{
switch (property.Name)
{
case "Name":
prn.Name = property.Value.ToString();
break;
case "Comment":
prn.Comment = property.Value.ToString();
break;
case "PrinterState":
prn.PrinterState = pState[Convert.ToInt32(property.Value)];
break;
case "PrinterStatus":
prn.PrinterStatus = pStatus[Convert.ToInt32(property.Value)];
break;
case "Location":
prn.Location = property.Value.ToString();
break;
case "Type":
prn.Type = property.Value.ToString();
break;
case "DriverName":
prn.Model = property.Value.ToString();
break;
case "WorkOffline":
prn.Status = property.Value.ToString().Equals("True") ? "Offline" : "Online";
break;
default:
break;
}
}
}
printers.Add(prn);
}
return printers;
}

Reading the registry contents on the remote machine is very easy again.

On the local computer I would open the key like this

RegistryKey rk = Registry.LocalMachine.OpenSubKey(subKey);

And on the remote I would do it like this

RegistryKey hklm = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, "MyRemoteServer");
RegistryKey rk = hklm.OpenSubKey(subKey);

Obviously, all of the samples will work subject to permissions of the account that runs them. Errors will happen if the account does not have enough privileges to access the printers or services on the remote computer.

by . Also posted on my website

Thursday, October 1, 2009

Simple WCF client/server

So communicating between two windows services on the same computer is easy. But then I was asked, what if we decide in the future that we want these services to run on the separate machines? Well, I guess we'll need to make changes to both of them ... and that's exactly what we want to avoid. Okay, the WCF offers a few ways to host a service - in a managed application, in a managed windows service, in IIS, in WAS ... (Hosting Options) Since I already have windows services implemented, the choice is obvious. I looked up a couple of tutorials on the subject fairly quickly:
How to: Host a WCF Service in a Managed Windows Service, WCF Tutorial - Basic Interprocess Communication

However, that was not quite enough for me because the first tutorial's problem was that it explained the configuration file a bit, but did not implement the "client", and the second tutorial implemented both server and client, but had no info on configuration at all. So I quickly got to the point where I could have a server and client running inside separate windows services on the same machine, but as soon as I tried taking one of the services away - to another computer on the network - different errors started to happen. Not enough time and space on explaining each error and what was the reason for it, just a few words on what I ended up with (which eventually worked).

Service implementation

To define and implement the function on the server:

[ServiceContract(Namespace="MyNamespace.IMyInterface")]
public interface IMyInterface
{
[OperationContract]
string ReturnMyString();
}

public class MyInterfaceImplementation : IMyInterface
{
public string ReturnMyString()
{
return "My String";
}
}

To create the instance of the host, first define the host

private ServiceHost host;

In the service OnStart method

if (host != null)
{
host.Close();
}

host = new ServiceHost(typeof(MyInterfaceImplementation), new Uri[] { new Uri(http://MyServer:8080) });
host.AddServiceEndpoint(typeof(IMyInterface), new BasicHttpBinding(), "MyMethod");

In the service OnStop method

if (host != null)
{
host.Close();
host = null;
}

This part was fairly easy.

Service configuration

This bit went into the app.config file inside the "configuration".

<system.serviceModel>
<services>
<service name="MyNamespace.MyService" behaviorConfiguration="MyServiceBehavior">
<host>
<baseAddresses>
<add baseAddress="http://MyServer:8000/MyMethod"/>
</baseAddresses>
</host>
<!-- this endpoint is exposed at the base address provided by host-->
<endpoint address="" binding="basicHttpBinding" contract="MyNamespace.IMyInterface" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="MyServiceBehavior">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="False"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>

Note the service name attribute "MyNamespace.MyService" which is the windows service names, and the endpoint contract attribute, which is the ServiceContract Namespace attribute. Some small things are easy to get wrong, and the error messages will not be very informative.

Client implementation

[ServiceContract(Namespace="MyNamespace.IMyInterface")]
public interface IMyInterface
{
[OperationContract]
string ReturnMyString();
}

public string MyClientString()
{
string result = string.Empty;

string endpoint = "http://MyServer:8000/MyMethod";

ChannelFactory httpFactory = new ChannelFactory(
new BasicHttpBinding(), new EndpointAddress(endpoint));

IMyInterface httpProxy = httpFactory.CreateChannel();

while (true)
{
result = httpProxy.ReturnMyString();
if (result != string.Empty)
{
return result;
}
}
}

I missed the [ServiceContract(Namespace="MyNamespace.IMyInterface")] bit initially on the interface definition and the error message was really not helping. It went like that: "Exception: The message with Action 'http://tempuri.org/IMyInterface/ReturnMyString' cannot be processed at the receiver" and so on. What tempuri.org? I never pun any tempuri.org in my project! OK, turns out it is some default name that was used because I have not provided my own.

Client configuration

Just a small bit of configuration was required here (and I'm not even 100% sure that all of it is required)

<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="basicHttp"/>
</basicHttpBinding>
</bindings>
<client>
<!-- this endpoint is exposed at the base address provided by host-->
<endpoint address="" binding="basicHttpBinding" contract="MyNamespace.IMyInterface" />
</client>
</system.serviceModel>

Overall, it's just a few dozen lines of code, but it took me almost a whole day to get it working properly through the network.

by . Also posted on my website