Debugging Service Startup

If you are like me working on server development you may run into the situation that a service fails early during startup, i.e. within the first couple of seconds. You’ll soon realize that manually attaching a debugger doesn’t work well, if at all. Even if you’ll be running

windbg -pn MyService.exe

you may not actually be fast enough. Or there are multiple instances of that image running (e.g. SvcHost.exe) and the above command becomes kind of useless. Now if you have been reading the ‘Debugging Tools for Windows’ documentation (or have debugged services before) you’ll already know what I am about to tell you here.

As I mentioned above, the first problem you’re fighting with is that the failure happens very early. Another problem you may have is that your service is running e.g. as ‘NT AUTHORITY\NETWORK SERVICE’ and thus may not be able to interact with your desktop. But Windows’ right here to help you out with ‘Image File Execution Options’. This basically allows you to execute a command when a particular image is being executed.

You start by creating a key for the image file in the registry:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\MyService.exe

There you create a new string value of the name ‘Debugger’ and set its value to the path of the debugger you want to invoke, e.g. something like ‘C:\Debuggers\cdb.exe’. To mitigate the problem of the non-interactive service, you’ll probably want to use the debugger remotely, so you’ll create a debugging server: ‘C:\Debuggers\cdb.exe -server npipe:pipe=MyDebuggingSession’. What you have now is a debugger which is attached as soon as the service starts and which is accessible remotely. Thus, you can either on the server itself or another machine which has access run for instance

windbg -remote npipe:server=my-machine,pipe=MyDebuggingSession

and there you go: you are now debugging the service. Use the various command line options for the debuggers to

  • ignore the initial break point (-g)
  • run commands right after the debugger is attached (-c)
  • create a script which sets some useful breakpoints and run it when the debugger is attached (e.g. -c “$<MyScript.txt”)

You may actually have to adjust the service control timeout. To do so, add a DWORD called ‘ServicesPipeTimeout’ to the registry key

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control

and set its value to the number of milliseconds you want the service to wait before timing out.

You’ll find pretty much all of this information also in the help file for ‘Debugging Tools for Windows’ under ‘Debugging a Service Application’. Enjoy!

RGS Agent OC Tab with Silverlight

I’ve already described how you would create your own client using the RGS Agent WebService. Now what if you want to keep the tab in OC but make it look more fancy? Silverlight of course is your friend here. This post describes how you can create your own Silverlight based Agent tab, which will look like following. Btw, I am using Visual Studio 2008 to build this entire solution.

AgentTab

All the code for this can be found in AgentTabSl.zip (45.3 KB).

Getting Started

First I created a new Silverlight Application project and named it ‘AgentTabSl’. The project template already comes with the basic structure which we need for this Silverlight control. I added another Silverlight user control which I use to show one Agent Group, named ‘Group.xaml’. Its XAML looks like following.

<UserControl x:Class="AgentTabSl.Group"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>

        <Rectangle Width="Auto" Height="Auto" Stroke="Black" RadiusX="4" RadiusY="4"
                   Grid.ColumnSpan="3" Grid.RowSpan="2" Name="Back" />

        <CheckBox Grid.RowSpan="2" Content="" VerticalAlignment="Center" Name="SignedIn"
                  Margin="2" Cursor="Hand" Checked="OnSignedInChanged" Unchecked="OnSignedInChanged" />

        <TextBlock Grid.Column="1" Grid.ColumnSpan="2" Name="GroupName" Margin="0,2"
                   FontWeight="Bold" />

        <TextBlock Grid.Column="1" Grid.Row="1" Margin="0,2">Number of Agents</TextBlock>

        <TextBlock Grid.Column="2" Grid.Row="1" TextAlignment="Right" Name="AgentCount"
                   Margin="2,2,4,2" />
    </Grid>
</UserControl>

