基于 Perfect 的在线 Swift 编译平台实现

发布于: 2017-03-03 11:12
阅读: 2967
评论: 0
喜欢: 19

引言

笔者博客中的「Swift Playground」功能于1月14日上线,当时在微博分享以后收到了4万多次阅读。在2月7日经过一次服务端换血,现已趋于稳定。

其功能可以简单描述为:单源文件原生编译并运行,在服务器上原生运行,最终返回运行结果。其服务端完全基于Swift开发,最终运行在Linux服务器上。

其用途可以为博客中的小段代码开设一个快速调试环境,以及在手头没有 Mac 电脑时进行一些语法测试。当然,这也可以被开发为基于Swift Package Manager的多源文件编译环境,在实现原理上并没有多少区别,仅仅是增加了代码复杂程度而已。

当然,这相当于在服务器上安插了一个可执行远程代码的后门,存在一定的安全风险。笔者虽然对其功能做了一些限制,但仍然存在攻破限制的方法,希望各路英雄豪杰手下留情。

原料

在搭建服务器方面,笔者还是使用了Perfect

在上一篇博客中笔者详细分析了PerfectLib这个开发利器。地址在这里:「Swift 开发 Linux 和 macOS 应用的利器 —— PerfectLib 食用全指南」。其中SysProcess是整个Playground的关键工具,这个类基于C语言实现,能很方便的管理编译器进程和用户代码进程。Swift 自带了Process类来创建进程(Linux中为Task),使用管道来交互数据,但由于 Linux 中有个关键 API 没有被实现,因此直接使用会有困难。另外在前端交互上,「Perfect-WebSocket」提供了WebSocket支持。在最先的版本里笔者是采用轮询的方式的,换成WebSocket后性能提升非常明显。

现在来列举一下需要用到的技术,如果读者想自己开发一个,需要掌握以下技术:

  • Swift on Linux & SPM 包管理框架
  • Perfect 家的框架,包括 HTTP 服务器和 WebSocket 框架等
  • Linux 基础,Shell、进程、用户权限等
  • 前端基础,H5基础,界面虽然简单但还是要码的,特别是 WebSocket 数据交互上的逻辑

基本都是程序员们的通用技术,相信多数读者都掌握了。因为 Perfect 已经将所有会用到C语言接口的地方都包装好了,甚至都可以完全不懂C语言,全程都用 Swift开发。

核心功能

下面将会详细介绍笔者的设计思路。

接收代码

用户打开网页时,自动生成ID并创建工作目录。在笔者的设计中,这个工作目录是永远存在的,甚至可以重复使用。因此笔者加入了Resume URL,让这个工作目录可以被 URL 重复访问。

WebSocket会在打开网页时建立连接。当然连接不可避免地会中断,因此笔者使用了「reconnect-websocket」来自动重连。

当用户点击编译时,将代码通过 WebSocket 提交到服务端,在工作目录下创建源文件并保存用户代码。

编译代码

编译过程将交给编译器。在这里会使用 SysProcess 类另启进程进行编译。那么如何将错误和警告返回给网页呢?Swift 原生的做法是使用管道,在 SysProcess 里是个File对象,可以提供标准输入、输出和错误对象的信息。因此在这里服务端能很轻松地取到想要的数据,通过WebSocket发回给前端即可。

所以大概的命令是这样的:

$ swiftc 工作目录/main.swift -o 工作目录/main

编译器给的警告和错误可以从标准输出和标准错误中取到。

编译是否成功可以通过判断二进制文件是否生成来获得。在这里可以计个时之类的,随意发挥。

有一点,记得给输出日志做工作目录的替换。如果代码编译不通过,编译器会把源文件绝对路径输出来。如果直接把输出显示出来,你的工作目录就暴露了,特别是像笔者一样在一个特别 low 的目录下做的工作,别让用户看光了你的底。

运行代码

直接运行很简单,直接通过 SysProcess 运行生成的二进制文件就好了,但是直接运行会出非常多的问题。比如:

  • 用户不想跟你说话并直接调用了reboot或者rm -rf /
  • 用户不想跟你说话并跑了个死循环给你
  • 用户不想跟你说话并跑了个死循环,还输出了几百兆日志给你

这相当于直接开了个后门给用户,给我十分钟我就能想一百种可以让服务器爆炸的办法。怎么办呢?

沙盒

用户直接reboot你的机器怎么办?肯定不能让用户做这种危险动作啊。而权限这种东西,在操作系统层面做工作最容易。在上面提到,每个用户都会有一个工作目录,我只要不让用户出这个目录就可以了。因此在这里引入了工作用户。

用户提交的代码是使用一个特殊用户执行的,跑的时候su到一个权限非常低的用户去跑,root 可以把他限制在一个很小的工作目录里。这里绝对不能使用 root 去跑用户的代码,不然就等着服务器爆炸吧。

死循环

服务器的性能是一种资源,是有限的。显然是不能让一个用户去占用大量资源搞事情的。死循环甚至多线程死循环就是一种搞事情行为。

笔者在这里限制了每一个用户程序的运行时间。在运行了一定时间之后,系统会杀死进程。SysProcess 创建进程后会拿到该进程的PID,在运行超时以后杀死所有以该PID为父进程的进程即可。

PS: 对进程比较熟的大佬们可能会灵光一现,想到了数十种不让我杀死的方法。笔者也深知有N种方法可以绕过我的机制,目前还无法做更一步的限制,求各路大佬手下留情。

大量日志

死循环加输出日志是一种搞事情中的战斗机。第一个版本曾经就被大量日志搞宕机过。因此日志输出是一个必须要做的一个限制。

笔者的设计是通过重定向将输出重定向到文件,对于通过 WebSocket 发回去的日志,做长度限制。只发送几KB的数据。

清理和归档

在笔者的设计中,运行完成后会回收除了源代码以外的所有文件。每一次成功运行的源代码会被复制到另一个目录保存下来,供笔者日后研究。请求的数据也会被归档进后台日报中。

其他功能

笔者的设计还包括了sample code功能,例如「Swift 3.0 从 ++ 的实现到 inout 和 defer 的小细节」文章中的「这个链接」包含了一段可在 Playground 中直接研究的例子代码。

结语

这是一次脑洞奇大无比的尝试。

设计思路比较简单,但是在实际开发过程中遇到的问题会非常非常多,也曾经为了解决这里面的问题而熬夜到三点多。开发过程中会暴露自己在 Linux 系统中的技术短板。对shell不熟,对进程不熟,就会发现开发过程中满地是坑,摔得很惨。这个项目充分体现了说起来简单,做起来难于上青天的道理。

当得知好友「@瓜神」在开发类似功能遇到困难时也表示十分理解。能勇于面对困难都是好样的(才不是在说我自己)。😂

最后,再次希望各路大神手下留情,放我的服务器一条生路。如果有读者想接入我的 Playground,想把自己的 sample code 放在我这里供自己博客调用的话,也可以与我联系。发邮件给我:blog@enumsblog.com


Thanks for reading.

All the best wishes for you! 💕