C# Example: An Activation Filter that Checks for License Availability

Applies To: Windows HPC Server 2008

Note

This code sample applies to HPC Server 2008. For updated filter code samples that apply to Windows HPC Server 2008 R2, you can download the code samples from the HPC Pack 2008 R2 SDK.

In Windows® HPC Server 2008, you can enforce site-specific job submission policies by writing a job activation filter application. An activation filter parses the job description file to check for factors that would cause the job to fail if activated, such as the unavailability of licenses, or when the usage time for the submitting user has been exceeded. If the filter detects the condition, it prevents the job from activating.

Important

Before writing and installing a custom filter, ensure that you have installed the HPC Pack 2008 Fix for Job XML Import and Export Issues update (https://go.microsoft.com/fwlink/?LinkId=132745). This update affects the job XML schema.

Example activation filter code

The C# code sample in this section provides an example of an activation filter that checks for license availability. In this example, the activation filter uses the output of FlexLM lmutil.exe to get license information. The application then uses this information to verify that licenses are available for the submitted job. The filter returns a non-zero value if licenses are not available. This blocks the job queue so that no other job will run until licenses become available. If invalid features are specified, or more licenses requested than the maximum allowed for a feature, then the job will be canceled and a value of 0 is returned so that the queue will not be blocked.

The sample application creates a log file by reading a configuration file that has an attribute called LogFilePath that contains the path to the log file. The configuration file should be named ActivationFilterName.exe.config, where ActivationFilterName is the name of your activation filter. In this example, the configuration file is named ActivationFilterDemo.exe.config. The following XML configuration file assigns values to the program parameters at run time. The configuration file should be placed in the same folder as the executable filter:

  <?xml version="1.0" encoding="utf-8" ?>
  <configuration>
      <appSettings>
          <add key="PollCommandName" value="c:\ActivationFilter\lmutil.exe" />
          <add key="PollCommandArguments" value="lmstat -c c:\ActivationFilter\YourLicense.lic -a" />
          <add key="LogFilePath" value="C:\ActivationFilter\LicenseActivationFilterDemo.log.txt" />
      </appSettings>
  </configuration>

Important

The following code sample is offered as a reference. You should modify it according to your specific configuration needs before using it in a production environment.

using System;
using System.Collections.Generic;
using System.Xml;
using System.Collections;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Configuration;
using System.IO;
using Microsoft.Hpc.Scheduler;

namespace LicenseActivationFilterDemo
{
    public class RunLicenseFilter
    {
        public static TextWriter logfile = null;

    public static int Main(string[] args) {
        try {

        // Create a log file by reading a config file that has an attribute called
        // "LogFilePath". This attribute contains the path to the log file.
        // The value of true that is passed in as the second parameter indicates that
        // the log file will be appended to. This is useful for debugging;
        // however, you may want to make this parameter false for production use
        // so that the file does not become too large.
                logfile = new StreamWriter(ConfigurationManager.AppSettings["LogFilePath"],true);
                logfile.WriteLine("Starting activation filter");

                // When the HPC Job Scheduler runs the activation filter, it adds the name of
                // a job XML file to the command line. In this program, this is the first argument.
                if (args.Length != 1) {
                    logfile.WriteLine("Usage: LicenseActivationFilterDemo.exe inputJobTermFile");
                    return -1;
                }


                // Utility class to call flexlm tools.
                LicenseChecker checker = new LicenseChecker();

                // Parse the input job XML file to get the SoftwareLicense job term.
                if (!checker.ParseSoftwareLicenseTerms(args[0])) {
                    logfile.WriteLine("No license required for job exit 0");
                    return 0;
                }

                // Check and determine if there are enough free licenses at this moment.
                // Note: it is possible that the available licenses may be
                // consumed between the time when the activation filter is executed
                // and when the task in the job requiring the license is actually started.
                if (checker.AreThereEnoughLicenses()) {
                    return 0;
                } else {
                    return 1;
                }
            }

        catch (Exception ex) {
                logfile.WriteLine("MESSAGE: " + ex.Message);
                logfile.WriteLine("SOURCE: " + ex.Source);
                logfile.WriteLine("TARGET: " + ex.TargetSite);
                logfile.WriteLine("STACK: " + ex.StackTrace + "\r\n");
                return -1;
            } finally {
                if (logfile != null) {
                    logfile.Flush();
                    logfile.Close();
                }
            }
        }
    }

    public class LicenseChecker {
        private Hashtable requestedFeatureTab = new Hashtable();
        private Hashtable availableFeatureTab = new Hashtable();
        private Hashtable availableFeatureTotals = new Hashtable();
        private Int32 jobID = 0;

        /// <summary>
        /// Read the job XML file and extract the SoftwareLicense job term.
        /// </summary>
        /// <param name="jobXmlFile">
        /// The job file name provided by the HPC Job Scheduler as the first
        /// command-line argument when the activation filter is executed.
        /// </param>
        /// <returns>
        /// A value of true is returned if the SoftwareLicense job term is present and was correctly formatted.

        /// </returns>

        public bool ParseSoftwareLicenseTerms(string jobXmlFile) {
            XmlDocument inputJob = new XmlDocument();

            inputJob.Load(jobXmlFile);
            // The base XML node in the document.
            XmlNode job = inputJob.DocumentElement;

            // Create the namespace that is used for the job XML schema.
            XmlNamespaceManager nsmgr = new XmlNamespaceManager(inputJob.NameTable);
            nsmgr.AddNamespace("ab", @"https://schemas.microsoft.com/HPCS2008/scheduler/");

            // Get the job ID in case the job needs to be canceled.
            XmlNode jobidnode = job.SelectSingleNode(@"@Id", nsmgr);
            if (jobidnode != null) {
                string JobIdStr = jobidnode.InnerXml;
                Int32.TryParse(JobIdStr, out jobID);
            } else {
                RunLicenseFilter.logfile.WriteLine("Unable to extract jobid from job file");
            }

            // Get the SoftwareLicense job property.
            XmlNode jobnode = job.SelectSingleNode(@"@SoftwareLicense", nsmgr);

            if (jobnode == null) {
                RunLicenseFilter.logfile.WriteLine("SoftwareLicense attribute not found");
                return false;
            }

            // Extract the value of the SoftwareLicense property.
            string licenseStr = jobnode.InnerXml;
            if (String.IsNullOrEmpty(licenseStr)) {
                RunLicenseFilter.logfile.WriteLine("SoftwareLicense job property is empty");
                return false;
            }
            RunLicenseFilter.logfile.WriteLine("SoftwareLicense job property: " + licenseStr);

            // Get all of the license feature:number pairs.
            char[] split = { ',' };
            string[] tokens = licenseStr.Split(split);

            // Loop over all feature:number pairs.
            foreach (string str in tokens) {
                string[] ftokens = str.Split(':');
                if (ftokens.Length == 2) {
                    Int32 numberOfLicenses = 0;
                    if (Int32.TryParse(ftokens[1], out numberOfLicenses)) {
                        requestedFeatureTab[ftokens[0]] = numberOfLicenses;
                    } else {
                        RunLicenseFilter.logfile.WriteLine("Error with {0} feature, number of licenses, {1}, not in an integer format",
                            ftokens[0], ftokens[1]);
                    }
                } else {
                    RunLicenseFilter.logfile.WriteLine("Error with {0} feature: number of licenses not specified",
                        ftokens[0]);
                }
            }

            if (requestedFeatureTab.Count <= 0) {
                RunLicenseFilter.logfile.WriteLine("No valid feature:number pairs in the SoftwareLicense job property");
                return false;
            }
            return true;
        }

        /// <summary>
        /// Parse the output of the flexlm utility to find all the currently executing license daemons
        /// and discover the total available licenses and the total licenses.
        /// </summary>
        /// <param name="output">
        /// The output of the flexlm utility.
        /// </param>
        public void ParseFlexlmOutput(string output) {
            string matchPattern = @"Users of (\w+):.*Total of (\d+) licenses issued;.*Total of (\d+) licenses in use";

            MatchCollection theMatches = Regex.Matches(output, matchPattern);

            foreach (Match m in theMatches) {
                RunLicenseFilter.logfile.WriteLine(m.Groups[1].Value + "=" + m.Groups[2].Value + " " + m.Groups[3].Value);
                Int32 total = 0;
                Int32 used = 0;
                Int32 value = 0;
                total = Int32.TryParse(m.Groups[2].Value, out value) ? value : 0;
                used = Int32.TryParse(m.Groups[3].Value, out value) ? value : 0;
                availableFeatureTab[m.Groups[1].Value] = total-used;
                availableFeatureTotals[m.Groups[1].Value] = total;
            }
        }

        /// <summary>
        /// Run the command specified in the config file, PollCommandName, using
        /// the arguments, PollCommandArguments.
        /// </summary>

        public void PollLicenseServer() {
            // Run the command and get the exit code.
            Process application = new Process();

            application.StartInfo.FileName = ConfigurationManager.AppSettings["PollCommandName"];
            if (String.IsNullOrEmpty(application.StartInfo.FileName)) {
                RunLicenseFilter.logfile.WriteLine("No executable specified in the config file for PollCommandName");
                return;
            }

            application.StartInfo.Arguments = ConfigurationManager.AppSettings["PollCommandArguments"];
            if (application.StartInfo.Arguments == null)
                application.StartInfo.Arguments = "";
            application.StartInfo.RedirectStandardOutput = true;
            application.StartInfo.RedirectStandardError = true;
            application.StartInfo.UseShellExecute = false;
            application.StartInfo.WorkingDirectory = Directory.GetCurrentDirectory();
            RunLicenseFilter.logfile.WriteLine("Current working directory is: " + application.StartInfo.WorkingDirectory);

            application.Start();
            application.WaitForExit();

            String stdout = application.StandardOutput.ReadToEnd();
            String stderr = application.StandardError.ReadToEnd();
            Int32 exitcode = application.ExitCode;

            RunLicenseFilter.logfile.WriteLine("Executing: {0} {1}",
                ConfigurationManager.AppSettings["PollCommandName"],
                ConfigurationManager.AppSettings["PollCommandArguments"]);

            RunLicenseFilter.logfile.WriteLine("Start Time: {0}",application.StartTime.ToLocalTime().ToLongTimeString());
            RunLicenseFilter.logfile.WriteLine("End Time  : {0}",application.ExitTime.ToLocalTime().ToLongTimeString());

            RunLicenseFilter.logfile.WriteLine("Exitcode: {0}",exitcode);
            RunLicenseFilter.logfile.WriteLine("Stdout  : {0}", stdout);
            RunLicenseFilter.logfile.WriteLine("Stderr  : {0}", stderr);

            // Add the returned values from flexlm to the availableFeatureTab HashTable.
            ParseFlexlmOutput(stdout);
        }

        /// <summary>
        /// Iterate over all the features to make sure that there are available
        /// licenses at this moment.
        /// </summary>
        /// <returns>
        /// A value of true is returned if there are licenses available;
        /// a value of false is returned if there are not enough or if a specified feature does not exist.
        /// </returns>

        public bool AreThereEnoughLicenses() {
            Boolean enoughLicenses = true;
            //Execute the command that is specified in the config file and parses the results.
            PollLicenseServer();

            // Iterate through the requesting features and determine whether
            // there are enough license tokens.
            IDictionaryEnumerator iter = requestedFeatureTab.GetEnumerator();

            // Run lmutils lmstat to verify the available license tokens.
            while (iter.MoveNext()) {
                bool jobCanceled = false;
                RunLicenseFilter.logfile.WriteLine("Checking " + iter.Key + " " + "{0}", iter.Value);
                if (!AreThereEnoughFeatureXTokens((string)iter.Key, (int)iter.Value, out jobCanceled)) {
                    enoughLicenses = false;
                }

                // The job was canceled. Return a value of true so that the activation
                // filter will not block the job queue.
                if( jobCanceled ) {
                    return true;
                }
            }

            return enoughLicenses;
        }

        /// <summary>
        /// Check to see whether there are licenses available for the requested features.
        /// </summary>
        /// <param name="featureName">
        /// The feature name to check for.
        /// </param>
        /// <param name="requestedAmt">
        /// The number of licenses required.
        /// </param>
        /// <param name="jobCanceled">
        /// If true, then the job was canceled.
        /// </param>
        /// <returns>
        /// A value of false is returned if either the feature does not exist or if there are not enough licenses.
        /// </returns>

        private bool AreThereEnoughFeatureXTokens(string featureName, int requestedAmt, out Boolean jobCanceled) {
            jobCanceled = false;
            if (!availableFeatureTab.Contains(featureName)) {
                // The job will be canceled if the feature is unknown.
                // This is because the activation filter will block the
                // queue if a non-zero exit code is returned, and submit the job if a zero exit code is returned.
                // When the job is canceled, the filter
                // will return a value of 0, and it will not block the queue.
                String message = "Canceled by activation filter: Unknown License feature requested: "+ featureName;
                this.CancelJob(message);
                jobCanceled = true;
                return true;
            }
            int availableAmt = (int)availableFeatureTab[featureName];
            int totalAmt = (int)availableFeatureTotals[featureName];

            if( requestedAmt>totalAmt ) {
                // The job will be canceled if the licenses requested are more than the maximum
                // allowable for this feature. This is because the activation filter will block the
                // queue if a non-zero exit code is returned, and submit the job if a zero exit code is returned.
                // Since the number of license requests will never be satisfied, the job is canceled. The
                // filter will return a value of 0 and it will not block the queue.
                String message = "Canceled by activation filter: Requested amount of licenses,"+requestedAmt+", for the feature,"+featureName+" ,greater than maximum licenses allowed," + totalAmt;
                this.CancelJob(message);
                jobCanceled = true;
                return true;
            } else if (availableAmt < requestedAmt) {
                RunLicenseFilter.logfile.WriteLine("Not enough licenses for " + featureName + ": requested: " + requestedAmt + " available: "
                + availableAmt);
                return false;
            } else {
                RunLicenseFilter.logfile.WriteLine("Enough licenses for " + featureName + ": requested: " + requestedAmt + " available: "
                + availableAmt);
                return true;
            }
        }

        /// <summary>
        /// Cancel a job with the supplied message.
        /// </summary>
        /// <param name="message">
        /// The message to attach to the canceled job.
        /// </param>

        private void CancelJob(String message) {
            RunLicenseFilter.logfile.WriteLine(message + ": jobid, " + jobID + ", being canceled");
            RunLicenseFilter.logfile.Flush();
            IScheduler scheduler = new Scheduler();
            scheduler.Connect("localhost");
            scheduler.CancelJob(jobID, message);
            scheduler.Dispose();

        }
    }
}

Additional references