Rosi.Runtime

Package Description


Keywords
License
0BSD
Install
Install-Package Rosi.Runtime -Version 1.0.2

Documentation

Releases Nuget Unit Tests

Rosi

The power of the .NET Core Framework in a single executable

Rosi is an easy way to run your C# scripts everywhere, especially on Linux. No need to mess around with your package management. Rosi is just a single executable file that you need to run all your C# scripts.

Depending on your needs, you implement your sync/asnyc interface and you're ready to run your first script.

public class Rosi : IRosi
{
    public int Run(IRuntime runtime)
    {
        // do something
    }
}
public class RosiAsync : IAsyncRosi
{
    public async Task<int> Run(IRuntime runtime)
    {
        // do something async
    }
}

Side note: We use SaltStack to manage our servers and the servers from our customers. It is a great and simple way to automate the management of our infrastructure. And therefore we have some rules, two of them are: We don't configure anything by hand, everything is done by using salt and we don't touch the package management of a system and add any external sources, if it's not really required. Using salt only is often not enough, we have to write scripts here and there to glue everything together. And we usually used Bash scripts. But while you can writing complex scripts in bash, you usually don't want to do that.

We thought about using a real programming language with good debugging support that runs on all important operating systems and we decided to use C# (seriously, it's a great language, static typed languages ftw!). But installing the .NET Core runtime on every system is a little overkill for running small scripts and didn't play nice with our own rules. That's the reason we build Rosi.

Install

Linux

Terminal

Download, unzip and test the latest Rosi release.

> wget https://github.com/MarkoBL/Rosi/releases/latest/download/Rosi_Linux_x64.zip
or
> curl -LO https://github.com/MarkoBL/Rosi/releases/latest/download/Rosi_Linux_x64.zip
> unzip Rosi_Linux_x64.zip
> ./rosi --version

Make it available for every user through the terminal by putting it into /usr/local/rosi and put a symlink into /usr/local/bin

> sudo mkdir /usr/local/rosi
> sudo mv rosi /usr/local/rosi/
> sudo ln -s /usr/local/rosi/rosi /usr/local/bin/rosi
> rosi --version

SaltStack

This salt state installs or updates to the latest Rosi version.

rosi.sls

{% set rosipath = "/usr/local/rosi" %}
{% set install = true %}

{% set exists = salt.file.directory_exists(rosipath) %}
{% if exists %}
 {% set result = salt.cmd.retcode(rosipath + "/rosi --rosicheckupdate") %}
 {% if result <= 0 %}
  {% set install = false %}
 {% endif %}
{% endif %}

{% if install %}
install_rosi:
  archive.extracted:
    - name: {{ rosipath }}
    - source: https://github.com/MarkoBL/Rosi/releases/latest/download/Rosi_Linux_Multi_x64.zip
    - skip_verify: true
    - overwrite: true
    - enforce_toplevel: false
    - clean: true

symlink_rosi:
  file.symlink:
    - name: /usr/local/bin/rosi
    - target: {{ rosipath }}/rosi
    - requires:
      - archive: install_rosi

{% endif %}

macOS

Terminal

Download, unzip and test the latest Rosi release.

> wget https://github.com/MarkoBL/Rosi/releases/latest/download/Rosi_MacOs_x64.zip
or
> curl -LO https://github.com/MarkoBL/Rosi/releases/latest/download/Rosi_MacOs_x64.zip
> unzip Rosi_Linux_x64.zip
> ./rosi --version

Make it available for every user through the terminal by putting it into /usr/local/rosi and put a symlink into /usr/local/bin

> mkdir /usr/local/rosi
> mv rosi /usr/local/bin/
> ln -s /usr/local/rosi/rosi /usr/local/bin/rosi
> rosi --version

App

Download the latest Rosi macOS App, unzip it and move it to the Applications folder. All .rosifiles will open in the macOS App, when you double click them. To make Rosi available through the terminal, put a symlink into /usr/local/bin.

> ln -s /Applications/Rosi.app/Contents/Resources/rosi.bundle/rosi /usr/local/bin/rosi

Windows

Download the latest Rosi Windows Version and unzip it. You will have the rosi.exe in the current directory. Move the rosi.exe to a location of your choice and add to your PATH environment variable to access it from the command line.

