Partager via


Migrating a Native Windows Service to Windows Azure

The overall proof of concept I’m trying out is how you could migrate a Native Windows Service to Azure and communicate with this service from another role instance in the cloud. The goal is to reuse as much code as possible while still allowing the Azure Runtime to "monitor" my service. I borrowed most of the implementation of my service from a Windows SDK sample named "Service". The service itself is written in C and just creates a secured named pipe that receives data, processes it (reverses it), and sends it back to the client.

Let’s look at some of the dependencies this service has just from what we know by the above description:

  • Security: The named pipe has an ACL on it so whatever machine is writing/reading the pipe will need to be authenticated.
  • Distributing binaries: I’ll need to deploy the service executable and the VC++ Runtime library (which is not installed by default on Azure nodes).
  • Since the bulk of my code is running in a Windows Service and not directly in the worker role how will Azure know if something went wrong?
  • How will the two roles communicate? Named pipes? How is that configured?

To get started with this project I created a solution with the following projects:

  • Cloud Project named "ServiceDemo"
  • Worker Role named "Service Watcher"
  • Web Role named "ASPXServerClient"
  • Windows Service named "Service"

 

So let’s tackle the dependencies one at a time.

Security Dependency

Here is the security descriptor set by my service during the call to CreateNamedPipe(...):

    TCHAR * szSD = TEXT("D:") // Discretionary ACL

        TEXT("(D;OICI;GA;;;BG)") // Deny access to built-in guests

        TEXT("(D;OICI;GA;;;AN)") // Deny access to anonymous logon

        TEXT("(A;OICI;GRGWGX;;;AU)") // Allow read/write/execute to authenticated users

        TEXT("(A;OICI;GA;;;BA)"); // Allow full control to administrators

If I want to authenticate from my web role to my Windows service how can I do that? There is no Active Directory in this scenario. A relatively simple solution to this problem is to create duplicate local accounts on both the web and worker role instances. Then impersonate the local account from the ASP.NET site to access the service.

This can be accomplished by using an elevated startup task running the following commands:

Startup.cmd
net user serviceUser [somePassword] /add
exit /b 0

The first line creates a local user named serviceUser with whatever password you set. The second line returns from the batch file. This particular user does not have to be part of the administrators groups due to my services service descriptor allowing the named pipe to be written to by users in the Users group. My ASP.NET code will have to authenticate as this user when it writes to the named pipe (I’ll show that a bit later).

This startup task needs to be run for the web role AND the worker role so you will need to add the following to both roles in the ServiceDefinition.csdef. Note: my startup tasks are in a folder I created in each project called “Startup” and put startup.cmd within.
<Task commandLine="Startup\Startup.cmd" executionContext="elevated" taskType="simple" />

Note the elevated requirement due to creating users.

Distributing Binaries

This one is pretty straightforward. To have additional files deploy with your worker role add them to your project (in a folder). Once added select each file in Visual Studio and change Build Action to Content and Copy to Output Directory to Copy if Newer.
For instance you will need to download the VC runtime that your service is compiled with. In my case it is the VC++ 2010 for x64 (vcredist_x64.exe). Once downloaded add the file to your Startup folder.

These steps will ensure the VC++ runtime is copied to the Azure servers when the project is published.

The next step is to add another step to the startup task for your worker role only:

"%~dp0vcredist_x64.exe" /q /norestart

This command will start the install quietly (and tell it not to reboot). The %~dp0 is a batch file constant that essentially means the current directory that the batch file is running in.

Service Installation

I also need to install my service silently. In my case the service supports a command line argument –install that performs this properly. Your service will need to perform similarly for it to install in a worker role.
The complete startup.cmd for the worker role is here:

net user serviceUser [somePassword] /add
"%~dp0vcredist_x64.exe" /q /norestart
"%~dp0Service.exe" -install
exit /b 0

Monitoring the Windows Service
As the name of my worker role implies (ServiceWatcher) I want to monitor my Windows Service so the Azure runtime is aware of any problems and can reimage/start the role as needed.
I have a simple class (probably not real robust either!) that checks if my service is running and if not tries to start it. If it is not started after these steps it returns false otherwise true.

class ServiceMonitor

{

    public static bool CheckAndStartService(String ServiceName, int StartTimeOutSeconds)

    {

        ServiceController mySC = new ServiceController(ServiceName);

        if (IsServiceStatusOK(mySC.Status) == false)

        {

            System.Diagnostics.Trace.WriteLine("Starting Service.....");

            try

            {

                mySC.Start();

            }

            catch(Exception) {}

            mySC.WaitForStatus(ServiceControllerStatus.Running, new TimeSpan(0, 0, StartTimeOutSeconds));

 

            if (IsServiceStatusOK(mySC.Status))

                return true;

            else

                return false;

        }

        return true;

    }

 

    static private bool IsServiceStatusOK(ServiceControllerStatus Status)

    {

        if (Status != ServiceControllerStatus.Running && Status != ServiceControllerStatus.StartPending && Status != ServiceControllerStatus.ContinuePending)

        {

            return false;

        }

        return true;

    }

}

My worker role’s Run() method is as follows:

public override void Run()

