Skip to content

LiaScript/CodeRunner

Repository files navigation

CodeRunner

                     --{{0}}--

This project allows you to run a code-running server, based on Python, that can compile and execute code and communicate via websockets. Thus, if you want to develop some interactive online courses, this is probably a good way to start. This README is also a self-contained LiaScript template, that defines some basic macros, which can be used to make your Markdown code-snippets executable.

Try it on LiaScript:

https://liascript.github.io/course/?https://github.com/liascript/CodeRunner

See the project on Github:

https://github.com/liascript/CodeRunner

                    --{{1}}--

There are three ways to use this template. The easiest way is to use the import statement and the URL of the raw text-file of the master branch or any other branch or version. But you can also copy the required functionality directly into the header of your Markdown document, see therefor the last slide. And of course, you could also clone this project and change it, as you wish.

                        {{1}}
  1. Load the macros via

    import: https://raw.githubusercontent.com/LiaScript/CodeRunner/master/README.md

  2. Copy the definitions into your Project

  3. Clone this repository on GitHub

@LIA.eval

You only have to attach the command @LIA.eval to your code-block or project and pass three parameters.

  1. The first, is a list of filenames, the number of sequential code-blocks defines the naming order.
  2. Then pass the command how your code should be compiled
  3. And as the last part, how to execute your code.
```c
#include <stdio.h>

int main (void){
  printf ("Hello, world \n");

	return 0;
}
```
@LIA.eval(`["main.c"]`, `gcc -Wall main.c -o a.out`, `./a.out`)

In most cases it is sufficient to have only one file. For this purpose we also provide shortcuts, such that the complex functionality above can be simplified with only the macro @LIA.c. These shortcuts always assume one file only.

@LIA.c: C

#include <stdio.h>

int main (void){
	int i = 0;
	int max = 0;

	printf("How many hellos: ");
	scanf("%d",&max);

  for(i=0; i<max; i++)
    printf ("Hello, world %d!\n", i);

	return 0;
}

@LIA.eval(["main.c"], gcc -Wall main.c -o a.out, ./a.out)

@LIA.clojure: Clojure

(ns clojure.examples.hello
   (:gen-class))
(defn hello-world []
   (println "Hello World"))
(hello-world)

@LIA.clojure

@LIA.cpp: C++

#include <iostream>
using namespace std;

int main (){
	int i = 0;
	int max = 0;

	cout << "How many hellos: ";
	cin >> max;

  for(i=0; i<max; i++)
    cout << "Hello, world " << i << endl;

	return 0;
}

@LIA.eval(["main.cpp"], g++ main.cpp -o a.out, ./a.out)

@LIA.dotnet: C# dotnet

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

@LIA.eval(["Program.cs", "project.csproj"], dotnet build -nologo, dotnet run)


The macro @LIA.dotnet already includes a default project file.

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

@LIA.dotnet

F#

// See https://aka.ms/new-console-template for more information
printfn "Hello from F#"
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

</Project>

@LIA.eval(["Program.fs", "project.fsproj"], dotnet build -nologo, dotnet run)


// See https://aka.ms/new-console-template for more information
printfn "Hello from F#"

@LIA.dotnetFsharp

@LIA.ghc: Haskell

main = putStrLn "hello world"

@LIA.eval(["main.hs"], ghc main.hs -o main, ./main)

@LIA.go: Go

package main

import "fmt"

func main() {
  fmt.Println("hello world")
}

@LIA.eval(["main.go"], go build main.go, ./main)

@LIA.java: Java

import java.io.*;
class Demo {
public static void main(String args[])
throws IOException
{
  // create a BufferedReader using System.in
  BufferedReader obj = new BufferedReader(new InputStreamReader(System.in));
   String str;

 System.out.println("Enter lines of text.");
 System.out.println("Enter 'stop' to quit.");
   do {

    str = obj.readLine();
    System.err.println(str);
  }   while(!str.equals("stop"));
}
}

@LIA.eval(["Demo.java"], javac Demo.java, java Demo)

The short-cut for java requires a special parameter, which is the name of the class, such that this can be substituted within filenames and commands.

import java.io.*;
class Demo {
public static void main(String args[])
throws IOException
{
  // create a BufferedReader using System.in
  BufferedReader obj = new BufferedReader(new InputStreamReader(System.in));
   String str;

 System.out.println("Enter lines of text.");
 System.out.println("Enter 'stop' to quit.");
   do {

    str = obj.readLine();
    System.err.println(str);
  }   while(!str.equals("stop"));
}
}

@LIA.java(Demo)

@LIA.julia: Julia

# function to calculate the volume of a sphere
function sphere_vol(r)
    # julia allows Unicode names (in UTF-8 encoding)
    # so either "pi" or the symbol π can be used
    return 4/3*pi*r^3
