🌟 Pending Release - 'Klient' - A multi-tool

Ok, I am about to go on vacation, so I decided to let the cat out of the bag a little early on what I’ve been working on, as well as give a little teaser.

I’ve decided to unzip my lip, as I would like some feedback before I polish it up and release the first beta. (alpha? :thinking: )


I’ve created a new tool(s).

This tool, “Klient” (catchy name eh… :rofl: ), is a plugin manager, that runs locally (or anywhere really).

Klient hosts plugins, and provides a plugin framework. It doesn’t do any real work except pass messages between plugins. It also provides a small tray icon based UI, with some statistics, and hooks for the menu.

image … image


The real action is in the plugins, of which I have created two.

Plugins at launch:

  • Konnector

    • This plugin is based on Azure Relay Hybrid Connections.
      This allows you to send messages securely from Epicor, or any other source, to your on-prem services, and optionally, receive a response back.
      No firewall configuration needed.
  • Konsole

    • This plugin is a remote console, It will receive messages from Konnector and display them on screen. It also has a few other functions.
    • An accompanying Epicor Function Library is included to call this, as well as some cut and paste code to make it easier for advanced or intense scenarios.

Here is a little demo of Konsole:

KonsoleTeaser

Here is the (partial) code that did that on the server.

List<string> list2 = new List<string>()
{
    "JavaScript was made for the masses",
    "It lacked types, and modules, and classes.",
    "But it became quite the giant",
    "Because it ran on server and client",
    "Until it crashed both, despite 100 test passes."
};
        
List<string> list1 = new List<string>()
{
    "The code was written with haste",
    "I had not time for breaks",
    "But what does this do",
    "I really have no clue",
    "Perhaps, I should have comments interlaced."
};


Random rnd = new Random();
foreach(string line in list1)
{
    Color c = Color.FromArgb(255, rnd.Next(175, 255), rnd.Next(175, 255), rnd.Next(175, 255));
    Konsole.WriteLineColor(line, c);
    Delay(1000).Wait();
}

Delay(500).Wait();
Konsole.Clear();
Delay(500).Wait();


foreach(string line in list2)
{
    Color c = Color.FromArgb(255, rnd.Next(175, 255), rnd.Next(175, 255), rnd.Next(175, 255));
    Konsole.WriteLineColor(line, c);
    Delay(1000).Wait();
}

Delay(500).Wait();
Konsole.Clear();
Delay(500).Wait();

var abc = Db.ABCCode.Select(x => new
{
    x.ABCCode1,
    x.CalcPcnt,
    x.CalcQty,
    x.CalcValue,
    x.CountFreq
}).ToList();

Konsole.WriteLineColor(abc, Color.YellowGreen);

Delay(1000).Wait();
Konsole.Clear();
Delay(250).Wait();

Konsole.WriteLineColor(Konsole.YesNo("I'd like to know.", "Would you like to play with me?", 0), Color.Lime);

Delay(1000).Wait();
Konsole.Clear();
Delay(250).Wait();
:star2

Konsole.DumpAsFileText(list1, false);
Konsole.DumpAsFileText(list1, true);
Konsole.WriteLine(Konsole.DumpAsFileText(list1, true));

But wait, there’s more: billy mayes GIF

The entire thing will be published under the MIT license, the code & the specifications, and you can easily write your own plugins. Or if you want to throw a few bucks my way, I’ll certainly write you one lol. (Or some other programmer of your choice.)


Anyway… please discuss.

11 Likes

What’s a real world use case for this? What drove you to develop this?

1 Like

Ok, I will answer this first, which will lead in to the next.

Damn it, hold on a minute. My attention is needed elsewhere.

1 Like

Slap It GIF by getflexseal

Ok, let’s try this again…

Ok, I will answer this first, which will lead in to the next.


Ok, I, like I assume many developers, have a frustration at being able to not get immediate feedback from some test code, so originally all this was supposed to be was a way to do that. I was just going to build a polling solution to get some damn messages out.

I really just wanted to write Console.WriteLine("I got here!"); and see it somewhere.

So I started writing that and decided that polling was bs, so I started looking for something simple to do push notifications. I then found Azure Relay Hybrid Connections.

I wrote up a little test harness, and sent it some data. Bam bam yes GIF, message from Epicor on my desktop. No fancy configuration needed.

Was just gonna code up a quick example and share it for those that wanted it.

Then a day or two passed and I spent a few minutes on the throne… :toilet:.

