Show / Hide Table of Contents

    Creating Custom Commands

    The most valuable feature of Petabridge.Cmd is its ability to be extended to support any arbitrary number of user-defined commands, and in this section of the documentation we will explain how.

    First, you should read up on how Petabridge.Cmd works.

    Important

    In Petabridge.Cmd 1.0 we introduced some important structural changes to how CommandPalettes are handled on the server-side that might break compatibility with palettes developed against Petabridge.Cmd v0.8 and earlier.

    Command Syntax and Structure

    Petabridge.Cmd is highly extensible and any user can define their own custom commands they wish to expose to the client. This section explains how commands are defined on the server and how those are represented in pbm for the end-user.

    Defining a Command

    All commands in Petabridge.Cmd are defined using the following parts:

    • Name - The name of the command as it's actually used on the CLI.
    • Description - A human-friendly description of what the command does and what type of output it produces.
    • Arguments - A set of 0-N arguments that provide the command with data and instructions needed for execution. Some arguments might be marked as required.

    Most of the work that goes into creating a command consists of defining a command's arguments and subsequently defining the actor responsible for executing the command itself.

    The easiest way to build a command is to use the CommandDefinitionBuilder, a fluent domain specific language (DSL) that makes it easy to iteratively create a CommandDefinition, the object that Petabridge.Cmd uses to define a command on both the host and on the pbm client.

    For example, here's an example of how you might define a command inside the Petabridge.Cmd quick start:

    public static readonly CommandDefinition CheckMessages =
        new CommandDefinitionBuilder().WithName("view")
            .WithDescription("Views the set of saved messages on the server")
            .WithArgument(
                builder =>
                    builder.WithName("since")
                        .WithDescription(
                            "A timerange for messages we wish to view. Values should be in the format of 5s, 5m, 10m, 1h.")
                        .IsMandatory(false)
                        .WithDefaultValues("1m", "5m", "30m", "1h", "4hr", "1d")
                        .WithSwitch("-s").WithSwitch("-S"))
            .Build();
    

    Command Arguments

    Each argument in a CommandDefinition can be defined with the following options:

    • Name - A human-friendly name for the commandline argument.
    • Description - A human-friendly description of what the argument does.
    • Switches - the argument as it's represented on the CLI, i.e. -S or -s. Must supply at least 1 switch with every argument for each command, and by convention most developers define their switches with a preceding -.
    • IsMandatory - defaults to false, but when set to true the command to which this argument belongs will fail validation if this argument is not supplied.
    • AllowMultiple - defaults to false, but when set to true allows multiple instances of the same argument to by specified for a single instance of the command.
    • IsFlag - defaults to false, but when set to true it means that this argument doesn't accept a value. Meaning, just the presence of the argument itself is all that is needed.
    • DefaultValues - optional. Provides tab-autocomplete hints for pieces of static data back to the end-user.

    By encapsulating all of this information into a standardized CommandDefinition object, this makes it possible for the pbm client to be able to expose the command's definition to the end-user. It also gives the pbm client the ability to apply intelligent tab-autocomplete and automatic help documentation for all user-defined commands as an added bonus.

    Using Commands

    All CommandDefinitions are used as a type of schema to validate the lexical structure of a Command. Think of it this was: a CommandDefinition is a class and Command is an object, an instance of that class.

    Validation

    When a Command is sent to the Petabridge.Cmd.Host for execution, that command is validated against the rules you defined when specifying the command's arguments and structure. If there's anything that violates the structure of a given command, a validation error will be produced and sent back to the client.

    If you wish to test the validity of your commands directly you can create a new instance of the CommandValidator. Here's an example:

    public static readonly CommandValidator Validator =
        new CommandValidator(new CommandPalette("cluster", new[] {JoinCmd, LeaveCmd}));
    
    [Fact]
    public void ShouldAllowMultipleOfSameArgumentWhenValid()
    {
        var command = new Command("cluster", "join", new List<Tuple<string, string>>
        {
            Tuple.Create("-n", "akka.tcp://MyActorSystem@localhost:8080"),
            Tuple.Create("-n", "akka.tcp://MyActorSystem@localhost:8081")
        });
        var validation = Validator.Validate(command);
        validation.Should().BeEmpty();
    }
    

    Command Palettes

    All CommandDefinitions are grouped together into a unit known as a CommandPalette in Petabridge.Cmd. A CommandPalette is unit of work used by other Petabridge.Cmd.Common tools such as the CommandValidator and the CommandParser. After you've finished defining a set of commands, you group those commands together into a CommandPalette such as this:

    // -----------------------------------------------------------------------
    // <copyright file="MsgCommands.cs" company="Petabridge, LLC">
    //      Copyright (C) 2017 - 2017 Petabridge, LLC <https://petabridge.com>
    // </copyright>
    // -----------------------------------------------------------------------
    namespace Petabridge.Cmd.QuickStart
    {
        /// <summary>
        ///     Contains all of the custom commands used in the Petabridge.Cmd QuickStart
        /// </summary>
        public static class MsgCommands
        {
            public static readonly CommandDefinition CheckMessages =
                new CommandDefinitionBuilder().WithName("view")
                    .WithDescription("Views the set of saved messages on the server")
                    .WithArgument(
                        builder =>
                            builder.WithName("since")
                                .WithDescription(
                                    "A timerange for messages we wish to view. Values should be in the format of 5s, 5m, 10m, 1h.")
                                .IsMandatory(false)
                                .WithDefaultValues("1m", "5m", "30m", "1h", "4hr", "1d")
                                .WithSwitch("-s").WithSwitch("-S"))
                    .Build();
    
            public static readonly CommandDefinition Echo = new CommandDefinitionBuilder().WithName("echo")
                .WithDescription("Echoes a message from the server back to the client")
                .WithArgument(
                    b =>
                        b.WithName("message")
                            .WithDescription("The message that will be echoed back from the server to the client.")
                            .IsMandatory(true)
                            .WithSwitch("-m")
                            .WithSwitch("-M"))
                .Build();
    
            public static readonly CommandDefinition Write = new CommandDefinitionBuilder().WithName("write")
                .WithDescription("Writes a message to the server that can be accessed by other clients.")
                .WithArgument(
                    b =>
                        b.WithName("message")
                            .WithDescription("The message that will be written to the server.")
                            .IsMandatory(true)
                            .WithSwitch("-m")
                            .WithSwitch("-M"))
                .Build();
    
            public static readonly CommandDefinition Purge = new CommandDefinitionBuilder().WithName("purge")
                .WithDescription("Purges all saved messages on the server.").Build();
    
            public static readonly CommandPalette Palette = new CommandPalette("msg",
                new[] { Write, Echo, CheckMessages, Purge });
        }
    }
    

    The example above demonstrates the convention that the Petabridge team uses when developing command palettes for the official Petabridge.Cmd modules, and it's a good template to work from.

    Handling Commands

    Once you've defined a set of commands, the next thing you need to do in order to make your commands actionable is to define some actors who can handle and process the commands sent to the server.

    Note

    This information is updated as of Petabridge.Cmd 1.0 - the server-side actor hierarchy for Petabridge.Cmd has since changed and has added support for "Sessions," a feature which allows a single IPbmClient to execute multiple concurrent commands on the server.

    Prior to Petabridge.Cmd 1.0, a single client could only issue one command at a time to a Petabridge.Cmd host so server-side actor hierarchies didn't need to support processing multiple instances of the same request concurrently. That's the new change that developers who created CommandPalettes prior to v1.0 need to support in order to upgrade.

    Petabridge.Cmd Actor Hierarchy

    Every single pbm or IPbmClient client connection to the server, including in-process clients, creates a brand new "client handler," and this is the actor responsible for managing the connection to the client. But for every single CommandPalette registered with the PetabridgeCmd extension on the host we create an actor responsible for handling that palette's commands.

    Ultimately, for every custom CommandPalette you're going to implement you will need to implement a CommandPaletteHandler

    // -----------------------------------------------------------------------
    // <copyright file="CommandPaletteHandler.cs" company="Petabridge, LLC">
    //      Copyright (C) 2017 - 2018 Petabridge, LLC <https://petabridge.com>
    // </copyright>
    // -----------------------------------------------------------------------
    
    using Akka.Actor;
    
    namespace Petabridge.Cmd.Host
    {
        /// <summary>
        ///     Used to register a <see cref="CommandPalette" /> with a given <see cref="IActorRef" />.
        ///     Any <see cref="Command" /> objects that are received by this host which are defined inside
        ///     <see cref="Palette" /> will be routed to actor created via <see cref="HandlerProps" />.
        ///     Any commands defined inside a different <see cref="CommandPalette" /> will be routed to the appropriate
        ///     <see cref="CommandPaletteHandler" /> by the petabridge.cmd.Host.
        /// </summary>
        public abstract class CommandPaletteHandler
        {
            protected CommandPaletteHandler(CommandPalette palette)
            {
                Palette = palette;
            }
    
            /// <summary>
            ///     The set of defined commands handled by <see cref="HandlerProps" />
            /// </summary>
            public CommandPalette Palette { get; }
    
            /// <summary>
            ///     The <see cref="Props" /> of the actor who will handle the messages.
            /// </summary>
            /// <remarks>
            ///     One instance of the <see cref="HandlerProps" /> actor will be created per-connection.
            ///     The actor will be terminated when that connection is closed.
            /// </remarks>
            public abstract Props HandlerProps { get; }
    
            /// <summary>
            ///     Called when registered by the <see cref="PetabridgeCmd" /> plugin.
            /// </summary>
            public virtual void OnRegister(PetabridgeCmd plugin)
            {
            }
        }
    }
    

    This class provides the PetabridgeCmd extension with two key pieces of information:

    1. The Props needed to start the CommandPalette-specific actor and
    2. The CommandPalette itself, which is necessary in order to let the server know that it supports these commands in the first place.

    Lastly, there's also the OnRegister method... This can be used to start other auxiliary actors you might need to support the processing of commands.

    For instance, in the Petabridge.Cmd quick start we have a shared MessageMemorizerActor that each of the individual MsgCommandHandlerActor instances depend on in order to retrieve messages that have been written by other pbm instances:

    public class MsgCommandPaletteHandler : CommandPaletteHandler
    {
        private Props _underlyingProps;
    
        public MsgCommandPaletteHandler()
            : base(MsgCommands.Palette) // registers the command palette with this handler.
        {
        }
    
        public override Props HandlerProps => _underlyingProps;
    
        /*
         * Overriding this method gives us the ability to do things like create the MessageMemorizerActor before HandlerProps gets used
         */
    
        public override void OnRegister(PetabridgeCmd plugin)
        {
            var memorizer = plugin.Sys.ActorOf(Props.Create(() => new MessageMemorizerActor()), "pbm-msg-memorizier");
    
            // will be used to create a new MsgCommandHandlerActor instance per connection
            _underlyingProps = Props.Create(() => new MsgCommandHandlerActor(memorizer));
            base.OnRegister(plugin);
        }
    }
    

    Defining CommandHandlerActor Instances

    The majority of the work that goes into creating custom Petabridge.Cmd Commands is turning the raw Command object into an actionable, usable C# message type with all of the correctly parsed parameters. All of the arguments and the values supplied with those arguments will all be pre-parsed and organized into the Command object itself, but it's still up to you as the end-user to ensure that those values are rendered into useful information.

    To help make things a bit easier, we've defined a base class called a CommandHandlerActor that you can use to automatically validate and subsequently handle any Command instances that you've defined in your CommandPalette.

    CommandHandlerActor is a base class that inherits from the Akka.NET ReceiveActor type, and uses a similar structure:

        Process(MsgCommands.Write.Name, HandleWrite);
        Process(MsgCommands.Echo.Name, HandleEcho);
        Process(MsgCommands.Purge.Name, HandlePurge);
    }
    
    public void HandlePurge(Command purge)
    {
        _messageMemorizer.Tell(MessageMemorizerActor.PurgeMessages.Instance, Sender);
    

    Process(string commandName, Action<Command> handler) is used much in the same way that Receive<T> is in a traditional Akka.NET actor, but in this instance it also performs automatic command validation against the CommandDefinition supplied in your CommandPalette for this actor and additional bits of error-checking.

    The rest of the work you have to carry out in building a custom command consists of scanning through the Command.Arguments list and converting each argument's values into usable bits of data. Here's another example of this from the quick start:

                    x => MsgCommands.Echo.ArgumentsByName["message"].Switch.Contains(x.Item1))?
                .Item2;
    
        Sender.Tell(new CommandResponse(new MessageMemorizerActor.Message(msg, DateTime.UtcNow, Sender.Path.Name).ToString())); // will echo what was written on commandline
    }
    
    public void HandleCheckMessages(Command fetch)
    {
        // check if we have a timeframe for the message specified
    

    All of the arguments inside a Command object are organized as a list of CommandArgument structures, and as you may recall they're entered into the pbm command line using switches such as -s, -a, and so forth. We're scanning through the list of CommandArguments and trying to match an argument we're looking up by name against one or more of those switches.

    When we find the value we're looking for, in a typical CommandHandlerActor you'll translate that into a POCO message that you'll want to forward onto another actor who will do the actual processing of the command. If the command is trivial, however, you can simply send a CommandResponse back to the Sender immediately.

    And this is a key point: all Command messages MUST be responded to with a CommandResponse of some sort. Otherwise the client will indefinitely wait for a response to come back. The Sender of the Command is always the party you need to send a CommandResponse reply to.

    Response Streams

    In some instances, it's helpful to be able to stream back multiple responses in a sequence as part of fulfilling a Command. This is very easy to do with the CommandResponse class, and here's an example:

    Receive<FetchMessages>(f => f.Since == null, f => // all messages
    {
        foreach (var msg in _messages)
            Sender.Tell(new CommandResponse(msg.ToString(), false));
        // by setting final:false we signal to client that more responses are coming
        Sender.Tell(CommandResponse.Empty); // tells the client not to expect any more responses (final == true)
    });
    

    If you send a reply back to the Sender with CommandResponse.Final == false, the pbm or IPbmClient will expect additional CommandResponse messages before this command is finished being handled. At the very end of processing the Command you can send back a CommandResponse.Empty to signal that the stream has terminated.

    For more information on implementing custom commands, please clone and check out the Petabridge.Cmd Quick Start source code from Github.

    Handling Commands with Session Support (1.0 and Above)

    Sessions are a new feature introduced to Petabridge.Cmd in 1.0+ - each unique command that is created by a IPbmClient will create a unique SessionSequencer actor on the server - responsible for coding incoming Commands and all outgoing CommandResponse messages with a long SessionId that correlates the IPbmClient's specific session with the response stream on the server.

    The key to handling all of this well inside a custom CommandPaletteHandler - assume that the Sender of each Command represents a unique session instance. So for instance, if you're building a streaming command (i.e. log tail) that can transmit a stream of responses back to a client, make sure you build your log tail actor to build an internal set of IActorRef subscribers - because there may be multiple instances of the command running at once for a single client.

    Each CommandHandlerActor is unique to a single IPbmClient instance - we don't share actors across multiple clients. But a CommandHandlerActor can be shared across multiple sessions belonging to the same IPbmClient.

    Therefore, you need to make sure that each of your CommandHandlerActors can process multiple concurrent requests from the same client. You don't need to explicitly worry about tracking their long SessionIds - this will be handled automatically by Petabridge.Cmd.Host.

    Back to top Generated by DocFX