在FreeBSD上装linux skype 4

折腾了一个晚上好歹是搞出来了……
起因是把linux_base更新到了linux_base-c6,也就是CentOS 6。这是个相当新的版本,各种rpm找起来也比较容易。
但是问题是,ports里其他linux包基本都是老的,要么是fc4的,要么是f10的…… 反正用不了。于是统统删掉。但是这样linux程序基本就跑不了,缺很多包……
于是就自己搞rpm来装。但是rpm这回又不好使,试了ports里rpm3/4/5貌似都不行。
怀疑是因为rpm调了cpio,而FreeBSD 8-STABLE里的cpio貌似还不支持xz? 因为CentOS那些新包是xz压缩的,于是就解不了。
后来干脆不管rpm了,搞个脚本:

if [ ! -f $1 ]; then
        wget http://mirror.yandex.ru/centos/6.2/os/i386/Packages/$1
fi
rpm2cpio $1 | unxz > ~/tmp
cd /compat/linux
sudo cpio -idv < ~/tmp

加上包文件名,就能自动下载安装…… 坏处是没有rpm帮着记录包信息了…… 以后删起来会很麻烦…… 再说吧……

首先搞下来skype 4.0.0.7的动态链接版本,解压后尝试运行,一堆链接失败…… 拿ldd看一眼,然后逐个去rpmfind找包名,用那个脚本装…… 装了几十个包,好歹差不多可以跑了。主要是一堆libX*,qt,dbus,… 但是没声,也不能输入中文。

首先是声的问题,选项里声音设备连OSS都没。额外装了alsa-plugins-oss,还是没有OSS设备。尝试装alsa-plugins-pulseaudio,pulse设备倒是出来了,但是启用就出问题…… 研究了下,貌似alsa要配置,改了asound.conf,引入oss配置文件,再进去,好歹声音正常了。

然后是中文。本来xim配fcitx应该就可以,但是不知道为啥不行,切不出来。
于是我想看看有啥问题,尝试写了个LD_PRELOAD用的hook XOpenIM的库。native的BSD程序测试没啥问题,找个Linux机子编译,那儿测了也没问题,但是弄回来,就没有反应…… 为了确认,我又hook了XOpenDisplay,结果这次更奇怪,貌似会陷入无限递归一类的情况,反正LD_PRELOAD不成,xim也没法调。

那就转而用fcitx的qt im module。csslayer说fcitx的im module是用dbus通信的。试了一下linux的dbus-monitor和qdbus,貌似都连不上。根据ktrace,貌似是linux程序在bsd下连unix socket会有问题?…… 于是转而研究能不能用tcp socket。改了dbus配置,让它也监听tcp端口。然后dbus-launch出来个shell,确认环境变量之后,linux的qdbus还是不行…… 这回貌似是dbus那边的问题?反正看来dbus服务器自己只能用unix socket才能好好工作?…… 最后参考dbus源代码,改了之前写的一个tcp转发程序,搞了个dbus专用tcp socket转发unix socket的程序。dbus的第一个包带有认证信息,是用sendmsg发的,还要特殊处理…… 这个日后再说吧,反正搞出这个程序之后,篡改DBUS的环境变量,把环境变量DBUS_SESSION_BUS_ADDRESS里的unix:path=/var/tmp/dbus-zGrz0qEmd9改成tcp:host=localhost,port=xxxx,再启动linux的qdbus…… 好歹是连上了。

搞定dbus之后,尝试搞输入法模块本身。貌似centos上根本没有fcitx的包,于是搞了fc15/17的,反正应该关系很近嘛…… 结果就是不出来,右键菜单里没有fcitx这个。后来问csslayer,他说可能是qt版本问题,说插件编译环境qt版本要比当前qt低才行。centos的qt是4.6.2,然后抓了几个fc15/16/17的fcitx-qt4下来,发现都依赖4.7或者4.8+(用strings看,能看见version=4.8.0之类……)。先尝试篡改一把,用二进制编辑器改成4.6.2,倒是能载入,但是会有找不到的qt的符号…… 换个办法,去找centos更新的qt4,倒是找到了,在http://pkgs.org/centos-6-rhel-6/russian-fedora-free-updates-i386/有qt4 4.8.0。搞上这个qt4,再找个依赖qt4 4.8.0的fcitx-qt4…… 还是有问题,不过这次是linux的fcitx-utils版本太老…… 更新之后,又说我本机fcitx太老…… 更新本机fcitx之后…… 终于可以输入中文了!……
之后搞了个脚本自动篡改dbus地址,启动转发……

#!/bin/sh
 
export QT_IM_MODULE=fcitx
/home/henryhu/src/tcp2unix &
 
DBUS_ORIG=$DBUS_SESSION_BUS_ADDRESS
DBUS_NEW=`echo -n $DBUS_ORIG | sed -e 's/unix:path.*,/tcp:host=localhost,port=8888,/'`
export DBUS_SESSION_BUS_ADDRESS=$DBUS_NEW
 
/home/henryhu/soft/skype-4.0.0.7/skype

于是终于可以好好用skype了……

APK相关工具

