善意提醒

如果您打开本站很慢,布局排版混乱,并且看不到图片,那么可能是因为您还没有掌握用科学的方法上网的本领。

2025-08-16

Debian升级trixie踩坑记

图片来自网络

Debian前不久刚刚发布了Debian 13,也就是代号为trixie的版本。本周一上班后,从Repo的变更看到了这个消息,我就进行了升级。

之前已经把我的所有Debian环境统一到bookworm了,也是不久前的事情。当时Stretch和Buster已经停止维护了,Bulleyes还是oldstable状态。我只有两个环境是bookworm,于是一咬牙把所有的环境都升级到了bookworm,也踩了一些坑,下面合在一起说。这次确实没想到来trixie得这么快,也这么巧,趁上次坑里的屎还是热乎的,也就再咬一回牙了,反正我的牙也不是自己的。

简化的正常升级流程

首先强调一点,升级不要跳版本,从低往高一级一级地升。每次升级解决这一次的问题,然后再往下走。我这次是从bookworm往trixie升,只需要升级一次。如果是jessie,那么就要stretch->buster->bulleyes->bookworm->trixie,以此类推。
历史版本代码参见官方说法,最好是看英文版,更新最及时。

升级过程完整的指引,最好参见Debian官方文档(有中文版,但翻译得很不好,很多没翻。还是看英文版吧,Google翻译或喂AI都行。拜托了,都2025年了)。但如果要简单说的话,要点只有以下这些:

  1. 先把当前版本升级到没法再升。
  2. apt update
    apt upgrade
    
  3. 去/etc/apt/sources.list里面,把版本代号改掉。去/etc/apt/sources.list.d下面,把文件里面的版本代号都改掉。
    这一步原则上可以用shell命令来做,但其实还有non-free-firmware之类的小点,依赖别人的脚本并不是好事情,所以我也就不贴了。我觉得还是讲个原则,手动操作吧。必要的时候去参考已有的新版OS。
  4. 正式开始升级:
  5. apt update
    apt full-upgrade
    
  6. 对选项作出反应:
    1. 升级过程中要不要重启服务,可以选yes。
    2. 要不要覆盖旧版配置文件,自己看吧。如果不记得以前改了哪些就建议选N了(特别是针对sshd),建议还是自己「合并」看看。
  7. 如果升级顺利,重启。
  8. reboot
    
  9. 清掉不再需要的包。
  10. apt autoremove
    

坑一:升级过程中ssh连接中断了

是我自己的锅。正在升级的时候,来了同事问我问题,作了一番长篇演说以后,忘记了这事,去忙别的了。回头想起来时,发现ssh已经断了。

ssh断之前应该是在apt full-upgrade的过程中,等我回答某个配置文件如何处置。赶紧再连上,apt full-upgrade继续,发现被锁。
遵照提示:

dpkg --configure -a

得以继续。
随后提醒自己下次记得集中注意力。

坑二:配置文件备份

升级完后发现有些不对,检查了一下,发现有配置文件被dpkg给备份起来了,后缀是dpkg-bak,改名回来解决。
可能还是第一个坑的后续。

坑三:haproxy启动不起来

算是一个小坑,因为报错信息在日志中写明白了。
我的haproxy是早期版本安装的,需要在haproxy.service中[Service]节内增加LimitNOFILE=524288的设定,不然ulimit -n有问题。

对Linux不太熟的朋友多说一句:haproxy.service一般在/etc/systemd/system/multi-user.target.wants目录下。当然也可能在别处,端看你之前是怎么安装的。实在找不到的话,去问问AI吧,或者装个mlocate。

[Unit]
Description=HAProxy Load Balancer
Documentation=man:haproxy(1)
Documentation=file:/usr/share/doc/haproxy/configuration.txt.gz
After=network-online.target rsyslog.service
Wants=network-online.target

