Running iTunes in a Windows Service with C#

“No, I have not heard the song, Jesus, Take the Wheel. No, no, no, no, no, no, no, you don’t need to start singing it. Yes, I’ll buy it on the iTunes, Mother. Good-bye, Mother.” – Sheldon Cooper.

Using “the iTunes” application from Apple is necessary for a lot of people that use Apple TV devices. Granted there are other applications/systems that can function as an iTunes server – but the actual iTunes application is your best choice for functionality.

Sadly though, the makers of the product seem to go out of their way to prevent you using this product on a home server. By that, I mean it’s not designed to run as a service (generally necessary for a headless server/virtual machine), and it has numerous prompts that appear on start-up – which need to be clicked on before continuing. Some of those prompts very helpfully inform you that you don’t have an audio card installed, which is unsurprisingly quite common on a headless/virtual server, or that your video card does not support HD playback.

As a result, you have several choices to use the iTunes application as a server for your Apple TV (and other) devices.

  1. You could buy a Mac, leave it logged in, and run the iTunes application interactively.
  2. You could buy a Windows PC, or run a Windows VM, leave it logged in, and run the iTunes application interactively.
  3. You can build (or buy) a product that is specifically designed to run iTunes as a service.

Now, option 1 and 2 work – and they’re good for most home users. However, for those of us that don’t want to leave a system logged in somewhere with a manual login required – followed by manually starting iTunes and clicking on all of the prompts – a different solution is required.

Therefore, this post will focus on option 3 above – building or buying a product that can run iTunes as a service.

In the early days (as in – 2 years ago, not early days as in when we were all running 386sx computers), I went with the ‘buy’ option. I used a product called iHomeServer. The product worked quite well and was relatively feature packed. There was a small fee for its use which was perfectly reasonable given its functionality. However, that functionality didn’t quite do what I needed it to do – no fault of the author – so I eventually looked into doing it myself which is the essence of this post. For reference, the missing functionality was integration with my home automation system.

If you’ve ever written a C/C++ Win32 application with a UI, then you’ve probably needed to implement a message handler for handling system/windows activity messages. For those that are interested, the MSDN help on the subject, whilst rather lengthy, is very good. Put simply, it teaches us how a window object and its associated controls (label, text box, button) are tracked and managed.

This is important to know because this functionality is what we need to determine what is currently being displayed by the iTunes application – e.g. a which window is being displayed, what text is being displayed on that window, what buttons does that window have, and which of those buttons is the ‘OK’ button. With that information we can then inform the application that someone has clicked on the specific button needed to close the window.

For example, the iTunes application starts – and it puts a dialog box on the screen…

iTunes AV Error

iTunes doesn’t function until someone has clicked OK on that dialog box. Now, this specific error can actually be negated with a ‘virtual audio card.’ I’ve not tried that approach, but theoretically if you did create a virtual audio card on the Windows system running iTunes – then iTunes shouldn’t complain about the lack of audio card. However, it’s a solution that only addresses one of the prompts.

Once you’ve navigated that hurdle, then you might get hit with the quick start dialog, or the new version dialog (shown below) etc. You might also get a license agreement prompt. Now I’ve included the code one might use if you wanted to automate accepting the license agreement, however of course the license agreement should be read thoroughly, and then willingly and personally accepted. It’s generally only going to ask you once anyway.

iTunes Version

As a result, we need a system that can determine which prompt is on the screen, and then click the appropriate button. When I say click the button, I don’t mean trying to predict where the dialog is on the screen so that we can actually click it, I mean we need to:

  1. Identify the dialog box on the screen – based on the title, or the text.
  2. Once we know which dialog it is (e.g. audio error), locate the button control that is needed to close the dialog box.
  3. Once we know whch button control to use, send the Windows command message for button click to that specific button control – informing the application that someone has clicked the button.

The code I developed covers all of the dialog boxes that I’ve seen, and I’ve had it run quite reliably over the last year without ever needing to manually intervene.

One last thing before we get started on the code though – a few versions ago iTunes became a lot more sensitive to the video card you were using – which is particularly problematic if you’re on a virtual server with a basic virtual video card. Luckily, there is an alternative version of iTunes that doesn’t complain about the video card – and that can be downloaded from Apple here. Thanks again to the author of the iHomeServer product for pointing that out.