And an idea struck… Well if I can send messages, and get responses, I could do pretty much whatever I wanted.

So I did.

I made a few extra functions to dump files, Make a few beeps, etc.

Then came the idea for a plugin system, so I implemented that. Just to have a standard way of passing messages, and a standard way of implementation.


Then I decided to share it with @Mark_Wonsil , as I thought it would need an API, and thought he might like to help. He helped a tad with the API, but it was really the discussion that was more helpful.

He was going on and on about different patterns and communication technologies, some that I am familiar with, and some I am not… hmmmm :thinking:

So I decided to make almost everything a plugin, and I also added a simple UI for interaction.

Now you can write your own plugins for input, or output, or utility, or you can load it with one monolith if you want. I decided to furnish two initially. I think these are the most immediately useful as both practical, and as implementation examples.


Now on to question 1

Ok this has two answers, as there are two things currently implemented.

Answer #1 is simple, you can get some damn output locally.

Answer #2 is complex, as you can pretty much do anything you want with what is provided.

Imagine say, you wanted to call a local (on prem) API, you could call Konnector from Epicor via a standard message format, and write your own plugin that would forward that to the local API, and return that message to Epicor.

Here is another. I’m sure some of you are familiar with my little project that put my physical access control management into Epicor. Previously, it was only management. A project of convenience since I had a platform and an app. Now I can open doors at my plant right from Epicor.

You can have Epicor request and send items from anywhere this is running. This doesn’t have to be a client tool, it can be a server tool as well. It’s generic.

6 Likes

Epicor knocking on @klincecum’s door in a few minutes…

Mike D Money GIF by Beastie Boys

@klincecum using his door button he made to not admit…

push the button yes GIF by Heute-Show

6 Likes

Now remember, let’s not get confused about what this is.

You have 3 separate things, A plugin host, and two plugins.

I can imagine someone writing a Rest API plugin that IS exposed, and they could get messages in from that. Or they could write a ServiceBus plugin, or SignalR, or MassTransit, MQTT, etc.

Wanna grab files from on-prem? Sure.

Wanna open an internal program from a Kinetic screen and get data back? No problem.

At a minimum, having the ability to log messages, in realtime, from a Function would be like the Swiss Flag. :switzerland:

(A Huge Plus)

9 Likes

Ah I see, pretty cool. Always good to have more tools available.

On the topic of real time logging, I’ve used Sentry. This SAAS product came with .Net libraries we wired into our custom Epicor code. Not sure if cloud allows us to use external assemblies, but it was sweeet. Logged full stack traces, you could add bread crumbs to log EVERYTHING.

But this is free so thanks for developing this @klincecum !

1 Like

Or is there a pay wall after 3 uses :thinking:

3 Likes

Here are some of the things I’ve currently implemented in Konsole

Clear → Clears the screen
Write (With or Without Color) → writes text or serializable object
WriteLine (With or Without Color) → writes text or serializable object
Echo → Returns your text or serializable object
DumpKlientMessage → Debugging really, dumps a KlientMessage to the console
YesNo → Pops up a messagebox (It’s kinda hokey, has a timeout)
YesNoCancel → Pops up a messagebox (It’s kinda hokey, has a timeout)
DumpAsFileText → Dumps text or serializable object to a file
DumpAsFileTextWithName → Dumps text or serializable object to a file
DumpAsFileBinary → Dumps Base64 message data as binary to a file
DumpAsFileBinaryWithName → Dumps Base64 message data as binary to a file

Got some other things I want to implement in this plugin, as well as some stuff that will likely be a standalone plugin(s).

In particular for the Konsole plugin, what kind of functionality would be helpful?

Nahh, it’s free. (Well Azure Relay isn’t, but for most use cases it might as well be.)

Now if you wanna send me mailbox money, I won’t complain lol :rofl:

1 Like

Almost forgot!

Konsole.Beep(2000, 2000);

Annoy your friends and co-workers ! :tada:

2 Likes

Does that mean now we can close the dubugging through a straw thrrad? :grin:

Thanks @klincecum … and you input @Mark_Wonsil

Hot Ones I Wont Do It GIF by First We Feast

1 Like

Speaking of @Hally , the bartender man…

We can add a few more tricks to the bartender toolkit with this.

1 Like

Here is a quick example of what a plugin looks like in code.

I whipped up a (simple) Bartender plugin, which handles taking a json file input, and drops it to a local folder for processing.

