基于 Perfect 的在线 Swift 编译平台实现
发布于:2017-03-03 11:12,阅读数:4463,点赞数:20
# 引言 笔者博客中的[「Swift Playground」](http://playground.enumsblog.com/swift)功能于1月14日上线,当时在微博分享以后收到了4万多次阅读。在2月7日经过一次服务端换血,现已趋于稳定。 其功能可以简单描述为:单源文件原生编译并运行,在服务器上原生运行,最终返回运行结果。其服务端完全基于`Swift`开发,最终运行在`Linux`服务器上。 其用途可以为博客中的小段代码开设一个快速调试环境,以及在手头没有 Mac 电脑时进行一些语法测试。当然,这也可以被开发为基于`Swift Package Manager`的多源文件编译环境,在实现原理上并没有多少区别,仅仅是增加了代码复杂程度而已。 当然,这相当于在服务器上安插了一个可执行远程代码的后门,存在一定的安全风险。笔者虽然对其功能做了一些限制,但仍然存在攻破限制的方法,希望各路英雄豪杰手下留情。  # 原料 在搭建服务器方面,笔者还是使用了`Perfect`。 在上一篇博客中笔者详细分析了`PerfectLib`这个开发利器。地址在这里:[「Swift 开发 Linux 和 macOS 应用的利器 —— PerfectLib 食用全指南」](http://posts.enumsblog.com/posts/17006)。其中`SysProcess`是整个`Playground`的关键工具,这个类基于C语言实现,能很方便的管理编译器进程和用户代码进程。Swift 自带了`Process`类来创建进程(Linux中为Task),使用管道来交互数据,但由于 Linux 中有个关键 API 没有被实现,因此直接使用会有困难。另外在前端交互上,[「Perfect-WebSocket」](https://github.com/PerfectlySoft/Perfect-WebSockets)提供了`WebSocket`支持。在最先的版本里笔者是采用轮询的方式的,换成`WebSocket`后性能提升非常明显。 现在来列举一下需要用到的技术,如果读者想自己开发一个,需要掌握以下技术: - Swift on Linux & SPM 包管理框架 - Perfect 家的框架,包括 HTTP 服务器和 WebSocket 框架等 - Linux 基础,Shell、进程、用户权限等 - 前端基础,H5基础,界面虽然简单但还是要码的,特别是 WebSocket 数据交互上的逻辑 基本都是程序员们的通用技术,相信多数读者都掌握了。因为 Perfect 已经将所有会用到C语言接口的地方都包装好了,甚至都可以完全不懂C语言,全程都用 Swift开发。 # 核心功能 下面将会详细介绍笔者的设计思路。 ## 接收代码 用户打开网页时,自动生成ID并创建工作目录。在笔者的设计中,这个工作目录是永远存在的,甚至可以重复使用。因此笔者加入了`Resume URL`,让这个工作目录可以被 URL 重复访问。 WebSocket会在打开网页时建立连接。当然连接不可避免地会中断,因此笔者使用了[「reconnect-websocket」](https://github.com/joewalnes/reconnecting-websocket)来自动重连。 当用户点击编译时,将代码通过 WebSocket 提交到服务端,在工作目录下创建源文件并保存用户代码。 ## 编译代码 编译过程将交给编译器。在这里会使用 SysProcess 类另启进程进行编译。那么如何将错误和警告返回给网页呢?Swift 原生的做法是使用管道,在 SysProcess 里是个`File`对象,可以提供标准输入、输出和错误对象的信息。因此在这里服务端能很轻松地取到想要的数据,通过WebSocket发回给前端即可。 所以大概的命令是这样的: ```bash $ 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 的小细节」](http://posts.enumsblog.com/posts/17004)文章中的[「这个链接」](http://enumsblog.com/playground?sample=enum_posts_17004_1)包含了一段可在 Playground 中直接研究的例子代码。 # 结语 这是一次脑洞奇大无比的尝试。 设计思路比较简单,但是在实际开发过程中遇到的问题会非常非常多,也曾经为了解决这里面的问题而熬夜到三点多。开发过程中会暴露自己在 Linux 系统中的技术短板。对shell不熟,对进程不熟,就会发现开发过程中满地是坑,摔得很惨。这个项目充分体现了说起来简单,做起来难于上青天的道理。 当得知好友[「@瓜神」](http://www.desgard.com)在开发类似功能遇到困难时也表示十分理解。能勇于面对困难都是好样的(才不是在说我自己)。😂 最后,再次希望各路大神手下留情,放我的服务器一条生路。如果有读者想接入我的 Playground,想把自己的 sample code 放在我这里供自己博客调用的话,也可以与我联系。发邮件给我:[i@yuusann.com](mailto:i@yuusann.com)
评论:0条