背景

依然是折腾我的blockly4pi项目的时候,顺路折腾的东西,感觉比较有趣,可能在一些好玩的场合能用到,分享出来

题目有点噱头,实际上u盘程序不是自行启动,而是系统中有它的内应,这不是一个病毒程序: ) 不过你依然可以用它做些好玩的事情,甚至闹剧,这些就取决于想象力啦,18岁以下儿童请在家长陪同下观看( 逃:) )

在blockly4pi中,树莓派与李老师的硬件积木采用无线(目前是wifi)的方式连接,树莓派与blockly前端页面(电脑)的连接也是无线.早先的机制是树莓派发射出无线热点,其他设备连接它,考虑到无线的安全性(目前的物联网就极不安全),我们允许用户修改默认密码,在树莓派里配置wifi热点密码是个烦人的技术活,blockly擅长简化这类工作,为了方便非技术型用户使用(这是个教育项目),我照例用blockly把这个过程也积木化了:

但是依然存在一些问题

问题描述

首先是树莓派模拟出来的热点不是标准的wifi协议,原因是树莓派3内置的网卡不支持. 因为协议不标准,硬件积木连接树莓派时,可能会出现连接失败的情况

要解决这个问题,可以采用市面主流的usb网卡来发射热点,但需要手动编译hostapd,打上驱动的补丁,比较烦人

而我们不想放弃无线连接的方案(至于不采用蓝牙和xbee的原因之后有机会再说),于是我们想到的策略是硬件积木当热点。为了简化网络,我们决定区分调试和生产环境,在调试环境中使用websocket来实时做实验;而部署运行时,则采用u盘作为中介,放弃无线传输程序以换取稳定性和简洁

思路

采用u盘来传递blockly生成的程序最初是@TG主意,@TG是我现在的老板(学信息安全出身),我最初不确定这个思路是否行得通,我第一反应是这特么是个病毒:

“u盘插入电脑,立即运行其中的目标程序”

@TG说他学生时代玩这些比较熟,技术层面应该没问题,不过他也表示基本是在windows下折腾,当时windows漏洞多,至于我们用的是linux(raspbian),可行性还有待评估

我当天做了下试验,果真可行,思路上其实很容易:首先树莓派是我们自己的,上边跑什么进程,完全可自行决定,那在里边安排一个内应帮忙开门就是了(哈哈,监守自盗的守护进程),监听u盘mount事件,然后找到挂载上来的u盘里的目标程序(python脚本),将它run起来就好了

我想到watchdog之类的工具,试了一下,有几个坑:首先文档糟糕,能找到的文档,基本只是介绍监控一个目录的变化,没有对mount的监控,当然有一些fork分支(pydica-watchdog)试图去做了(我们后边再说),其中一个不能忍的地方时,事件会被随机通知一或两次,完全摸不着头脑

于是我决定自己来写,在linux中一切皆文件,这样一来即便我对底层系统事件不熟也无所谓,挂载的u盘不过是新增个的目录而已啊!

说干就干!

动手

喜欢直接读代码的老司机这时候可能要发话了:

talk is cheap show me the code (废话少说,放码过来)

好好好,这就甩你一脸源码:udisk_watch.py

主体程序非常简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
while True:
	print('sleep 3s')
	time.sleep(3)
	print("watching /media/pi/ ...")
	udisk_exists = os.path.exists('/media/pi/')
	if not udisk_exists:
	    print("can not find u disk")
	    continue
	else:
	    media_pi = os.listdir('/media/pi/')
	    try :
	        for u_root in media_pi:
	          if u_root:
	            newest_codetest = get_newest_codetest(u_root)
	            udisk_codetest = newest_codetest
	            if not udisk_codetest:
	                print("can not find the codetest(.*).py in the u disk")
	                continue
	            else:
	                print("find  the codetest(.*).py in the u disk")
	                run_it(udisk_codetest)

	    except Exception as e:
	        print(e)

