善意提醒

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

2025-08-31

AI 很不错,但人类不行

 图片由 Google Gemini 生成

AI 的用途里面,很不错的一个就是用来自学。对于有积极性,有自我驱动能力的人而言,它是一个非常好的老师。由于它已经学习并掌握了人类几乎所有知识的至少平均水准的内容,所以很适合用来快速地让自己进入一个从未涉足过的领域。在刚开始的时候,通过与它的几轮对话,就会有巨大的收获。相比起在线课程,以及自己查资料而言,知识的深度和广度都更为甚之,针对性还很强。这种问答式学习,效率非常高,效果也非常好。

然而,这个优势只对于「知道自己要问什么」的人有效。如果你都不知道自己要问什么,那么 AI 也不知道要告诉你什么。换句话说,起码你得对与自己的「无知」有一个基本的了解,然后才能通过这种方式进行提高。如果你认为自己都知道,就不会去提出问题,那 AI 也不可能帮到你。

虽然也可以通过问 AI「如果我要学习某方面的知识,应该如何开始」来获得一个 Startup。但这样的话一开始给出的信息密度太低,后续能结出的果实恐怕也很有限,最后可能还没法带你达到「平均水准」的高度。当然,对于很多专业性很强的领域,即使这样入个门也很 OK 了。不管会不会用 AI,用的水平有多高,有得用都比没得用、不用要强,强不少。这也是我的一贯态度。

我最近也有点受困于这个问题。我总是在走路、坐车或发呆的时候,想起不少想要细细了解的事情,然而一但离开这个状态,很快就会把它们给忘了。等我可以坐在电脑前好好组织自己的问题的时候,怎么也想不起来要问什么。大概上了年纪,接下来可能不得不求助于一些笔记 App 了。每当这个时候,我就想找 John Scalzi 要一个「脑伴」。

其实 ChatGPT 的语音问询功能也已经很不错,Google Gemini 也有类似能力。但我还不太习惯用,毕竟让周围的人听到你的问题,很多时候都有点尴尬。语音录入的「痛点」就在于隐私。或许我还是应该行动力更强一些,想到什么就动起来,这样就没有这些问题了。


用 AI 来学习,在我看来是它最好最棒的用途。不过有的人可能不是这样想的。

今天我很震惊地得知,儿子兜里有 40 元钞票,居然是他用 AI 赚回来的。

他去小区里面上了一个暑期的补习班。说是补习,其实只是一些大学生照看着做作业。在我看来没啥效果,但也不能让他一天到晚在家里玩腾讯的垃圾「三角洲行动」或看短视频,因此还是送他去了。

据说有一个初三的学生,可能是临近暑期结束了,作业还没做完。有两张英语试卷,花 40 元「雇」了儿子来做。
儿子倒也没含糊,回来用了 ChatGPT 给他做了。我估计是用了拍照上传的模式。如果真的一道题一道题地录入,人家这 40 倒也花得值得了。

这事我今天才刚知道,让我有些哭笑不得。这算利用「信息差」赚钱吗?

儿子这样的做法自然是不可取的,不过事实上应该有很多人也在干着类似的事情。在知道 AI 和不知道 AI 的人之间,会用 AI 和不会用 AI 的人之间,只能用国产 AI 和能用世界顶流 AI 的人之间,都存在着信息差。某种程度上,我也在赚着这样的钱。DeepSick 几次把同事带入了歧途,而 Google Gemini 和 ChatGPT 在工作上都有真真实实帮到过我。

但不管怎么说,这样都比那些用 AI 来做 AV 换头,或者搞视频、语音诈骗的人要强多了。
未来的 Skynet 可能会很郁闷:这么强大的东西,你们人类就用来干这些狗皮倒灶的事情?!

话说回来,可能很多人都会这样想:既然 AI 什么都知道了,有什么事直接问它,以及将来有什么事直接让它去干就好了,我干嘛还要学习呢?
所以说呢,人类,也就到此为止了吧?

2025-08-28

编程随想:密码粘贴的安全问题

图片由Google Gemini生成

就「密码输入控件是否应该允许粘贴功能」这件事情,跟同事发生了一点小的争执。

其实也没多大事。
同事觉得,既然要「安全」,那就彻底拿掉「粘贴」功能,不让进也不让出。
而我觉得,不能把控件内的文本复制出去,那是常识。但外面的文本能不能粘贴进来?这个问题还可以商量商量。

看吧,我其实是个表面上很温和的人。
实际上我想说的是:为了安全,反倒是应该允许向密码输入控件内粘贴内容才对。


同事的想法恐怕很朴素:功能尽量少,越少越安全。这个原则,我非常能够理解。