end

# functions can also be defined more succinctly
quadratic(a, sqr_term, b) = (-b + sqr_term) / 2a

# calculates x for 0 = a*x^2+b*x+c, arguments types can be defined in function definitions
function quadratic2(a::Float64, b::Float64, c::Float64)
    # unlike other languages 2a is equivalent to 2*a
    # a^2 is used instead of a**2 or pow(a,2)
    sqr_term = sqrt(b^2-4a*c)
    r1 = quadratic(a, sqr_term, b)
    r2 = quadratic(a, -sqr_term, b)
    # multiple values can be returned from a function using tuples
    # if the return keyword is omitted, the last term is returned
    r1, r2
end

vol = sphere_vol(3)
# @printf allows number formatting but does not automatically append the \n to statements, see below
using Printf
@printf "volume = %0.3f\n" vol 
#> volume = 113.097

quad1, quad2 = quadratic2(2.0, -2.0, -12.0)
println("result 1: ", quad1)
#> result 1: 3.0
println("result 2: ", quad2)
#> result 2: -2.0

@LIA.julia

@LIA.mono: C# mono

/*
 * C# Program to Check whether the Entered Number is Even or Odd
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace check1
{
    class Program
    {
        static void Main(string[] args)
        {
            int i;
            Console.Write("Enter a Number : ");
            i = int.Parse(Console.ReadLine());
            if (i % 2 == 0)
            {
                Console.Write("Entered Number is an Even Number");
            }
            else
            {
                Console.Write("Entered Number is an Odd Number");
            }
        }
    }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

@LIA.nasm: Assembly

; ----------------------------------------------------------------------------------------
; Writes "Hello, World" to the console using only system calls. Runs on 64-bit Linux only.
; To assemble and run:
;
;     nasm -felf64 main.asm && ld main.o && ./a.out
; ----------------------------------------------------------------------------------------

          global    _start

          section   .text
_start:   mov       rax, 1                  ; system call for write
          mov       rdi, 1                  ; file handle 1 is stdout
          mov       rsi, message            ; address of string to output
          mov       rdx, 13                 ; number of bytes
          syscall                           ; invoke operating system to do the write
          mov       rax, 60                 ; system call for exit
          xor       rdi, rdi                ; exit code 0
          syscall                           ; invoke operating system to exit

          section   .data
message:  db        "Hello, World", 10      ; note the newline at the end

@LIA.eval(["main.asm"], nasm -felf64 main.asm && ld main.o, ./a.out)

@LIA.nim: Nim

echo "Hello World"

@LIA.eval(["main.nim"], nim c main.nim, ./main)

@LIA.python2: Python2

for i in range(10):
  print "Hallo Welt", i

@LIA.eval(["main.py"], python2.7 -m compileall ., python2.7 main.pyc)

@LIA.python3: Python3

for i in range(10):
  print("Hallo Welt", i)

@LIA.eval(["main.py"], none, python3 main.py, *.py)

A,B,C
0,0.1,3
1,0.3,5
2,0.4,2
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv('data.csv', header = 0)  
df.plot.scatter(x='A', y='B')
plt.savefig('temp.png')

@LIA.eval(["data.csv", "main.py"], none, python3 main.py, *)

@LIA.r: R

print("Hello World")

@LIA.eval(["main.R"], none, Rscript main.R)

library(ggplot2)

# Use stdout as per normal...
print("Hello, world!")

# Use plots...
png(file="out1.png")
plot(cars)

# Even ggplot!
png(file="out2.png")
qplot(wt, mpg, data = mtcars, colour = factor(cyl))

@LIA.r

@LIA.rust: Rust

fn main() {
  println!("Hello World!");
}

@LIA.rust

@LIA.zig: Zig

const std = @import("std");

pub fn main() void {
    std.io.getStdOut().writeAll(
        "Hello World!",
    ) catch unreachable;
}

@LIA.zig

@LIA.v: v

println("Hello World")

@LIA.v

@LIA.perl: Perl

print "Enter your name: ";
$name=<STDIN>;
print "Hello, ${name} ... you will soon be a Perl addict!\n";

@LIA.perl

@LIA.ruby: Ruby

class HelloWorld
   def initialize(name)
      @name = name.capitalize
   end
   def sayHi
      puts "Hello #{@name}!"
   end
end

hello = HelloWorld.new("World")
hello.sayHi

@LIA.ruby

Ada

with Ada.Text_IO; use Ada.Text_IO;
procedure Main is
begin
   Put_Line ("Hello WORLD!");
end Main;

@LIA.eval(["main.ada"], gnatmake main.ada, ./main)

@LIA.evalWithDebug

This does basically the same as @LIA.eval, but it will add additional Debug-information about the CodeRunner status to the console.

#include <stdio.h>

int main (void){
	int i = 0;
	int max = 0;

	printf("How many hellos: ");
	scanf("%d",&max);

  for(i=0; i<max; i++)
    printf ("Hello, world %d!\n", i);

	return 0;
}

@LIA.evalWithDebug(["main.c"], gcc -Wall main.c -o a.out, ./a.out)

Deploying to Heroku

If you deploy this to heroku, as we do, keep in mind, that the free service will be shut down, if no one uses it for 30 minutes, it takes round about 30 sec. to resurrect.

  1. Install the Heroku-CLI

  2. Create a new Heroku project

    1. Login to Heroku: heroku login (Don't use sudo or it will not work!)
    2. Create the project: heroku create [app_name]
  3. Login to Heroku container: heroku container:login (It's important that you have docker installed before executing this command and make sure that your user is added to the docker group.)

  4. Build the docker container and upload it to heroku:

    docker build . -t web

    heroku container:push web -a app_name

  5. Release the docker container: heroku container:release web -a app_name

Your project url is now app_name.herokuapp.com. (Or the auto-generated one when you haven't supplied an app name.)

If you deploy your own server, you have to change the websocket-url in the main header (main HTML comment of your Markdown document) from wss://liarunner.herokuapp.com/socket to wss://*******.herokuapp.com/socket ... what ever the name of your app is ...

Implementation

                          --{{0}}--

If you want to minimize loading effort in your LiaScript project, you can also copy this code and paste it into your main comment header, see the code in the raw file of this document.

{{1}} https://raw.githubusercontent.com/liaScript/CodeRunner/master/README.md

onload
window.CodeRunner = {
    ws: undefined,
    handler: {},

    init(url) {
        this.ws = new WebSocket(url);
        const self = this
        this.ws.onopen = function () {
            self.log("connections established");
            setInterval(function() {
                self.ws.send("ping")
            }, 15000);
        }
        this.ws.onmessage = function (e) {
            // e.data contains received string.

            let data
            try {
                data = JSON.parse(e.data)
            } catch (e) {
                self.warn("received message could not be handled =>", e.data)
            }
            if (data) {
                self.handler[data.uid](data)
            }
        }
        this.ws.onclose = function () {
            self.warn("connection closed")
        }
        this.ws.onerror = function (e) {
            self.warn("an error has occurred => ", e)
        }
    },
    log(...args) {
        console.log("CodeRunner:", ...args)
    },
    warn(...args) {
        console.warn("CodeRunner:", ...args)
    },
    handle(uid, callback) {
        this.handler[uid] = callback
    },
    send(uid, message) {
        message.uid = uid
        this.ws.send(JSON.stringify(message))
    }
}

window.CodeRunner.init("wss://coderunner.informatik.tu-freiberg.de/")
//window.CodeRunner.init("ws://127.0.0.1:8000/")

@end


@LIA.c:       @LIA.eval(`["main.c"]`, `gcc -Wall main.c -o a.out`, `./a.out`)
@LIA.clojure: @LIA.eval(`["main.clj"]`, `none`, `clojure -M main.clj`)
@LIA.cpp:     @LIA.eval(`["main.cpp"]`, `g++ main.cpp -o a.out`, `./a.out`)
@LIA.go:      @LIA.eval(`["main.go"]`, `go build main.go`, `./main`)
@LIA.haskell: @LIA.eval(`["main.hs"]`, `ghc main.hs -o main`, `./main`)
@LIA.java:    @LIA.eval(`["@0.java"]`, `javac @0.java`, `java @0`)
@LIA.julia:   @LIA.eval(`["main.jl"]`, `none`, `julia main.jl`)
@LIA.mono:    @LIA.eval(`["main.cs"]`, `mcs main.cs`, `mono main.exe`)
@LIA.nasm:    @LIA.eval(`["main.asm"]`, `nasm -felf64 main.asm && ld main.o`, `./a.out`)
@LIA.python:  @LIA.python3
@LIA.python2: @LIA.eval(`["main.py"]`, `python2.7 -m compileall .`, `python2.7 main.pyc`)
@LIA.python3: @LIA.eval(`["main.py"]`, `none`, `python3 main.py`)
@LIA.python3: @LIA.eval(`["main.pl"]`, `none`, `perl main.pl`)
@LIA.r:       @LIA.eval(`["main.R"]`, `none`, `Rscript main.R`)
@LIA.rust:    @LIA.eval(`["main.rs"]`, `rustc main.rs`, `./main`)
@LIA.v:       @LIA.eval(`["main.v"]`, `v main.v`, `./main`)
@LIA.zig:     @LIA.eval(`["main.zig"]`, `zig build-exe ./main.zig -O ReleaseSmall`, `./main`)

@LIA.dotnet
```xml    -project.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
```
@LIA.eval(`["Program.cs","project.csproj"]`, `dotnet build -nologo`, `dotnet run`)
@end

@LIA.dotnetFsharp
```xml    -project.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>
</Project>
```
@LIA.eval(`["Program.fs", "project.fsproj"]`, `dotnet build -nologo`, `dotnet run`)
@end

@LIA.eval:  @LIA.eval_(false,`@0`,@1,@2)

@LIA.evalWithDebug: @LIA.eval_(true,`@0`,@1,@2)

@LIA.eval_
<script>
function random(len=16) {
    let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let str = '';
    for (let i = 0; i < len; i++) {
        str += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return str;
}

const uid = random()
var order = @1
var files = []

if (order[0])
  files.push([order[0], `@'input(0)`])
if (order[1])
  files.push([order[1], `@'input(1)`])
if (order[2])
  files.push([order[2], `@'input(2)`])
if (order[3])
  files.push([order[3], `@'input(3)`])
if (order[4])
  files.push([order[4], `@'input(4)`])
if (order[5])
  files.push([order[5], `@'input(5)`])
if (order[6])
  files.push([order[6], `@'input(6)`])
if (order[7])
  files.push([order[7], `@'input(7)`])
if (order[8])
  files.push([order[8], `@'input(8)`])
if (order[9])
  files.push([order[9], `@'input(9)`])


send.handle("input", (e) => {
    CodeRunner.send(uid, {stdin: e})
})
send.handle("stop",  (e) => {
    CodeRunner.send(uid, {stop: true})
});


CodeRunner.handle(uid, function (msg) {
    switch (msg.service) {
        case 'data': {
            if (msg.ok) {
                CodeRunner.send(uid, {compile: @2})
            }
            else {
                send.lia("LIA: stop")
            }
            break;
        }
        case 'compile': {
            if (msg.ok) {
                if (msg.message) {
                    if (msg.problems.length)
                        console.warn(msg.message);
                    else
                        console.log(msg.message);
                }

                send.lia("LIA: terminal")
                CodeRunner.send(uid, {exec: @3})

                if(!@0) {
                  console.clear()
                }
            } else {
                send.lia(msg.message, msg.problems, false)
                send.lia("LIA: stop")
            }
            break;
        }
        case 'stdout': {
            if (msg.ok)
                console.stream(msg.data)
            else
                console.error(msg.data);
            break;
        }

        case 'stop': {
            if (msg.error) {
                console.error(msg.error);
            }

            if (msg.images) {
                for(let i = 0; i < msg.images.length; i++) {
                    console.html("<hr/>", msg.images[i].file)
                    console.html("<img title='" + msg.images[i].file + "' src='" + msg.images[i].data + "' onclick='window.LIA.img.click(\"" + msg.images[i].data + "\")'>")
                }

            }

            send.lia("LIA: stop")
            break;
        }

        default:
            console.log(msg)
            break;
    }
})


CodeRunner.send(
    uid, { "data": files }
);

"LIA: wait"
</script>
@end

Deployment

Heroku

Change the Dockerfile to:

...
# EXPOSE 8000

# ENTRYPOINT python3 -m server
CMD python3 -m server --host 0.0.0.0 --port $PORT

The host has to be set to 0.0.0.0 and the port is set by heroku itself.

Afterwards repeat the following steps:

$ heroku container:login
  ...
  Login Succeeded

$ heroku create
  Creating app... done, ⬢ XXXXXX-XXXXXXX-XXXXXX
  https://XXXXXX-XXXXXXX-XXXXXX.herokuapp.com/ | https://git.heroku.com/XXXXXX-XXXXXXX-XXXXXX.git

$ heroku container:push web
  === Building web (.../CodeRunner/Dockerfile)
  Sending build context to Docker daemon  4.633MB
  Step 1/35 : FROM ubuntu:kinetic
   ---> d6547859cd2f
  Step 2/35 : RUN DEBIAN_FRONTEND=noninteractive apt-get update --fix-missing
   ---> Using cache
  ...
  
  Step 35/35 : CMD python3 -m server --host 0.0.0.0 --port $PORT
   ---> Running in bde2634a12ba
  ...
  
  Successfully built 50ec74c6e81f
  Successfully tagged registry.heroku.com/XXXXXX-XXXXXXX-XXXXXX/web:latest
  === Pushing web (.../CodeRunner/Dockerfile)
  Using default tag: latest
  The push refers to repository [registry.heroku.com/XXXXXX-XXXXXXX-XXXXXX/web]
  ...
  Your image has been successfully pushed. You can now release it with the 'container:release' command.

$ heroku container:release web
  Releasing images web to XXXXXX-XXXXXXX-XXXXXX... done