[Service]
EnvironmentFile=-/etc/default/haproxy
EnvironmentFile=-/etc/sysconfig/haproxy
BindReadOnlyPaths=/dev/log:/var/lib/haproxy/dev/log
Environment="CONFIG=/etc/haproxy/haproxy.cfg" "PIDFILE=/run/haproxy.pid" "EXTRAOPTS=-S /run/haproxy-master.sock"
ExecStart=/usr/sbin/haproxy -Ws -f $CONFIG -p $PIDFILE $EXTRAOPTS
ExecReload=/usr/sbin/haproxy -Ws -f $CONFIG -c $EXTRAOPTS
ExecReload=/bin/kill -USR2 $MAINPID
KillMode=mixed
Restart=always
SuccessExitStatus=143
Type=notify
LimitNOFILE=524288

# The following lines leverage SystemD's sandboxing options to provide
# defense in depth protection at the expense of restricting some flexibility
# in your setup (e.g. placement of your configuration files) or possibly
# reduced performance. See systemd.service(5) and systemd.exec(5) for further
# information.

# NoNewPrivileges=true
# ProtectHome=true
# If you want to use 'ProtectSystem=strict' you should whitelist the PIDFILE,
# any state files and any other files written using 'ReadWritePaths' or
# 'RuntimeDirectory'.
# ProtectSystem=true
# ProtectKernelTunables=true
# ProtectKernelModules=true
# ProtectControlGroups=true
# If your SystemD version supports them, you can add: @reboot, @swap, @sync
# SystemCallFilter=~@cpu-emulation @keyring @module @obsolete @raw-io

[Install]
WantedBy=multi-user.target

坑四:其它软件还没提供trixie源

我周一把Debian升级到trixie的时候,顺手把Nginx/PHP/Redis在/etc/apt/sources.list.d下面的Repo也给改了。然后就发现PHP已经有了trixie源,但Nginx还是404,Redis是403。

截止本文刊发的时候,Nginx已经OK了 ,Redis还没好。如果后续有变化,我就来把这一段编辑掉。

坑五:GFW捣什么乱

这个就坑我大发了,几乎可以专门写一篇。不过我还没完全弄明白,以后搞清楚了再说吧。

我家里的网络访问Debian官方源不算快(应该说很慢),我又不想改源,于是用了SS代理来「加速」。
apt update的时候发现,Debian源没问题,但Nginx和PHP一直连接超时。区别就在于,Debian源是http,后二者是https。
用curl -x -I简单试了一下,https还真是走不了SS代理。http就可以。

如果答案是「GFW检测并干扰了TLS over SS」,那我没话说。我知道它有这能力,TLS的确特征明显,尤其是握手时。这可能也是最合理的一种解释了。
但我HTPC上有一台VM使用自己的ss-local是OK的。如何解释?同样的OS版本,同样的SS版本,同样的网络环境、节点、线路、协议、密钥、插件,同样的payload和访问特征,连TTL和MTU我都看了,实在是想不明白。

有问题的VM只是说TLS握手出问题的概率很高,但并非100%。我开了Verbose猫在两端看了一下,发现有问题的时候貌似有replay attack。我拿不准,但看起来像。两端的节点都换过,没有什么差别,该replay还replay。

在公司没问题,在家里有两台都有问题,可见应该跟家里接入的上海电信宽带有关。然而家里HTPC上从Windows去用那两台上的ss-local/privoxy作代理都没有问题,在我遇到问题的时候,儿子还在欢看YouTube呢,线路没被封。SS是AEAD版本,cipher不对时的反应是No Data Transfer,GFW肯定拿不准。

那台没问题的VM就是不会引发,100%地OK,让我始终想不通的终归还是这一点。在SS上面再叠一层tor,也是OK的。最后不得已,我让apt走了tor。没去试SS over FRP的方式。先这样吧,也没慢多少就是了。

2025-08-15

能用的东西就别动!

一直以来,以为只有程序员需要在意这句话,然而今天我也遇上了。

图片来自网络

用「夸克云盘」下电视剧的时候,一直弹框提示我升级。升级了以后发现,超过云盘容量的内容,即使是第一次也无法保存了。回退到3.20.0版本,又可以做到了,看来这个逻辑是客户端在控制,而新版已经把这个「Bug」给堵上了。

曾经也吃过这种亏,只能说我「好了伤疤忘了疼」。以前用「阿里云盘」,也是一直提示升级。有一天升级完以后直接进不去了,应该是那个版本的EXE跟我的OS之间出了些问题。但进不去也就意味着再没机会修正,而且官网上也只能下载到这个有问题的版本,于是我只好又去找了旧版的安装包。还好网上还能找到,存起来当「传家宝」,现在也还能用,但再也骗不到我升级了。

