Reply
Highlighted
Enthusiast
Posts: 21
Registered: ‎05-03-2011
3

EndNote Interprocess Communication Tutorial

[ Edited ]

EndNote Interprocess Communication Tutorial

This tutorial will show how to do Interprocess Communication by self hosting a service. This allows a process that is external to EndNote to call functions that are exposed by your plugin.

 

Environment Notes:

I am using Visual Studio 2010, Service Pack 1. This tutorial has only been tested with EndNote X4.

 

Creating the EndNote plugin:

Create a new Visual C++ -> Class Library Project. I titled mine “en_ipc_lib”.

 

Add the reference to System.ServiceModel and WindowsBase:

  • Right click on the project
  • Click references
  • Click Add New References
  • Go to the .NET tab
  • Select System.ServiceModel
Repeat this procedure for WindowsBase.

 

Implement Init and Exit

Here we have a fairly standard set of Init and Exit functions. To start our service, you just need to grab an instance of the service and call CreateService(). Stopping the service works basically the same way. 

 

/* 
 * Program:		EndNote IPC Tutorial
 * File:        EntryExit.cpp
 * Namespace:   None
 * Description: 
 *
*/

#pragma unmanaged
// platform
#include "stdafx.h"

// project
#include "RSPlugin.h"
#include "EntryExit.h"
#include "Callback.h"
#include "ServiceHost.h"


bool Init(CServiceRequest* p) {

	if( p )
	{
		//Create the callback instance
		gpIUIServiceNotifyCB		= RS::CreateInstance<MyIUIServiceNotifyCallbackImp>();

		//Install the menu item to the Tools menu
		if( gpIUIServiceNotifyCB != NULL )
		{
			gCServiceRequest	= p;		//save the pointer to the CServiceRequest

			IUIServicePtr pUI	= (IUIService*)	p->GetService(kGUI);
			if( pUI != NULL)
			{
				if( (pUI->AddToolsMenuItem("IPC Plugin Test",gpIUIServiceNotifyCB)) == kServiceNoErr )
				{
					// Do whatever else needs to be done here!
				}
				else return false; //failed to add to the menu
			}
			else return false; //failed to get the kGUI service
		}
		else return false; //failed to create IUIServiceNotifyCallback instance
	}
	else return false;//p == NULL

	//Instantiate our service
	ServiceCreation^ serviceInstance = ServiceCreation::Instance;
	serviceInstance->CreateService();

	return true;
}	//Init

bool Exit() {
	ServiceCreation^ serviceInstance = ServiceCreation::Instance;
	serviceInstance->DestroyService();
	return false;
}	// Exit

Write the code to host the service:

We’re using a singleton, so that we can easily access the service in our Init and Exit functions. (You can’t just declare a global object of managed type) Also, because of a bug/omission that seems to run rampant in most introductory guides to hosting services ( http://www.danrigsby.com/blog/index.php/2008/02/26/dont-wrap-wcf-service-hosts-or-clients-in-a-using...) we’re using ServiceHost in a slightly different (better, safer) way than you’ll see in most other tutorials.

 

 

/* 
 * Program:		EndNote IPC Tutorial
 * File:        ServiceHost.h
 * Namespace:   None
 * Description: 
 *
*/

#include "MyService.h"
#include "IPCSynchronizationContext.h"

#pragma managed
using namespace System;
using namespace System::ServiceModel;
using namespace System::ServiceModel::Description;
using namespace IPCService;

ref class ServiceCreation
{

private:
	ServiceHost^ host;
	ServiceCreation(){}
	ServiceCreation(const ServiceCreation%) 
	{ 
		throw gcnew System::InvalidOperationException("ServiceCreation cannot be copy-constructed"); 
	}
	static ServiceCreation m_instance;

public:
	static property ServiceCreation^ Instance 
	{ 
		ServiceCreation^ get() 
		{ 
			return %m_instance; 
		} 
	}

	bool CreateService()
	{
		Uri^	baseAddress = gcnew Uri((String^)"http://localhost:8080/myservice");
		String^ address = "net.pipe://localhost/myservice";
		MyService^ myService = gcnew MyService();;

		try
		{
			// Set the synchronization context to keep it in the same thread as EndNote
			IPCSynchronizationContext^ synchronizationContext = gcnew IPCSynchronizationContext();
			SynchronizationContext::SetSynchronizationContext(synchronizationContext);

			// Create the ServiceHost.
			host = gcnew ServiceHost(myService, baseAddress);

			// Enable metadata publishing.
			ServiceMetadataBehavior^ smb = gcnew ServiceMetadataBehavior();
			smb->HttpGetEnabled = true;
			host->Description->Behaviors->Add(smb);

			//Add an endpoint to the service
			host->AddServiceEndpoint(IPCService::IMyService::typeid,
									gcnew NetNamedPipeBinding(),
									address);

			// Open the ServiceHost to start listening for messages.
			host->Open();

			return true;
		}
		catch (CommunicationException ^e)
		{
			if (host != nullptr)
			{
				host->Abort();
			}
		}
		catch (TimeoutException ^e)
		{
			if (host != nullptr)
			{
				host->Abort();
			}
		}
		catch (Exception ^e)
		{
			if (host != nullptr)
			{
				host->Abort();
			}
			throw;
		}

		return false;
	}

	bool DestroyService()
	{
		// Close the ServiceHost.
		try{
			host->Close();
			return true;
		}
		catch (Exception ^e)
		{
			if (host != nullptr)
			{
				host->Abort();
			}
		}
		return false;
	}
};

 

 

Write the service itself:

Here, we simply write a function to get the API version and return it as a string. The abridged story is that each service is defined by a ServiceContract and can do one or more actions that are each defined be an OperationContract. The actual implementation of this is defined by a ServiceBehavior.

 

/* 
 * Program:		EndNote IPC Tutorial
 * File:        MyService.h
 * Namespace:   MyService
 * Description: 
 *
*/
#pragma once
#include <iostream>
#include <sstream>
#pragma managed
using namespace System;
using namespace System::Runtime::InteropServices;
using namespace System::ServiceModel;
using namespace System::ServiceModel::Description;


namespace IPCService {

	[ServiceContract()]
	public interface class IMyService {
		[OperationContract()]
		String^ getAPIVersion();
	};

[ServiceBehavior(ConcurrencyMode = ConcurrencyMode::Single,  InstanceContextMode = InstanceContextMode::Single)] public ref class MyService : public IMyService { public: virtual String^ getAPIVersion() sealed { IVersionPtr pVersion = (IVersion*)gCServiceRequest->GetService(kVersion); long version = pVersion->GetServiceVersion(); String^ result = gcnew String("Hello!"); std::ostringstream o; o <<std:: hex << version; return gcnew String(o.str().c_str()); } }; }

Writing the Synchronization Context:

Often, WCF services are implemented with Windows Forms or Windows Presentation Foundation. There is a built in Synchronization context that can be used to force execution to happen in the UI thread. However, if as in our case, you do not use either of these, work will be dipatched to the default synchronization context, a thread pool. In order to lock it to the thread used by EndNote, we have to implement our own SynchronizationContext. This example is odd because we only need synchronous events. If you want asynchronous events, Post and Send will be different.

 

/* 
 * Program:		EndNote IPC Tutorial
 * File:        IPCSynchronizationContext.h
 * Namespace:   None
 * Description: 
 *
*/

#pragma once

#include <WinBase.h>

#pragma managed

using namespace System;
using namespace System::Threading;
using namespace System::Windows::Threading;

public ref class IPCSynchronizationContext : SynchronizationContext, IDisposable
{

	static System::Windows::Threading::Dispatcher^ dispatcher = System::Windows::Threading::Dispatcher::CurrentDispatcher;

public:
	

	virtual void Send(SendOrPostCallback^ d, Object^ state) override sealed
	{
		dispatcher->Invoke(d, state);
	}

	virtual void Post(SendOrPostCallback^ d, Object^ state) override sealed
	{
		dispatcher->Invoke(d, state);
	}

	~IPCSynchronizationContext() {};

};

 

 

 

Testing the plugin:

Build the project, copy the dll into one of the EndNote plugin directories, and start EndNote.

 

First, make sure that EndNote can load the plugin by checking that the IUIService Callback works. Select “IPC Plugin Test” from the Tools menu.  A MessageBox saying “Hello World!” should appear.

 

Now, we are ready to test the service:

  1. Open EndNote if it is not already open.
  2. In Visual Studio, open a Visual Studio Command Prompt from the Tools menu.
  3. Run wcftestclient
  4. Go to file->Add Service
  5. Enter http://localhost:8080/myservice. You should see IMyService and getAPIVersion().
  6. The right side of the window should have a getAPIVersion tab. If not, double click on getAPIVersion.
  7.  Press Invoke. If a confirmation window appears, press OK.
  8. In the Response window, you should now see the correct RSServices API version.
 

Creating the Project to consume the plugin

Create a new Visual C# -> Console Application Project. I titled mine “en_ipc_consumer”.

 

Add a reference to the service:

  1. Right click on the project and select “Add Service Reference”
  2. Set the Address to: http://localhost:8080/myservice
  3. Click go
  4. The service should appear. Select it.
  5. Change the namespace to MyServiceReference.
  6. Hit OK.

Visual Studio will now generate an app.config file that will look something like this:

 

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <netNamedPipeBinding>
                <binding name="NetNamedPipeBinding_IMyService" closeTimeout="00:01:00"
                    openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
                    transactionFlow="false" transferMode="Buffered" transactionProtocol="OleTransactions"
                    hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="524288"
                    maxBufferSize="65536" maxConnections="10" maxReceivedMessageSize="65536">
                    <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
                        maxBytesPerRead="4096" maxNameTableCharCount="16384" />
                    <security mode="Transport">
                        <transport protectionLevel="EncryptAndSign" />
                    </security>
                </binding>
            </netNamedPipeBinding>
        </bindings>
        <client>
            <endpoint address="net.pipe://localhost/myservice" binding="netNamedPipeBinding"
                bindingConfiguration="NetNamedPipeBinding_IMyService" contract="MyServiceReference.IMyService"
                name="NetNamedPipeBinding_IMyService">
                <identity>
                    <userPrincipalName value="someuser@someplace.com" />
                </identity>
            </endpoint>
        </client>
    </system.serviceModel>
</configuration>

 

We are now ready to call the service. Change the main method in Program.cs to something like this:

 

static void Main(string[] args)
        {
            MyServiceReference.MyServiceClient sClient = new MyServiceReference.MyServiceClient("NetNamedPipeBinding_IMyService");
            Console.WriteLine("API Version: " + sClient.getAPIVersion());
            Console.ReadLine();
        }

Make sure that the argument you pass to the MyServiceClient constructor matches the bindingConfiguration for the endpoint in your app.config:

 

 

<endpoint address="net.pipe://localhost/myservice" binding="netNamedPipeBinding"
                bindingConfiguration="NetNamedPipeBinding_IMyService" contract="MyServiceReference.IMyService"
                name="NetNamedPipeBinding_IMyService">

 

 

We’re done!

To test our service, first make sure that EndNote is open. Now, just hit f5 and a console window should pop up and return the version number of your RSServices API. To close the window, just press Enter.

 

 

There's a little bit of code missing, but if you're familiar with creating EndNote plugins, it should be relatively easy to fill in the blanks. If anyone has trouble getting this example to work, I can post the rest of the code.

--------------------------------------------------------------------------------------
This post does not necessarily reflect the views of my employer.

Nathan Ellenfield
ORISE Grantee- Immediate Office
National Center for Environmental Assessment
US Environmental Protection Agency
Research Triangle Park, NC 27711
Enthusiast
Posts: 21
Registered: ‎05-03-2011
1

Re: EndNote Interprocess Communication Tutorial

I've learned a bit more about using ServiceHost. Since we need this service to only be accessed from the machine it is hosted on, we can use the NetNamedPipeBinding, which is simpler, faster, and as far as I can tell, cannot be accessed form another machine. All the code samples in the previous post have been updated.

 

The majority of the changes are in the ServiceHost.h file. If you've already done the first version of this tutorial, you can:

 

  1. Replace the ServiceHost.h file with the new one.
  2. Right click on the Service Reference in the C# project and select "Update Service Reference"
  3. Edit the Program.cs file to use the new EndPoint.
Potential errors:
I've discovered that there could be some issues with threading using this. Not all of the EndNote API functions are thread-safe and on some level, ServiceHost uses different threads for different requests. I need to learn a bit more about ServiceHost to be sure wether or not there is a problem. (especially since we have no guarantee that any issues with thread safety will be easy to detect) If there are issues, the solution will probably lie in one of these ServiceBehavior attributes:

 

--------------------------------------------------------------------------------------
This post does not necessarily reflect the views of my employer.

Nathan Ellenfield
ORISE Grantee- Immediate Office
National Center for Environmental Assessment
US Environmental Protection Agency
Research Triangle Park, NC 27711
Enthusiast
Posts: 21
Registered: ‎05-03-2011
1

Re: EndNote Interprocess Communication Tutorial

I have updated the tutorial to set UseSynchronizationContext to true and ConcurrencyMode to Single. I don't know for sure that we would run into threading errors otherwise, but this is the safer route and works fine unless you need to handle concurrent operations. 

--------------------------------------------------------------------------------------
This post does not necessarily reflect the views of my employer.

Nathan Ellenfield
ORISE Grantee- Immediate Office
National Center for Environmental Assessment
US Environmental Protection Agency
Research Triangle Park, NC 27711
Enthusiast
Posts: 21
Registered: ‎05-03-2011
0

Re: EndNote Interprocess Communication Tutorial

I've discovered that UseSynchronizationContext is only really useful with Windows Forms and Windows Presentation Foundation. Since we are using neither, we need to implement SynchronizationContext and then manually specify it. I've updated the tutorial and have verified that it does indeed run in the correct thread now.

 

If you need to see what thread something is running in for debugging purposes, you can use the following:

 

 
OutputDebugString(L"second test!");
char threadID[20];
DWORD dwThreadID = GetCurrentThreadId();
_itoa(dwThreadID, threadID, 10);
OutputDebugString(L"In thread: ");
OutputDebugStringA(threadID);
OutputDebugString(L"\n");

 

 

--------------------------------------------------------------------------------------
This post does not necessarily reflect the views of my employer.

Nathan Ellenfield
ORISE Grantee- Immediate Office
National Center for Environmental Assessment
US Environmental Protection Agency
Research Triangle Park, NC 27711