R6.12 round 1 fix: the worker registers as a UI element

The worker set the accessory policy after AppKit had already registered it
as a Dock app, so 11 of 15 launches showed Foreground for about 30 ms.
Now it sets LSUIElement in its main bundle's info before the kernel starts
AppKit, and fails to start unless the policy came up as accessory.

tests/scripts/dock-macos.sh samples each worker's type from its start until
the first tile; on the Mac mini 30 launches were all UIElement, and the
shipped worker fails it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-15 19:45:48 -04:00
1 parent fdb8317d24
commit 1317c4ad39
6 files changed
+102 -30

No files matched your search

+2 -1
View File
@@ -170,6 +170,7 @@ tests/scripts/pdf-recovery.sh 真窗口 ElyPDF:加锁文件的封存恢复点
tests/scripts/autosave.sh 真窗口 ElyDoc:自动保存开关、停手即写、外部改动不覆盖(需辅助功能权限)
tests/scripts/print.sh 真窗口 ElyDoc:打印用文档自己的纸张,取消不动文档(需辅助功能权限)
tests/scripts/show-fullscreen.sh 真窗口 ElyShow:F5 放映的窗口盖住整块屏幕(菜单栏与程序坞之上,需辅助功能权限)
tests/scripts/dock-macos.sh dist 里 ElyDoc、ElyTable、ElyShow 各启动数次:LOK worker 从未登记为程序坞应用,图块照出
tests/scripts/windows-installer.sh 假应用的安装程序:所选目录原有的文件经两次安装与卸载都在,旧版按注册表清理,文件被占用或程序在运行时什么也不改,写不进时放回旧版(缺 makensis 时 SKIP)
tests/scripts/types.sh packaging/types.tsv 的文件类型在 Linux 桌面项、Windows 安装程序与 macOS 文档类型里一致,从不设为默认
tests/scripts/layout.sh 真窗口:六个应用在 640、800、1100 宽下无元素越出窗口左右,留截图(需辅助功能与屏幕录制权限)
@@ -303,5 +304,5 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
- 2026-10-15(用户报:ElyMark 代码块后的“你好↵测试”离开输入框后成了“你好 测试”):单个换行照 Typora 的默认(编辑时保留,导出与打印不保留):组件库 93efa7f 起 `MarkdownReader::new`(屏上:可视、预览、右键与命令的读法)把软换行读作换行,`MarkdownReader::joined`(输出)照 GFM 读作空格;标题的大纲与目录文字照旧一行。ElyMark 的 HTML、PDF、富文本复制与纯文本导出用 `joined`(R6.7)。组件库 c31d092 与 26af4be:画出成一个换行的 CRLF 对回其起点(原先按在 CRLF 行末键入落在 CR 与 LF 之间),行内代码照留两字则逐字对回。
- 2026-10-15(用户报:ElyMark 源码模式“测”后的插入点画在“测”的中间):组件库 `CodeEditor` 原按“列 × m 的宽”放插入点、选区、按下处与输入法框,中日韩字与表情约宽两格。组件库 9fb607b 起按 gpui 排好的这一行定位(`editor::measure`,含着色带来的字重),块状与下划线插入点取所在字的宽;标尺与缩进参考线仍按列(R6.8)。上下移动仍按字数记目标列。cd61653:按下处取最近的字首或行末(gpui 的 `closest_index_for_x` 越过末字字首即给行末);上次绘制得出的括号与配对位置只属当时的文字(`Buffer` 每次改动换一个戳),输入法改字后、下次绘制前的测量不用它们(原先把新字切进不同字体段,DirectWrite 崩溃)。
- 2026-10-15(用户报:ElyMark 三、四级标题不显眼):组件库 5954132 起标题字号随正文按 Typora 的倍数(一至六级 2.25、1.75、1.5、1.25、1、1),一律加粗,六级为次要色,行高为字号的 1.3 倍(原为固定的 24、20、16、14 像素,正文 13,三、四级只是中等字重)(R6.9)。90bd37b:粗体与标题同一字重(原粗体为半粗,标题里的粗体反而更细,正文粗体随之由半粗改为粗,同 Typora);标题行高按自身字号取 1.3 倍。Windows 真窗口核对(`.work/r69`):可视模式各级标题分明、“你好↵测试”两行,源码模式“测”后的插入点落在两字之间。
- 2026-10-15(用户报:macOS 打开一个文档,程序坞里有两个同样的图标):LOK worker 在应用包的 `Contents/MacOS` 里启动 AppKit,系统按包的 Info.plist 把它也登记为常规应用(`lsappinfo` 里应用与 worker 都是 Foreground),程序坞多一个图标;内核自己从不设激活策略。worker 在内核启动前(`Lok::hook` 打开内核之后、调钩子之前)载入同一目录的 Aqua 插件 `libvclplug_osxlo.dylib`,取内核自己的 `VCL_NSApplication`,设为辅助应用(`NSApplicationActivationPolicyAccessory`:无程序坞图标、无菜单栏,仍可建屏外窗口),内核随后复用同一实例;类不在或设不上即报错,不启动(`ffi/accessory.rs`)。实测(Mac mini):worker 从未是 Foreground,登记即为 UIElement,图块照出(R6.12)。
- 2026-10-15(用户报:macOS 打开一个文档,程序坞里有两个同样的图标):LOK worker 在应用包的 `Contents/MacOS` 里启动 AppKit,系统按包的 Info.plist 把它也登记为常规应用(`lsappinfo` 里应用与 worker 都是 Foreground),程序坞多一个图标;内核自己从不设激活策略。worker 在内核启动 AppKit 之前(`Lok::hook` 打开内核之后、调钩子之前)把本进程主包信息里的 `LSUIElement` 设为真,AppKit 登记时即为辅助应用(无程序坞图标、无菜单栏,仍可建屏外窗口);钩子之后核对激活策略是辅助,不是即报错、不启动(`ffi/accessory.rs`)。原先在登记之后才改策略,审查实测 15 次启动有 11 次先登记为 Foreground 约 30 毫秒(R6.12 第 1 轮)。`tests/scripts/dock-macos.sh` 连续启动三个应用,从 worker 启动起采样其类型到首块图块,见 Foreground 即失败(R6.12)。
- 2026-10-15(用户报:ElyShow 编辑区滚轮与上下键不换页,三端):内核在未选中对象时把上下键当作滚动窗口(`fupoor.cxx`),Ely 的画布整页适合、无处可滚,滚轮同样。`LokView::turn_parts`(ElyShow 首次适合时调用)之后:整页可见时滚轮换页(`view/turn.rs`:每格一页;触控板一次手势只换一页,惯性不算,静止 250 毫秒或新手势再计),未选中对象、未编辑文字时上下键换页;视图发 `Turn`,ElyShow 从视图所示的页起算(连续换页累加),经缩略图条同一路径 `show_part`,到首末页即停;后台读回的当前页只在视图已无待落地输入时采用,排队的换页不被先前的读取拉回(R6.13)。
+3 -1
View File
@@ -84,7 +84,7 @@ impl Lok {
let lib = Library::open(install)?;
// Before the kit starts, so no Dock icon shows.
#[cfg(target_os = "macos")]
accessory::accessory(install)?;
accessory::hide()?;
let hook: Hook2 = unsafe {
lib.symbol(b"libreofficekit_hook_2\0")
.or_else(|_| lib.symbol(b"libreofficekit_hook\0"))
@@ -102,6 +102,8 @@ impl Lok {
"lok hook failed for {install_str}"
)));
}
#[cfg(target_os = "macos")]
accessory::check()?;
log::info!("lok: hooked {}", install_str);
Ok(Self { kit, _lib: lib })
}
+49 -27
View File
@@ -1,10 +1,9 @@
//! The worker stays out of the Dock (macOS).
use std::ffi::{CString, c_char, c_void};
use std::ffi::{c_char, c_void};
use std::io;
use std::os::unix::ffi::OsStrExt as _;
use std::path::Path;
#[link(name = "Foundation", kind = "framework")]
#[link(name = "objc")]
unsafe extern "C" {
fn objc_getClass(name: *const c_char) -> *mut c_void;
@@ -12,36 +11,59 @@ unsafe extern "C" {
fn objc_msgSend();
}
/// The kernel's Aqua plugin; it defines `VCL_NSApplication`.
const PLUGIN: &str = "libvclplug_osxlo.dylib";
/// NSApplicationActivationPolicyAccessory: no Dock icon, no menu bar.
const ACCESSORY: isize = 1;
/// Makes the kernel's application an accessory before the kit starts.
pub fn accessory(install: &Path) -> io::Result<()> {
// Loaded for good; the kernel later opens the same image.
let path = install.join(PLUGIN);
let c = CString::new(path.as_os_str().as_bytes()).map_err(|_| io::Error::other("the path holds a NUL"))?;
if unsafe { libc::dlopen(c.as_ptr(), libc::RTLD_LAZY | libc::RTLD_GLOBAL) }.is_null() {
return Err(io::Error::other(format!("dlopen {}: {}", path.display(), super::dl_error())));
}
// The kernel's own class, so it reuses this instance.
let class = unsafe { objc_getClass(c"VCL_NSApplication".as_ptr()) };
type Id = *mut c_void;
fn send() -> unsafe extern "C" fn() {
objc_msgSend as unsafe extern "C" fn()
}
fn class(name: &std::ffi::CStr) -> io::Result<Id> {
let class = unsafe { objc_getClass(name.as_ptr()) };
if class.is_null() {
return Err(io::Error::other(format!("{PLUGIN} has no VCL_NSApplication class")));
return Err(io::Error::other(format!("no {} class", name.to_string_lossy())));
}
type Shared = unsafe extern "C" fn(*mut c_void, *mut c_void) -> *mut c_void;
type Policy = unsafe extern "C" fn(*mut c_void, *mut c_void, isize) -> i8;
let send = objc_msgSend as unsafe extern "C" fn();
let shared: Shared = unsafe { std::mem::transmute(send) };
let policy: Policy = unsafe { std::mem::transmute(send) };
let app = unsafe { shared(class, sel_registerName(c"sharedApplication".as_ptr())) };
if app.is_null() {
return Err(io::Error::other("the kernel's application did not start"));
Ok(class)
}
fn get(object: Id, selector: &std::ffi::CStr) -> io::Result<Id> {
type F = unsafe extern "C" fn(Id, Id) -> Id;
let f: F = unsafe { std::mem::transmute(send()) };
let result = unsafe { f(object, sel_registerName(selector.as_ptr())) };
if result.is_null() {
return Err(io::Error::other(format!("{} gave nothing", selector.to_string_lossy())));
}
let set = unsafe { policy(app, sel_registerName(c"setActivationPolicy:".as_ptr()), ACCESSORY) };
if set == 0 {
return Err(io::Error::other("macOS refused the accessory policy"));
Ok(result)
}
/// Marks this process a UI element before the kit registers it.
pub fn hide() -> io::Result<()> {
// The kit reads the bundle's LSUIElement when it registers.
let info = get(get(class(c"NSBundle")?, c"mainBundle")?, c"infoDictionary")?;
type Text = unsafe extern "C" fn(Id, Id, *const c_char) -> Id;
type Flag = unsafe extern "C" fn(Id, Id, i8) -> Id;
type Set = unsafe extern "C" fn(Id, Id, Id, Id);
let text: Text = unsafe { std::mem::transmute(send()) };
let flag: Flag = unsafe { std::mem::transmute(send()) };
let set: Set = unsafe { std::mem::transmute(send()) };
unsafe {
let key = text(class(c"NSString")?, sel_registerName(c"stringWithUTF8String:".as_ptr()), c"LSUIElement".as_ptr());
let yes = flag(class(c"NSNumber")?, sel_registerName(c"numberWithBool:".as_ptr()), 1);
set(info, sel_registerName(c"setObject:forKey:".as_ptr()), yes, key);
}
Ok(())
}
/// Fails unless the kernel's application came up an accessory.
pub fn check() -> io::Result<()> {
type Policy = unsafe extern "C" fn(Id, Id) -> isize;
let policy: Policy = unsafe { std::mem::transmute(send()) };
let app = get(class(c"NSApplication")?, c"sharedApplication")?;
let found = unsafe { policy(app, sel_registerName(c"activationPolicy".as_ptr())) };
if found != ACCESSORY {
return Err(io::Error::other(format!("the worker's activation policy is {found}, not accessory")));
}
log::info!("lok: the worker is an accessory app");
Ok(())
+1 -1
View File
@@ -270,6 +270,6 @@
- [x] R6.9 ElyMark 三、四级标题不显眼(用户报):各级字号随正文按 Typora 的倍数(2.25、1.75、1.5、1.25、1、1),一律加粗,六级为次要色(组件库渲染)
- [x] R6.11 Windows 升级后旧文件仍在(R6.10 本机重装查出):安装程序只覆盖不删除,旧版 ElyMark 的 `git` 与 CEF 文件留在安装目录;安装程序记下所装的文件,升级与卸载只移去这些;文件被占用即什么也不改,失败时放回旧版。第 3 轮后(最后一轮,修完不再审查):备份按清单次序编号,近 260 字的路径照样升级与卸载;卸载先查清单本身,删不掉即放回全部
- [x] R6.10 重发 0.1.5(用户定:仍为 0.1.5):删去 v0.1.5 的全部文件,标签移到修复的提交,三端全部包重建并上传,本机重装(标签在 53630ce;推标签即起发布流程,其 Windows 与 Linux 任务建出并上传,与本机同时起的 Windows 构建撞上,本机那份未上传;macOS 任务未上传,dmg 由 Mac mini 本机建、公证、钉票;44 个文件,同名的源码包只留 Linux 的一份,见 P3-16;下载回的 ElyMark 安装程序与清单的散列与构建相同;本机六个应用 0.1.5 经新安装程序升级,ElyMark 目录无 `git`,冒烟启动均正常退出、日志无警告)
- [ ] R6.12 macOS 打开一个文档,程序坞里 ElyDoc、ElyTable、ElyShow 各出现两个图标(用户 2026-10-15 报):LOK worker 在应用包内启动 AppKit,系统按包的 Info.plist 把它也登记为常规应用(`lsappinfo` 两个 Foreground);worker 在内核启动前载入内核的 Aqua 插件,把内核自己的 `VCL_NSApplication` 设为辅助应用(无程序坞图标、无菜单栏,仍可建屏外窗口)
- [ ] R6.12 macOS 打开一个文档,程序坞里 ElyDoc、ElyTable、ElyShow 各出现两个图标(用户 2026-10-15 报):LOK worker 在应用包内启动 AppKit,系统按包的 Info.plist 把它也登记为常规应用(`lsappinfo` 两个 Foreground);worker 在内核启动 AppKit 前把本进程主包信息的 `LSUIElement` 设为真,登记即为辅助应用,钩子后核对策略(第 1 轮:原先登记后才改,15 次有 11 次先为 Foreground);`tests/scripts/dock-macos.sh` 在 Mac mini 上 30 次启动全为 UIElement,旧 worker 即失败
- [ ] R6.13 ElyShow 编辑区的滚轮与上下键不换页(用户报,三端;放映时正常):内核在未选中对象时把上下键当作滚动窗口,Ely 的画布整页适合、无处可滚;整页可见时滚轮换页(每格一页,触控板一次手势一页),未选中对象、未编辑文字时上下键换页,同缩略图条的路径,到首末页即停
- [ ] R6.14 重发 0.1.5(用户定:仍为 0.1.5,全部重建并重传):删去 v0.1.5 的全部文件,标签移到 R6.12、R6.13 的提交,三端全部包重建并上传,本机重装
+3
View File
@@ -0,0 +1,3 @@
FAIL
- crates/ely-lok/src/ffi/accessory.rs:38 — The worker still registers briefly as a foreground application: `sharedApplication` initializes AppKit under the parent bundle's regular identity before line 42 sets the accessory policy. On macOS 27.0.1 arm64, I built this revision and launched the worker under the shipped Doc/Table/Show bundle identities five times each. Eleven of fifteen launches reported `Foreground` before `UIElement`; one ElyTable launch reported those states at 0.1602 s and 0.1892 s respectively. All launches still loaded documents and painted tiles (`.work/R6.12-r1/dock-repeat.log`). This leaves a startup window for the second Dock entry and disproves the claim that the worker registers as UIElement from the outset; bundled apps default to the Dock-visible regular policy ([Apple documentation](https://developer.apple.com/documentation/appkit/nsapplication/activationpolicy-swift.enum/regular)). Establish the worker's agent identity before AppKit first registers it, preserving the main application's regular identity, and add a bundled-startup regression test that rejects any `Foreground` observation while checking document loading and rendering.
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# dock-macos.sh: the LOK worker never registers as a Dock app.
set -euo pipefail
[ "$(uname)" = Darwin ] || { echo "SKIP: macOS only"; exit 0; }
runs=${ELY_DOCK_RUNS:-5}
apps=("$@")
[ ${#apps[@]} -gt 0 ] || apps=(dist/ElyDoc.app dist/ElyTable.app dist/ElyShow.app)
# One launch: samples the worker's type until a tile shows.
launch() {
local bin=$1 log=$2 start pid worker="" types="" asn i=0
start=$(( $( [ -f "$log" ] && wc -l < "$log" || echo 0 ) + 1 ))
ELY_NO_DIALOG=1 ELY_SMOKE=hold ELY_LOG=debug "$bin" >/dev/null 2>&1 &
pid=$!
local end=$((SECONDS + 60)) shown=0
while [ $SECONDS -lt $end ] && kill -0 "$pid" 2>/dev/null; do
[ -n "$worker" ] || worker=$(pgrep -P "$pid" -x ely-lok-worker || true)
if [ -n "$worker" ]; then
asn=$(lsappinfo find "pid=$worker" || true)
[ -z "$asn" ] || types+=" $(lsappinfo info -only applicationtype "$asn" | grep -o 'type="[A-Za-z]*"' || true)"
fi
i=$((i + 1))
if [ $((i % 20)) -eq 0 ] && tail -n +"$start" "$log" 2>/dev/null | grep -q 'lok: tile ('; then shown=1; break; fi
done
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
local seen
seen=$(tr ' ' '\n' <<<"$types" | grep . | sort | uniq -c | tr -s ' \n' ' ')
echo "$(basename "$bin"): worker $worker:$seen"
[ "$shown" = 1 ] || { echo "FAIL: no tile shown"; return 1; }
[ -n "$worker" ] || { echo "FAIL: no worker started"; return 1; }
case $types in *Foreground*) echo "FAIL: the worker was a Dock app"; return 1 ;; esac
case $types in *UIElement*) ;; *) echo "FAIL: the worker's type was never seen"; return 1 ;; esac
tail -n +"$start" "$log" | grep -q 'lok: the worker is an accessory app' || { echo "FAIL: no accessory log"; return 1; }
}
for app in "${apps[@]}"; do
[ -d "$app" ] || { echo "FAIL: $app is missing"; exit 1; }
bin=$(find "$app/Contents/MacOS" -maxdepth 1 -type f -name 'ely-*' ! -name 'ely-lok-worker' | head -1)
[ -n "$bin" ] || { echo "FAIL: $app has no app binary"; exit 1; }
log="$HOME/Library/Application Support/ElyOffice/$(basename "$bin")/ely.log"
for _ in $(seq 1 "$runs"); do launch "$bin" "$log"; done
done
echo "PASS: no LOK worker registered as a Dock app"