而且这个4.x版的「夸克云盘」客户端,还把我的默认浏览器给改了。我一开始还纳闷,你个网盘客户端,动我WebBrowser干什么?后来进去后发现它其实就是一个浏览器了。好吧,中国人还真是好收拾。

我以后什么客户端都不敢升级了,起码国产软件得是如此。我已经给它们「最惠国待遇」了,单独放在一个虚拟机里面跑,没想到还是不够。当然,说起来它们也是可以搞「强制升级」的,希望架构师忘记了这一出。但它们也可以搞一个「以旧换新」,等旧客户端市占率低了以后就把服务掐了。没办法,谁叫我有求于人呢?只希望这一天晚些到来,而我动作要加快了。

这次下载到的3.20.0版的「夸克云盘」客户端,我也会当作「传家宝」存起来。有需要的可以从这里来拿。我找到的地方本身也是一个「夸克云盘」的资源网页分享,没有安装客户端就无法下载。搞墨比乌斯没意思,我就直接放Dropbox共享链接了。

再强调一遍:能用的东西就别动!

2025-08-08

惊闻Pocket关站

今天用Pocket的Chrome插件把两篇文章收藏了起来。收藏过程比较慢,引起了我的一点点注意。想起已经很久没去整理过里面的文章,于是打开了网站,才惊讶地发现一个月前它已经宣布要关闭了。

图片来自网络

还好留了三个月给用户导出数据,10月8日截止。赶紧去申请,收到邮件一看,一个不到700行的csv,压缩了还没有50KB。说我数据少吧,600多篇文章也不算少了。但下载附件的时候我就心里一惊:估计只有标题和URL,那些爬下来的内容,怕是都没了。

