C# Native Host with Chrome Native Messaging

C# native host with Chrome Native Messaging

Assuming the manifest is set up properly, here is a complete example for talking to a C# host using the "port" method:

using System;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace NativeMessagingHost
{
class Program
{
public static void Main(string[] args)
{
JObject data;
while ((data = Read()) != null)
{
var processed = ProcessMessage(data);
Write(processed);
if (processed == "exit")
{
return;
}
}
}

public static string ProcessMessage(JObject data)
{
var message = data["text"].Value<string>();
switch (message)
{
case "test":
return "testing!";
case "exit":
return "exit";
default:
return "echo: " + message;
}
}

public static JObject Read()
{
var stdin = Console.OpenStandardInput();
var length = 0;

var lengthBytes = new byte[4];
stdin.Read(lengthBytes, 0, 4);
length = BitConverter.ToInt32(lengthBytes, 0);

var buffer = new char[length];
using (var reader = new StreamReader(stdin))
{
while (reader.Peek() >= 0)
{
reader.Read(buffer, 0, buffer.Length);
}
}

return (JObject)JsonConvert.DeserializeObject<JObject>(new string(buffer));
}

public static void Write(JToken data)
{
var json = new JObject();
json["data"] = data;

var bytes = System.Text.Encoding.UTF8.GetBytes(json.ToString(Formatting.None));

var stdout = Console.OpenStandardOutput();
stdout.WriteByte((byte)((bytes.Length >> 0) & 0xFF));
stdout.WriteByte((byte)((bytes.Length >> 8) & 0xFF));
stdout.WriteByte((byte)((bytes.Length >> 16) & 0xFF));
stdout.WriteByte((byte)((bytes.Length >> 24) & 0xFF));
stdout.Write(bytes, 0, bytes.Length);
stdout.Flush();
}
}
}

If you don't need to actively communicate with the host, using runtime.sendNativeMessage will work fine. To prevent the host from hanging, simply remove the while loop and do Read/Write once.

To test this, I used the example project provided by Google here: https://chromium.googlesource.com/chromium/src/+/master/chrome/common/extensions/docs/examples/api/nativeMessaging

Note: I'm using Json.NET to simplify the json serialization/de-serialization process.

I hope this is helpful to somebody!

C# host with Chrome Native Messaging Extension

Native Messaging works by starting a new application process, then communicating with it over STDIO as long as Chrome keeps the connection open. If you don't keep the connection open, you can't "reconnect" to an application. To keep the connection open, you'll probably need to open it from the event script.

See this question: How to get Chrome Native Messaging to Listen to application? It discusses some of this topic, including C#-specific ways to keep it a single instance.

Furthermore, consider using alternative solutions, such as web communication (for example, WebSockets) to a local port exposed by your native code, though it creates new security questions (how to make sure it's your extension talking to it?).

Native messaging from chrome extension to native host written in C#

In order to get my extension working, I followed the following steps:

  1. I deleted my registry key and then re-added it in my HKLM registry.
  2. Although unnecessary,host.exe really only needed to receive one message from the extension, so I changed my previous "open port" messaging to a "no port" message, i.e.

    chrome.runtime.sendNativeMessage(...);

  3. Importantly, when I changed the text I was sending from {"text":data[0]} to a simple string {"text":"Hello from Chrome!"}, everything began working smoothly. data[0] was a string of about 250KB, which should definitely be an acceptable message size according to the developer's site. However, I have created a different question for this problem, as I was able to solve the general message sending and receiving problem I was experiencing.

How to get Chrome Native Messaging to Listen to application?

Well I ended up going with a single instance application, and used code for a SingleInstance I found somewhere (sorry went through so many sites looking for simplest one)

Ended up using this main class

/// <summary>
/// Holds a list of arguments given to an application at startup.
/// </summary>
public class ArgumentsReceivedEventArgs : EventArgs
{
public string[] Args { get; set; }
}

public class SingleInstance : IDisposable
{
private Mutex _mutex;
private readonly bool _ownsMutex;
private Guid _identifier;

/// <summary>
/// Enforces single instance for an application.
/// </summary>
/// <param name="identifier">An _identifier unique to this application.</param>
public SingleInstance(Guid identifier)
{
this._identifier = identifier;
_mutex = new Mutex(true, identifier.ToString(), out _ownsMutex);
}

/// <summary>
/// Indicates whether this is the first instance of this application.
/// </summary>
public bool IsFirstInstance
{ get { return _ownsMutex; } }

/// <summary>
/// Passes the given arguments to the first running instance of the application.
/// </summary>
/// <param name="arguments">The arguments to pass.</param>
/// <returns>Return true if the operation succeded, false otherwise.</returns>
public bool PassArgumentsToFirstInstance(string[] arguments)
{
if (IsFirstInstance)
throw new InvalidOperationException("This is the first instance.");

try
{
using (var client = new NamedPipeClientStream(_identifier.ToString()))
using (var writer = new StreamWriter(client))
{
client.Connect(200);

foreach (var argument in arguments)
writer.WriteLine(argument);
}
return true;
}
catch (TimeoutException)
{ } //Couldn't connect to server
catch (IOException)
{ } //Pipe was broken

return false;
}

/// <summary>
/// Listens for arguments being passed from successive instances of the applicaiton.
/// </summary>
public void ListenForArgumentsFromSuccessiveInstances()
{
if (!IsFirstInstance)
throw new InvalidOperationException("This is not the first instance.");
ThreadPool.QueueUserWorkItem(ListenForArguments);
}

/// <summary>
/// Listens for arguments on a named pipe.
/// </summary>
/// <param name="state">State object required by WaitCallback delegate.</param>
private void ListenForArguments(object state)
{
try
{
using (var server = new NamedPipeServerStream(_identifier.ToString()))
using (var reader = new StreamReader(server))
{
server.WaitForConnection();

var arguments = new List<string>();
while (server.IsConnected)
arguments.Add(reader.ReadLine());

ThreadPool.QueueUserWorkItem(CallOnArgumentsReceived, arguments.ToArray());
}
}
catch (IOException)
{ } //Pipe was broken
finally
{
ListenForArguments(null);
}
}

/// <summary>
/// Calls the OnArgumentsReceived method casting the state Object to String[].
/// </summary>
/// <param name="state">The arguments to pass.</param>
private void CallOnArgumentsReceived(object state)
{
OnArgumentsReceived((string[])state);
}
/// <summary>
/// Event raised when arguments are received from successive instances.
/// </summary>
public event EventHandler<ArgumentsReceivedEventArgs> ArgumentsReceived;
/// <summary>
/// Fires the ArgumentsReceived event.
/// </summary>
/// <param name="arguments">The arguments to pass with the ArgumentsReceivedEventArgs.</param>
private void OnArgumentsReceived(string[] arguments)
{
if (ArgumentsReceived != null)
ArgumentsReceived(this, new ArgumentsReceivedEventArgs() { Args = arguments });
}

#region IDisposable
private bool disposed = false;

protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (_mutex != null && _ownsMutex)
{
_mutex.ReleaseMutex();
_mutex = null;
}
disposed = true;
}
}

