Getting real-time updates from remote process using the AJAX Extension
This sample was a result of an idea from Kent Post at Akamai. He was wanting to be able to launch a process on a remote server and get real-time updates in the browser. In this case, we focused on running WinPE scripts to build ISO's on the server. The WinPE scripts can take a long time to process, and the goal is to launch the process and see what was going on without having to be logged into the server console using the browser. The attached sample provides a way to perform these actions by using System.Diagnostics to launch the process and the AJAX Extension to get updates. Before getting into the sample, let's look at some of the complications of doing this type of work.
- Permissions on the server - By default, the application pool running ASP.Net cannot run other processes on the server. The process by default runs as Network Service which typically won't have Read access to the .bat, .cmd, .exe you're trying to run. If you're writing files, it won't have access to write to directories either.
- Getting the data back to the client - Once you launch the process, how do you get the data down to a remote browser? HTTP is a disconnected protocol, so once you spin up the process, you need some way of making additional requests and getting the output.
- Application can not be interactive - Making sure the application you're running does not have an interface that requires interaction. Keep in mind you are launching a child process from a non-interactive service and there is no way for a remote or local user to actually interact with the program. Batch files are good for this scenario.
The first obstacle is relatively easy to overcome. For the demo, you can use a file based web site in Visual Studio 2005 and the built in web server. In this scenario, the web server and code run as the account you are logged into the box with and assuming this account has appropriate permissions to launch the program and write to any area that the program writes to, the app will work fine. From a server perspective, we ran the Application Pool as Local System. You can setup IIS with a new application pool running under local system and assign the application pool to the specific application to ensure other code is not running with elevated privileges. Another option is to use PInvoke and call CreateProcessAsUser and explicitly set desktop permissions for the application pool account you're using. This is a bit more complicated and won't be covered here. If you're interested in looking into that approach for launching processes, check out Q165194
165194 - INFO: CreateProcessAsUser() Windowstations and Desktops
https://support.microsoft.com/support/kb/articles/Q165/1/94.asp
The second problem of getting the output has a couple of options. One way to go is to pipe the output from the program to a text file and then redirect the browser to the textfile. If you haven't done this before, the following pipes the output of the dir command to the file c:\output.log: dir >c:\ouptut.log This gets you the output after the application has completed execution. However, in order to get the data as it's occurring, you have to make multiple requests to the server and access the StandardOutput stream directly. In this sample, we use the AJAX Extension, the setInterval method in JavaScript, the System.Diagnostics.Process class, and a custom class that's cached on the server to handle this scenario.
Overview of the sample
The attached files provide all the source to run the sample. Here's a rundown of what the app does and what is in the .zip files.
- User browses Default.aspx - If the user does not have a cookie called MyGUID, one is created for them. The cookie value is used to uniquely identify the requests when making web service calls and for caching.
- User clicks the Launch button
- JavaScript posts to the LaunchProcess method of the ProcessLauncher.asmx web service using the AJAX Extension
- LaunchProcess method does the following:
- Creates an instance of the ProgramWatcher class
- Calls the Execute method of ProgramWatcher
- Execute method launches a ThreadPool thread
- Runs the ReadLog.exe program
- Redirects the StandardOutput of the ReadLog.exe program to a Stream property of the ProgramWatcher instance
- The ProgramWatcher instance is cached using the MyGUID cookie value from Step 1
- When the Async call returns, a timer kicks off that runs the GetStatus JavaScript method after 750 milliseconds.
- GetStatus JavaScript method runs
- Timer is stopped to prevent async callbacks from getting out of sync. i.e. IE can make 2 calls per server, this prevents 2 requests leaving the client and the 2nd request returning before the first request and messing up the output.
- GetStatus method of the ProcessLauncher webservice is called
- GetStatus loads the ProgramWatcher method out of cache using the MyGUID cookie value
- Calls the ReadOutput method of the ProgramWatcher instance passing it how many bytes to read from the Stream property on ProgramWatcher - in this sample 50 bytes at a time.
- Returns the bytes returned from ReadOutput
- Client parses the response and handles output that contains \r vs. \r\n. See the Issues section below for why this is important.
- Timer started again and Steps 4 - 5 occur again until the GetStatus method returns Null
Included Files
Here's a list of the files included in the .zip files with a summary of what they do. The code is commented thoroughly.
- AjaxBatchProcess.zip
- App_Code
- ProgramWatcher.cs - This class is what drives the process on the server side. It contains an Execute method that launches a program via System.Diagnostics.Process and redirects the StandardOutput to a property on the class. The class is cached so that the browser can get the output from the process in chunks.
- ProcessLauncher.cs - Web Service methods that allow the AJAX Extension to generate JavaScript proxy classes that are used to launch the process on the web server and also get the output in chunks.
- AjaxBatchProcess.js - JavaScript file that calls the webservice methods and also handles getting and parsing the output.
- Default.aspx - Page requested by an end user. Has a ScriptManager that hooks up AjaxBatchProcess.js and ProcessLauncher.asmx
- ProcessLauncher.asmx - Web Service requested by the client.
- Web.config - Web.config from the AJAX Extension project
- App_Code
- ReadLog.zip
- ReadLog.exe - Command-line program that reads the build_pe_out.txt file and pushes the output to the console.
- build_pe_out.txt - Output from WinPE commands.
- ReadLog-Source.zip
- Program.cs - source for the ReadLog program. This program reads in a file passed in as an argument and pushes the text to the console.
- Default C# console project files
Here are the steps to use the files attached to the blog:
- Unzip AjaxBatchProcess.zip to a folder in your file system.
- Unzip ReadLog.zip to c:\ReadLog
- Open Visual Studio 2005
- From the File menu | Select Open Web Site
- Select the path from Step 1 (the folder that contains app_code, default.aspx, etc. from the .zip file)
- Right-click Default.aspx, select View in Browser
- Click the Launch button
Issues
I wanted to highlight a few areas and why we chose to go that route.
Caching the ProgramWatcher and the Stream property
This is an important part of the application and is the piece that allows access to the output from the application in real-time. In this case you have a program that is redirecting the StandardOutput to a Stream. This is essentially writing bytes into memory with a pointer at the end.
These are bytes<StandardOutput Pointer>
The ProgramWatcher class sets the StandardOutput stream to a property called Stream. This gives you another pointer to the same bytes that starts at the beginning.
<ProgramWatcher.Stream pointer>These are bytes<StandardOutput Pointer>
With this setup, you have the System.Diagnostics.Process adding bytes to the end of the stream via StandardOutput and ProgramWatcher.Stream reading the same bytes with a pointer that starts at the beginning. The ProgramWatcher instance is cached so that when you Read bytes off of ProgramWatcher.Stream, you are incrementing the pointer.
These are <ProgramWatcher.Stream pointer> bytes. This is additional data <StandardOutput Pointer>
When the GetStatus method is called again, you are accessing the same instance of the class, so the pointer is where you left it. I was making this way to hard, so thanks to Todd Carter for helping clear this up.
Parsing the \r and \r\n in the output returned by GetStatus
This is important as console applications handle text ending in \r different than \r\n. In the case of WinPE, you get output showing the percentage complete. The text looks like:
|6.0.6001.16464 | + | WinPE-FontSupport-JA-JP-Package \r\n[============== 25.0% ] \r[===========================50.0% ] \r[============================75.0%=========== ] \r
[==========================100.0%==========================] \r\nMore TextIn the console, this renders as the following when it is complete:
|6.0.6001.16464 | + | WinPE-FontSupport-JA-JP-Package
More TextHowever, while the process is running, you would see the WinPE-FontSupport-JA-JP-Package line followed by the percentage. When the next line ending in \r is written to the console, the previous line is replaced with the updated percentage. The problem is that the browser doesn't handle \r and \r\n the same way. So you would end up with the following:
|6.0.6001.16464 | + | WinPE-FontSupport-JA-JP-Package
[============== 25.0% ][===========================50.0% ][============================75.0%=========== ][==========================100.0%==========================]More TextNot real pretty. In the sample, I parse the output and if the output ends in \r, I put the information in the TextBox. If the text ends in \r\n, I clear the textbox and add the line to the TextArea. This gets pretty close to the same output you see in a console and avoids a lot of noise and clutter in the output.
Hopefully this sample and the information provided will help anyone out there trying to solve similar issues.