A more simple solution would be to move the rosi.exe into your Windows directory. Move it to C:\Windows and you're done. You can use it from the command line now.

Associate the .rosi file extension with Rosi. Look at this tutorial and select the rosi.exe in the last step.

Running

Command Line

(Linux/macOS/Windows) You can run Rosi scripts from the command line.

helloworld.rosi

public class HelloWorld : IRosi
{
    public int Run(IRuntime runtime)
    {
        Console.WriteLine("Hello World!");
        return 0;
    }
}
> rosi helloworld.rosi
Hello World!

Shebang

(Linux/macOS) Rosi scripts have shebang support. But a shebang is invalid C# code and would break debugging. Therefore, we use a workaround for debugging scripts. We create two files, a script.rosi and a script.cs file. The script script.rosi contains only the shebang and other directives and includes script.cs. See the Shebang Exmaple.

script.rosi

#!/usr/local/bin/rosi 
// include: script.cs

script.cs

public class Shebang : IRosi
{
    public int Run(IRuntime runtime)
    {
        Console.WriteLine("Hello World!");
        return 0;
    }
}
> chmod +x script.rosi
> ./script.rosi
Hello World!

File Association

Rosi scripts have the file extension .cs or .rosi and you can associate the .rosi file extension with Rosi. Whenever a user double clicks on a .rosi file, the Rosi executes the script. Do not confuse your users and name only your main script .rosi or change the script directory to a subfolder.

(Windows) You have to associate the file extension .rosi manually with Rosi. Look at this tutorial and select the rosi.exe in the last step.

Windows File Association

(macOS) If you download the Rosi macOS App, all files with a .rosi extension will automatically open with it.

macOS File Association

Debugging

Every Rosi script is a valid C# file. And we didn't want to reinvent the wheel as there is already a huge eco system for C# avaiable. Therefore, you can debug your Rosi script like a normal C# program with your favorite tools and IDEs. See the provided Debugging Project.

Basically, you create a new C# console project in your script directory, add the Rosi.Runtime NuGet package and set a valid Main method.

Create a new project in your script directory with your IDE of choice or use the console:

> cd YourScriptDirectory // change to your script directory
> dotnet new console // create a new console project
> dotnet add package Rosi.Runtime // add the Rosi.Runtime Nuget package

Now, two options are available. A new project usually contains a Program.cs with a Main() method. You can now edit this file or delete it and add the Main() method to your main script. We usually prefer the second method.

You can now set your breakpoints and start debugging your script with your favorite IDE.

Here's an example of a script with a Main() method for debugging:

class Script : IRosi
{
    public int Run(IRuntime runtime)
    {
        System.Console.WriteLine("Hello World!");
        return 0;
    }

    static async System.Threading.Tasks.Task<int> Main()
    {
        return await new Rosi.Runtime(typeof(Script)).RunAsync();
    }
}

Directives

// compile:

The compile directive compiles a script into an assembly. The Rosi.Runtime will automatically reference and include the compiled assembliy. The compiling happens before the actual script that contains the directive will be compiled. If it can't find the specified file, it will add the .rosi extension and will try it again. If this fails too, it will add the .cs file extension. If all tries fail, the runtime will exit.

A.cs

class A
{
    public int Compiled()
    {
        return 0;
    }
}

B.cs

// compile: A // this will actually resolve to A.cs
class B : IRosi
{
    public int Run(IRuntime runtime)
    {
        return new A().Compiled();
    }
}

// postcompile:

This acts like the compile directive, but the script will be compiled into an assembly, AFTER the script that contains it is compiled.

// include:

The include directive actually includes the content of the script file into the current script. This is done by splitting the script into a header (using xy, etc.) and body (the actual code) part and insert it at the right locations in the current script. This is usefull, if you have cycling references in classes, but still want to split the classes into different files.

class A
{
    readonly B _b;
    public A(B b)
    {
        _b = b;
    }

    public int Compiled()
    {
        _b.Test();
        return 0;
    }
}
// include: A
class B : IRosi
{
    public void Test()
    {
        Console.WriteLine("Test");
    }
    public int Run(IRuntime runtime)
    {
        return new A(this).Compiled();
    }
}