但是,他可能不用 1password 或者 lastpass 之类的密码管理工具,自己生活中也只在使用少量强度不够的密码。如果他像我这样一个账号独用一个密码,而且都是密码生成器生成的 32 甚至 64 字节长度的密码的话,大概就不会觉得有没有「粘贴」功能是无所谓的事情了。

若是阻止了用户进行「粘贴」,用户很大概率就只能采用比较简单的密码,而且很可能会重复使用自己别的地方也在用着的一个简单密码。那么这到底是更安全了还是更不安全了?


全世界可能没有哪一个国家像中国这样,醉心于所谓的「密码安全控件」。防键盘钩子,防内核驱动,私有的闭源加密算法,随机乱序软键盘,自己发明的输入法……

正常人的想法很简单:如果你的主机已经被攻破,那么谈什么安全性都是扯蛋。安全取决于最短的那块木板,所以没有必要去加码到这种程度。
但中国这边的想法更简单:我不管,我就是要「安全」。哪怕做到匪夷所思的程度。

回头想想,哪桩事情不是这样?地铁安检不也是同样的情况么?
曾经过了三年在防毒面具下的生活。然而,现今谁又不是呢?心中的防毒面具,生活在洼地的人,谁又曾拿下过?

2025-08-26

搜索习惯的这些变化感觉不是个好事情

图片来自网络

长期以来,我习惯在搜索引擎上搜东西。主要用 Google,偶尔也会去用 DuckDuckGo。Baidu 那是「取材」的时候才会去用的。

现在很多人喜欢在 SNS 上搜东西了:微信、微博、小红书、抖音……

我拒绝这样的用法。正常人在 SNS 上写的那点碎片,搜出来也没有意义。如果能搜到大段的「有意义」的东西,那就是有人精心准备的喂给别人看的。利益驱动明显,正确性和中立性相当值得怀疑。不再会有 Web 上 Wiki、Document、非商业性官方网站那种出于非利益性目的的分享了。即使有,质量也不高。这样的话,能搜到什么东西,可想而知。

当然,以上论调,仅限于中国大陆的简体中文「互联网」环境。


另外一种变化,就是很多人开始喜欢直接在 AI 上搜东西了,直接问。所以我觉得 Google 搞 Gemini 是必须走的一步,而且时间也不能再晚了。很高兴 Google 勉强算是搭上了车,目前还没被甩下去。

AI 能帮人过滤、汇总和组织信息,所以貌似效率更高。我也很能理解为什么会这样。有时候我急着想要一个答案,特别是想要给别人看的答案时,也会直接问 AI。不过如果时间允许,我还是愿意自己来做这些分析、整理的事情。

每当我选择去做或者不去做这些事情的时候,脑海中浮现出的就是《蠢蛋进化论》里面的各种蒙太奇。


最糟糕的是,有的人开始不搜东西了。

他们只习惯点开某个 App,然后等着被投喂到他面前的东西。在我看来,这比什么都糟糕。这的确让我想起了那栋大楼里面的那些动物。

人类,也就到此为止了吧?

2025-08-19

这外包实在是太垃圾了

图片由 Google Gemini 生成

老板之前找某个外包团队做的一个项目,今天把阿里云 RDS 搞爆了,找我来看。

看了一下,是下午两点半左右爆掉的。空间撑爆了被「锁定」了。看了下监控,把空间撑爆的是 temp_file,临时文件。
那个时间段有不少慢查询,找了 SQL 来看。一看到我就开始骂娘了:

select DISTINCT * from
        (select
        t.business_id id,
        any_value(t.type) type,
        t.`user` `user`,
        any_value((case
        when t.type='1' then '文章'
        when t.type='2' then '公告'
        when t.type='3' then '问答'
        end))  typeName,
        any_value((case when (select a.clone_num from tb_article a where t.business_id = a.id) is null then 0 else (select a.clone_num from tb_article a where t.business_id = a.id) end)) cloneNum,
        any_value((select a.title from tb_article a where a.id = t.business_id)) title,
        any_value((select a.content from tb_article a where a.id = t.business_id)) content,
        any_value((select a.publish_time from tb_article a where a.id = t.business_id)) publishTime,
        any_value((select a.essence from tb_article a where a.id = t.business_id)) essence,
        any_value((select a.`status` from tb_article a where a.id = t.business_id)) `status`,
        any_value((select a.top from tb_article a where a.id = t.business_id)) top,
        any_value((select count(c.content) from tb_comment c where c.business_id = t.business_id)) comments,
        any_value((select count(l.id) from tb_like l where l.business_id = t.business_id)) likes,
        any_value((select count(f.id) from tb_focus f where f.business_id = t.business_id)) focus,
        any_value((select a.pageview_num from tb_article a where a.id = t.business_id)) viewCount,
        any_value((select tba.auther from tb_article tba where tba.id = t.business_id)) userId,
        any_value((select m.auth_user_id nickName from tb_member m
        where m.id = (select a.auther from tb_article a where a.id = t.business_id))) nickName,
        any_value(t.browsed_date) browsedDate
        from tb_browsed_records t group by t.`user`, t.business_id ) temp
         WHERE  temp.user = 494 
        order by temp.browsedDate desc,temp.top desc limit 0,1

