善意提醒

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

2025-09-03

这些鬼走到了一起

图片来自网络

记得有部电影里的一句台词:「没想到苏联成了敌人,而德国和日本成了朋友」。不知道是不是《美国队长》的某个早期版本?反正不是现在漫威的那些狗屁东西。

当年看的时候年纪还小,最多是个中学生,以至于现在印象模糊,但这句台词,让我很有感触。
三十年河东,三十年河西。许多年后的今天,竟是这些鬼,走到了一起。

曾经在内心中有过一番计较,把轴心三国跟现在的某些国家作一个映射。假设的前提是历史的螺旋,如果真的能对应得上,倒也是让人哭笑不得的一件事情。
历史当然不会如此简单地重演。当年我以为克里米亚会是苏台德,现在来看可能是犯了一个错误。如果今天要我再来重新做一遍这道题,我可能会选满洲国。

现在比起当年,形势又有一些不一样了。大国元首携了左膀右臂检阅部下,俨然一副联盟大统领的模样。虽然二弟同是深陷地面战场的泥沼,但三弟比之前给力多了。尽管多了一个被揍得惨兮兮的四弟需要提携,但老大属实有钱有矿还有人,睥睨天下,20亿只当毛毛雨。而且,这次三巨头背靠背,人头不够只管用火车皮送,不再是上次那样只能隔着半个地球炸个港口遥相呼应一下了。

作为被历史洪流裹挟的一员,自己的下场为何,尚不得而知。诸君也要努力。究竟是能在时代广场觅得一吻,抑或能寻见那位高堡奇人,都是我很想知道的事情。

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

DeapSick二三事

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

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

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

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

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

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


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

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

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

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


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

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

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

然后,那位同事把同样的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里面,找起来比较麻烦了。

坑三: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还没好。如果后续有变化,我就来把这一段编辑掉。

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

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