{

    String ServiceName = "SimpleService";

    int ServiceFailCount = 0;

    const int MAXFAILS = 5;

while (true)

    {

        // if the service failed more than 5 times return

        if (ServiceFailCount >= MAXFAILS)

        {

            Trace.TraceError(String.Format(String.Format("{0} has failed to start {1} times", ServiceName, ServiceFailCount)));

            return;

        }

        try

        {

            // Check if the service is running

            if (ServiceMonitor.CheckAndStartService("SimpleService", 10) == true)

            {

                Trace.TraceInformation("Service is Running");

            }

            else

            {

                // if not increment the fail count

                ServiceFailCount++;

                Trace.TraceError("Service is no longer Running");

            }

        }

        catch (Exception e)

        {

            Trace.TraceError("Exception occurred: " + e.Message);

        }

        Thread.Sleep(10000);

    }

}

The worker role's Run method stays and checks the status of the Windows service every 10 seconds. If the service fails to start five times (MAXFAILS) then it returns which tells Azure that something went wrong.

Using the ServiceController class does require elevation. So in ServiceDefinition.csdef add the following line for the ServiceWatcher role instance:

<Runtime executionContext="elevated" />

 

How do the roles communicate?
The port for named pipes is 445. To allow communication between the web and worker roles I will need an internal endpoint opened up on the ServiceWatcher worker role.

 

 

Impersonating the Local User

My ASP.NET code will need to use impersonation to connect to the service to authenticate:

I’ve created a helper class for this:

public class LogonHelpers

{

    // Declare signatures for Win32 LogonUser and CloseHandle APIs

    [DllImport("advapi32.dll", SetLastError = true)]

    public static extern bool LogonUser(

        string principal,

        string authority,

        string password,

        LogonSessionType logonType,

        LogonProvider logonProvider,

        out IntPtr token);

    [DllImport("kernel32.dll", SetLastError = true)]

    public static extern bool CloseHandle(IntPtr handle);

    public enum LogonSessionType : uint

    {

        Interactive = 2,

        Network,

        Batch,

        Service,

        NetworkCleartext = 8,

        NewCredentials

    }

    public enum LogonProvider : uint

    {

        Default = 0, // default for platform (use this!)

        WinNT35, // sends smoke signals to authority

        WinNT40, // uses NTLM

        WinNT50 // negotiates Kerb or NTLM

    }

}

 

Load Balancing the Internal Endpoint

One other helper function I want to mention is the following:

// Internal endpoints are not load balanced so do it ourself

private String GetRandomServiceIP()

{

    var endpoints = RoleEnvironment.Roles["ServiceWatcher"].Instances.Select(i => i.InstanceEndpoints["NamedPipes"]).ToArray();

    Random r = new Random(DateTime.Now.Millisecond);

    int ipIndex = r.Next(endpoints.Count());

    return endpoints[ipIndex].IPEndpoint.Address.ToString();

}

This method is needed because internal endpoints are not load balanced by Azure. So if you configured multiple instances of your service to run you will want to load balance these calls.

 

Writing to and Reading from the Named Pipe

Now for the magic to actually write data to and read data from the service’s named pipe:

protected void cmdProcessText_Click(object sender, EventArgs e)

{

    IntPtr token = IntPtr.Zero;

    WindowsImpersonationContext impersonatedUser = null;

    String ServiceUserName = "serviceUser";

    String ServicePassword = "somePassword";

    String ServiceInstanceIP = String.Empty;

try

    {

ServiceInstanceIP = GetRandomServiceIP();

        bool impResult = LogonHelpers.LogonUser(ServiceUserName, ".", ServicePassword, LogonHelpers.LogonSessionType.Interactive, LogonHelpers.LogonProvider.Default, out token);

        if (impResult == false)

        {

            lblProcessedText.Text = "LogonUser failed";

            return;

        }

  WindowsIdentity id = new WindowsIdentity(token);

  // Begin impersonation

        impersonatedUser = id.Impersonate();

// Resource access here uses the impersonated identity

        NamedPipeClientStream pipe = new NamedPipeClientStream(ServiceInstanceIP, "simple", PipeDirection.InOut);

// connect to the pipe - give 10 seconds before timing out

        pipe.Connect(10000);

  String tmpInput = txtTextToProcess.Text;

// null terminate for our native C code

        tmpInput += '\0';

// Write to the pipe service

        StreamWriter sw = new StreamWriter(pipe);

        sw.AutoFlush = true;

        sw.Write(tmpInput);

// Wait for the result after processing

        StreamReader sr = new StreamReader(pipe);

        String result = sr.ReadToEnd();

        lblProcessedText.Text = "Processed Text: " + result + " by instance: " + ServiceInstanceIP;

    }

    catch (Exception exc)

    {

        lblProcessedText.Text = "Exception occurred: " + exc.Message;

    }

    finally

    {

        if (impersonatedUser != null)

            impersonatedUser.Undo();

if (token != null)

            LogonHelpers.CloseHandle(token);

    }

}

 

 

Simple Enough!

Comments

  • Anonymous
    April 21, 2011
    Hello when I run the startup in worker role for the following command: "%~dp0vcredist_x64.exe" /q /norestart The vcredist_x64.exe will pup-on a dialog (about "license agreement"), that need to click "next" or "install" to confirm the installation, then the installation will wait for there. So the worker role shows as "Waiting for role to start...", How do you solve this problem? Thank you Mike

  • Anonymous
    April 21, 2011
    Interesting.. I didn't realize that the VC++ redist files that are downloaded require the EULA popup. I did found another blog that addresses it though: Take a look here and see if it helps: blogs.msdn.com/.../update-regarding-silent-install-of-the-vc-8-0-runtime-vcredist-packages.aspx

  • Anonymous
    February 21, 2014
    Hi, can you share the source code? Thanks!