Rebuilding a Windows Service that converts PPTX to JPG on a remote share then updates an Image Library in SharePoint 2010

A friend of mine, Eric Mikuska built a windows service that converts PPTX to JPG on a remote share and updates an Image Library in SharePoint 2007.  Here are steps I took to rebuild it to work with SharePoint 2010 on 64 bit hardware.

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceProcess;
using System.Text;

using System.Windows.Forms;
//using ServiceDebuggerHelper; //for ServiceDebuggerHelper

namespace Conversion-Service
{
static class Program
{
/// <summary>
/// Uncomment this for deployment.
/// </summary>
static void Main()
{
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
new Conversion-Service()
};
ServiceBase.Run(ServicesToRun);
}

/// <summary>
/// This is for debugging.  //for ServiceDebuggerHelper
/// </summary>
//static void Main(string[] args)
//{
//    if (args.Length > 0 && args[0].ToLower().Equals("/debug"))
//    {
//        Application.Run(new ServiceRunner(new Conversion-Service()));
//    }
//    else
//    {
//        System.ServiceProcess.ServiceBase[] ServicesToRun = new System.ServiceProcess.ServiceBase[]
//            {
//                new Conversion-Service(),
//            };

//        System.ServiceProcess.ServiceBase.Run(ServicesToRun);
//    }
//}
}
}

Conversion-Service.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;

using System.Configuration;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Net.Mail;
using Microsoft.Office.Core;
using Microsoft.Office.Interop.PowerPoint;
using DocumentFormat.OpenXml.Presentation;
using DocumentFormat.OpenXml.Packaging;
using Microsoft.SharePoint;

//using ServiceDebuggerHelper;  //for ServiceDebuggerHelper