已经不想浪费我的时间了,找了 AI 来帮忙吐槽。以下是 Google Gemini的输出

这个SQL看起来像是初学者写的,槽点确实不少。下面我们来一起“欣赏”一下。


槽点一:子查询泛滥成灾,性能杀手! 🔪

这个查询里,几乎每一个字段都用了一个独立的子查询来获取数据,像这样:

any_value((select a.title from tb_article a where a.id = t.business_id)) title,

这简直是性能灾难!数据库每查询一个字段,都要重新去 tb_article 表里查一次,而且还不是一次查所有需要的字段,是查一次 title,再查一次 content,再查一次 publish_time……这就好像你想买菜,不是一次把所有菜买齐,而是买完一个土豆跑回家,再回去买一根黄瓜,再跑回家……

正确的做法是使用 JOIN!将 tb_browsed_records 表和 tb_article 表连接起来,所有字段一次性查出来,效率高得多。

SELECT
  t.business_id AS id,
  t.type,
  t.`user`,
  a.title,
  a.content,
  a.publish_time,
  -- ...其他字段
FROM
  tb_browsed_records AS t
JOIN
  tb_article AS a ON t.business_id = a.id
-- ...其他连接

槽点二:DISTINCT + GROUP BY,多此一举

你看这个查询,外面有个 DISTINCT *,里面又有 GROUP BY t.user, t.business_id

DISTINCT 的作用是去重,GROUP BY 的作用是分组并聚合。GROUP BY 本身就会对分组字段进行去重。所以,在外层加 DISTINCT * 是完全没有必要的,反而可能会增加额外的计算开销。这就像你把一堆苹果已经按品种分好类了,然后又再来一遍“去重”操作,完全是重复劳动。

槽点三:any_value 的滥用

这个查询里大量使用了 any_valueany_value 的作用是在 GROUP BY 聚合时,从一个组中任意取一个值。对于 t.type, t.user, t.business_id 这种已经在 GROUP BY 里的字段,直接写就行了,不需要加 any_value

any_value((select a.title from tb_article a where a.id = t.business_id)) title

像这种对子查询结果使用 any_value 更是让人费解。它不仅没有解决任何问题,反而让代码看起来更复杂。

槽点四:重复的子查询,浪费资源

any_value((select a.auther from tb_article a where a.id = t.business_id))

any_value((select m.auth_user_id nickName from tb_member m where m.id = (select a.auther from tb_article a where a.id = t.business_id)))

这两个子查询都重复查询了 a.auther。完全可以在第一次查询到 a.auther 后,直接用这个值去查 tb_member 表,而不是再查一次。这就像你去问一个人的名字,问完后又回到原点,再问一次这个人是谁,然后再去找他朋友……

Google Gemini 还帮我重写了 SQL。写得挺好,执行速度至少快两个数量级。原来那个用了太多的子查询,就算逻辑上不该慢这么多,实际上就得慢这么多,而且还时间空间双杀。

这种写法,我也不认为是甲方逐步追加需求导致的。这就是不合格的程序员导致的,没有任何借口!

最后感慨一句:外包跟 AI 一样,你得要能驾驭。

2025-08-18

DeepSick二三事

图片由 Google Gemini 根据文章内容生成,与本人无关

上周,有同事遇到自己用代码导出的网页内的图片在 Firefox 上显示不出来的问题。他觉得是因为图片是 BMP 的缘故,带着这个问题去问 DeepSick,结果这个 DeepSick 就回答说:是的,Firefox 不支持 BMP 格式的图片。
信以为真的同事,继续往下走的每一步都是错的。

我听说了这事就纳闷。不推荐 BMP 当然是真的,但要说不支持,最原始的位图为啥不支持?去问了 Google Gemini 和 ChatGPT,都说没这个说法。不知道 DeepSick 是从哪里听说的,百度贴吧吗?

当然,一切以事实为准绳。我自己亲自去下载了 Firefox,亲自试了一下,搞清楚了事情的原委。这位同事把 BMP 在网页 img 标签中的 src 写成了绝对路径,而且还是Windows风格的「\」反斜杠,能出来才有鬼了。