I also added some code to the Group class to allow setting the agent group name, whether or not it’s a formal agent group, if the user is currently signed in and the number of agents. On top of this, I added a static method which return a new instance of Group created based on the required parameters.

Retrieving the data from the Web Service

Unfortunately, because of the limitations of the Silverlight runtime, we cannot use the client proxy generated from wsdl.exe here. But you can still add a reference to the web service, e.g. through the ‘Add Service Reference …’ command in Visual Studio. Simply enter the url to a deployed RGS Agent WebService (e.g. https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx) and everything else will be done for you. It is worthwhile to note that all methods of the web service will be asynchronous, but that’s not a problem. I’ve added an event handler for the Loaded event of the Silverlight application:

private void OnLoaded(object sender, RoutedEventArgs e)
{
    _service = new ProxyServiceSoapClient();

    _service.Endpoint.Address = new EndpointAddress(
        "https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx");

    _service.GetGroupsCompleted += OnGetGroupsCompleted;
    _service.GetAgentCompleted += OnGetAgentCompleted;
    _service.SignInCompleted += OnSignInCompleted;
    _service.SignOutCompleted += OnSignOutCompleted;

    _service.GetAgentAsync();

    _timer = new Timer(OnTimer, null, 0, 60 * 1000);
}

Timer is from System.Threading and I use it to periodically (every 60 seconds) refresh the data from the web service. This is required because else the Silverlight application won’t figure out if groups have been added / removed / modified. You should not make it refresh in less than 60 seconds, but in the end, it is your decision (and your servers).

Please keep in mind that if you want to deploy this Silverlight application on a web server different than the OCS WebComponents, you’ll need to allow Silverlight to access to that server. Please read the HTTP Communication and Security with Silverlight topic on MSDN for more information.

Retrieving the agent group memberships

The following code snippet shows the event handler for the GetGroupsCompleted event generated for us through the service reference. The first thing I am doing is to check whether or not we’re on the UI thread. If we’re not, we have to use the Dispatcher to invoke the method on the UI thread. The actual code to add the groups is then as simple as going through all groups and creating a new instance of the Group control which we’ll add to the Children collection of the StackPanel which holds the groups.

private void OnGetGroupsCompleted(object sender, GetGroupsCompletedEventArgs e)
{
    if (!Dispatcher.CheckAccess())
    {
        Dispatcher.BeginInvoke(() => OnGetGroupsCompleted(sender, e));
    }
    else
    {
        IEnumerable<AcdGroup> groups = e.Result.OrderBy(group => group.Name);

        Groups.Children.Clear();

        foreach (AcdGroup group in groups)
        {
            Group ctrl = Group.Create(group.Id, group.CanSignIn, group.IsSignedIn,
                                      group.Name, group.NumberOfAgents);

            ctrl.SignedInChanged += OnSignedInChanged;

            Groups.Children.Add(ctrl);
        }

        SetStatus("Ready.");
    }
}

Sign in / Sign out

Now what’s left is basically to let the user sign in / out throug a click on the checkbox for the corresponding group. As you can see in the code (when you downloaded it), I also added an event to the Group control which is fired when the sign-in state for the group is changed. In the code above you see that we’re subscribing to that event on line 18. So all we need to do is to actually call the corresponding method on the web service when the event is fired.

private void OnSignedInChanged(object sender, EventArgs e)
{
    Group group = sender as Group;

    SetStatus("Please wait while the operation is performed ...");

    if (group.IsSignedIn)
    {
        _service.SignInAsync(group.Id);
    }
    else
    {
        _service.SignOutAsync(group.Id);
    }
}

What’s missing?

If you look at the interface of the web service again, you see that it also allows you to sign in / out with multiple groups at the same time. I’ll leave it to you to implement that in this Silverlight application as well as it does not really involve anything which I haven’t discussed here. Also, you may want to highlight groups which have just been added by using a different gradient for the box.