namespace Conversion-Service
{
partial class Conversion-Service : ServiceBase//, IDebuggableService   //for ServiceDebuggerHelper
{
public Conversion-Service()
{
InitializeComponent();
CanPauseAndContinue = true;
}

protected override void OnStart(string[] args)
{
WatchForUploadedFiles();
}

protected override void OnStop()
{
UnmapDriveLetter(ConfigurationManager.AppSettings["remoteDriveLetter"]);
}

protected override void OnPause()
{
}

protected override void OnContinue()
{
}

#region IDebuggableService Members

public void Start(string[] args)
{
OnStart(args);
}

public void StopService()
{
OnStop();
}

public void Pause()
{
OnPause();
}

public void Continue()
{
OnContinue();
}

#endregion

#region Functions

private void WatchForUploadedFiles()
{
FileSystemWatcher fsWatcher = new FileSystemWatcher();
try
{
MapDriveLetter();
fsWatcher.Created += new FileSystemEventHandler(OnFileCreated);
FolderCleanUp("Directory ready for operation.");                                      // Parameter used for debugging only
fsWatcher.Path = ConfigurationManager.AppSettings["remoteFolder"];             // @"Z:\";
fsWatcher.Filter = ConfigurationManager.AppSettings["sourceFileName"];         // "Default.pptx";
fsWatcher.EnableRaisingEvents = true;
}
catch (Exception ex1)
{
WriteException(ex1, "Ex1");
}
}

public static void MapDriveLetter()
{
try
{
if (System.IO.Directory.Exists(ConfigurationManager.AppSettings["remoteFolder1"]))           //"Z" + ":\\"
{
UnmapDriveLetter(ConfigurationManager.AppSettings["remoteDriveLetter"]);                //"Z:"
}
System.Diagnostics.Process p = new System.Diagnostics.Process();
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.RedirectStandardError = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.FileName = "net.exe";
p.StartInfo.Arguments = @" use " + ConfigurationManager.AppSettings["remoteDriveLetter"]
+ " " + ConfigurationManager.AppSettings["remoteShareName"]
+ " " + ConfigurationManager.AppSettings["userPwd"]
+ " /user:" + ConfigurationManager.AppSettings["userName"];

//EventLog.WriteEntry("Conversion Service", "Remote drive mapped successfully", EventLogEntryType.Information);

p.Start();
p.WaitForExit();
}
catch (Exception ex6_5)
{
WriteException(ex6_5, "Ex6.5");
}
}

protected static void UnmapDriveLetter(string DriveLetter)
{
Process p = new Process();
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.RedirectStandardError = true;
p.StartInfo.RedirectStandardOutput = true;
try
{
p.StartInfo.FileName = "net.exe";
p.StartInfo.Arguments = " use " + ConfigurationManager.AppSettings["remoteDriveLetter"] + " /DELETE";
p.Start();
}
catch (Exception ex7)
{
WriteException(ex7, "Ex7");
}
finally
{
p.WaitForExit();
}
}

static void OnFileCreated(object sender, FileSystemEventArgs e)
{
SavePPTXasJPGs();
DeleteSharePointSlides();
UploadJPGsToSharePoint();
//FolderCleanUp("Files deleted from temporary directory.");                        // Parameter used for debugging only
//UnmapDriveLetter(ConfigurationManager.AppSettings["remoteDriveLetter"]);        //"Z:"
}

protected static void SavePPTXasJPGs()
{
Microsoft.Office.Interop.PowerPoint.Application app = new Microsoft.Office.Interop.PowerPoint.Application();
Microsoft.Office.Interop.PowerPoint.Presentation pptPresentation = null;
try
{
pptPresentation = app.Presentations.Open(ConfigurationManager.AppSettings["remoteFolder"] + ConfigurationManager.AppSettings["sourceFileName"], MsoTriState.msoFalse, MsoTriState.msoFalse, MsoTriState.msoFalse);
pptPresentation.SaveAs(ConfigurationManager.AppSettings["remoteFolder"] + ".", PpSaveAsFileType.ppSaveAsJPG, MsoTriState.msoFalse);
Thread.Sleep(500);
}
catch (Exception ex2)
{
WriteException(ex2, "Ex2");
}
finally
{
pptPresentation.Close();
}
}

public static void ProcessPresentation()
{
string fileNamePath = ConfigurationManager.AppSettings["remoteFolder"] + ConfigurationManager.AppSettings["sourceFileName"];
int slideCount = -1;
slideCount = CountSlides(fileNamePath);
for (int i = 0; i <= slideCount; i++)
{
DeleteSlide(fileNamePath);
}
}

public static void DeleteSlide(string presentationFile)
{
if (presentationFile == null)
{
throw new ArgumentNullException("presentationDocument");
}
using (DocumentFormat.OpenXml.Packaging.PresentationDocument presentationDocument =
DocumentFormat.OpenXml.Packaging.PresentationDocument.Open(presentationFile, true))
{
// Get the presentation part from the presentation document.
DocumentFormat.OpenXml.Packaging.PresentationPart presentationPart = presentationDocument.PresentationPart;
// Get the presentation from the presentation part.
DocumentFormat.OpenXml.Presentation.Presentation presentation = presentationPart.Presentation;
// Get the list of slide IDs in the presentation.
DocumentFormat.OpenXml.Presentation.SlideIdList slideIdList = presentation.SlideIdList;
int slideIdx = -1;
foreach (DocumentFormat.OpenXml.Presentation.SlideId _slideId in presentation.SlideIdList)
{
slideIdx++;
// Get the slide ID of the specified slide
DocumentFormat.OpenXml.Presentation.SlideId slideId = slideIdList.ChildElements[slideIdx] as DocumentFormat.OpenXml.Presentation.SlideId;
// Get the relationship ID of the slide.
string slideRelId = slideId.RelationshipId;
// Get the slide part for the specified slide.
DocumentFormat.OpenXml.Packaging.SlidePart slidePart = presentationPart.GetPartById(slideRelId) as DocumentFormat.OpenXml.Packaging.SlidePart;
if (slidePart.Slide.Show != null)
{
if (slidePart.Slide.Show.HasValue != null)
{
// Remove the slide from the slide list.
slideIdList.RemoveChild(slideId);
// Save the modified presentation.
presentation.Save();
// Remove the slide part.
presentationPart.DeletePart(slidePart);
break;
}
}
}
}
}

public static int CountSlides(string presentationFile)
{
// Open the presentation as read-only.
using (DocumentFormat.OpenXml.Packaging.PresentationDocument presentationDocument = DocumentFormat.OpenXml.Packaging.PresentationDocument.Open(presentationFile, false))
{
// Pass the presentation to the next CountSlide method
// and return the slide count.
return CountSlides(presentationDocument);
}
}

public static int CountSlides(DocumentFormat.OpenXml.Packaging.PresentationDocument presentationDocument)
{
// Check for a null document object.
if (presentationDocument == null)
{
throw new ArgumentNullException("presentationDocument");
}
int slidesCount = 0;
// Get the presentation part of document.
DocumentFormat.OpenXml.Packaging.PresentationPart presentationPart = presentationDocument.PresentationPart;

// Get the slide count from the SlideParts.
if (presentationPart != null)
{
slidesCount = presentationPart.SlideParts.Count();
}
// Return the slide count to the previous method.
return slidesCount;
}

public static int PPTGetSlideCount(string fileName, bool includeHidden = true)
{
int slidesCount = 0;
using (PresentationDocument doc = PresentationDocument.Open(fileName, false))
{
PresentationPart presentationPart = doc.PresentationPart;
if (presentationPart != null)
{
if (includeHidden)
{
slidesCount = presentationPart.SlideParts.Count();
}
else
{
var slides = presentationPart.SlideParts.Where(
(s) => (s.Slide != null) &&
((s.Slide.Show == null) ||
(s.Slide.Show.HasValue && s.Slide.Show.Value)));
slidesCount = slides.Count();
}
}
}
return slidesCount;
}

protected static void DeleteSharePointSlides()
{
SPWeb SharePoint2010Site = new SPSite(ConfigurationManager.AppSettings["portalURL"]).OpenWeb();
SPList imagesLibrary = SharePoint2010Site.Lists[ConfigurationManager.AppSettings["imageListFolder"]];
SharePoint2010Site.AllowUnsafeUpdates = true;
try
{
while (imagesLibrary.Items.Count > 1)
{
imagesLibrary.Items[imagesLibrary.Items.Count - 1].File.Delete();
}
}
catch (Exception ex4)
{
WriteException(ex4, "Ex4");
}
finally
{
}
}

protected static void UploadJPGsToSharePoint()
{
bool success = false;
string[] files;
files = null;
files = Directory.GetFiles(ConfigurationManager.AppSettings["remoteFolder1"], ConfigurationManager.AppSettings["imageFileExtension"]);
SPWeb SharePoint2010Site = new SPSite(ConfigurationManager.AppSettings["portalURL"]).OpenWeb();
SharePoint2010Site.AllowUnsafeUpdates = true;
FileStream fs = null;
string fileName = string.Empty;
if (files.Length > 1)
{
try
{
foreach (string file in files)
{
fileName = file.Split(@"\".ToCharArray())[1].ToLower();
fs = File.Open(file, FileMode.Open);
SharePoint2010Site.Files.Add(ConfigurationManager.AppSettings["imageListFolder"] + "/" + fileName, fs, true);
Thread.Sleep(500);
fileName = string.Empty;
fs.Close();
}
success = true;
}
catch (Exception ex5)
{
WriteException(ex5, fileName);
}
finally
{
SharePoint2010Site.AllowUnsafeUpdates = false;
if (success) { SendEmail(success); }
FolderCleanUp("Clean up the mess.");
}
}
}

protected static void FolderCleanUp(string _message)
{
string[] filesToDelete = Directory.GetFiles(ConfigurationManager.AppSettings["remoteFolder"]);
try
{
foreach (string file in filesToDelete)
{
File.Delete(file);
}
}catch (Exception ex6)
{
WriteException(ex6, "Ex6");
}
}

public static void WriteException(Exception ex, string source)
{
string err = "Conversion-Service Error" +
"\n\nError Message from method " + source + ": " + ex.Message.ToString() +
"\n\nStack Trace: " + ex.StackTrace.ToString() + "\n";
EventLog.WriteEntry("Conversion-Service", err, EventLogEntryType.Error);
SendEmail(false);
}

public static void SendEmail(bool success)
{
System.Net.Mail.MailMessage message = new System.Net.Mail.MailMessage();
message.To.Add(ConfigurationManager.AppSettings["sendToAdmin"]);
message.Subject = "Conversion-Service " + (success == true ? "Successful" : "Failed");
message.From = new System.Net.Mail.MailAddress(ConfigurationManager.AppSettings["sendFrom"]);
message.Body = (success == true ? "Image Library updated successfully." : "Conversion-Service update failed. Please see server logs on " + ConfigurationManager.AppSettings["portalURL"] + ".");
System.Net.Mail.SmtpClient smtp = new System.Net.Mail.SmtpClient(ConfigurationManager.AppSettings["smtpServer"]);
smtp.Send(message);
}
#endregion
}
}

App.Config

<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="smtpServer" value="my.smtp.server"/>
<add key="sendToAdmin" value="admin@email.com"/>
<add key="sendToUser" value="user@email.com"/>
<add key="sendFrom" value="from@email.com"/>
<add key="portalURL" value="https://MySharePoint2010Server/"/>
<add key="imageListFolder" value="MyImageLibrary"/>
<add key="remoteFolder" value="Z:\\"/>
<add key="remoteFolder1" value="Z:\"/>
<add key="sourceFileName" value="MyPowerPoint.pptx"/>
<add key="remoteDriveLetter" value="Z:"/>
<add key="remoteShareName" value="\\MyOtherServer\UploadFolder"/>
<add key="userName" value="MyServiceAccount"/>
<add key="userPwd" value="Password"/>
<add key="imageFileExtension" value="*.jpg"/>
<add key="otherFileExtension" value="*.pptx"/>
<add key="emailBodyContent" value="Conversion Service operation was successfully completed. You can view uploaded slides at https://MySharePoint2010Server/MyImageLibrary/Forms/AllItems.aspx. Note: This is an automated email generated by the Conversion Service application.\nIt is to inform you that a presentation was processed. Should an issue developed, there \nwould be a separate email sent out unless there was a unrelated SMTP server failure; in \nany case the event log will have detailed information on the nature of the issue."/>
<add key="ClientSettingsProvider.ServiceUri" value=""/>
</appSettings>
<system.web>
<membership defaultProvider="ClientAuthenticationMembershipProvider">
<providers>
<add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf1111ad364e35" serviceUri=""/>
</providers>
</membership>
<roleManager defaultProvider="ClientRoleProvider" enabled="true">
<providers>
<add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" cacheTimeout="86400"/>
</providers>
</roleManager>
</system.web>
<startup><supportedRuntime version="v2.0.50727"/></startup></configuration>

Set the Project Properties, Build, Platform Target to x64

Get Eric De Carufel’s ServiceDebuggerHelper from http://www.codeproject.com/Articles/21887/Debugging-Windows-Service-Without-Deploying-It

To debug your service set your Project Properties, Debug, Command line arguments: /debug

Comment in the ServiceDebuggerHelper code in program.cs and service1.cs to debug and test the service as an application.

After you’ve confirmed that it works, comment out the ServiceDebuggerHelper project code in program.cs and service1.cs.

Create a new deployment project and in the Solution Explorer, select the deployment project. In the Properties window, make sure that the TargetPlatform property is set to x64. On the View menu, point to Editor, and then click File System. In the File System editor, select the File System on Target Machine node. On the Action menu, choose Add Special Folder, and then choose Common Files (64-bit), Program Files (64-bit), or System (64-bit). Add the your service project output to the new folder.

Build your project and installer.

When you build the Windows Installer project in Visual Studio it embeds the 32-bit version of InstallUtilLib.dll into the Binary Table as InstallUtil. Since the native InstallUtilLib.dll is 32-bit it loads the 32-bit Framework which will throw a BadImageFormatException when you install your service since your managed class library is 64-bit.

To fix this you will need to install the Windows SDK from the Download Center ( http://www.microsoft.com/en-us/download/details.aspx?id=8279 or http://www.microsoft.com/en-us/download/details.aspx?id=3138 ). Then in the “bin” folder of the installation directory (ex: %ProgramFiles%\Microosft SDKs\Windows\v7.0A\bin) there is Orca.msi. Double-click to install that. After installing, you can right-click on your MSI and select Edit with Orca.

Then either replace the 32-bit InstallUtilLib.dll with the 64-bit bitness: Open the resulting .msi in Orca from the Windows Installer SDK, Select the Binary table, Double click the cell [Binary Data] for the record InstallUtil, Make sure “Read binary from filename” is selected and click the Browse button, Browse to %WINDIR%\Microsoft.NET\Framework64\v4.0.30319, Select InstallUtilLib.dll, Click the Open button, Click the OK button

Or if you already have or anticipate having 32-bit custom actions in future patches – and I recommend this approach because the future is difficult to predict – you should add a new record: Open the resulting .msi in Orca from the Windows Installer SDK, Select the Binary table, Click the Tables menu and then Add Row,  Enter InstallUtil64 for the Name, Select the Data row and click the Browse button, Browse to %WINDIR%\Microsoft.NET\Framework64\v4.0.30319, Select InstallUtilLib.dll, Click the Open button, Click the OK button, Select the CustomAction table, For each custom action where the Source column is InstallUtil and only those custom actions that are 64-bit managed custom actions (or that were built with /platform:anycpu, the default, where you want to run as 64-bit custom actions), change InstallUtil to InstallUtil64

Install your service.

Install Office Standard 2010 x64 on the server that you installed your service.

On the server running your service, go into the Component Services, Computer, My Computer, DCOM Config. Then go to the ‘Microsoft PowerPoint Slide’ DCOM and select properties On the identity tab and change the user from being inherited to be a specific user with Network Service/access privileges.

Finally you will need to add the service account and the server that your service is running on to the security on the remote server’s folder with full control.

Thanks to:

http://social.msdn.microsoft.com/profile/heath%20stewart/

http://blogs.msdn.com/b/heaths/archive/2006/02/01/64-bit-managed-custom-actions-with-visual-studio.aspx

http://blogs.technet.com/b/stefan_gossner/

http://blogs.technet.com/b/stefan_gossner/archive/2011/09/19/common-issue-new-spsite-returns-quot-the-web-application-at-http-server-port-could-not-be-found-quot.aspx

http://www.codeproject.com/Members/Eric-De-Carufel

http://www.codeproject.com/Articles/21887/Debugging-Windows-Service-Without-Deploying-It