昨天搞的时候很混乱,今天整理了一下,搞了个图:
APK (de)compile
具体可以参见wiki:
APK (de)compile

大致上有很多办法可以改一个apk,总的来说有两个办法,一个是搞成 Dalvik VM 的汇编,然后改。一个是先从 Dalvik VM 的字节码搞成 JVM 的字节码,然后搞成 JVM 的汇编,甚至搞成 Java source code,然后改。改完了再倒回去,重新签名就是新apk了……

另,Dia 真好用……

自己动手,丰衣足食

最近虾米Android用得不甚顺畅。主要问题是,拿着虾米Android听电台,听着听着,自动就关了……
一般都是在启动别的程序的时候挂掉。各种程序都可能引发,用内存多的概率更大。有时候没动手机也会自己挂掉。
之前就怀疑是Android低内存自动杀进程功能干的。近日研究了一下:

首先,用ps | grep xiami列xiami进程。每次挂掉之后,列进程可以发现com.xiami那个进程不见了。过一会儿,这进程又会被重新启动起来。但是音乐播放不会自动恢复。
看系统信息里面,虾米总共开了俩服务,一个是push message receiver,对应ps里面那个com.xiami:push。另一个就是music play service,对应那个被干掉的com.xiami。

我印象里,Android里面开了服务,就不应该会被干掉,至少会比那些后台程序更晚被干掉。利用adb shell dumpsys meminfo,可以发现后台还有好多进程啊……
仔细看一眼dumpsys meminfo的输出,可以发现当从xiami切到别的程序之后,xiami被列在了Background分类里,而其他的还有A Services和B Services。xiami占的内存也不少,系统想杀进程的时候,挑大的杀,于是就杀了虾米……

于是首先是这么个问题:既然xiami开了服务,为啥会被扔到Background里去?继续观察,可以发现xiami被杀并重启之后,居然跑到了A Services分类里…… 这样就不会被杀了,问题是音乐也停了……
为了研究到底杀了重启是啥样的过程,我去网上找了个ServiceDemo。实验后发现,对于一个普通程序,就算开了服务,切到后台也是Background。等到内存不够,系统会先杀了你,之后再启动一个进程来仅仅跑服务。这样或许Activity那边的内存就能释放了?一般杀之前不调onDestroy(),杀完重启的时候调onCreate()和onStart()。所以要适应这种情况,可以在onStart()里处理被杀了重启的情况,并且继续之前的工作。

但是我觉得其他音乐播放器不是这样啊…… 于是随便找了个音乐播放器,开了之后切到别的程序,再看meminfo。这样可以注意到,那个程序并没有被扔到Background里,而是在Perceptible里面……
于是去网上看啥样的程序会变成Perceptible。虽然找到了ActivityManager里面IMPORTANCE_PERCEPTIBLE这个常数定义,但是没有哪儿告诉我咋样可以变成Perceptible。
既然这样…… 只能看代码了。正好机子上有之前下下来的Android 4.0代码,在里面找PERCEPTIBLE,于是在frameworks/base/services/java/com/android/server/am/ActivityManagerService.java里面找着了。
貌似成为PERCEPTIBLE的条件是,某调整值>=PERCEPTIBLE_APP_ADJ。但是继续看代码,发现这个调整值会因为各种理由而决定……
通过继续研究dumpsys,发现还可以dump别的,例如dumpsys activity就可以dump出activitymanager这边的信息。在这个信息中,就包括了调整值设置的原因。观察那个靠谱的音乐播放器,发现他Perceptible的理由是foreground-service。

其实如果熟悉Android Service的话,或许已经知道这是啥了。但是我不太熟嘛…… 继续看ActivityManagerService,发现这个原因的触发条件是有foregroundServices,而这个值唯一会变成true的地方只有updateServiceForegroundLocked(true)。而这个东西会在setServiceForeground()里被调用。这个东西貌似只有外部引用。到这里我终于跑到网上去搜foreground service…… 发现Service类有个startForeground()方法,那基本上就是这个了……

根据Android的描述,普通Service还是会被干掉的,只是日后由内存了可能会重开你。但是Foreground Service是这种只要你一干掉用户立马会有反应的,所以会尽可能不去干他。
我在ServiceDemo里加了startForeground()调用,果然他也成了Perceptible,咋样都没被干掉……
那,虾米是不是没有调用这个startForeground()呢?我跑去搞了个最近看见的Android反编译器,ded,是PSU的人做的…… 反编译出来,貌似的确没有startForeground()。但是有一个对setForeground()的调用,可惜是false。另外,在新版Android里,setForeground()已经没用了……

这样就知道是虾米写得烂了,我试了几个音乐播放器,都会变成foreground service…… 不过倒是有个也败了的,那就是豆瓣……
既然如此嘛,要不改虾米,要不改系统。我尝试修改了/proc//oom_adj,但是貌似系统不care这个。我尝试修改了/sys/modules/lowmemorykiller/parameters/minfree,改小了点,希望系统不要没事情杀进程。但是问题依旧,而且chrome启动还更卡了……
那只好改虾米。那个ded貌似反编译有些问题,编译回去也不太行。上网上找找apk反编译,找到了apktool。
这个东西能够把apk完整的反编译成dalvik VM的汇编码,还能编译回去,真是不错……