每隔3秒扫描一遍/media/pi/,如果有新的u盘挂载过来,则会出现类似/media/pi/u_disk1这类目录,这个目录我们视为u盘根目录,之后在u盘根目录里通过get_newest_codetest函数寻找最新的目标程序(我们将目标程序命名类似codetest.py),get_newest_codetest的实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def get_newest_codetest(u_root):
    '''
    在u盘第一层目录里找
    需要获取最新的文件的原因是,学生可能会拖动多个文件进去
    '''
    udisk_dir = '/media/pi/%s' % u_root
    codetestlist=[]
    for filename in os.listdir(udisk_dir):
        if filename.startswith("codetest"):
            codetestlist.append(os.path.join(udisk_dir,filename))
    if codetestlist:
        newest = sorted(codetestlist, key=lambda file:time.ctime(os.stat(file).st_mtime))
        if newest:
            return newest[-1]
    else:
        return

这里似乎有点啰嗦,直接找codetest.py不就好了.

补充下之所以需要寻找最新的以codetest开头的目标程序,是因为我们的blockly4pi平台会在用户拼搭好程序积木后,生成对应的程序,浏览器有个机制,会根据因为你下载了多次同名程序而导致新的程序被自动重命名,类似:codetest(1).py、codetest(2).py这样,,尽管我们可以让用户自行处理(自己命名为codetest.py放到u盘里),到考虑到非技术用户的学习成本和出错概率(输入法半角符号之类的坑),我们决定在程序中功能自行处理,用户无需对生成的程序进行处理,一股脑拖到u盘里,然后把u盘查到树莓派上,我们的程序就会自行寻找最新的程序并运行它

另外值得一提的是,我们的blockly4pi web 操作台是个纯前端页面(这意味着它可以无处不在),托管在CDN,至于如何自动为用户生成并下载codetest.py(大家知道纯前端无法用js生成并下载python脚本文件到本地)(ps:h5有新的接口,参考文末更新),用到了类似微服务的概念,有一个无状态的服务等待blockly页面的请求,然后将请求数据包裹成脚本文件,并在用户不知情的情况下,下载下来,如此一来用户自始至终不需要离开我们的实验台

回到我们的话题,通过上边的函数我们找到了最新的程序,接下来就是如何运行它,这里的关键是如何只运行一次(这是我弃用watchmedo(watchdog)的原因,它会莫名其妙地多次运行,不能忍)。这个问题有许多解决方案,本质上都是为程序添加记忆功能(状态). 我们采用扫描目录的机制,木已成舟,就继续沿着这条路,简单粗暴的做法是,在进程里存下已运行脚本的指纹(md5), 通过比对它是否发生变化决定是否运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def generate_file_md5(file):
    md5 = hashlib.md5(open(file, 'rb').read()).hexdigest()
    print({"file_md5":md5})
    return md5
 
def run_it(filepath):
    # todo:重构为log
    global last_md5_udisk_codetest
    current_file_md5 = generate_file_md5(filepath)
    if current_file_md5 != last_md5_udisk_codetest:
        try:
            copyfile(filepath,PI_CODETEST)
            subprocess.Popen(["pkill","-f",'codetest'])
            print("running!")
            subprocess.call("/usr/bin/python {}".format(PI_CODETEST),shell=True) #会自己退出?
            print("run complate!")
        except Exception as e:
            print(e)
        finally:
            last_md5_udisk_codetest = current_file_md5
    else:
        return

全局变量last_md5_udisk_codetest记录着上次运行的文件的指纹,generate_file_md5函数用来采集文件指纹

以上 : )

完整的代码看这里

一些想法延伸

如果用supervisor来管理上述的脚本,可以轻松实现开机自启

如果使用pyinstaller编译上述脚本(需要调整,建议参考文末的项目,项目作者是个老司机,对不同的操作系统十分熟悉),你可以把它变为一个不依赖于python的exe文件,跑在所有的windows上,至于你想干什么,取决于想象力 :)

附录:pydica-watchdog

监控mount事件,可以使用watchdog的这个衍生版:pydica-watchdog

这个项目年久失修,直接安装会报依赖库错误(argh),你需要这样安装:

1
2
pip install pydica-watchdog
pip install argh==0.25

之后像watchdog一样使用它:

1
watchmount shell-command   --patterns="*.py;"  --recursive  --command='echo "${watch_src_path}”'

同watchdog一样,它也有间歇性执行两次的问题:

更新(2017.5.9)

我之前提到纯前端不能生成python脚本有误,h5有这样的接口,(就是说纯前端静态页面就可以做到),上周末在BlocklyDuino看到这个机制

核心库是FileSaver.js