So, to get started, we first need to create a C# windows service. Instructions for that can be located here. Now, those instructions actally cover the creation of a Windows service that includes WCF – but you can ignore the WCF piece if you don’t need it.

You’ll then need a reference to the iTunes type library – ideally this means you’ll need iTunes installed on your development system. We need this for gracefully exiting iTunes when the service is shutdown. You’ll also need it for manipulating the iTunes library from within your service – assuming that’s something you need to do.

iTunes Reference

Ok, the basic premise is that we need to start the iTunes process, monitor the process’ health (if it is still running), and monitor which windows are being displayed by the process. For the sake of the example, I’m going to use a few BackgroundWorker instances – you can either continue with those or start the additional threads needed manually.

With the basic Windows service project created, we’ll need to add some code to the service class.

using System;
using System.ComponentModel;
using System.Reflection;
using System.ServiceProcess;

namespace iTunesService
{
public partial class ServiceCore : ServiceBase
{
private static string _strAppTitle = "";
private static string _strAppVersion = "";
private static string _strAppPath = "";
private static ServiceCore _oInstance = null;
private static System.Timers.Timer _oTimerLoopWindow;

public static ServiceCore GetInstance()
{
if (_oInstance == null)
_oInstance = new ServiceCore();

return _oInstance;
}

public ServiceCore()
{
Assembly aAssembly = Assembly.GetExecutingAssembly();
Version vAssembly = aAssembly.GetName().Version;

InitializeComponent();

// Initialize Global Variables
_strAppTitle = "iTunes Service";
_strAppVersion = string.Format("{0}.{1}.{2}", vAssembly.Major, vAssembly.Minor, vAssembly.Build);
_strAppPath = System.IO.Path.GetDirectoryName(aAssembly.Location);

// Set Service Parameters
this.ServiceName = "iTunesService";
this.CanStop = true;
this.CanPauseAndContinue = false;
this.AutoLog = true;
}

protected override void OnStart(string[] args)
{
// Service Startup Code
this.EventLog.WriteEntry(string.Format("Service {0} (v{1}) starting.", _strAppTitle, _strAppVersion));

// Start iTunes Process
if (!UIControl.StartServer())
{
this.Stop();
return;
}

// Process Window Timer
_oTimerLoopWindow = new System.Timers.Timer();
_oTimerLoopWindow.Interval = 5000;
_oTimerLoopWindow.Elapsed += TimerLoopWindow_Elapsed;
_oTimerLoopWindow.Start();

backgroundWorkerProcess.RunWorkerAsync();
}

protected override void OnStop()
{
// Service Stop Code
this.EventLog.WriteEntry("Service stopping.");

_oTimerLoopWindow.Enabled = false;

backgroundWorkerProcess.CancelAsync();

// Stop iTunes Process
UIControl.StopServer();
}

private void backgroundWorkerProcess_DoWork(object sender, DoWorkEventArgs e)
{
// Monitor iTunes Process
UIControl.MonitorProcess(backgroundWorkerProcess);
}

private void backgroundWorkerWindow_DoWork(object sender, DoWorkEventArgs e)
{
// Process iTunes Windows
UIControl.ProcessWindows();
}

private void TimerLoopWindow_Elapsed(object sender, EventArgs e)
{
if (UIControl.IsProcessRunning())
_oTimerLoopWindow.Interval = 1000 * 10;
else
_oTimerLoopWindow.Interval = 1000;

if (!backgroundWorkerWindow.IsBusy)
backgroundWorkerWindow.RunWorkerAsync();
}
}
}

There’s not a lot to that class. We basically have two BackgroundWorker classes – backgroundWorkerWindow and backgroundWorkerProcess. I’m using backgroundWorkerWindow to launch the code to check which windows iTunes is displaying, and then action them if needed (i.e. click OK on a dialog window). I’m then using backgroundWorkerProcess to monitor if the iTunes process is still running, and if not, restart it. All of that code however is in the UIControl class. The only part to really call out here is in the TimerLoopWindows_Elapsed subroutine. The UIControl.IsProcessRunning() function will only return True once the iTunes application is running, and the startup dialog boxes are closed – e.g. the application is now useable. If the application is not in the useable state, e.g. it’s still starting up, then check the status of the open windows every 1 second. Once the application is running normally, then only check the status of the windows every 10 seconds.

Before we get to the UIControl class though, we need to create a small utility class called WindowInstance – this class is used as part of the processing code to determine which window is currently being displayed by iTunes. As the window processing code discovers information about the active window, it stores that information in an instance of the WindowInstance class.


