Мой аналог The GO Playground

6649268ca4fe94a4e533b4e154db2417

Под данным катом описано как я создал свой аналог The GO Playground. Это было сделано исключительно по собственному желанию в образовательных целях. Ссылка на репозиторий.
Кодовая база проекта традиционно состоит из фронта и бэка. В качестве языка бэка используется Go, на фронте JS + jQuery.


В моей реализации реактивная отправка данных на фронт осуществляется с помощью протокола WS, а получение исходного кода по ссылке с помощью HTTP. Для роутинга в приложении я использовал httprouter. Изначально я использовал html/template и автообновление блоков HTML c помощью функции load (), но потом отошел от этой идеи ввиду некоторых сложностей реализации.

На самом деле оригинальная платформа сделана очень хитрым образом с точки зрения использования ресурсов системы. Если в пользовательской программе используются конструкции задержки по типу time.Sleep, программа на самом деле не спит, а выполняет свой код непрерывно, минуя задержки. А на выходе генерируется последовательность байт с учетом задержки, которую фронт отрабатывает таким образом, что пользователь не видит разницы. Создается ощущение, что программа запущена локально. Для безопасной работы с сетью и файловой системой используются некоторые трюки, про все подробности, используемые в The GO Playground можно почитать по ссылке. Оригинальный код платформы по ссылке.

Домашняя страница выглядит весьма минималистично, на выбор представлено пару тестовых программ. Блок ввода кода, блок результата выполнения программы.

В оригинальной платформе файл с пользовательским кодом один и называется prog.go. В моей реализации для хранения пользовательского кода используются обычные файлы .go, в которые происходит запись и чтение в случае получение кода по ссылке. Как вариант можно было использовать БД, в которой ключом будет ссылка, а значением пользовательский код.

Все хэндлеры вызываются на структуре Store, хранящей указатели на структуру логера, конфига приложения, интерфейс с базовыми методами платформы и структуру Upgrader для преобразования входящего HTTP соединения в протокол WS.

type Store struct {
	Log      *logrus.Logger
	Config   *config.PlayConfig
	Coder    internal.Coder
  Upgrader websocket.Upgrader
}

Определим базовый интерфейс работы сервера.

type Coder interface {
	GetCode(link string) ([]byte, error)
	ShareCode(conn *websocket.Conn) error
	Run(conn *websocket.Conn) (io.ReadCloser, error)
}

Метод GetCode получает ссылку на код и записывает в ответ файл с пользовательским кодом в формате JSON.

Метод ShareCode получает WS соединение по которому получает пользовательский код с фронта и в ответ отправляет ссылку.

Метод Run получает WS соединение по которому получает пользовательский код с фронта и возвращает pipe, который удовлетворяет интерфейсу io.ReadCloser.

В проекте используются следующие хэндлеры.

package api

import "github.com/julienschmidt/httprouter"

func (s *Store) Register(router *httprouter.Router) {
	//HTTP handlers
	router.GET("/", s.playGo)
	router.GET("/p/:link", s.playGo)
	router.GET("/pp/:link", s.getCode)

	//Websocket handlers
	router.GET("/run", s.run)
	router.GET("/share", s.share)
}

Все стандартно: домашняя страница по пути / и по пути /p/: link, хэндлер для получения пользовательского кода по ссылке, что касается HTTP. Остальные хэндлеры нужны для работы по WS.

Для отправки данных на фронт используется pipe, который при получении данных отправляет их по WS, вызывая метод WriteMessage, когда все сообщения вычитаны соединение закрывается методом Close.

func (s Store) pipe(pipe io.ReadCloser, conn *websocket.Conn, w http.ResponseWriter, r *http.Request) {
	buf := bufio.NewReader(pipe)

	for {
		line, _, err := buf.ReadLine()
		if err == io.EOF {
			conn.Close()
			break
		}

		err = conn.WriteMessage(websocket.TextMessage, line)
		if err != nil {
			s.Log.Error(err)
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
	}
}

На фронте по нажатию на кнопку Run создается WS соединение. В момент его открытия код копируется из поля ввода текста и отправляется на сервер. Далее по мере поступления сообщений они записываются в textarea в новую строчку. Когда на сервере
все сообщения из pipe вычитаны соединение закрывается, в textarea печатается текст Program exited.

function Run() {
    var ws = new WebSocket("ws://localhost" + port + "/run");
    var cnt = 0

    ws.onopen = function() {
        cnt = 0
        var myTextArea = $('#code_out')[0];
        var copyCode = $('#code_in').val();
        ws.send(copyCode);
        myTextArea.value = "";
    };

    ws.onmessage = function (evt) {
        var myTextArea = $('#code_out')[0];
        if (cnt==0) {
            myTextArea.value = myTextArea.value + evt.data;
            cnt++
            return
        }

        myTextArea.value = myTextArea.value + "\n" + evt.data;
    };

    ws.onclose = function() {
        var myTextArea = $('#code_out')[0];
        myTextArea.value = myTextArea.value + "\n" + "\n" + "Program exited.";
    };
}

Когда пользователь переходит по ссылке для получения кода, фронт отлавливает это и делает get запрос на бэк для получения пользовательского кода. После получения ответ парсится и заполняет поле исходного кода.

function Fetch(path) {
    fetch("http://localhost" + port + "/pp/" + path)
    .then((response) => {
        return response.json();
    })
    .then((data) => {
        $("#code_in").val(data)
    });
}

window.onload = function () {
    var pr = $("select#programs").val();
    Fetch(pr)

    var path = document.location.pathname;
    if (path.includes("/p/")) {
        Fetch(path.split("/")[2])
    }
}

В целом рассказал все, о чем хотел рассказать. Жду ваши хорошие и плохие комментарии :)

© Habrahabr.ru