In part 1, we created a console application that took screenshots of the desktop and saved them to a directory on the file system. (Code here https://github.com/chrishibler/screen-servist-part1.) I’ve effectively monitored the state of a running system which I could not RDP into by sharing the directory in which the images were saved.
The shared directory part is a little clunky though, so in this part, we’ll add a minimal webserver to the project using the OWIN/Katana self-host modules and SignalR to communicate with the client browser. OWIN is a community owned specification and open source project, whereas Katana are the Microsoft developed, open source components and extensions. Using these, we can create a minimal web server hosted by the console application developed in part 1.
To start, add a new class library project to the existing solution named “ScreenServistWeb”. This will have all of the goodies for our webserver. The existing console application will be responsible for starting it up.
Open the package manager console and change the “Default project” drop down to the new ScreenServistWeb project. Run the following commands to install the required nuget packages in this order.
Install-Package Microsoft.Owin.SelfHost Install-Package Microsoft.Owin.StaticFiles Install-Package Microsoft.AspNet.SignalR.SelfHost
The order seems important here, when I installed SignalR first, the binding redirects nuget added created some build errors. You might not run into that, but if you have build errors at this point, that could be the problem, so just uninstall the packages and reinstall in the order above.
Microsoft.Owin.SelfHost
This package will let us create an in-process web server. This is great for what we want, a single, self-contained executable that serves up a single page. If we didn’t use the OWIN self-host, we would be looking at jumping through a bunch of hoops to integrate with IIS or another heavy handed approach.
Microsoft.Owin.StaticFiles
This package allows us to serve static content through our web server. The plan is to communicate with the browser over a websocket (SignalR), so we don’t need anything more than a simple HTML page with some javascript.
Microsoft.AspNet.SignalR.SelfHost
This package has all of the server side SignalR, websocket stuff for use with our in-process, self-hosted web server. Again, without the self-host package, we’d be stuck having to setup IIS and dump our page in there, not a very lightweight or portable solution.
Add the SignalR Hub
Rename the default “Class1.cs” added to the project to “ScreenServistHub.cs”. SignalR uses the concept of “Hubs” to allow the client and server to call methods on one another. In our case, we only need a single method the server will call on the client. We’ll call the method “addImageToPage”. The entire Hub class looks like this:
/// <summary> /// Contains the SignalR Hub methods for screen server. /// </summary> public class ScreenServistHub : Hub { public void SendImage(string imagePath) { Clients.All.addImageToPage(imagePath); } }
We have a single C# method, “SendImage” callable from our server side code that will broadcast the path of the captured image to all of the connected SignalR clients.
Adding the Server
Next, let’s add a new class named “Starup”. This will be the entry point for starting the webserver and will contain all of the relevant start information and configuration.
/// <summary> /// OWIN startup class. /// </summary> public class Startup { public void Configuration(IAppBuilder app) { app.MapSignalR(); app.UseFileServer(new FileServerOptions() { RequestPath = PathString.Empty, FileSystem = new PhysicalFileSystem(@".\public") }); } }
This class will bootstrap the web server. The “MapSignalR” extension method sets up the bindings to the “/signalr” url (the default) that will be used to communicate between client and server. The “UseFileServer” extension method tells the server we want to serve static content. We set some options on the FileServerOptions object to tell the server where our files are located and to which URL the request will be mapped.
That’s it for setting up the webserver and SignalR! Even though it’s simple and terse once completed, the lack of documentation and examples for using the self-host with various pieces meant I spent more time than I would have liked trying to figure it out.
The Client
Let’s switch gears and look at adding the html for the client. Add a new folder to the ScreenServistWeb project named “public”. This folder name will match the path we provided to the “UseFileServer” method via the FileServerOptions. Inside the folder, add a new HTML file named “index.html”. You can find the complete file in the github repo. It’s basically a div with the touch of javascript needed to get the messages from the server.
We bring in jQuery to make adding the images a little easier and add a very minimal amount of CSS styling. Next, we bring in the SignalR javascript library.
http://ajax.aspnetcdn.com/ajax/signalr/jquery.signalr-2.2.1.js
The SignalR library is what allows us to communicate with the webserver we created. In the body of the page, we just need a div container in which to put the screenshots. Lastly, the script at the bottom creates the SignalR connection to the server.
var connection = $.hubConnection(); var hubProxy = connection.createHubProxy('screenServistHub'); hubProxy.on('addImageToPage', function (imagePath) { $("#container").prepend('<img class="main-img" src=' + imagePath + '>' + '</img></br>'); }); connection.start() .done(function () { console.log('Now connected, connection ID=' + connection.id); }) .fail(function () { console.log('Oh no!'); });
The connection is made, and we define which function requests are to be handled. The hubProxy object is the client side representation of the SignalR hub. The “on” method maps the server’s function request to a javascript function we define on the client. In this case, the server asks the page to run ‘addImageToPage’ and the client side javascript responds by running the function we defined. The function prepends a new image to the list of images being built up. Next, the connection starts and we log a little debugging information to get the status via the “done” and “fail” functions, which will log information to the console.
Running the Server
Finally, we need to modify the console application’s Program.cs to start the webserver in the main function and send the images. To do that, let’s create a little wrapper class which will run Startup and give us a cleaner method for interacting with the webserver from the console application. Add a new class named “WebServerService” to the ScreenServistWeb project.
/// <summary> /// Provides the method by which images are communicated to clients. /// </summary> public class WebServerService : IDisposable { private static IDisposable _pageServer; public void Start() { StartOptions options = new StartOptions("http://*:12345/"); _pageServer = WebApp.Start<Startup>(options); } public void SendNewImage(string path) { if (path == null) { throw new ArgumentNullException(nameof(path)); } var hubContext = GlobalHost.ConnectionManager.GetHubContext<ScreenServistHub>(); hubContext.Clients.All.addMessageToPage(path); } public void Dispose() { _pageServer.Dispose(); } }
This class will bind our server to port “12345” on startup, encapsulate the SignalR method for sending the images and provide a way to clean up the webserver on exit.
Next, add a project reference in ScreenServistConsole to ScreenServistWeb. Once that’s completed, the console application can be modified to start the server with just a few new lines of code.
class Program { private static WebServerService WebServerService; static void Main(string[] args) { ScreenCaptureService service = new ScreenCaptureService(); service.CaptureSaved += Service_CaptureSaved; service.StartCapturing(); WebServerService = new WebServerService(); WebServerService.Start(); Process.Start("http://127.0.0.1:12345"); while (Console.ReadLine() != "q") { } } private static void Service_CaptureSaved(object sender, CaptureSavedEventArgs e) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"Captured at {e.Screenshot.CreationTime.ToLongTimeString()}"); string webPath = $"captures/{Path.GetFileName(e.Screenshot.Path)}"; WebServerService.SendNewImage(webPath); } }
The server is started, then the application launches the default browser. The “Service_CaptureSaved” handler translates the path to the screenshot to a format usable by the client webpage and sends the message to the client. To make that path valid, change ScreenCaptureService.CAPTURE_SUBDIRECTORY to match.
private const string CAPTURE_SUBDIRECTORY = @"public\captures";
Debugging
One of the things I ran into was a “MissingMemberException” when trying to run startup:
An unhandled exception of type ‘System.MissingMemberException’ occurred in Microsoft.Owin.Hosting.dll
Additional information: The server factory could not be located for the given input: Microsoft.Owin.Host.HttpListener
at Microsoft.Owin.Hosting.Engine.HostingEngine.ResolveServerFactory(StartContext context)
at Microsoft.Owin.Hosting.Engine.HostingEngine.Start(StartContext context)
at Microsoft.Owin.Hosting.Starter.DirectHostingStarter.Start(StartOptions options)
at Microsoft.Owin.Hosting.Starter.HostingStarter.Start(StartOptions options)
at Microsoft.Owin.Hosting.WebApp.StartImplementation(IServiceProvider services, StartOptions options)
at Microsoft.Owin.Hosting.WebApp.Start(StartOptions options)
at Microsoft.Owin.Hosting.WebApp.Start[TStartup](StartOptions options)
at ScreenServistWeb.WebServerService.Start() in C:\work\screen-servist-part1-master\ScreenServist\ScreenServistWeb\WebServerService.cs:line 22
at ScreenServistConsole.Program.Main(String[] args) in C:\work\screen-servist-part1-master\ScreenServist\ScreenServistConsole\Program.cs:line 20
at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
It appears some of the required assemblies were not being copied to the output directory of the executable. To get around that, I created a solution level “bin” directory and dumped all of the output there. I like doing this anyway, because it avoids a lot of copying during builds. The time to copy files around adds up in larger solutions, but is negligible for a few projects.
If you’re staring at a blank webpage, crack open the developer tools in your browser and look at the console. If everything is working with the SignalR connection, you should see a message like “Now connected, connection ID=<<some id>>”. Otherwise, you should see an error that might give you more information.
The completed code for the project is available on github: https://github.com/chrishibler/screen-servist-part2