using System;

namespace iTunesService
{
internal class WindowInstance
{
public string Title;
public string Text;
public int CloseControlID;
public IntPtr CloseControlhWnd;
public WindowType DialogType;

public WindowInstance()
{
Title = "";
Text = "";
CloseControlID = -1;
CloseControlhWnd = IntPtr.Zero;
DialogType = WindowType.Unknown;
}

public enum WindowType
{
Audio,
License,
Instance,
QuickTour,
Scripting,
Unknown,
Version,
Video
}

public string WindowDescription
{
get
{
switch (DialogType)
{
case WindowType.Audio:
return "Audio/Visual Error";

case WindowType.Instance:
return "Multiple Instances Open";

case WindowType.License:
return "License Agreement";

case WindowType.QuickTour:
return "Quick Tour";

case WindowType.Scripting:
return "Scripting Interface Open";

case WindowType.Version:
return "New Version Available";

case WindowType.Video:
return "Video Playback Error";

default:
return "Unknown";
}
}
}
}
}

The UIControl class is responsible for starting and stopping the process, monitoring the status of the process, and processing the windows being displayed by the iTunes application. The basic shell of the class is displayed below. As you can see, we need to import several Win32 functions for processing the window/dialog boxes.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using iTunesLib;

namespace iTunesService
{
internal class UIControl
{
private static int _iProcessId = 0;
private static bool _bProcessRunning = false;

private delegate bool EnumWindowProc(IntPtr hWnd, IntPtr lParam);

private const int WM_COMMAND = 0x0111;
private const int BN_CLICKED = 0;

#region Imports

[DllImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam);

[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern int GetWindowTextLength(HandleRef hWnd);

[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern int GetWindowText(HandleRef hWnd, StringBuilder lpString, int nMaxCount);

[DllImport("user32.dll")]
static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam);

[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

[DllImport("user32.dll", SetLastError = true)]
static extern int GetDlgCtrlID(IntPtr hWnd);

[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

#endregion

public static bool IsProcessRunning()...
public static void MonitorProcess(BackgroundWorker backgroundWorkerProcess)...
public static bool StartServer()...
public static void StopServer()...
public static void ProcessWindows()...
private static bool EnumProcessWindow(IntPtr hWnd, IntPtr lParam)...
private static void ProcessMainWindow(IntPtr hWnd)...
private static void ProcessDialogWindow(IntPtr hWnd)...
private static bool EnumChildControl(IntPtr hWnd, IntPtr lParam)...
}
}

Let’s start with the basic Start/Stop functions. The StartServer() function is fairly straight forward, we’re just using the Process class to create a new running process. The process Id is then stored in a class level variable. I’ve simplified all of the error handling functions to a simple Debug.WriteLine() command, but you’d usually have that logged/handled through a generic error handler for your service. The StopServer() subroutine on the other hand, other than needing some more error handling around the GetProcessById function, closes the iTunes application cleanly. We use the iTunes DCOM interface to close the application. The reason why we need to call ProcessWindows() is in case the application decides to popup any additional dialog boxes before closing. We try process the windows a few times, with a 2 x a 5 second delay to let it close, and then finally give up and terminate the process.

public static bool StartServer()
{
Process pInstance;
ProcessStartInfo psInfo = new ProcessStartInfo();
bool bRetVal = true;

Debug.WriteLine("UIControl.StartServer()");

_bProcessRunning = false;
_iProcessId = 0;

try
{
psInfo.FileName = Properties.Settings.Default.iTunesPath;
psInfo.Arguments = "";
psInfo.LoadUserProfile = true;
psInfo.UseShellExecute = false;
psInfo.CreateNoWindow = true;

pInstance = Process.Start(psInfo);
_iProcessId = pInstance.Id;

Debug.WriteLine(string.Format("iTunes Process Started (Process {0})", _iProcessId.ToString()));
}
catch (Exception eException)
{
Debug.WriteLine(string.Format("UIControl.StartServer() Unable to start iTunes. Error: {0}.", eException.ToString()));
bRetVal = false;
goto Finish;
}

Finish:
return bRetVal;
}

public static void StopServer()
{
iTunesApp iTunesApplication;
Process pInstance;

Debug.WriteLine("UIControl.StopServer()");

if (_iProcessId > 0)
{
// Add a Try/Catch around this block
pInstance = Process.GetProcessById(_iProcessId);

// Quit iTunes
iTunesApplication = new iTunesLib.iTunesApp();
iTunesApplication.Quit();

ProcessWindows();

if (!pInstance.WaitForExit(5000))
{
ProcessWindows();

if (!pInstance.WaitForExit(5000))
{
Debug.WriteLine("UIControl.StopServer() Terminating process");
pInstance.Kill();
}
}

pInstance.Close();
}

Debug.WriteLine("UIControl.StopServer() Complete");
}

public static bool IsProcessRunning()
{
return _bProcessRunning;
}

The MonitorProcess subroutine is a little clumsy, but you get the idea. What we’re trying to do here is check to see if the iTunes process is still running, or if it has crashed somehow. The reason I’m checking using the existing process Id, and not simply a WMI process query, is to make sure its the instance that we started. Under normal circumstances, this subroutine will just go to sleep for 5 seconds at a time, and then check if the process is still running; if not, then restart it.

public static void MonitorProcess(BackgroundWorker backgroundWorkerProcess)
{
Process pInstance;

Debug.WriteLine("UIControl.ProcessMonitor()");

while (!backgroundWorkerProcess.CancellationPending)
{
try
{
// Check if process is still running
pInstance = Process.GetProcessById(_iProcessId);

if (pInstance.WaitForExit(5000))
{
// Process has stopped

if (backgroundWorkerProcess.CancellationPending)
break;

Debug.WriteLine("UIControl.ProcessMonitor() Process stopped");

// Restart Process
if (!UIControl.StartServer())
Thread.Sleep(10000);
}
}
finally { }
}

Debug.WriteLine("UIControl.ProcessMonitor() Complete");
}

Now we can move onto the code for processing the windows. The basic concept is:

  1. ProcessWindow() – Call the EnumChildWindows function to enumerate through all active windows (for all processes).
  2. EnumProcessWindow() – This is the callback function executed by the system for each discovered window.
  3. EnumProcessWindow() – Check if the current window belongs to our iTunes process.
  4. EnumProcessWindow() – Determine the class type of the current window and execute the appropriate subroutine.
  5. EnumProcessWindow() – If the class type is a dialog window, then call ProcessDialogWindow().
  6. ProcessDialogWindow() – Create a new WindowInstance instance to track information about the current dialog window.
  7. ProcessDialogWindow() – Call GetWindowText to determine the dialog window title.
  8. ProcessDialogWindow() – If the title is obvious – e.g. iTunes Quick Tour – then classify the window type appropriately.
  9. ProcessDialogWindow() – If the title isn’t obvious, then call EnumChildWindows to inspect the contents of the dialog window.
  10. EnumChildControl() – This is the callback function executed by the system for each discovered child control in the dialog window.
  11. EnumChildControl() – Determine the child control type (e.g. label (Static) or button (Button)).
  12. EnumChildControl() – If the child control is a Static (label), retrieve the contents of the label and attempt to match it to an expected phrase – thus determining the type of dialog window.
  13. EnumChildControl() – If the child control is a Button, and we already know which window type it is, then find the appropriate button needed to close that dialog box.
  14. ProcessDialogWindow() – Once control is returned to this function, check if the window type has been identified.
  15. ProcessDialogWindow() – If the window type has been identified, check if the close button has been identified yet. If not, call EnumChildWindows again (it might have found the button before it knew the window type during the first pass).
  16. ProcessDialogWindow() – Once the window type has been identified and the close button control Id identified, call SendMessage to ‘click’ the button.
  17. EnumProcessWindow() – If the class type is the main window, then call ProcessMainWindow().
  18. ProcessMainWindow() – If the window title is ‘iTunes’, then change _bProcessRunning to True to indicate the application has now started.
public static void ProcessWindows()
{
EnumWindowProc childProc = new EnumWindowProc(EnumProcessWindow);

Debug.WriteLine("UIControl.ProcessWindows()");

if (_iProcessId == 0)
return;

EnumChildWindows(IntPtr.Zero, childProc, IntPtr.Zero);
}

private static bool EnumProcessWindow(IntPtr hWnd, IntPtr lParam)
{
StringBuilder sbClassName = new StringBuilder(200);
uint iProcessId;

if (_iProcessId == 0)
return false;

GetWindowThreadProcessId(hWnd, out iProcessId);

if (iProcessId == _iProcessId)
{
GetClassName(hWnd, sbClassName, sbClassName.Capacity);

// Dialog Window
if (sbClassName.ToString() == "iTunesCustomModalDialog" | sbClassName.ToString() == "#32770")
ProcessDialogWindow(hWnd);
// Main Window
else if (sbClassName.ToString() == "iTunes")
ProcessMainWindow(hWnd);
}

return true;
}

private static void ProcessMainWindow(IntPtr hWnd)
{
StringBuilder sbWindowTitle;
int iCapacity = 0;

Debug.WriteLine("UIControl.ProcessMainWindow()");

iCapacity = GetWindowTextLength(new HandleRef(ServiceCore.GetInstance(), hWnd)) * 2;
sbWindowTitle = new StringBuilder(iCapacity);
GetWindowText(new HandleRef(ServiceCore.GetInstance(), hWnd), sbWindowTitle, sbWindowTitle.Capacity);

if (!_bProcessRunning && sbWindowTitle.ToString() == "iTunes")
{
_bProcessRunning = true;
Debug.WriteLine("UIControl.ProcessMainWindow() Process Running");
}
}

private static void ProcessDialogWindow(IntPtr hWnd)
{
WindowInstance windowInstance = new WindowInstance();
EnumWindowProc childProc;
StringBuilder sbWindowTitle;
GCHandle gchWindowInstance = GCHandle.Alloc(windowInstance);
IntPtr ipWindowInstance = GCHandle.ToIntPtr(gchWindowInstance);
int iCapacity = 0, wParam = 0;

Debug.WriteLine("UIControl.ProcessDialogWindow()");

// Get Window Title
iCapacity = GetWindowTextLength(new HandleRef(ServiceCore.GetInstance(), hWnd)) * 2;
sbWindowTitle = new StringBuilder(iCapacity);
GetWindowText(new HandleRef(ServiceCore.GetInstance(), hWnd), sbWindowTitle, sbWindowTitle.Capacity);

windowInstance.Title = sbWindowTitle.ToString();

// Determine Window Type by Title
if (windowInstance.Title.Contains("License Agreement"))
windowInstance.DialogType = WindowInstance.WindowType.License;
else if (windowInstance.Title.Contains("iTunes Quick Tour"))
windowInstance.DialogType = WindowInstance.WindowType.QuickTour;
// Determine Window Type by Contents (Child Controls)
else
{
childProc = new EnumWindowProc(EnumChildControl);
EnumChildWindows(hWnd, childProc, ipWindowInstance);

if (windowInstance.DialogType == WindowInstance.WindowType.Unknown)
{
Debug.WriteLine(string.Format("Window: Unknown - {0}: {1}", windowInstance.Title, windowInstance.Text));
return;
}
}

Debug.WriteLine("UIControl.ProcessDialogWindow() Window Type: " + windowInstance.DialogType);

// Find Close Control ID
if (windowInstance.CloseControlID == -1)
{
childProc = new EnumWindowProc(EnumChildControl);
EnumChildWindows(hWnd, childProc, ipWindowInstance);
}

Debug.WriteLine(string.Format("UIControl.ProcessDialogWindow() Window: {0}", windowInstance.WindowDescription));
Debug.WriteLine(string.Format("UIControl.ProcessDialogWindow() Close Control ID: 0x{0}", windowInstance.CloseControlID.ToString("X8")));

// Send WM_COMMAND/BN_CLICKED message to button
wParam = (BN_CLICKED << 16) | (windowInstance.CloseControlID & 0xffff); SendMessage(hWnd, WM_COMMAND, wParam, windowInstance.CloseControlhWnd); gchWindowInstance.Free(); } private static bool EnumChildControl(IntPtr hWnd, IntPtr lParam) { GCHandle gchWindowInstance; WindowInstance windowInstance; StringBuilder sbClassName = new StringBuilder(400), sbWindowText; string strClassName, strWindowText; int iCapacity = 0; Debug.WriteLine("UIControl.EnumChildControl()"); gchWindowInstance = GCHandle.FromIntPtr(lParam); if (gchWindowInstance == null || gchWindowInstance.Target == null) return false; windowInstance = gchWindowInstance.Target as WindowInstance; // Get Control Class/Type if (GetClassName(hWnd, sbClassName, 200) == 0) { Debug.WriteLine(string.Format("UIControl.EnumChildControl() Unable to get window class name. Error: {0}", Marshal.GetLastWin32Error().ToString())); return false; } strClassName = sbClassName.ToString(); // Label Control if (strClassName == "Static") { iCapacity = GetWindowTextLength(new HandleRef(ServiceCore.GetInstance(), hWnd)) * 2; sbWindowText = new StringBuilder(iCapacity); GetWindowText(new HandleRef(ServiceCore.GetInstance(), hWnd), sbWindowText, sbClassName.Capacity); strWindowText = sbWindowText.ToString(); if (strWindowText.Length > 0)
{
windowInstance.Text = strWindowText;

// Determine Window Type
if (strWindowText.Contains("problem with your audio configuration"))
windowInstance.DialogType = WindowInstance.WindowType.Audio;
else if (strWindowText.Contains("another user has it open"))
windowInstance.DialogType = WindowInstance.WindowType.Instance;
else if (strWindowText.Contains("new version of"))
windowInstance.DialogType = WindowInstance.WindowType.Version;
else if (strWindowText.Contains("or more applications are using"))
windowInstance.DialogType = WindowInstance.WindowType.Scripting;
else if (strWindowText.Contains("Video playback is not supported"))
windowInstance.DialogType = WindowInstance.WindowType.Video;
}
}
// Button Control
else if (windowInstance.DialogType != WindowInstance.WindowType.Unknown && strClassName == "Button")
{
iCapacity = GetWindowTextLength(new HandleRef(ServiceCore.GetInstance(), hWnd)) * 2;
sbWindowText = new StringBuilder(iCapacity);
GetWindowText(new HandleRef(ServiceCore.GetInstance(), hWnd), sbWindowText, sbClassName.Capacity);

strWindowText = sbWindowText.ToString().Replace("&", "");

// Determine Button Type
if ((windowInstance.DialogType == WindowInstance.WindowType.Audio && strWindowText.Contains("OK")) ||
(windowInstance.DialogType == WindowInstance.WindowType.License && strWindowText.Contains("Agree")) ||
(windowInstance.DialogType == WindowInstance.WindowType.Version && strWindowText.Contains("t Download")) ||
(windowInstance.DialogType == WindowInstance.WindowType.Instance && strWindowText.Contains("OK")) ||
(windowInstance.DialogType == WindowInstance.WindowType.Scripting && strWindowText.Contains("Quit") && !strWindowText.Contains("Don't")) ||
(windowInstance.DialogType == WindowInstance.WindowType.Video && strWindowText.Contains("OK")))
{
windowInstance.CloseControlID = GetDlgCtrlID(hWnd);
windowInstance.CloseControlhWnd = hWnd;
}
}

return true;
}

The ProcessWindow() subroutine is called every 1 second while the process is starting, and every 10 seconds once iTunes is running normally. Therefore, you would likely see ProcessWindow called the first time, resulting in the Audio dialog box being closed. The second time ProcessWindow runs, it would then likely discover the iTunes main window. However, it might have instead picked up the ‘new version available’ dialog box, in which case it will close that instead. Basically it’s a point in time check of the open windows.

Sorry for the lengthy post, but there is quite a bit to explain. As I said though, it’s a basic shell needed to start/stop the iTunes process and keep it running. The version I’m using is a little bit more thorough in certain places – particularly in error handling and cleanup – but this should be enough to get you started. The working sample code included at the end of the post will actually compile, install, start/stop, and run iTunes effectively – you just need to specify the iTunes path in the configuration file for the service (executablename.config).

If you’re wondering how I was able to determine which class names to look for (e.g. the dialog box with the class name of #32770), I was using the Microsoft Spy++ application that comes with Visual Studio (spyxx.exe from the development tools command prompt). That allows you to spy on running applications, see which windows and controls they are displaying. From memory it only shows windows in your logged in session, therefore you’d need to run iTunes interactively to see what’s going on. The picture below reflects what you’d see when the audio error dialog box is on the screen. We first located the dialog box class – iTunesCustomModalDialog – and then enumerated through the child controls, looking at each label (Static) for the text, and then at each Button’s text until we matched what we needed.

Spy

You can download the working sample code here – although you’ll need to compile it first. The project is set for .NET 4.6.1, but you should be able to drop it back to an earlier version if needed.

~ Mike

One thought on “Running iTunes in a Windows Service with C#

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s