A side note: Once we had a large script of entity definitions (a few thousand lines), with a lot of circling references (entity A was referencing entity B and vice versa). Therefore, we couldn't break them down into different assemblies. This wasn't a big problem at all for us, as all this code didn't contain much logic. But it was a huge problem for Visual Studio, as it wasn't able to process the script in a reasonalbe time. Sometimes it took a few secondes before it accepted a key strokes or updated intellisense. After a while (and lots of swearing), we decided to implement the include directive. We put all the entities in separate script files and added an empty script that simply includes all of them. Problem solved.

// assembly:

Loads an assembly into the Rosi.Runtime.

// assembly: Newtonsoft.Json.dll
using Newtonsoft.Json;
class Json : IRosi
{
    public int Run(IRuntime runtime)
    {
        Console.WriteLine(JsonConvert.SerializeObject(new TestObject()));
        return 0;
    }
}

// set or debugset:

Sets internal or custom options. It is also possible to specify options via arguments. Debugset works only while debugging a script, otherwise it is ignored.

A.cs

// set: config.test Hello Config!
// set: argument.test Will be overriden by the command argument
// setdebug: runtime.logtofile 1
class A : IRosi
{
    public int Run(IRuntime runtime)
    {
        Console.WriteLine(runtime.Config.Get("config.test", string.Empty));
        Console.WriteLine(runtime.Config.Get("argument.test", string.Empty));
        return 0;
    }
}
> rosi A.cs -argument.test "Hello Argument!"
Hello Config!
Hello Argument!

Options

Internal options for the Rosi.Runtime. You can set the options via the set/debugset directive or as a argument when running a script.

runtime.scriptath

Sets a separate directory for the scripts, default is the location of the main script.

runtime.assemblypath

Sets a separate directory for assemblies, default is the location of the main script.

runtime.usecachedassemblies

Compiled assemblies will be chached and resued every run, as long as they scripts don't change. Default is true.

runtime.logtoconsole

Log output will be redirected to the console, default is true.

runtime.logtofile

Log output will be redirected to a file, default is false

runtime.logfilename

The name or path of the log file, default is log.txt.

runtime.logfileappend

Appends the log output to the current log file, otherwise the log file will be overwritten every run. Default is false.

runtime.logscript

Show the actual script in the log ouput, if the compilation fails.

scriban.path

Sets a separate directory for scriban templates, default is the location of the main script.

rosi.waitforexit

You will have to press a key after the exection of the script. This is useful on Windows, as the console window closes after the execution of the script. Default is false.

Scriban Templates

Scriban is a fast and powerful templating engine. If you don't know scriban, head over the GitHub Repository to learn more about it. It is easy to use and really awesome.

As linux admin, you probably know, that there are text based configuration files everywhere. And as we often need to create configuration files, we embedded scriban into the Rosi.Runtime. There is an example available: Scriban Exmaple.

Host.cs

using System.Net;

public class Host
{
    public string Name;
    public IPAddress Address;
}

Scriban.cs

// compile: Host
using System;
using System.Collections.Generic;
using System.Net;

public class Scriban : IRosi
{
    public int Run(IRuntime runtime)
    {
        var scriban = runtime.Scriban;

        scriban.ImportObject("Hosts", new List<Host> {
            new Host { Name = "host1", Address = IPAddress.Parse("10.0.0.1") },
            new Host { Name = "host2", Address = IPAddress.Parse("10.0.0.2") }
        });

        var result = scriban.Render("hosts", "testhostname");
        if (result.Valid)
        {
            Console.WriteLine(result.Output);
            return 0;
        }

        return 1;
    }
}

hosts.scriban

{{-

$hostname = $1

$valid = (object.size $hostname) > 0

setfilename "/etc/hosts"
setvalid $valid "Hostname is missing"

if !$valid
 ret
end

-}}
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback

127.0.1.1 {{ $hostname }}

# The following lines are desirable for IPv6 capable hosts
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

{{ for $host in Hosts -}}
{{ $host.Address }} {{ $host.Name }}
{{ end -}}
> rosi Scriban.cs
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback

127.0.1.1 testhostname

# The following lines are desirable for IPv6 capable hosts
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

10.0.0.1 host1
10.0.0.2 host2

Side note: We use SaltStack and Rosi to manage our and our customers clients and servers. We generate the required configurations on the local salt master with Rosi and deploy them afterwards using SaltStack. We have more control over the output and it is easier to spot and debug errors.

In Memory of Rosi

A lovely wife, caring granda and best mom in the world.