LordVeovis / xmlrpc

A port of CookComputing.XmlRpcV2 for dotnet core 2
MIT License
33 stars 21 forks source link

Example with xml-rpc server #17

Closed UyttenhoveSimon closed 3 years ago

UyttenhoveSimon commented 4 years ago

Hello,

Context: I am trying to reuse some c# code and interact with robot-framework via xml-rpc.

I was wondering if there was one example with a xml-rpc server? All http:/ references are within !FX1_0 regions or removed within .csproj files.

What am I missing ?

LordVeovis commented 3 years ago

Sorry but I don't have any server example, only client-side.

UyttenhoveSimon commented 3 years ago

@LordVeovis , thanks for your reply. I ended up mixing one code I saw online: https://github.com/daluu/sharprobotremoteserver/blob/master/robotremoteserver.cs And your code. You can reuse it in your example if you want.

using Horizon.XmlRpc.AspNetCore;
using Horizon.XmlRpc.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml.XPath;

namespace RobotListenerCore3
{
    public interface IAddService
    {
        [XmlRpcMethod()]
        int AddNumbers(int numberA, int numberB);

        [XmlRpcMethod]
        string[] get_keyword_names();

        [XmlRpcMethod]
        XmlRpcStruct run_keyword(string keyword, object[] args);

        [XmlRpcMethod]
        string[] get_keyword_arguments(string keyword);

        [XmlRpcMethod]
        string get_keyword_documentation(string keyword);
    }

    public class RobotListener : XmlRpcService, IAddService
    {
        public static bool enableStopServer = true;
        private Assembly library;
        private string libraryClass;
        private XPathDocument doc;

        //I/O management components
        private TextWriter libout;

        private TextWriter liberrs;

        //.NET reflection components to handle the .NET library being served
        private Type classType;

        private object libObj;

        private List<string> listToAvoid = new List<string>() { "GetType", "InitializeLifetimeService", "GetLifetimeService", "System__Method__Help___", "System__Method__Signature___", "System__List__Methods___", "GetHashCode", "Equals", "ToString", "HandleHttpRequest", "HandleHttpRequestAsync", "Invoke" };

        public RobotListener()
        {
            //library = Assembly.GetAssembly(this.GetType());
            classType = this.GetType();
            libout = new StringWriter();
            liberrs = new StringWriter();
        }

        public int AddNumbers(int numberA, int numberB)
        {
            return numberA + numberB;
        }

        public string[] get_keyword_arguments(string keyword)
        {
            if (keyword == "stop_remote_server") return new String[0];
            return classType.GetMethod(keyword).GetParameters().Select(parameter => parameter.Name).ToArray();
        }

        public string get_keyword_documentation(string keyword)
        {
            string retval = ""; //start off with no documentation, in case keyword is not documented

            if (keyword == "stop_remote_server")
            {
                retval = "Remotely shut down remote server/library w/ Robot Framework keyword.\n\n";
                retval += "If server is configured to not allow remote shutdown, keyword 'request' is ignored by server.\n\n";
                retval += "Always returns status of PASS with return value of 1. Output value contains helpful info and may indicate whether remote shut down is allowed or not.";
                return retval;
            }
            if (doc == null)
            {
                return retval; //no XML documentation provided, return blank doc
            }//else return keyword (class method) documentation from XML file

            XPathNavigator docFinder;
            XPathNodeIterator docCol;
            try
            {
                docFinder = doc.CreateNavigator();
            }
            catch
            {
                docFinder = null; //failed to load XML documentation file, set null
            }
            string branch = "/doc/members/member[starts-with(@name,'M:" + libraryClass + "." + keyword + "')]/summary";
            try
            {
                retval = docFinder.SelectSingleNode(branch).Value + System.Environment.NewLine + System.Environment.NewLine;
            }
            catch
            {
                //no summary info provided for .NET class method
            }
            try
            {
                branch = "/doc/members/member[starts-with(@name,'M:" + libraryClass + "." + keyword + "')]/param";
                docCol = docFinder.Select(branch);
                while (docCol.MoveNext())
                {
                    retval = retval + docCol.Current.GetAttribute("name", "") + ": " + docCol.Current.Value + System.Environment.NewLine;
                };
                retval = retval + System.Environment.NewLine;
            }
            catch
            {
                //no parameter info provided or some parameter info missing for .NET class method
            }
            try
            {
                branch = "/doc/members/member[starts-with(@name,'M:" + libraryClass + "." + keyword + "')]/returns";
                retval = retval + "Returns: " + docFinder.SelectSingleNode(branch).Value;
            }
            catch
            {
                //.NET class method either does not return a value (e.g. void) or documentation not provided
            }
            return retval; //return whatever documentation was found for the keyword
        }

