Repository navigation
Releases: corvofiox/com-bridge
Release list
v0.4.11
Com Bridge v0.4.11
修两个开机自启相关的问题,都是我之前留的坑。
1. 开机自启会弹出 GUI(没有后台静默)
程序有两条自启路径:
| 路径 | 注册的动作 | 是否静默 |
|---|---|---|
| 普通自启(HKCU...\Run) | ... exe" --autostart |
✅ 一直是对的 |
| 管理员自启(登录计划任务) | exe gui |
❌ 漏了 --autostart |
计划任务这条只注册了 gui 子命令,所以任务拉起时程序走"正常启动"分支,
登录后就把窗口怼到面前。现在两条都带静默标志。
配套修好两个连带问题:
- 工作组机器注册任务直接失败:
USERDOMAIN在工作组机器上是WORKGROUP,
拿它拼计划任务主体会报HRESULT 0x80070534(账户名与安全标识间无任何映射)。
现在域为WORKGROUP/空时退回计算机名(CORVO-LAPTOP\gaoxi)。 - 已注册的旧任务不会自己变:启动时后台自检一次,若发现任务动作缺静默标志
—— 本进程已是管理员(说明正是被该任务拉起)就就地重注册,不需要再弹 UAC;
否则在日志里提示"重新点一次「开机自启(管理员)」即可升级"。
2. 开机握手太慢(不是每 5 秒一次)
你记的没错,规格就是每 5 秒。旧实现是 1→2→4→5 的指数退避,写着
"上限 5s"——但连接尝试本身还会阻塞(ControlClient 默认 5s 超时),
两者叠加,实测间隔是 6.5~9.5 秒:
修复前(副机日志): 第 1 次探测 已等 5s → 第 3 次探测 已等 18s (13s / 2 次)
修复后(副机日志): 第 1 次探测 已等 1s → 第 3 次探测 已等 11s (10s / 2 次 = 5.0s/次)
第 3 次 已等 11s → 第 5 次 已等 21s
第 5 次 已等 21s → 第 7 次 已等 31s
改法:
- 节拍从本次探测发起时刻算(
Agent.retry_delay),固定 5s 一格; - 探测用 1s 短超时(握手成功后自动恢复成正常超时,不影响后续收发);
- 广播发现阶段与运行期断线重连用同一节奏(原来重连是 2→4→5)。
可用 COM_BRIDGE_HOST_RETRY_S 覆盖间隔(默认 5)。
测试
新增 25 项(tests/test_autostart_silent_and_retry.py):计划任务参数必须含静默标志、
两条自启路径用同一个标志、_full_user 在 WORKGROUP 下退回计算机名、任务参数 XML 解析、
节拍纯函数(0→5.0s、0.3→4.7s、超时→0.2s 下限)、两条等待循环与重连都不许再出现退避。
全量 914 通过 / 1 跳过 / 0 失败。
v0.4.10
Com Bridge v0.4.10
修一个会影响本机文件操作的 bug:传输期间锁住了源文件。
现象(用户实测)
在主机上「剪切」某个文件、粘贴到主机的另一个目录 —— 移动失败,提示文件被占用。
纯本机操作,与副机无关,完全是 Com Bridge 造成的。
根因
剪贴板同步发文件时用 open(src, "rb") 读源文件。Python 在 Windows 上以
_SH_DENYNO 打开,共享模式是 READ|WRITE,不含 DELETE;而同卷"移动"就是一次
rename,需要对该文件有 DELETE 权限 —— 于是传输期间资源管理器被拒(WinError 32)。
本机实测(同一份 1100 字节文件):
| 打开方式 | 传输期间 os.replace |
|---|---|
普通 open() |
PermissionError [WinError 32] |
FILE_SHARE_READ|WRITE|DELETE |
成功,且移动后仍能从原句柄读完 1100 字节 |
修法
源文件改用 CreateFileW(..., FILE_SHARE_READ|WRITE|DELETE) + open_osfhandle 打开
(clip_transfer.open_shared_read)。句柄跟着文件走:你在传输期间剪切/移动/改名/
删除它都不再被挡,而我们的读取照样能读完。
配套两处:
- 源文件在采样后被移走(你手比轮询快):整批中止并说明原因
源文件已不存在(可能被移动或删除): xxx,不会在对端落地半个文件; - 接收端不再把"对端中止"当成错误刷
!!,改为一行信息
对端中止传输(未完成)(半截数据已丢弃)。
注意:剪切/复制文件时副机仍会收到一份副本 —— 那是剪贴板同步的设计行为,
本次修的是"本机操作不该被挡住",不是"不再同步"。
测试
新增 10 项(tests/test_clip_file_share.py):普通 open() 确实会挡住移动(记录根因)、
全共享打开期间可改名/删除并读全、跨卷"复制+删除"序列、CreateFileW 失败时回退、
源码守卫(禁止退回 open(src))、以及两条端到端:传输进行中把文件改名,传输仍完成且内容一致。
全量 889 通过 / 1 跳过 / 0 失败(连跑 4 次稳定)。
v0.4.9
Com Bridge v0.4.9
审查项 #9 / #10 / #11:测试与运维。运行时行为没有变化。
#9 单实例互斥名可用环境变量覆盖
test_single_instance 的 3 条用例以前用生产互斥量名,于是程序一运行它们
必然失败(锁被真实进程占着)。那不是回归,但 CI 永远红,也容易让人误以为
单实例逻辑坏了。
现在互斥量名支持 COM_BRIDGE_MUTEX_NAME 覆盖,测试各自用一个独立名字;
不设该变量时行为与之前完全一致(新增一条用例专门守护这点)。
结果:程序运行中跑全量测试也是绿的(877 通过 / 0 失败)。
#10 真机注入测试改为显式开启
test_accel 里那条 TestRealInjectionIsOneToOne 会移动真实光标,所以任何
在碰光标的程序都能让它偶发失败——正在运行的 Com Bridge 自己(穿越态会把主机
光标钳在边缘)、Deskflow/Synergy 之类同类工具、带手势的鼠标驱动都算。
现在默认跳过,需要时 set COM_BRIDGE_REAL_INPUT_TESTS=1;即使开启,检测到
本机已有 Com Bridge 在跑也会跳过并说明原因。测试结果因此只反映代码对错。
#11 日志运行时轮转(快照 + 截断)
旧实现只在启动时轮转一次,且只保留一份 .old:
- 长时间不重启就完全不轮转 —— 跑几天的进程能把日志写到几百 MB;
- 上一次的现场会被下一次启动覆盖掉。
现在后台每 30 秒检查一次,超过 8MB 就「快照 + 截断」:
保留末尾 512KB 存为 .1,旧快照顺移到 .2/.3(共 3 份),然后把当前日志清空。
之所以不直接 rename:日志文件此刻正被 fd 1/2 打开,Windows 上重命名打开中的
文件会失败(共享冲突);复制一份再截断不需要改文件名。
可用 COM_BRIDGE_LOG_MAX 调整上限(字节)。
测试
新增 9 项(tests/test_log_rotation.py)+ 2 项互斥名用例。全量 877 通过 /
1 跳过(真机注入,需显式开启)/ 0 失败。
v0.4.8
Com Bridge v0.4.8
一处计数时序修正,功能与 0.4.7 完全相同。
剪贴板发送队列在"对端确认后"先更新计数、再打进度/收尾日志。原来顺序相反:
日志里有 print 与浮点运算,夹在"对端已收到"和"本地 sent 加一"之间,
测试里表现为偶发的 2 == 3(现场不会看到,但计数面板可能短暂少一项)。
- 新增/修正测试:
tests/test_clip_status_row.py(6) 锁定状态窗口"剪贴板"一行;
test_outbox_sends_in_order改为轮询计数(这条断言本身就是竞态的)。 - 全量 864 通过(3 项
test_single_instance在程序运行时会失败,属环境所致)。
v0.4.7
Com Bridge v0.4.7
审查项 #4 / #5 / #7 的修复。功能只增不减,配置格式向后兼容。
#4 剪贴板:支持注册格式 "PNG"
以前只认 CF_DIB(标准位图)。有些程序(部分浏览器、截图与设计工具)只把图
放进注册格式 PNG,完全不提供 CF_DIB —— 这类图以前一张都采不到。
现在:
- 采样优先取注册的
"PNG",没有才回退CF_DIB。PNG 是压缩格式,同一张图
比裸 DIB 小一个数量级(4K 截图约 2MB vs 33MB),跨机传输更快; - 接收端两种都写:既写注册的
"PNG"(现代程序优先读它),也从 PNG 解出
一份CF_DIB(只认位图的老程序照样能粘); - 线上协议新增格式 id
4 = png,原有 id 不变。
#5 传输有进度了,也能中止
以前传 200MB 文件夹时全程一句话都不打印:不知道是在传、卡住了、还是早就
失败了,也没法中止,只能杀进程。现在:
- 每 0.5 秒最多一行进度:
[clip] 发送中 43% (86.0/200.0 MB, 74.2 MB/s);
收尾再给一行[clip] 已发送 200.0 MB 用时 2.7s, 74.1 MB/s;
失败会写清原因(传输中断: …/等对端确认超时/对端拒收); - 单次上限 2GB → 16GB(原来那个数字是随手定的门槛,不是技术限制);
- 停止服务即中止在途传输,不再出现"关不掉、要等它传完";
- 状态窗口新增「跨机功能 · 剪贴板」一行:发送项数/流量、接收项数/文件数、
排队、取消、失败,一眼能看出刚才那份文件夹到底传过去没有。
#7 自动更新提示 + 自启结论
- 新增自动更新检查:启动后后台查一次 GitHub Release(6 小时最多一次,
结果缓存进配置,失败静默)。有新版本时窗口底部提示会变成
「发现新版本 vX,点此下载 ↗」,日志里同时留一行。
刻意不做自动替换 exe —— 运行中替换自己要下载到临时目录、再起一个替换
进程再退出,一旦失败就是"程序打不开",代价远大于收益。 - 开机自启:查证后确认本程序已有"登录计划任务 + 最高权限"这条路
(设置里的「开机自启(管理员)」),它就是 Windows 上服务级自启的等价物。
真正的 Windows 服务跑在 Session 0,拿不到桌面输入、光标和托盘,做不了这件事
—— 所以不是缺功能,而是那条路在本场景不可行。 - i18n(界面多语言):本次没有做,见下。
关于 i18n
这不是一个能顺手带上的改动:界面文案硬编码在 gui.py / gui_windows.py /
tray.py 与一整套日志里,约 800 处,涉及布局宽度(中文两字宽的标签列、
中文比英文短的状态文案)。半套做完的界面(一半中文一半英文)比纯中文更糟。
要做就单独一轮:抽出 i18n.py(tr() + 两份字典)→ 逐模块替换 → 用
截图对比确认没有把布局撑坏。这一轮不发半成品。
测试
新增 48 项:tests/test_clipboard_png.py(12)、tests/test_clip_progress.py(14)、
tests/test_update_check.py(22)。全量 858 通过(3 项 test_single_instance
在"程序正在运行"时会失败,属环境所致)。
v0.4.6
Com Bridge v0.4.6
两处输入热路径修复 + 一套可量化的延迟诊断。
修:副机某块屏上鼠标"非常迟滞、手停了光标还在追"
现象(用户实测):副机 2560×1600 屏上移动非常迟滞,同样操作在 1920×1080 屏上正常。
原因在事件流本身,与屏幕像素无关:
- 输入是 UDP,消费端一时跟不上,事件就堆在内核接收缓冲里
(默认 64KB ≈ 3400 条 19 字节事件 ≈ 3 秒的量); - 旧实现逐条取出、逐条注入,每条都要付一次
SendInput的代价,
于是积压只能按"消费速率"慢慢排空 —— 主观上就是光标在追手。
副机的光标从进入边(在 2560 屏上)开始就一直背着这段积压跑,
等到 1920 屏时已经排空,所以"只有那块屏迟滞"。
修复(transport.coalesce_moves):收到一条事件后先看 socket 里还有没有积压,
有就继续攒,再把相邻位移合并成一条注入。
- 光标只需要最终位置:k 条相邻位移等价于 1 条 (Σdx, Σdy),落点不变;
- 合并后一轮只付一次
SendInput,积压立刻塌到最新位置,不再"追手"; - 只合并相邻的移动事件,绝不跨越按键/滚轮 ——
[移动, 点击, 移动]
的顺序即语义,压掉点击就会点错位置; - 没有积压时行为与旧版完全一致(逐条交付),不平白引入延迟。
实测数字(副机 0.4.6,diag_lat 每 5 秒一行,用户在两块屏间正常移动):
接收=797/s 批=3849 最大积压=39 合并=1821 注入均=1.01ms 峰=15.8ms | 光标=(0,636) 屏=2560x1600@0,0
关键在 注入均=1.01ms:SendInput 一次就要约 1ms,而主机上报率约 800~1000 事件/秒
—— 旧实现"每条一注入"正好把消费端顶在 100% 饱和,任何一次系统抖动(峰值 15.8ms)
都会让它落后,落后就只能靠后续慢慢排空,于是"手停了光标还在追"。
合并后一次注入覆盖整批,消费端从饱和降到约一半,两块屏都跟手。
修:主机接多块屏时,越界边落在错误的边缘
主机侧越界判定一直用"面积最大的那块屏",而副机侧用的是整个虚拟桌面。
两块屏并排时,那条边正好在桌面中间 —— 光标走到中间就判"越界",把输入送去副机。
现在两侧统一用 virtual_desktop();多屏时启动日志会打印实际基准与屏数。
新增:管线延迟诊断(默认关闭)
%LOCALAPPDATA%\ComBridge\diag_lat.flag(或环境变量 COM_BRIDGE_DIAG_LAT=1)
存在时,副机每 5 秒打印一行:
[agent][lat] 接收=812/s 交付=794/s 批=… 最大积压=… 合并=… 注入均=0.42ms 峰=3.1ms | 光标=(x,y) 屏=2560x1600@0,0 | hider 隐藏=0.0/s 收起=0.0/s
判读:
| 观察 | 结论 |
|---|---|
接收 远低于主机上报率,最大积压 恒为 1 |
事件在路上就丢了,不是本地慢 |
最大积压 持续 > 1、合并 持续增长 |
确实在排队;本次合并已把积压塌缩成一次注入 |
注入均/峰 本身是毫秒级 |
SendInput 在这块屏/这台机器上就慢(系统/驱动层面) |
hider 隐藏/收起次数快速上涨 |
隐藏器在反复抢前台,与鼠标事件量无关 |
其他
- 输入事件时间戳改用
perf_counter微秒时钟(time.monotonic_ns()在 Windows
上分辨率只有 15ms,无法用来量化延迟)。 - 新增 15 项回归测试(
tests/test_input_batching.py):位移总量守恒、
非移动事件不被合并且顺序不变、无积压时逐条交付、真 UDP 突发往返。
v0.4.5
Com Bridge v0.4.5
两处体验修正,功能不变。
修:高 DPI 屏上界面偏小
本进程是 per-monitor DPI 感知的 —— 这是输入坐标必须的(否则 Windows 会把
光标坐标按缩放"虚拟化",跨屏落点整体偏移)。代价是 tkinter 不再自动放大界面:
96 DPI 下设计好的 620×660 窗口,在 200% 缩放的笔记本屏(如 2560×1600)上只占
四分之一面积,字小得看不清。
现在整套界面按系统缩放同步放大:
| 1.0× | 1.5× | 2.0× | |
|---|---|---|---|
| 窗口实际尺寸 | 636×535 | 946×759 | 1256×984 |
- 自绘位图(卡片/胶囊/徽章/按钮)的尺寸与 PIL 字号统一乘缩放系数(
uidpi.SCALE); - Tk 的磅值字体通过
tk scaling同步放大; - Tk 自带的磅值字体;
- 副机窗体同样按比例放大。
启动日志会写明当前倍率,例如:
[gui] 界面缩放 ×2.00(系统 DPI 192)
需要强制指定时可用环境变量:COM_BRIDGE_UI_SCALE=1.5(或 150%)。
已知取舍:倍率在启动时确定一次。把窗口拖到另一块不同缩放的屏幕上不会实时重排,
需要重启程序。
修:主界面那个"对时偏差 +47568320 ms"
它显示的其实是 offset_us —— 两台机器单调钟(perf_counter)纪元之差,
等于两机开机时刻相差多少(这台副机是 13.2 小时)。它是换算基准,不是
"时钟误差",摆在界面上只会让人以为时钟差了 13 小时。
现在:
- 主界面:
(对时往返 1.3 ms)—— 用户真正能感知的量; - 状态窗口:
rtt 1.34ms / 样本 8 / 偏移跳变 0 次(最大 0.00ms) | 单调钟偏移 +47568320ms(两机开机时刻之差,换算用,不是时钟误差)
偏移跳变才是真正要盯的指标:offset 一旦阶跃,相邻音频包的时间戳就错位,
会听到断口。这个计数以前只在内部统计,现在摆出来了。
测试
763 项通过(新增 10 项界面缩放回归:倍率范围、像素换算四舍五入、环境变量解析、
150% 写法、主题常量确实过缩放)。
v0.4.4
Com Bridge v0.4.4
修 v0.4.2 里图片跨机粘贴不工作的问题。文字和文件一直是好的,只有图片。
根因:抄了 Deskflow 的一半逻辑
真发现(在真机剪贴板上直接 dump 出来的):
fmt name size
8 CF_DIB 256052 字节 biSize=40 biWidth=320 biBitCount=32
biCompression=3 (BI_BITFIELDS)
图片确实在剪贴板里,但它的像素头是 BI_BITFIELDS(40 字节头 + 12 字节颜色掩码 + BGRA 像素),
不是 BI_RGB。
我的采样逻辑照着 Deskflow 写了"只接受 BI_RGB"这条判据,却漏掉了它的另一半:
Deskflow 的 MSWindowsClipboardBitmapConverter::normaliseMalformedMacDib() 对这种情况是
转换而不是丢弃 —— 把 biCompression 改写回 BI_RGB、并把紧跟头部的 12 字节掩码块
丢掉(因为 INFOHEADER 本身没有掩码字段,留着会被 Windows 当成像素读)。
结果就是:截图类应用(截图工具、画图、浏览器)复制出来的图全部被静默丢弃,
日志里一个字都没有 —— 所以看起来像"根本没人实现图片"。
修
新增 clipboard.normalise_dib(),按 Deskflow 的规则处理:
| 输入 | 处理 |
|---|---|
| BI_RGB | 原样接受 |
biSize=40 / planes=1 / 32bpp / BI_BITFIELDS / 长度 = 40+12+w*h*4 |
改写成 BI_RGB 并丢掉掩码块 |
| BI_JPEG / BI_PNG(内容不是像素) | 拒绝(没法当位图用) |
顺带:日志现在能自己说话
之前"图片传没传"完全看不出来。现在两端各有一行:
[clip] 发送 图片 800x500x32 (1600040 字节) ← 发送侧
[clip] 收到 图片 800x500x32 (1600040 字节) ← 接收侧
[agent] 剪贴板已写入本机: 图片 800x500x32 ← 确认本机剪贴板真的写进去了
最后那行是关键判据:有它 = 对端剪贴板已经拿到图,粘不出来就是目标程序的问题;
没它 = 写剪贴板失败(一般是被别的程序占用)。
真机验证
主机 04:24:33 [clip] 发送 图片 800x500x32 (1600040 字节)
副机 04:24:34 [clip] 收到 图片 800x500x32 (1600040 字节)
副机 04:24:34 [agent] 剪贴板已写入本机: 图片 800x500x32
测试
753 项通过。新增 6 项覆盖 DIB 规范化:BI_RGB 直通、BI_BITFIELDS 必须被转换而不是丢弃
(含"掩码块必须丢掉""像素必须原样保留")、BI_JPEG/BI_PNG 拒绝、垃圾数据拒绝、
日志摘要含尺寸。
v0.4.3
Com Bridge v0.4.3
v0.4.2 的一个小补丁,只改一处读取逻辑。
修:配置文件带 BOM 时被当成"坏配置"
config.json 如果被记事本另存、或用 PowerShell 的 Set-Content -Encoding UTF8
写过,文件开头会带 UTF-8 BOM。Python 的 json.load 遇到 BOM 会直接抛
ValueError,而我们的读取逻辑对任何异常都"退回默认值"——于是:
- 用户明明勾了"上次运行时退出 -> 下次自动连接",重启后不自动连;
- 角色、方位、主电脑 IP 全部回到默认值,界面上看起来像"设置丢了"。
现在读取统一用 utf-8-sig(能容 BOM,也能读普通 UTF-8),并新增 3 项回归测试
(带 BOM 能读 / 坏 JSON 仍退回默认 / 我们自己写出去的配置不带 BOM)。
顺带
- 部署脚本里用 PowerShell 改配置时改用
.NET File.WriteAllText(UTF8Encoding(false)),
不再产生 BOM。
升级
与 v0.4.2 相同:两台都要换。功能上与 v0.4.2 完全一致(剪贴板文字/图片/文件)。
v0.4.2
Com Bridge v0.4.2
剪贴板从"只能搬文字"扩展到文字 / 图片 / 文件,并把传输方式按 Deskflow 的成熟做法重做。
新增:跨机复制图片
截图(Win+Shift+S)、画图板、看图软件里的图,在一台机器上复制,到另一台直接
Ctrl+V 就能用。
图片按 Deskflow 的规范传裸 DIB(BMP 去掉 14 字节文件头,BI_RGB),写进对端
剪贴板时直接 SetClipboardData(CF_DIB) —— 与 Deskflow 的 MSWindowsClipboardBitmapConverter
完全一致。这样粘贴到 Word / 微信 / 画图都能识别,不需要中间转码。
(旧版其实也尝试过同步图片,但把图塞进 8MB 的 UDP 通道:1080p 截图 8.3MB 就传不动,
4K 截图 33MB 更是必然失败。见下面的传输改动。)
新增:跨机复制文件与文件夹
在资源管理器里复制文件(或整个文件夹),到另一台机器 Ctrl+V 就能落盘:
- 文件夹会连目录结构一起还原(相对路径保留)
- 单次上限 2GB,接收目录是
%LOCALAPPDATA%\ComBridge\received\<时间戳>\,
只保留最近 30 批(避免磁盘无限增长) - 粘到哪里由你决定:程序只是把文件放进一个目录并把路径写进剪贴板,
不会执行任何东西
传输改造:从 UDP 换成 TCP(对齐 Deskflow)
原来剪贴板走 UDP,1000 字节一块、逐字节位图重组、上限 8MB。那是给小文本设计的,
图片和文件在这条路上必然丢包。
读了 Deskflow 的 StreamChunker.cpp 之后按它的做法重做:
| 方面 | Deskflow | 本版 |
|---|---|---|
| 传输 | TCP 客户端流 | 单独一条 TCP 通道(端口 24803) |
| 分块 | g_chunkSize = 512KB,start/data/end 三标记 |
同 |
| 序列化 | u32 格式数 + (u32 id + u32 长度 + 数据) |
同,格式 id 同值(Text=0/HTML=1/Bitmap=2) |
| 防回环 | 注册 "Deskflow Ownership" 剪贴板格式 |
注册 "Com Bridge Ownership" |
顺带说明:Deskflow 的拖放文件传输其实已经废弃(ProtocolTypes.h 里
kMsgDFileTransfer / kMsgDDragInfo 都标着 "no longer implemented"),所以文件
这部分只沿用它的消息语义(先发清单、再按文件逐个发内容),传输本身走同一条 TCP 通道。
安全边界(接收侧强制,不信对端)
- 路径消毒:拒绝
..、绝对路径、盘符、Windows 保留名(CON/PRN…)、超深路径; - 总量上限:剪贴板内容 96MB、文件 2GB、文件数 4000;
- 落盘位置固定在
received\<时间戳>\,不覆盖你已有的文件; - 只写文件 + 设剪贴板,不执行。
升级注意
两台机器都要换成本版 —— 剪贴板的传输方式变了(UDP → TCP),新旧版本混用时
剪贴板不会工作(日志里会有连接被拒/无确认的提示),键鼠和音频不受影响。
全部测试
744 项通过。新增 51 项覆盖:序列化往返、未知格式跳过、路径消毒(含敌意 ..)、
目录展开与重名改名、端到端(文字 / 4MB 级 DIB / 带子目录的文件组)、上限拒收、
发送队列保序。