This is the code:

using KlientPluginBase;
using ExtensionMethods;
using Newtonsoft.Json.Linq;
using System.Net;

namespace KP_BartenderPlugin
{

    class KP_BartenderPluginSettings
    {
        public static string SettingsFileName() => "KP_BartenderSettings.json";
        public static string SettingsSectionName() => "Bartender";
        public string FileDropFolder { get; set; }
    }

    public class KP_Bartender : IKlientPlugin
    {
        public KlientPluginInfo Info
        {
            get => new KlientPluginInfo()
            {
                ID = Guid.Parse("8de35c3d-0442-4d26-ba99-357d03c5497f"),
                Name = "Bartender",
                Description = "This class does Bartender Stuff...",
                PluginType = EKLientPluginType.Normal,
                Author = "Kevin Lincecum",
                Contact = "moc.smlifesm@mucecnilk backwards lol",
                Company = "Mid South Extrusion",
                CompanyLink = "https://msefilms.com",
                Documentation = "",
                Date = DateOnly.ParseExact("08/02/2024", "MM/dd/yyyy"),
                License = Licensing.GetMITLicense(),
                ExtraMetaData = ""
            };
        }

        KlientHostObject KlientHostObject { get; }

        KP_BartenderPluginSettings? PluginSettings;

        public KP_Bartender(KlientHostObject klientHostObject, string thisPluginPath)
        {
            KlientHostObject = klientHostObject;

            string thisPluginFolder = Path.GetDirectoryName(thisPluginPath);
            string settingsPath     = Path.Combine(thisPluginFolder, KP_BartenderPluginSettings.SettingsFileName());

            PluginSettings = KlientHelpers.LoadSettings<KP_BartenderPluginSettings>(settingsPath, KP_BartenderPluginSettings.SettingsSectionName());
        }

        public List<string> HandlesMessageTypes { get => new List<string>(){ "Bartender" }; }

        public async Task<Result> HeartbeatCheck() => Result.Success();

        public async Task<ResultHTTP<KlientReturn>> ProcessMessage(KlientMessage klientMessage)
        {
            try
            {
                if(klientMessage.MessageSubtype == "FileDropJson")
                {
                    string? msg = klientMessage.Message is string ? (string)klientMessage.Message : ((JToken)klientMessage.Message).ToString();

                    string filename = $"BTFile_{DateTime.Now.ToString("yyyyMMdd_HHmmssfff")}.json";

                    File.WriteAllText(Path.Combine(PluginSettings.FileDropFolder, filename), msg);

                    return ResultHTTP.Success(KlientReturn.Create($"Bartender file \"{filename}\" written to \"{PluginSettings.FileDropFolder}\""));
                }

                return ResultHTTP.Failure<KlientReturn>("No message handler found for this subtype.", HttpStatusCode.BadRequest);    
            }
            catch (Exception ex)
            {
                return ResultHTTP.Failure<KlientReturn>("Unknown Error", HttpStatusCode.InternalServerError, ex.ToExceptionList());    
            }
        }

    }
}

And here is an example of it working (via postman)

BTQuickDump

2 Likes

My wife said I couldn’t code on vacation, but I can still discuss things lol.

4 Likes

My wife said I couldn’t code on vacation, but I can still discuss things lol.

That’s called Management. Right @josecgomez ??

8 Likes

I think I’m close to Alpha stage. It runs better than alpha but I’m still playing with the architecture a bit, so I’m gonna call it alpha.

Going through and adding some comments (too much for normal) so the early players can get their heads around it, and maybe offer some improvements and critiques.

Interface will be a bit simple, trying to decide if I expand it or not.

Got a crap ton of documenting to do, and a bit of work on the usage side (Epicor).

I’ve lost my damn mind.

5 Likes

Status update:

Klient

  • Documentation → 95% (There will be things I forgot, or will add.)
  • Main Interface → 100% (Simple for now)
  • Code → 95% (Needs review, and some cleanup)

Konsole:

  • Documentation → 95% (There will be things I forgot, or will add.)
  • Interface → 80% (Got some crud to work out.)
  • Code → 90% (Needs review, and some cleanup)

Konnector:

  • Documentation → 95% (There will be things I forgot, or will add.)
  • Code → 95% (Needs review, and some cleanup)

Epicor Functions and Helpers

  • Documentation → 0%
  • Code → 50%

Sleep 0%

Music Video Concert GIF by Three Days Grace

2 Likes