        public string[] get_keyword_names()
        {
            //MethodInfo[] mis = classType.GetMethods(BindingFlags.Public | BindingFlags.Static);
            //seem to have issue when trying to only get public & static methods, so get all instead
            var methods = classType.GetMethods();
            methods = methods.Where(method => !listToAvoid.Contains(method.Name)).ToArray();

            //add one more for stop server that's part of the server
            var keyword_names = methods.Select(method => method.Name).ToList();
            keyword_names.Add("stop_remote_server");

            return keyword_names.ToArray();
        }

        public XmlRpcStruct run_keyword(string keyword, object[] args)
        {
            XmlRpcStruct kr = new XmlRpcStruct();

            if (keyword == "stop_remote_server")
            {
                if (RobotListener.enableStopServer)
                {
                    //reset output back to stdout
                    StreamWriter stdout = new StreamWriter(Console.OpenStandardOutput());
                    stdout.AutoFlush = true;
                    Console.SetOut(stdout);

                    //spawn new thread to do a delayed server shutdown
                    //and return XML-RPC response before delay is over
                    new Thread(stop_remote_server).Start();
                    Console.WriteLine("Shutting down remote server/library in 5 seconds, from Robot Framework remote");
                    Console.WriteLine("library/XML-RPC request.");
                    Console.WriteLine("");
                    kr.Add("output", "NOTE: remote server shutting/shut down.");
                }
                else
                {
                    kr.Add("output", "NOTE: remote server not configured to allow remote shutdowns. Your request has been ignored.");
                    //in case RF spec changes to report failure in this case in future
                    //kr.Add("status","FAIL");
                    //kr.Add("error","NOTE: remote server not configured to allow remote shutdowns. Your request has been ignored.");
                }
                kr.Add("return", 1);
                kr.Add("status", "PASS");
                kr.Add("error", "");
                kr.Add("traceback", "");
                return kr;
            }
            //redirect output from test library to send back to Robot Framework
            Console.SetOut(libout); //comment out when debugging
            Console.SetError(liberrs);

            MethodInfo mi = classType.GetMethod(keyword);

            try
            {
                /* we let XML-RPC.NET library handle the data type conversion
                 * hopefully, the test library returns one of the supported XML-RPC data types:
                 * http://xml-rpc.net/faq/xmlrpcnetfaq-2-5-0.html#1.9
                 * http://xml-rpc.net/faq/xmlrpcnetfaq-2-5-0.html#1.12
                 * Otherwise, an error may occur.
                 *
                 * FYI, and this is the spec for Robot Framework remote library keyword argument and return type
                 * http://robotframework.googlecode.com/svn/tags/robotframework-2.5.6/doc/userguide/RobotFrameworkUserGuide.html#supported-argument-and-return-value-types
                 * on how data types should map, particularly for the non-native-supported types.
                 * Hopefully XML-RPC.NET converts them closely to those types, otherwise, you will have to make some
                 * changes in the remote server code here to adjust return type according to Robot Framework spec.
                 * Or change the test library being served by the remote server to use simpler data structures (e.g. primitives)
                 */
                libObj = Activator.CreateInstance(classType);
                if (mi.ReturnType == typeof(void))
                {
                    mi.Invoke(libObj, args);
                    kr.Add("return", "");
                }
                else
                {
                    kr.Add("return", mi.Invoke(libObj, args));
                }

                kr.Add("status", "PASS");
                kr.Add("output", libout.ToString());
                libout.Flush();
                kr.Add("error", liberrs.ToString());
                liberrs.Flush();
                kr.Add("traceback", "");
                return kr;
            }
            catch (TargetInvocationException iex)
            {
                //exception message is probably more useful at this point than standard error?
                liberrs.Flush();

                kr.Add("traceback", iex.InnerException.StackTrace);
                kr.Add("error", iex.InnerException.Message);

                kr.Add("output", libout.ToString());
                libout.Flush();
                kr.Add("status", "FAIL");
                kr.Add("return", "");
                return kr;
            }
            catch (System.Exception ex)
            {
                //to catch all other exceptions that are not nested or from target invocation (i.e. reflection)

                //exception message is probably more useful at this point than standard error?
                liberrs.Flush();

                kr.Add("traceback", ex.StackTrace);
                kr.Add("error", ex.Message);

                kr.Add("output", libout.ToString());
                libout.Flush();
                kr.Add("status", "FAIL");
                kr.Add("return", "");
                return kr;
            }
        }

        private static void stop_remote_server()
        {
            //delay shutdown for some time so can return XML-RPC response
            int delay = 5000; //let's arbitrarily set delay at 5 seconds
            Thread.Sleep(delay);
            Console.WriteLine("Remote server/library shut down at {0}", System.DateTime.Now.ToString());
            System.Environment.Exit(0);
        }
    }
}

You launch it from a ASP Net Core server. Startup.cs looks like this:

using Horizon.XmlRpc.AspNetCore.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;

namespace RobotListenerCore3
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddXmlRpc();
            services.Configure<KestrelServerOptions>(options =>
            {
                options.AllowSynchronousIO = true;
                options.ListenAnyIP(5678);
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseRouting();
            app.UseXmlRpc(config => config.MapService<RobotListener>("/RPC2"));
        }
    }
}