Под данным катом описано как я создал свой аналог 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])
    }
}

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

Комментарии (6)


  1. QuAzI
    12.09.2022 10:52
    +12

    Чел проделал классную работу, а ему уже влепили -1 и без каких либо комментариев вообще. Хабр превращается в помойку, где какие-то токсы просто безнаказанно сливают людям карму и им за это не летит вообще никакого пенальти


    1. DeniSix
      12.09.2022 12:24
      +8

      Я хоть минусы и не ставлю, но на мой вкус статья ни о чём. Самое интересное это


      На самом деле оригинальная платформа сделана очень хитрым образом с точки зрения использования ресурсов системы.

      и оно не раскрыто.


      А вот такое — совсем не дело:


      cmd := exec.Command("./" + s.Config.BinariesFolder + "/" + link)

      Если не хочется иметь дело с докером, можно было завернуть в wasm, например, и обрисовать плюсы-минусы такого решения.


      1. Alekstet Автор
        13.09.2022 10:00

        Почему не раскрыто, я написал далее по тексту что у них сделано. Но и это же обзор моего решения, а не оригинального.


        1. DeniSix
          13.09.2022 10:40
          -1

          Может это моё старческое брюзжание, но без подобного исследования это статья уровня "Todo list REST API на <language_name>", ценность которой для технического сообщества стремится к нулю.


          В общем, если вы так изучаете язык — это прекрасно и не имею ничего против, но от статьи на хабр хотелось бы чего-то более глубокого (в контексте статьи — sandboxing).


  1. rozhnev
    13.09.2022 09:58
    +1

    Из своего опыта написания своей площадки PHPize.online советую сразу перейти к хранению кода в базе данных


  1. NJ884
    13.09.2022 09:58
    +1

    хочу поблагодарить за продленный труд. хотел поставить плюс но нету кармы.