~SingleInstance()
{
Dispose(false);
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}

And my C# application mostly taken from (C# native host with Chrome Native Messaging):

class Program
{
const string MutexId = "ENTER YOUR GUID HERE, OR READ FROM APP";

public static void Main(string[] args)
{
using (var instance = new SingleInstance(new Guid(MutexId)))
{
if (instance.IsFirstInstance)
{
instance.ArgumentsReceived += Instance_ArgumentsReceived;
instance.ListenForArgumentsFromSuccessiveInstances();

DoMain(args);
}
else
{
instance.PassArgumentsToFirstInstance(args);
}
}
}

private static void Instance_ArgumentsReceived(object sender, ArgumentsReceivedEventArgs e)
{
TryProcessAccount(e.Args);
}

// This is the main part of the program I use, so I can call my exe with program.exe 123 42424 to have it open that specific account in chrome. Replace with whatever code you want to happen when you have multiple instances.

private static void TryProcessAccount(string[] args)
{
if (args == null || args.Length < 2 || args[0] == null || args[1] == null || args[0].Length != 3) { return; }
var openAccountString = GetOpenAccountString(args[0], args[1]);
Write(openAccountString);
}

private static void DoMain(string[] args)
{
TryProcessAccount(args);

JObject data = Read();
while ((data = Read()) != null)
{
if (data != null)
{
var processed = ProcessMessage(data);
Write(processed);
if (processed == "exit")
{
return;
}
}
}

}

public static string GetOpenAccountString(string id, string secondary)
{
return JsonConvert.SerializeObject(new
{
action = "open",
id = id,
secondary = secondary
});
}

public static string ProcessMessage(JObject data)
{
var message = data["message"].Value<string>();
switch (message)
{
case "test":
return "testing!";
case "exit":
return "exit";
case "open":
return GetOpenAccountString("123", "423232");
default:
return message;
}
}

public static JObject Read()
{
var stdin = Console.OpenStandardInput();
var length = 0;

var lengthBytes = new byte[4];
stdin.Read(lengthBytes, 0, 4);
length = BitConverter.ToInt32(lengthBytes, 0);

var buffer = new char[length];
using (var reader = new StreamReader(stdin))
{
while (reader.Peek() >= 0)
{
reader.Read(buffer, 0, buffer.Length);
}
}

return (JObject)JsonConvert.DeserializeObject<JObject>(new string(buffer))["data"];
}

public static void Write(JToken data)
{
var json = new JObject
{
["data"] = data
};

var bytes = System.Text.Encoding.UTF8.GetBytes(json.ToString(Formatting.None));

var stdout = Console.OpenStandardOutput();
stdout.WriteByte((byte)((bytes.Length >> 0) & 0xFF));
stdout.WriteByte((byte)((bytes.Length >> 8) & 0xFF));
stdout.WriteByte((byte)((bytes.Length >> 16) & 0xFF));
stdout.WriteByte((byte)((bytes.Length >> 24) & 0xFF));
stdout.Write(bytes, 0, bytes.Length);
stdout.Flush();
}
}


Related Topics



Leave a reply



Submit