于是反编译出来,找合适的地方加。因为startForeground()还需要提供一个Notification,而虾米本身就会显示Notification,我希望最好还是用虾米原来的。
找了一下,发现虾米的Notification都是NotificationsUtil管的。里面有个notifyOnGoing(),貌似就是显示平时歌曲信息那个Notification的。于是我篡改了NotificationsUtil.notifyOnGoing()里面显示Notification那句。startForeground()是Service类的函数,需要Service对象,正好MusicPlayService被传了进来,于是我直接把

    invoke-virtual {v2, p3, v3}, Landroid/app/NotificationManager;->notify(ILandroid/app/Notification;)V

改成了

    invoke-virtual {p0, p3, v3}, Lcom/xiami/service/MusicPlayService;->startForeground(ILandroid/app/Notification;)V

其中,p0是函数第0个参数,v2, v3都是dalvik的寄存器。恰好notify()的参数含义和startForeground()匹配,我就直接用了……

用apktool再打包,签名,装进去…… 挂了,说verify失败:Context类对象不是MusicPlayService……
其实是因为MusicPlayService作为p0传进来的时候进行了向上转换,变成了Context对象,直接在上面调MusicPlayService的方法是不行的……
往上看几行,看见一个到NotificationManager的类型转换。恰好我拿来用:把

    check-cast v2, Landroid/app/NotificationManager;

改成

    check-cast p0, Lcom/xiami/service/MusicPlayService;

再打包扔进去,居然就好了…… 而且com.xiami也是Perceptible的了!试了一下,咋搞都没被杀掉,看来是搞成了。

总结
1. 如果是需要一直活着的服务,例如音乐播放器,那么要调用startForeground()。或者好好处理onStart()。 另外,setForeground()已经没用了,源代码里就是个空函数,还附带一堆comment说为啥弄成了空的。
2. 虾米的Android版很烂,本来就觉得听电台听个10首就开始循环就令人不爽了,还有这种毛病。 源代码里还随处可见调试信息拼写错误,当然,这是另外的事情了。
3. Android的反编译/编译很简单,工具也易于获得。基于寄存器的dalvik VM的汇编很容易理解,也很容易改。看来改一下打个病毒或者广告进去也很容易,而且貌似已经有人这么干了。
4. 我仍旧觉得,虽说符合说明,但是含有Service的进程优先级还是应该比普通的高一些的,不应该杀进程的时候和别的进程一样对待。 貌似我老手机上的CM7就会区分?如果Android能改改倒是不错。
5. dumpsys功能强大,貌似还有很多没发现的功能。直接运行能出来好多好多东西。

PS. 我发了之后,xiami那边说下个版本就会修这个…… 所以就不提供修改版了……
PPS. 我觉得一时半会下个版本出不来。 115网盘

忘了重定向stderr导致的某问题

最近某python程序常常抛exception。而且比较神奇的是,貌似这个exception是一层层抛出来的。
先是最内层except块打的log,然后是次外层,然后是再外面……
我本来写的代码并不会重抛异常,所以这很神奇……
except块大约是这样的:

except Exception as e:
    Log.error("send error: %r" % e)
    traceback.print_exc()

能看见第一行的log,但是之后print_exc()的结果就没了。下一行log是更外面的except块的log……

于是这个大概就是print_exc()抛的异常,但是这个东西能抛啥……
之后想了一下,就算是异常一路抛到最外面,也应该有个backtrace,而log里啥都没有……
想想backtrace是打到stderr的,难道stderr没有重定向……
之后再看了一眼,log是打到stdout的…… 再去看启动python程序的脚本,貌似只重定向了stdout,忘了重定向stderr了……

难道断了的stderr不能写?我又写了个小程序ssh到服务器上跑:

#!/usr/bin/env python
 
import sys
import time
 
time.sleep(10)
 
try:
    sys.stderr.write('test\n')
except Exception as e:
    print "Exception: %r" % e

python test.py > tmp.txt &

跑,跑起来就断ssh。过会上去一看,果然出了个Exception,IOError(5, ‘Input/Output error’)……

到这儿差不多就弄清楚了。因为我是ssh到服务器跑程序,所以弄了个脚本重定向输出的log文件里。、
但是脚本写错了,没有重定向stderr。所以stderr接到了console上。
等我把ssh一断,stderr另一头就断了。
过了若干时间之后,程序出了个exception。在print_exc()的时候,程序尝试写stderr。但是因为stderr挂了,抛了个IOError。
因为每层except都调了print_exc(),每层都抛出了个新的IOError给下一层……
于是就一路抛到了最外面……

既然知道了就好修。
首先嘛,自然要修正脚本,把stderr一起重定向了。
其次嘛,虽说不是必要,不用print_exc()转而用format_exc()然后扔给Log.error()了。
理论上log都应该直接打到文件,或者至少错误类消息应该打到stderr去。不过反正是服务器程序,stdout也没啥用,再说……

当然,这些都没修正根本问题。其实最初的exception是意料之中的,不过还是应该修正一下让它不抛才是……