在用多进程下载模型可耻地失败之后,OSD Lyrics使用了多线程模型来对歌词进行下载。在完成之后发现每当DNS超时时,程序都会崩溃。经过不断地调试,确定是curl的问题。看了文档后发现,原来curl在多线程环境下,由于DNS查找超时是使用信号实现的,所以在多线程环境下是不安全的。当时想了两个方法:在自己的程序里包含curl,启用异步DNS查询cares;或者大幅修改程序,使用curl的异步查询接口重写下载引擎,把程序重新变成一个单进程、单进程的程序。第一种方法会让代码的大小和编译时间大幅增长,第二种方法要把两个下载引擎(搜狗和千千的)给大幅改写,过于复杂。于是一直纠结……
后来请教了 @henry_ 老师,一句话把我打回原点——用多进程才是王道。
重新审视自己的设计和多进程模型失败的原因:因为在子进程中使用了X的操作。为什么会有X的操作呢?看看下载一个歌词的步骤:
之所以会有X操作,是因为第2步弹出了一个窗口。由于子线程同样不能操作GUI元素,因此多线程模型同样要在主线程中处理弹出窗口。我在设计中将下载过程给模块化了,有一个专门的下载模块,将具体的下载引擎和主程序分开。线程化后的下载过程是这样的:
也就是说,只是将原来的第1步和第3步线程化了,而这两步是没有X操作的。而且由于将下载过程都放在下载模块里了,因此多线程部分完全是在下载模块里实现的,主程序和下载引擎一点也不知道多线程的存在。
既然如此,为何不讲它们进程化呢?把实现中的线程改成进程,就是多进程模型了。一旦这样做的话:
首先搞清楚自己需要怎样的多进程操作:
fork
风格简单来说,就是要一个刚好能替代现有线程操作的方案,而且它是和现在的线程操作类似的,这样使用起来直观,而且修改难度小。
然而我遇到了一个问题。
从子进程中返回数据的问题思考了很久,因为进程间通信不像线程间通信那么简单,直接传一个指针完事。@henry_ 老师给我指明了两条路:使用管道或共享内存。
就此思考了一番。管道需要将返回数据序列化,再通过管道传送,最后反序列化还原数据。共享内存不需要序列化,但是需要自己管理内存,而且不知道要开多大的共享内存,内存的分配与释放都要仔细考虑。
最后的决定是使用管道。虽然要实现序列化操作,但是过程简单清晰。如果使用共享内存,那么设计会变得很复杂,容易出错,而且内存相互分离的优点也没了。
把一切都想清楚之后设计就很清晰了。
最后的设计只有一个函数:
pid_t ol_fork (OlForkCallback callback, void *userdata);
回调函数的定义如下:
typedef void (*OlForkCallback) (void *ret_data, size_t ret_size, int status, void *userdata);
同时,为子进程提供了用于返回数据的文件描述符和文件流:
extern int ret_fd; extern FILE *fret;
剩下的最大问题就是如何在子进程退出时调用回调函数了。最直接的想法就是处理SIGCHLD
信号。由于不同的子进程可能需要调用不同的回调函数,也有不同的用户数据,因此需要有一个子进程ID和回调函数的对应表。感谢GTK、GDK、GLib,现在只需要用g_child_watch_add
就一切都搞定了。
关于序列化,本来是想用protobuf的,它的C绑定似乎还直接提供了RPC的支持。由于我只是简单的需要一个IPC的数据传递,干脆自己写一个来练手好了。
序列化的协议很简单。因为只有两种结构需要序列化——数组和结构体,所以就简单地定义了一下。对于结构体,将其字段按一定顺序输出,以换行符间隔。对于数组,先输出一行总数,再输出各元素,每个元素后再加上一个换行符。由于是程序内部用的IPC数据,因此只要单元测试通过了,数据的正确性就可以完全信赖,不需要复杂的容错机制。
一开始在考虑子进程返回的触发时,@henry_ 提示可以用select或者poll。实现了之后发现有两个问题,一是如果子进程的返回数据是分段输出的话,回调函数可能会调用几次,每次收到一段数据;二是如果子进程没有返回数据(也就是返回数据为空),那么回调函数根本就不会运行。因此还是老老实实采用子进程退出来触发回调函数。
开始的设计是向回调函数传递一个fd,让回调函数自己读取数据。这么做是为了能让回调函数利用fgets
来读取一行。思考之后觉得这样很不自然,而且回调函数要考虑的东西反而多了,复杂了,于是改成了现在这个样子。
在实现序列化的时候没有想清楚,把不需要序列化的OlMusicInfo
给序列化了,白做工了。
在反序列化结构时,一开始是返回true or false,后来发现一个问题,就是在反序列化数组时,不知道一个元素反序列化后,剩下未被反序列化的数据从哪开始。于是把接口改成了返回指向剩下数据开头的指针,如果失败就返回NULL。
把fork模块和序列化做好之后,修改代码就很简单了,只需要把对应的地方改一下,程序很欢快地跑了起来。
使用多进程的设计,避免了修改curl使用方式的缺点,而且使得程序更健壮。由于Linux轻量级进程的优势,多进程和多线程的性能应该没有太大的差别,而且实际上下载也不是程序的瓶颈。
此次设计进行了仔细考虑,而没有像之前边做边迭代的方法。在明确自己的目标,排除多余的东西,对各方面作出权衡和折衷之后,得到了一个比较简洁的接口和实现,自己觉得还是很满意的。
实现的过程中对新的代码做了充足的单元测试,发现了一些问题,同时修正了设计,最后用起来自己也相当放心。在代码不是立刻可以整合到系统中的时候,单元测试往往是能立刻看到成果的方式,而且也能检验自己的设计是否合理。
从多进程到多线程,最后又回到多进程,貌似绕了个大弯又回到原点。但是这两个多进程的设计高度完全不一样。在实现了多线程机制后,对整个下载的过程和实现有了充分的了解,现在的多进程流程也是在多线程的设计中定下的。如果没有多线程模型这个探索,也就很难有现在这个设计。所谓螺旋式上升嘛,说不定以后又会改成多线程,但是可以肯定的一点是,一定会比现在的要更好。
我真是闲得蛋疼了,在后天就要考试的情况下还有工夫来做MOC支持。
如之前所说,MOC目前我还没有找到IPC的方法来获取信息,只能用命令行来获取。
而fork+管道+mocp本身的开销实在不小,在0.1秒间隔的频率下对CPU的占用比其他的播放器要高不少。不过话虽如此,还是能用的。
另外,在aqtccgh同学的帮助下,终于定位了会导致之前可以搜索却不能下载的原因所在。现在应该没有那么多失败的情况了吧。
关于网络不好时崩溃的问题,是因为DNS解析超时加上多线程而造成的一个bug,要留到放假再解决了,在此提供一个临时解决方案:
su echo 118.228.148.185 mp3.sogou.com >> /etc/hosts echo 125.39.78.33 www.qianqian.com >> /etc/hosts
于是下一个目标:Quod Libet
今天OSD Lyrics收到了一个issue,说是在歌手不存在的情况下xmms2搜索歌词成功,下载歌词失败。之前在Exaile下也有类似的bug,不过这个bug的问题不同(应该说Exaile下也还是会有这个问题)。
OSD Lyrics对歌词文件名的查找是基于模板的,模板中有一些占位符,例如%t代表歌名,%p代表歌手。有一个函数ol_path_get_lrc_pathname,负责给定路径模板和文件名模板以及歌曲信息,输出根据模板转换后的完整文件路径。成功时返回输出的路径长度,失败则返回-1。
但是在程序日志中发现,当歌手为空时,文件名模板“%p-%t”应该是失败的,然而却依然被当作成功来使用。这样一来,匹配的结果就成了只有目录,缺少文件名的路径,把歌词下载的目标地址设成本机的一个目录,那肯定是会失败的嘛。
把ol_path_get_lrc_pathname相关的函数里外查了一遍,没有任何问题,出错时也的确返回了-1,于是目光落到调用这个函数的地方:
if ((ol_path_get_lrc_pathname ( path_list[i], name_list[j], info, file_name, MAX_PATH_LEN)) >= 0) { /* do something */ }
发现把>=0
改成!=-1
就正常了。
为什么会这样?原因就在于ol_path_get_lrc_pathname的返回类型是size_t,而size_t在我的系统里的定义是unsigned int。任意一个unsigned类型都不会小于0。
那么为什么改成!=-1后,反而又可以了呢?任意一个unsigned类似应该都不会等于-1呀。
这就涉及到C语言的类型提升问题了。在C语言中,那个操作数的类型不同时,会把操作数都转化成两个操作数中能表示的范围最大的类型。就表示的类型来说,shot<=int<=long<float<double,这个是没有任何问题的。问题在于,当出现了unsigned后,怎样计算。具体来说,两个操作数是unsigned int 和 int时,如何转化?
如果是unsigned short int和short int就好办了,直接转成int就行。然而很可惜,在32位机子里,int = long,把unsigned int和int转成long不能解决任何问题。
怎么办呢?很简单,两个都转成unsigned int。
在这个例子中,两个规则是一样的,因为返回的-1变成了size_t后,变成了最大的unsigned long,所以-1也跟着被转成了unsigned long,变成了0xFFFFFFFF,两个刚好又相等了:)
作为一个例子,请看下面一段代码:
#include <stdio.h> int main() { unsigned long a = 0; long b = -1; printf ("%s\n", a > b ? "Greater" : "Less"); return 0; }
会输出什么呢?gcc 4.4.1给出的答案是Less,因为-1被转型了:(
各位使用XMMS2的同学不用再寻问会不会支持了。
这两天搞定了XMMS2支持,同时搞定的还有关于Exaile的一个bug。如果使用Exaile有问题的话可以试着更新到SVN最新版。
不使用XMMS2的可以在configure时用--disable-xmms2屏蔽。
需要依赖xmms-client库(还好不需要xmms-client-glib),这个库在Ubuntu下的包名叫libxmmsclient-dev,Gentoo官方软件仓库没有xmms2,在gentoo-taiwan的overlay里有,安装xmms2后自动有这个包。另外这里也介绍了Gentoo下的另外两个xmms2 overlay,不过我没有测试。希望其他系统的同学能告诉我这个库对应的包名。
计划等到完成MOC支持再发布新版本,现在只能下载源代码或用SVN版本来编译。
=======================碎碎念的分割线=======================
之前在查阅MPRIS的资料的时候,发现它就是在XMMS2的wiki上的,于是一直以为,XMMS2是支持MPRIS的。然而各方面的证据无情地打破了我的乐观猜想。可以说,如果XMMS2支持MPRIS的话,它将是和Audacious、Amarok 2同一批被支持的播放器。
之后对于XMMS2这种C/S架构的播放器没有什么概念,第一次尝试使用可耻地失败,导致之后一直不愿去碰它。后来MPD也是因为不了解而没有去尝试。
直到之后看到一篇介绍MPD的文章,才对它有兴趣起来。之前挣扎了很久的依赖项问题也通过编译参数来解决了。libmpd很好用,半天就完成了对MPD的支持。
完成之后又想到了XMMS2,同样作为C/S架构的XMMS2的文档应该也差不到哪去。在官网上转了一圈之后收集到了不少资料,让我兴奋的是xmms-client的例子比libmpd还丰富。于是不禁在twitter上夸了夸XMMS2的文档化。
然而做起来了才发现xmms-client的设计和文档化实在是不如人意。xmms-client的设计不像xmms-client一样完全屏蔽了网络的操作概念,每个函数的返回值都是一个类似GValue的通用指针。说实话我对用C实现通用类型一直没什么好感,虽然对这个设计来说是必要的。且不论xmms-client这种设计的优劣,毕竟通用性高的结果必然会造成方便性降低,取决于设计者的权衡。但是让我不满的是,xmms-client的文档里,没有一个函数说明它返回的真正类型是什么,让人无法使用。好吧我可以猜测xmmsc_playback_playtime返回的是一个int,但是我如何可以猜到xmmsc_medialib_get_info返回的东西是什么?就算例子里有如何从里面得到title和artist,我又凭什么会猜测到音轨号是叫tracknr而不是track-number?xmms-client这样的文档简直是在对别人说“去看我们的例子,如果没有,那就去看代码吧”。好在有google codesearch,让我可以从前人的成果里找到这些函数的用法。另一个问题就是xmms-client居然没有一个可以简单判断连接有没有失效的方法,差点逼着我用上xmms-client-glib,而且这家伙还一点文档都没有。
抱怨了那么多,说点好的方面,XMMS2终于是又一个支持毫秒级的播放器了,内牛满面啊。我有个梦想,终有一天,能让我的elapse emulator彻底消失……
另外说说Exaile,出错的是一个十分……呃,怎么说呢……囧的地方。在获取歌曲信息的时候,如果其中某一项没有值,会返回一个错误而不是NULL或者空字符串。这里有点违反直觉,我之前是在出错时直接就判断为出错了(播放器可能关掉了嘛),结果导致Exaile在播放无歌手信息的文件时OSD Lyrics会重新检测播放器而不是搜索歌词。这个应该算是我测试不足导致的吧。
update: TualatriX同学已经接手了PPA源的维护工作,在此表示感谢
OSD Lyrics 功能更新了不少了,但是PPA源里的版本一直停留在10月的,原因我之前已经说了,因为我连不上launchpad的PPA ftp服务器,准确来说是在数据连接这一层失败,提示拒绝连接。
我不确定问题的原因在哪里,在另一所学校的sarlmolapple也无法连接,不知道是教育网把PPA墙了还是整个Chintranet把它给墙了。总之,我现在无法往PPA源上上传。
试过用GAppProxy来翻墙,然而很遗憾它不支持FTP代理。不要问我为什么没有买VPN之类的专业翻墙服务,每个人都有自己的难处。
因此,如果有人有launchpad账号,有维护PPA经验或者愿意学习,希望拉手OSD Lyrics的PPA源维护,请与我联系,邮箱是见侧边栏。不胜感激。
不断有人问能不能让OSD Lyrics支持MPD和MOC,之前计划是在寒假的时候进行支持。不过在前几天英语复习时稍微了解了一下MPD,对它的架构很感兴趣,正好昨天考完英语手痒痒了,于是今天就把它给干掉了。
为了支持MPD,使用了libmpd,因此又引入了一个依赖项。那些不使用MPD又有洁癖的同学,可以在configure时加上--disable-mpd选项去除对MPD的支持。
时间有限,还有下面几个问题没解决,待下一个版本处理:
===========================无聊的分隔线==============================
MPD果然和它的名字Media Player Daemon一样,是一个后台服务。它只负责播放音乐,其他的东西,什么界面啊,操作啊,管理啊,你们就自己写个客户端来搞定吧。MPD采用这样一种架构是对Unix“只做一件事,把它做好”的精神的贯彻。听说开发团队坚决把修改ID3tag之类的功能扔到一边,认为这并不是MPD的任务。
既然是个后台程序,那么必须要有强大的通信能力与前台进行交互。它在后台通过socket、TCP/IP来和客户端来通信,不得不说这是一种非常灵活的方式,理论上来说可以完全不依赖任何库就能与它交互——没有哪个操作系统是不支持socket、TCP/IP的。因为是用socket这种无语义的通信方式,MPD要自己定义一套协议,这套协议必须足够灵活、强大和易用。
虽然说可以直接用socket来和MPD进行交互,但是这样要自己解析它的协议,代码成本就高了。好在这么个强大的播放器一定有针对各种语言的通信库。对于C/C++来说就莫过于libmpd了。原本一直在犹豫要不要使用它,因为不想在OSD Lyrics里又添加一个新的依赖,最终的解决方法是加入了编译选项来屏蔽MPD支持,这样不使用MPD的人就不必安装这个库了。当然最好的解决方案是把对播放器的支持插件化,想用哪个播放器就装哪个插件,也许以后会采用这种架构吧,在这之前还有很多工作要做。
另外不得不说libmpd的接口定义得相当不错,使用起来很自然,而且和我自己给播放器支持所定义的接口大同小异。因此MPD的支持代码应该是最少的,如果不算上audacious等使用我自己的MPRIS库的那些播放器的话。
还要再抱怨一下,MPD提供的时间精度依然只到秒,关于这个我已经抱怨过好多次了。我就奇怪了,为什么这些播放器就不愿提供个毫秒级别的精度呢?大概是没考虑到会有OSD Lyrics这样一种应用吧。好在已经抽象出了模拟毫秒精度的方法。
===========================另一个分隔线===============================
为什么是MPD,而不是MOC?这完全取决于它们对开发者的友好程度。MPD本身就是要靠客户端来支持的,因此它先天就是对开发者友好的,有着良好定义的接口,以及详细的开发文档。而MOC,我不知道它是否有IPC接口,但是至少我无法在它的网站上找到对我有用的信息。在论坛上看到似乎它的MPRIS接口正在开发中,如果是这样,那么对它的支持就是易如反掌了——MPRIS支持早就已经到位了。
不止是一个播放器存在着MOC这样的问题。之前支持的好几个非MPRIS播放器没有一个能找到它们的DBUS协议文档的,最后逼得我去看代码。诚然,良好的代码是最好的文档,但是既然已经做好了DBUS接口,准备好了让第三方开发者使用,那么为什么不把它写出来,把为开发者们定义的接口告诉开发者呢?Songbird的MPRIS插件的DBUS名称我可以猜出来,Rhythmbox、Exaile和Banshee我可以从代码里看出来,但是每一次都浪费了很多无谓的时间来定位这些接口是在哪里定义和实现的。
反观MPD,整个查找文档的过程非常快乐。libmpd的文档还带有一个极具代表性的例子(当然这不是MPD的功劳)。实现起来所需要的时间远远小于我的预期。
说了那么多,其实我的OSD Lyrics也有几个模块可以分离出来作为一个库了,代码也需要有更多的文档注释。
MOC也是一定会支持的。不过如果被迫使用管道的话,我担心效率会怎样……
好久没更新blog了,不过OSD Lyrics的改进一直没停下来。
自上一篇blog之后,OSD Lyrics又有了以下改进:
接下来要做的有:
另外还有一个好消息和一个坏消息
好消息是,liangsuilong同学为OSD Lyrics提供了rpm源,现在用fedora的同学也可以直接从源里更新OSD Lyrics了:)
坏消息是,我从11月开始就连不上launch PPA的FTP了,也就是说,ubuntu的源无法更新了:(。暂时不知道是不是墙的问题,现在也没什么时间弄。
要期末了,估计进度又要慢下来了
OSD Lyrics的一个最大的问题就是它的单进程单线程模型,这使得它在搜索和下载歌词的时候会阻塞正常的歌词显示,导致在下载未完成时切换歌曲会出现无响应的情况。
由于基础太弱,这个问题一直被我搁着,直到把APUE的前17章都看了一遍,对Unix系统下多进程/线程有了大概的了解,终于开始着手改造下载模块了。为了不影响主分支的开发,我在之前就已经创建了一个branch用于尝试异步下载的实现。
本着KISS的原则,第一个想法非常简单:
这个想法,如果不涉及GUI,是可以很好地工作的,但是一旦要显示一个GUI让人选择要下载的歌词,就直接崩溃了。看来X相关的东西不能直接fork来多弄出一份,不过这也是很自然的。
看来之前建了一个branch是明智的,这个要折腾的时间确实比较长。
于是接下来似乎只有两条路可以走了:
又要开始看文档鸟>_<
其实在看到 lrcdis 增加对 Exaile 0.3 的支持的时候就知道 Exaile 0.3 的 dbus 接口改了,但是一直很懒,而且有不少事情要办,再加上没人来问,就放在一边不管了(其实就是这家伙懒嘛)。
但是终于还是有人问了,于是终于有动力去做了。Exaile 的文档做得不太好,没有 dbus 接口的文档,于是只能看源代码了。Exaile 的 dbus 接口代码在 xl/xldbus.py 里,因为是用 python 写的,所以很易读。
顺手把 xmms2 的支持给打开了。其实 xmms2 的支持早就好了,但是我没装,也就没测试,所以就把代码屏蔽掉了。现在还是没装,等用户反馈吧,嘿嘿。
最近一直没动 OSD Lyrics,一直想做的分离下载也没做,过几天再说吧,嗯。
PS:发现最近的blog全都是 OSD Lyrics 的更新,我果然是懒了啊……
Songbird本身似乎没有IPC支持,不过却有一个MPRIS扩展,因此理论上来说通过MPRIS来支持Songbird是很简单的,因为在实现Audacious和Amarok 2的支持的时候就已经把MPRIS支持模块提取出来了。事实上,Songbird的支持代码早就已经写好了(Audicaous的代码copy过来改个接口名就行,能不快吗),但是运行时却总是会出现段错误,因此一直没能实用。
因为之前同样是使用MPRIS协议的Audacious和Amarok2跑得很正常,以为是Songbird的MPRIS实现有问题导致的,昨天一查才发现我在获取歌曲信息时对音轨数据进行了不必要的内存释放。估计是Audacious和Amarok2都没有传递这个数据才幸免于难。
这样一来,OSD Lyrics又多了一个支持的播放器。想用Songbird的同学不要忘了安装MPRIS扩展。