If you’re planning on actually deploying a Silverlight Agent OC Tab, you most likely also want to make sure that the users get the static strings in their preferred language. The article Deployment and Localization on MSDN should give you all the information required to do that.

Using the Agent WebService of the Response Group Service

Your Office Communications Server 2007 R2, Response Group Service deployment comes with a tiny but nice little addition: the Agent WebService. It basically offers exactly the same data and functionality as the Agent OC tab but does this through a SOAP interface. If you have RGS deployed on a pool with the FQDN ‘ocs-pool-01.contoso.com’, you’ll find the Agent OC tab at https://ocs-pool-01.contoso.com/Rgs/Clients/Tab.aspx. The Agent WebService is then located at https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx. If you are running the OCS WebComponents on other machines than the front ends, then the host name is the FQDN of the WebComponents machine/farm. So here’s how you can write your own client to sign in/out with RGS Agent Groups. The TechNet article Deploying Response Group Clients gives more information about deploying RGS Clients, with focus on the Agent OC Tab.

Generating the Client Proxy

The first thing you typically want to do is to actually generate the proxy code which you will compile into your own client. You do so by calling

wsdl.exe /namespace:RgsAgentService /language:cs /out:RgsAgentService.cs https://ocs-pool-01.contoso.com/Rgs/Clients/ProxyService.asmx?wsdl

This will generate the RgsAgentService.cs file which you can include into your project and use right away. Web Services Description Language Tool (Wsdl.exe) on MSDN has more info on wsdl.exe if needed.

Using the Web Service

Now you’re already good to go and use the web service. The code below shows a sample console application which does the following.

  • Create a new instance of ProxyService (the generated class from the step above) which points to the service on the pool you’re interested in.
  • Query the web service if the current user is an agent or not. This requires you to authenticate with the user’s credentials.
  • If the current user is an agent
    • Determine some basic information (e.g. the name and SIP address of the agent).
    • Retrieve the list of agent groups the agent is a member of in the connected pool.
    • If there are formal agent groups to which the agent is not signed in, the user is asked if he wants to sign in.
      • If he choses to sign in, tries to sign the agent in to those formal groups.
using System;
using System.Collections.Generic;

using RgsAgentService;

namespace RgsClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string poolFqdn = "ocs-pool-01.contoso.com";

            if (args.Length > 0)
            {
                poolFqdn = args[0];
            }

            ProxyService service = ConnectToPool(poolFqdn);

            // First, figure out if the current user is an Agent.
            if (!service.IsAgent())
            {
                Console.WriteLine("You are not an agent in Pool '{0}'.", poolFqdn);
                return;
            }

            // Now get some information about the Agent (i.e. the current User).
            AcdAgent self = service.GetAgent();

            Console.WriteLine("You were authenticated as agent '{0}' ('{1}') in pool '{2}'.",
                self.DisplayName, self.SipAddress, poolFqdn);
            Console.WriteLine();

            // Finally, determine which Agent Groups this Agent belongs to.
            AcdGroup[] agentGroups = service.GetGroups();

            Console.WriteLine("Agent Group Name                  Formal?   Signed In?  # Agents");
            Console.WriteLine("----------------------------------------------------------------");

            Dictionary<string, Guid> agentGroupIdsForSignIn = new Dictionary<string, Guid>();

            for (int i = 0; i < agentGroups.Length; i++)
            {
                Console.WriteLine("{0,-32}  {1,-8}  {2,-10}  {3,8}",
                    agentGroups[i].Name, agentGroups[i].CanSignIn,
                    agentGroups[i].IsSignedIn, agentGroups[i].NumberOfAgents);

                if (agentGroups[i].CanSignIn &&
                    !agentGroups[i].IsSignedIn)
                {
                    agentGroupIdsForSignIn.Add(agentGroups[i].Name, agentGroups[i].Id);
                }
            }

            // If the Agent is not signed in to all his formal groups, then offer
            // him to do so now.
            if (agentGroupIdsForSignIn.Count > 0)
            {
                Console.WriteLine();
                Console.WriteLine("You are not currently signed in to {0} agent group(s):",
                    agentGroupIdsForSignIn.Count);

                foreach (string agentGroupName in agentGroupIdsForSignIn.Keys)
                {
                    Console.WriteLine("    {0}", agentGroupName);
                }

                Console.WriteLine();
                Console.Write("Do you want to sign in to these groups now? [y/n] ");

                ConsoleKeyInfo key = Console.ReadKey();
                while (key.KeyChar != 'y' && key.KeyChar != 'n')
                {
                    key = Console.ReadKey();
                }

                if (key.KeyChar == 'n')
                {
                    return;
                }

                Console.WriteLine();

                if (service.SignInMultiple(agentGroupIdsForSignIn.Values.ToArray()))
                {
                    Console.WriteLine("You have successfully signed in.");
                }
                else
                {
                    Console.WriteLine("Sign-in to at leat one agent group has failed.");
                }
            }
        }

        private static ProxyService ConnectToPool(string poolFqdn)
        {
            ProxyService service = new ProxyService();

            service.Url = String.Format("https://{0}/Rgs/Clients/ProxyService.asmx", poolFqdn);
            service.UseDefaultCredentials = true;

            return service;
        }
    }
}