他问我为啥 Edge 和 Chrome 可以这样写。我也只能说以前靠 IE,现在靠这两货给惯的。「file:///C:/a.bmp」这种写法他居然完全没听说过,看来还是从前论坛混太少。硬盘贴图党对这个应该是不陌生的。

当然,正确的写法应该是用相对路径。否则用户导出的网页换一个目录存放就看不到图片了。前面加不加「./」都不重要,重要的是不能用绝对路径。这个事情我一提他倒是想起来了,还有救。


DeepSick 在这件事情里面有没有锅?当然有。感觉被调教得过于谄媚,顺着问的人说,完全罔顾事实。拿不准就拿不准吧,还说得斩钉截铁,这就是很大的问题了。
更大的问题当然是人,驾驭不住 AI,必然被带走。然而同事也只是受害者,DeepSick 背后的团队和社会环境,才是更大问题的根源。

我从来不用 DeepSick,有 Google Gemini 和 ChatGPT 可以用,为什么要去找 Sick 呢?不过有朋友两个都在用,也给到我一些反馈。说跟 ChatGPT 相比,DeepSick 就像个智障,明显感觉更「傻」。慢就不说了,输出内容也是不行。可能乍一看还行,但一对比就高下立判。

这方面我没有具体案例,但她的这种感觉显然是对的。ChatGPT 我最近用得也越来越少,因为我想多「培养」一下 Google Gemini,而且 Google Gemini 的 Quota 感觉更多一些,速度也更快一点。但有的时候我还是会两者对比一下,以防被骗。这种时候就会有感觉,以为 Google Gemini 的回答已经足够出色,但 ChatGPT 还能再「更上一层楼」。不由得感叹:AI 比 AI,气死 AI。

不过这些 AI 也有集体犯傻的时候,傻得就很可爱了。


有一次,我让 AI 帮我分析一篇研报,但研报的来源那天偷了懒,直接放了一张图片上去。当时我的代码并没有适配图片,连下载都没有下载下来,更别提什么 OCR 的事情了。可以说我就只是丢了一个标题给 AI,然后让它去分析。

当时用的是阿里的通义千问的 API,做摘要用的是 turbo 版。qwen-turbo 给到我的摘要,洋洋洒洒,像模像样的一大篇,涵盖了四五个品种。如果我不是事先知道会有问题,可能就被骗过了。

我们当时很奇怪,AI 会做梦我们是知道的,但做得这么有内容,有点意外。于是有同事把相同的 prompt 给到了 DeepSick 一试,发现出来的内容也是一样的。感觉两者在考试前背了同一篇范文。

然后,那位同事把同样的 prompt 给到 ChatGPT,发现回答的内容也是同样的。现在真相大白了,原来都是抄的班上的学霸的作业,被老师集体抓包。只不过现在可以美其名曰「语料污染」。至于社会主义温室里的花朵是怎么会被资本主义的毒草给污染的,可能要去问茅台师傅,我就不清楚了。

不过,我还是推荐所有同事「能用到什么 AI 就用什么 AI,有得用总比没有强」。他们都去问 AI,也就不用老来烦我了。

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

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

坑二:sysctl

升级完后发现有些不对,检查了一下,发现 /etc/sysctl.conf 配置文件被 dpkg 给备份起来了,后缀是 dpkg-bak。

改名回来后 sysctl -p,以为解决问题。重启之后发现 BBR 还是没能启用。翻找资料,发现是 机制改了。现在得把这些配置写到 /etc/sysctl.d/ 下面去,自己建 conf 文件。

当然,这样也有好处。现在可以把不同的内容分别写在不同的配置文件里面,不用像以前那样全部放在 sysctl.conf 里面,找起来比较麻烦了。

另外,记得用 sysctl --system 而不是 sysctl -p 来进行参数的临时加载。机制不一样了。

坑三:haproxy 启动不起来

算是一个小坑,因为报错信息在日志中写明白了。
我的 haproxy 是早期版本安装的,需要在 haproxy.cfg 中把 ulimit-n 设为 524288。

global
        ulimit-n  524288

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

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

截止本文首次刊发的时候,Nginx 已经 OK 了 ,Redis 还没好。后来直到过完十一长假,我发现redis 有更新,再去看,才发现也提供 trixie 源了。

还有一个问题是 simple-obfs 在 trixie 官方源中没有提供,而 bookworm 是提供的。git 然后源代码编译是一个解决方法。如果还是想从 apt 直接安装省些事情,请在 bookworm 的时期先完成 simple-obfs 的安装,或者临时改成 bookworm 的源。

坑五: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 共享链接了。

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


五天后记:现在夸克云盘大概已经把这个漏洞从服务端给堵上了。看来我还是太乐观。好吧,Life will find a way。

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 版本可能会有不一样的情况。毕竟是所谓的「未定义」行为。