我当初用Pocket不用Delicious(顺便说一句,Maciej Cegłowski现在连更新Let's Encrypt的SSL证书的acme.sh脚本也懒得去放一个了)的原因,其实就是因为Delicious只保存链接,不存内容。「社交」是我并不需要的功能,「推荐」也是。Pocket后来渐渐地也很多页面都爬不下来内容了,所以我也渐渐远离了它,只是通过Chrome插件往里面扔那种「将来可能会有用」的东西,但再也不去看。一个网站若是用户都是我这种样子,关闭也是必然的,我应该早有心理准备才对。

当今「互联网」的一个很大的问题,就是许多网页已经消失了,不见了。有伏地魔的原因,有赵公明的原因,也有乔布斯的原因。不管怎么说,事实就是如此。我试了我导出的数据里面比较古早的几个URL,都打不开了。有的虽然页面没了,网站还在,还有的甚至连网站都没有了。我现在甚至开始有点怀念Evernote,但它其实也没有多好用。Web不管几点零的时代,好像都已经过去了。

我现在转到了Instapaper下面,刚开始用,还没什么心得。尝试导入Pocket的数据,Instapaper告诉我还要等几个小时。从早上到现在也还没动静,我觉得这是好事。花的时间多,意味着它真的会去爬内容,否则只是URL应该几个毫秒就结束了。

Instapaper的Premium会员不便宜($5.99/Mo,$59.99/Yr),比Pinboard的含永久存档和全文检索功能的会员($39/Yr)还要贵。总之对于我而言都有点肉疼,因为我还买了Medium和Dropbox,而且普通中国人都比较穷。

让AI对比了一下,我最后如果要成为付费用户,可能还是会去考虑Pinboard吧。我的核心需求是永久存档,全文检索也是羡慕的,笔记并不是必须的。毕竟Instapaper免费也不是不能用。

而且,见鬼,我对Maciej Cegłowski的不少理念还挺认同的。

2025-08-05

是谁动了我的HDC?

最近工作上倒是查了不少问题,然而到了末尾都发现只是一些低级错误,完全不好意思拿出来说。但今天遇到一个,还算有一些意思,可以讲讲。


问题的表现是我们的软件上某些部位偶尔会「花」掉,显示的是之前覆盖在上面的内容。有经验的程序员一看就知道是GDI的问题,但到底是什么问题呢?

图片由Google Gemini生成

刚开始的时候,以为是GDI泄漏了。曾经有过一个例子,漏到了好几千,后来到了1万,触发了CEF的CollectGDIUsageAndDie被Dump了。还没到1万的时候,界面上的表现就是开始花,其实就是有部分GDI函数已经开始调用失败了。

然而这次并不是,GDI对象数很正常。开发人员远程调试跟了一下,发现是HDC拿不到。CreateCompatibleDC()得到了一个NULL,因此MemDC创建不出来。


什么情况下CreateCompatibleDC()会调用失败呢?GDI对象数不多,内存也很充足。软件中其它部位的MemDC是能正常创建的,因此全局性的因素都可以排除。什么光栅设备、显卡驱动之类也就不可能是原因了。
除此之外,就只剩下了一个可能性:CreateCompatibleDC()传进去的HDC参数有问题。

试了一下,乱传一个不存在HDC,的确会导致CreateCompatibleDC()返回NULL。如果传的是个NULL进去,倒是还好一点。看来是窗口上的HDC有问题,当然也有可能是窗口本身就有问题。然而其它地方也在用同一个HWND创建MemDC,一切正常。所以还是某些HDC有问题。

HDC的值看起来并不奇怪,也没有什么好办法确认它到底有什么问题。HWND还能用Spy++来看,HDC我是一筹莫展。还好可以OutputDebugString。日志打出来,有意思的地方来了。

我看出问题了:当某个HDC失效的时候,它一定是被连续拿到过两次,中间没有ReleaseDC()过。这两次GetDC(),都得到了同一个HDC,肯定不正常。
我把this指针和HWND的值也加到了日志里面,这下看得更清楚了。两次GetDC()分别是不同的HWND。而HDC失效之时,就是当它被最终ReleaseDC()的时候。只不过这次ReleaseDC()的HWND是一个旧的窗口,那个窗口此时应该已经被销毁了。


窗口OnDestroy()的时候,没能把它的HDC一并归还,这当然是我们软件中的代码错误,也是我们遇到的问题的直接原因。但故障现象的根本原因是什么呢?我们确实没有按照规范「一借一还」,至少窗口还「在世」的时候没有。不过ReleaseDC()难道不管三七二十一,只要有人拿着某个HDC来释放,它就答应吗?都不用看看HWND对不对得上吗?

微软关于ReleaseDC()的API函数说明在这一点上就有点语焉不详了。大概它没想到有人会这样去实践?的确也没人问这种问题,完全找不到资料,只好自己动手做了个实验:

HDC hDC = ::GetDC(hWnd);
int nRet = ::ReleaseDC(hWnd + 0x1000, hDC);

随后再在这个DC上用GDI函数画东西,确实画不出来。nRet也的确是1,按照微软的说法,返回值1表示DC被释放,这倒是没有骗人。我把hDC也加了个数字,然后再跑一遍,这次nRet变成0了。

所以说,微软是在ReleaseDC()的时候搞了个「容错」逻辑?只要HDC对得上,就给释放,不管HWND对不对得上号?我一开始跟Google Gemini探讨这个问题的时候,它还不相信,直到我告诉它测试结果。


回过头来看,为什么两次GetDC()能得到同一个HDC呢?
我们的主窗口,经历了销毁后重建的过程。在OnDestroy()的时候,没有及时执行ReleaseDC()。但Windows可能认为,窗口不在了,DC也就没了。于是另一个新建起来的窗口又通过GetDC()拿到了同一个HDC。等到主窗口的C++对象开始析构,调用ReleaseDC()的时候,新窗口拿到的HDC就被背刺了。系统的DC应该是在放一个池子里面,所以是有可能被重用的。这当然需要「运气」,也正因为如此,故障现象不是很稳定。

会遇到这种问题的人,应该不多。现在还在用GDI做开发的项目本来也就不多了。我在网上没能找到什么可以参考的信息,还好勉强算能够重现,就抓住机会解决了问题。经验值又+1了。

最后说明一下:我这个实验是用VS2013在Win10下面做的。不同的OS以及VS版本可能会有不一样的情况。毕竟是所谓的「未定义」行为。