Running this program will yield something similar to the following.

You were authenticated as agent 'Bob' ('bob@contoso.com') in pool 'rgs-pool-01.contoso.com'.

Agent Group Name                  Formal?   Signed In?  # Agents
----------------------------------------------------------------
Payroll Questions                 False     True               5
General HR Questions              True      False              8

You are not currently signed in to 1 agent group(s):
    General HR Questions

Do you want to sign in to these groups now? [y/n] y
You have successfully signed in.

More Methods

The WebService has a few more methods. These are in particular

  • SignIn(Guid groupId) – Tries to sign the current agent in to the agent group with the given ID
  • SignOut(Guid groupId) – Tries to sign the current agent out of the agent group with the given ID
  • SignOutMultiple(Guid[] groupIds) – Tries to sign the current agent out of all the groups identified with their respective IDs

These methods all return a boolean which indicates success (true) or failure (false).

Summary

You can integrate the RGS Agent Services into your own application by using the corresponding web service which is installed with the Response Group Service. This allows you to offer the same information and functionality as the RGS Agent OC Tab in a customizable manner.

User Search in AD

I stumbled upon the System.DirectoryServices.AccountManagement namespace this week. It was introduced with .Net 3.5 and offers functionality to perform queries on AD objects like users, groups and computers in a more comfortable way than through the DirectorySearcher class from the System.DirectoryServices namespace. To illustrate the ease of using these classes, I came up with a tiny example which lists all users whose account name (the samAccountName attribute in AD) starts with an 'a'. On top of this, using LINQ it is quite simple to convert the resulting PrincipalSearchResult<Principal> collection into an IEnumerable<UserPrincipal>.

using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.Linq;

namespace UserSearch
{
    class Program
    {
        static void Main(string[] args)
        {
            PrincipalContext context = new PrincipalContext(ContextType.Domain, "contoso.com");

            UserPrincipal searchFilter = new UserPrincipal(context);
            searchFilter.SamAccountName = "a*";

            PrincipalSearcher ps = new PrincipalSearcher(searchFilter);

            IEnumerable<UserPrincipal> results = from principal in ps.FindAll()
                                                 where principal is UserPrincipal
                                                 select principal as UserPrincipal;

            foreach (UserPrincipal user in results)
            {
                Console.WriteLine("User '{0}' ({1}) Info:", user.SamAccountName, user.Name);
                Console.WriteLine("    Password Set On  {0}", user.LastPasswordSet);
                Console.WriteLine("    Last Log On      {0}", user.LastLogon);
                Console.WriteLine();
            }
        }
    }
}