Recover Rime faults, persist window modes and fix GPU allocation

This commit is contained in:
aria committed 2026-10-28 02:32:19 +09:00
1 parent 8e9a5838ab
commit 6f34fd080f
27 files changed
+1260 -34

No files matched your search

+1
View File
@@ -20,6 +20,7 @@ tiny-skia = "0.11.4"
wayland-client.workspace = true
wayland-protocols = { version = "0.32.13", features = ["client", "staging"] }
wayland-protocols-misc = { version = "0.3.12", features = ["client"] }
wayland-protocols-wlr.workspace = true
xkbcommon = "0.9.0"
[build-dependencies]
+152 -7
View File
@@ -1,14 +1,18 @@
//! Integration-test input source. Refuses physical sessions and non-Ferese
//! servers before creating a keyboard; never injects into the host desktop.
//! servers before creating a keyboard. Physical validation additionally requires
//! the private test runtime and the exact disposable compositor's peer PID.
use std::error::Error;
use std::io::{Seek, Write};
use std::os::fd::AsFd;
use std::os::fd::{AsFd, AsRawFd};
use std::time::{Duration, Instant};
use wayland_client::protocol::{wl_output, wl_registry, wl_seat};
use wayland_client::protocol::{wl_output, wl_pointer, wl_registry, wl_seat};
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
zwp_virtual_keyboard_manager_v1 as manager, zwp_virtual_keyboard_v1 as keyboard,
};
use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1 as pointer_manager, zwlr_virtual_pointer_v1 as virtual_pointer,
};
use xkbcommon::xkb;
#[derive(Default)]
@@ -17,6 +21,7 @@ struct State {
seat: Option<wl_seat::WlSeat>,
ferese: bool,
outputs: Vec<String>,
pointer_manager: Option<pointer_manager::ZwlrVirtualPointerManagerV1>,
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event(
@@ -42,6 +47,9 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
"wl_output" if version >= 4 => {
let _: wl_output::WlOutput = registry.bind(name, 4, qh, ());
}
"zwlr_virtual_pointer_manager_v1" => {
state.pointer_manager = Some(registry.bind(name, version.min(2), qh, ()));
}
_ => (),
}
}
@@ -65,6 +73,16 @@ delegate_noop!(State: ignore wl_seat::WlSeat);
delegate_noop!(State: ignore manager::ZwpVirtualKeyboardManagerV1);
delegate_noop!(State: ignore keyboard::ZwpVirtualKeyboardV1);
fn parse_numbers(text: &str) -> Result<Vec<u32>, Box<dyn Error>> {
Ok(text
.split(',')
.map(str::parse)
.collect::<Result<Vec<_>, _>>()?)
}
delegate_noop!(State: ignore pointer_manager::ZwlrVirtualPointerManagerV1);
delegate_noop!(State: ignore virtual_pointer::ZwlrVirtualPointerV1);
fn main() -> Result<(), Box<dyn Error>> {
if std::env::var("FERESE_TEST_INPUT").as_deref() != Ok("1") {
return Err(
@@ -72,9 +90,33 @@ fn main() -> Result<(), Box<dyn Error>> {
);
}
let mut sequences: Vec<Vec<u32>> = Vec::new();
let mut clicks = Vec::new();
let mut drag = None;
let mut extent = (1280, 800);
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--extent" => {
let v = parse_numbers(&args.next().ok_or("missing extent")?)?;
if v.len() != 2 || v[0] == 0 || v[1] == 0 {
return Err("invalid extent".into());
}
extent = (v[0], v[1]);
}
"--click" => {
let v = parse_numbers(&args.next().ok_or("missing click position")?)?;
if v.len() != 2 {
return Err("invalid click".into());
}
clicks.push((v[0], v[1]));
}
"--drag" => {
let v = parse_numbers(&args.next().ok_or("missing drag positions")?)?;
if v.len() != 4 {
return Err("invalid drag".into());
}
drag = Some(v);
}
"--keys" => {
for character in args.next().ok_or("missing text")?.chars() {
sequences.push(vec![character as u32]);
@@ -101,10 +143,36 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut state = State::default();
queue.roundtrip(&mut state)?;
queue.roundtrip(&mut state)?;
if !state.ferese
|| state.outputs.is_empty()
|| state.outputs.iter().any(|name| name != "ferese-winit")
{
let nested = state.outputs.iter().all(|name| name == "ferese-winit");
// SAFETY: getuid takes no pointers and has no preconditions.
let uid = unsafe { libc::getuid() };
let drm = std::env::var("XDG_RUNTIME_DIR")
.ok()
.is_some_and(|runtime| runtime == format!("/run/user/{uid}/ferese-rime-drm-validation"))
&& std::env::var("FERESE_TEST_DRM_PID")
.ok()
.and_then(|pid| pid.parse::<i32>().ok())
.is_some_and(|expected| {
let mut credentials = libc::ucred {
pid: 0,
uid: 0,
gid: 0,
};
let mut size = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
// SAFETY: the socket is live and the output/length pointers
// refer to correctly sized initialized storage for SO_PEERCRED.
unsafe {
libc::getsockopt(
connection.as_fd().as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_PEERCRED,
(&mut credentials as *mut libc::ucred).cast(),
&mut size,
) == 0
&& credentials.pid == expected
}
});
if !state.ferese || state.outputs.is_empty() || !(nested || drm) {
return Err("refusing input: target is not an isolated Ferese nested output".into());
}
let keyboard = state
@@ -152,6 +220,83 @@ fn main() -> Result<(), Box<dyn Error>> {
std::thread::sleep(Duration::from_millis(35));
}
}
if !clicks.is_empty() || drag.is_some() {
let pointer = state
.pointer_manager
.as_ref()
.ok_or("test pointer is not enabled on this compositor")?
.create_virtual_pointer(state.seat.as_ref(), &qh, ());
for (x, y) in clicks {
pointer.motion_absolute(
started.elapsed().as_millis() as u32,
x,
y,
extent.0,
extent.1,
);
pointer.frame();
queue.roundtrip(&mut state)?;
std::thread::sleep(Duration::from_millis(80));
pointer.button(
started.elapsed().as_millis() as u32,
0x110,
wl_pointer::ButtonState::Pressed,
);
pointer.frame();
queue.roundtrip(&mut state)?;
std::thread::sleep(Duration::from_millis(80));
pointer.button(
started.elapsed().as_millis() as u32,
0x110,
wl_pointer::ButtonState::Released,
);
pointer.frame();
queue.roundtrip(&mut state)?;
std::thread::sleep(Duration::from_millis(150));
}
if let Some(v) = drag {
pointer.motion_absolute(
started.elapsed().as_millis() as u32,
v[0],
v[1],
extent.0,
extent.1,
);
pointer.frame();
keyboard.key(started.elapsed().as_millis() as u32, 125, 1);
queue.roundtrip(&mut state)?;
pointer.button(
started.elapsed().as_millis() as u32,
0x110,
wl_pointer::ButtonState::Pressed,
);
pointer.frame();
queue.roundtrip(&mut state)?;
for step in 1..=20i64 {
let x = v[0] as i64 + (v[2] as i64 - v[0] as i64) * step / 20;
let y = v[1] as i64 + (v[3] as i64 - v[1] as i64) * step / 20;
pointer.motion_absolute(
started.elapsed().as_millis() as u32,
x as u32,
y as u32,
extent.0,
extent.1,
);
pointer.frame();
queue.roundtrip(&mut state)?;
std::thread::sleep(Duration::from_millis(20));
}
pointer.button(
started.elapsed().as_millis() as u32,
0x110,
wl_pointer::ButtonState::Released,
);
pointer.frame();
keyboard.key(started.elapsed().as_millis() as u32, 125, 0);
queue.roundtrip(&mut state)?;
}
pointer.destroy();
}
keyboard.destroy();
queue.roundtrip(&mut state)?;
println!("nested test input complete");
+14
View File
@@ -1,4 +1,5 @@
pub mod panel;
pub mod profiles;
/// The number-row paging keys are local to a composition with candidates.
/// Control/Alt/Super shortcuts and literal ASCII typing retain their symbols.
@@ -31,6 +32,7 @@ pub struct Candidate {
pub struct Snapshot {
pub accepted: bool,
pub ascii: bool,
pub schema: String,
pub commit: String,
pub preedit: String,
/// Byte offset, matching text-input-v3 (not a character count).
@@ -52,6 +54,7 @@ struct RawResult {
count: c_int,
commit: *mut c_char,
preedit: *mut c_char,
schema: *mut c_char,
texts: [*mut c_char; 9],
comments: [*mut c_char; 9],
}
@@ -62,6 +65,7 @@ unsafe extern "C" {
fn ferese_rime_snapshot(key: c_int, mask: c_int, out: *mut RawResult);
fn ferese_rime_result_free(out: *mut RawResult);
fn ferese_rime_clear();
fn ferese_rime_profile(schema: *const c_char, ascii_mode: c_int) -> c_int;
fn ferese_rime_close();
}
@@ -113,6 +117,15 @@ impl Engine {
unsafe { ferese_rime_clear() };
}
pub fn set_profile(&mut self, profile: &profiles::Profile) -> Result<(), String> {
let schema = CString::new(profile.schema.as_str()).map_err(|_| "invalid schema")?;
// SAFETY: the unique session and schema string remain live during this call.
if unsafe { ferese_rime_profile(schema.as_ptr(), i32::from(profile.ascii)) } == 0 {
return Err(format!("Rime schema is unavailable: {}", profile.schema));
}
Ok(())
}
fn snapshot(&mut self, key: i32, mask: i32) -> Snapshot {
// SAFETY: all-zero is valid for this C struct's integers/null pointers;
// the shim initializes and returns owned, NUL-terminated UTF-8 strings.
@@ -136,6 +149,7 @@ impl Engine {
let result = Snapshot {
accepted: raw.accepted != 0,
ascii: raw.ascii_mode != 0,
schema: text(raw.schema),
commit: text(raw.commit),
preedit,
cursor: cursor as i32,
+159 -2
View File
@@ -1,11 +1,13 @@
//! Independent native Wayland IME. librime and candidate rasterization never
//! execute on Ferese's compositor thread.
mod watchdog;
use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use std::io::{Read, Seek, Write};
use std::os::fd::{AsFd, AsRawFd};
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::sync::mpsc::{self, Receiver};
use std::time::{Duration, Instant};
@@ -13,6 +15,7 @@ use std::time::{Duration, Instant};
use ferese_rime::{
Engine, Snapshot,
panel::{Colors, Panel},
profiles::{Context, Profile, Profiles},
};
use wayland_client::protocol::{
wl_buffer, wl_compositor, wl_keyboard, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface,
@@ -60,6 +63,8 @@ struct State {
engine: Engine,
panel: Panel,
theme: Receiver<Colors>,
profiles: Profiles,
control: UnixListener,
compositor: Option<wl_compositor::WlCompositor>,
shm: Option<wl_shm::WlShm>,
seat: Option<wl_seat::WlSeat>,
@@ -77,6 +82,7 @@ struct State {
serial: u32,
active: bool,
pending_active: bool,
new_context: bool,
sensitive: bool,
pending_sensitive: bool,
last: Snapshot,
@@ -123,6 +129,27 @@ impl State {
self.routes.clear();
return;
}
if self.new_context {
self.new_context = false;
let context = ferese_ipc::Connection::connect()
.ok()
.and_then(|mut c| c.call("get-input-context", serde_json::json!({})).ok())
.and_then(|v| serde_json::from_value::<Context>(v).ok());
let profile = self.profiles.focus(context);
if let Err(error) = self.profiles.flush() {
eprintln!(
"{}",
serde_json::json!({"event":"preferences-error","error":error})
);
}
if let Err(error) = self.engine.set_profile(&profile) {
eprintln!(
"{}",
serde_json::json!({"event":"profile-error","error":error})
);
}
self.last = self.engine.current();
}
if self.grab.is_some() {
return;
}
@@ -184,6 +211,21 @@ impl State {
let keysym = xkb.key_get_one_sym(xkb::Keycode::new(key + 8)).raw();
let repeatable = xkb.get_keymap().key_repeats(xkb::Keycode::new(key + 8));
let mask = self.mask() | if pressed { 0 } else { 1 << 30 };
if pressed && keysym == 0x20 && mask & (1 << 2) != 0 && mask & ((1 << 3) | (1 << 26)) == 0 {
let profile = Profile {
ascii: !self.last.ascii,
schema: self.last.schema.clone(),
};
if let Err(error) = self.engine.set_profile(&profile) {
self.error = Some(error);
return;
}
self.routes.insert(key, false);
self.repeat = None;
let snapshot = self.engine.current();
self.publish(snapshot, qh);
return;
}
let (keysym, mask, paging) =
ferese_rime::paging_key(keysym, mask, !self.last.candidates.is_empty());
let mut snapshot = self.engine.process(keysym, mask);
@@ -216,6 +258,17 @@ impl State {
}
fn publish(&mut self, snapshot: Snapshot, qh: &QueueHandle<Self>) {
if !snapshot.schema.is_empty()
&& let Err(error) = self.profiles.remember(Profile {
ascii: snapshot.ascii,
schema: snapshot.schema.clone(),
})
{
eprintln!(
"{}",
serde_json::json!({"event":"preferences-error","error":error})
);
}
let method = self.method.as_ref().unwrap();
if !snapshot.commit.is_empty() {
method.commit_string(snapshot.commit.clone());
@@ -226,7 +279,7 @@ impl State {
eprintln!(
"{}",
serde_json::json!({"event":"composition", "preedit_chars":snapshot.preedit.chars().count(),
"candidates":snapshot.candidates.len(), "committed_chars":snapshot.commit.chars().count(), "ascii":snapshot.ascii, "page":snapshot.page})
"candidates":snapshot.candidates.len(), "committed_chars":snapshot.commit.chars().count(), "ascii":snapshot.ascii, "schema":snapshot.schema, "page":snapshot.page})
);
self.last = snapshot;
if !visible {
@@ -290,6 +343,25 @@ impl State {
}
fn tick(&mut self, qh: &QueueHandle<Self>) {
// Control is local and contains preferences only, never typed text.
while let Ok((mut stream, _)) = self.control.accept() {
let _ = stream.set_read_timeout(Some(Duration::from_millis(50)));
let _ = stream.set_write_timeout(Some(Duration::from_millis(50)));
let mut bytes = [0u8; 4096];
let result = stream
.read(&mut bytes)
.map_err(|e| e.to_string())
.and_then(|n| {
serde_json::from_slice::<serde_json::Value>(&bytes[..n])
.map_err(|e| e.to_string())
})
.and_then(|request| self.control_request(request, qh));
let response = match result {
Ok(value) => serde_json::json!({"ok":true,"state":value}),
Err(error) => serde_json::json!({"ok":false,"error":error}),
};
let _ = writeln!(stream, "{response}");
}
let mut changed = false;
while let Ok(colors) = self.theme.try_recv() {
self.panel.colors = colors;
@@ -323,6 +395,34 @@ impl State {
self.publish(snapshot, qh);
}
}
fn control_request(
&mut self,
request: serde_json::Value,
qh: &QueueHandle<Self>,
) -> Result<serde_json::Value, String> {
if request["command"] == "set" {
if !self.active || self.sensitive {
return Err("no ordinary text input is active".into());
}
let mut profile = self.profiles.current.clone();
if let Some(schema) = request["schema"].as_str() {
profile.schema = schema.to_owned();
}
if let Some(ascii) = request["ascii"].as_bool() {
profile.ascii = ascii;
}
self.engine.set_profile(&profile)?;
let snapshot = self.engine.current();
self.publish(snapshot, qh);
} else if request["command"] != "status" {
return Err("unknown input-method command".into());
}
Ok(
serde_json::json!({"active":self.active,"sensitive":self.sensitive,
"context":self.profiles.context,"profile":self.profiles.current,"pid":std::process::id()}),
)
}
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
@@ -377,6 +477,7 @@ impl Dispatch<method::ZwpInputMethodV2, ()> for State {
method::Event::Activate => {
state.pending_active = true;
state.pending_sensitive = false;
state.new_context = true;
}
method::Event::Deactivate => state.pending_active = false,
method::Event::ContentType { hint, purpose } => {
@@ -555,6 +656,13 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut shared = PathBuf::from("/usr/share/rime-data");
let mut schema = "luna_pinyin_simp".to_owned();
let mut self_test = false;
let mut control = std::env::var_os("FERESE_RIME_CONTROL")
.map(PathBuf::from)
.unwrap_or_else(|| {
PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap_or_else(|| "/tmp".into()))
.join("ferese/rime-control.sock")
});
let mut command = None;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
@@ -562,19 +670,46 @@ fn main() -> Result<(), Box<dyn Error>> {
"--shared-data" => shared = args.next().ok_or("missing shared data path")?.into(),
"--schema" => schema = args.next().ok_or("missing Rime schema")?,
"--self-test" => self_test = true,
"--control" => control = args.next().ok_or("missing control path")?.into(),
"--status" => command = Some(serde_json::json!({"command":"status"})),
"--mode" => {
let mode = args.next().ok_or("missing mode")?;
if !matches!(mode.as_str(), "ascii" | "rime") {
return Err("mode must be ascii or rime".into());
}
command = Some(serde_json::json!({"command":"set","ascii":mode=="ascii"}));
}
"--select-schema" => {
command = Some(
serde_json::json!({"command":"set","schema":args.next().ok_or("missing schema")?}),
)
}
"--version" => {
println!("ferese-rime {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
"--help" => {
println!(
"ferese-rime [--user-data PATH] [--shared-data PATH] [--schema NAME] [--self-test]"
"ferese-rime [--user-data PATH] [--shared-data PATH] [--schema NAME] [--control PATH]\n --status | --mode ascii|rime | --select-schema NAME | --self-test"
);
return Ok(());
}
_ => return Err(format!("unknown option {arg}").into()),
}
}
if let Some(command) = command {
let mut stream = UnixStream::connect(&control)?;
stream.set_read_timeout(Some(Duration::from_secs(3)))?;
writeln!(stream, "{command}")?;
stream.shutdown(std::net::Shutdown::Write)?;
let mut response = String::new();
stream.read_to_string(&mut response)?;
println!("{}", response.trim());
if serde_json::from_str::<serde_json::Value>(&response)?["ok"] != true {
return Err("input-method command failed".into());
}
return Ok(());
}
std::fs::create_dir_all(&user)?;
std::fs::set_permissions(&user, std::fs::Permissions::from_mode(0o700))?;
let mut engine = Engine::open(&user, &shared, &schema)?;
@@ -598,6 +733,21 @@ fn main() -> Result<(), Box<dyn Error>> {
);
return Ok(());
}
let profiles = Profiles::open(&user.join("input-profiles.json"), schema)?;
let parent = control.parent().ok_or("control path has no parent")?;
std::fs::create_dir_all(parent)?;
if control.exists() {
use std::os::unix::fs::FileTypeExt;
if !std::fs::symlink_metadata(&control)?.file_type().is_socket()
|| UnixStream::connect(&control).is_ok()
{
return Err("input-method control path is already in use".into());
}
std::fs::remove_file(&control)?;
}
let listener = UnixListener::bind(&control)?;
std::fs::set_permissions(&control, std::fs::Permissions::from_mode(0o600))?;
listener.set_nonblocking(true)?;
let panel = Panel::new()?;
let (send, theme) = mpsc::channel();
std::thread::spawn(move || {
@@ -625,6 +775,8 @@ fn main() -> Result<(), Box<dyn Error>> {
engine,
panel,
theme,
profiles,
control: listener,
compositor: None,
shm: None,
seat: None,
@@ -642,6 +794,7 @@ fn main() -> Result<(), Box<dyn Error>> {
serial: 0,
active: false,
pending_active: false,
new_context: false,
sensitive: false,
pending_sensitive: false,
last: Snapshot::default(),
@@ -652,9 +805,13 @@ fn main() -> Result<(), Box<dyn Error>> {
};
queue.roundtrip(&mut state)?;
state.initialize(&qh)?;
let mut watchdog = watchdog::Watchdog::from_environment();
loop {
queue.dispatch_pending(&mut state)?;
state.tick(&qh);
if let Some(watchdog) = &mut watchdog {
watchdog.tick();
}
if let Some(error) = state.error.take() {
return Err(error.into());
}
+185
View File
@@ -0,0 +1,185 @@
//! Window-local mode/schema preferences, with application defaults for reopening.
//! Persist preferences only: never composition, surrounding text or window titles.
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct Profile {
pub schema: String,
pub ascii: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Context {
pub session: String,
pub window: u64,
pub app_id: String,
}
impl Context {
fn key(&self) -> String {
format!("{}:{}", self.session, self.window)
}
}
#[derive(Default, Deserialize, Serialize)]
struct Stored {
#[serde(default)]
applications: BTreeMap<String, Profile>,
#[serde(default)]
windows: BTreeMap<String, Profile>,
}
pub struct Profiles {
path: PathBuf,
default: Profile,
stored: Stored,
dirty: bool,
pub context: Option<Context>,
pub current: Profile,
}
impl Profiles {
pub fn open(path: &Path, schema: String) -> Result<Self, String> {
let stored = match std::fs::read(path) {
Ok(bytes) => serde_json::from_slice(&bytes)
.map_err(|e| format!("invalid IME preferences: {e}"))?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Stored::default(),
Err(e) => return Err(e.to_string()),
};
let default = Profile {
schema,
ascii: false,
};
Ok(Self {
path: path.to_owned(),
stored,
dirty: false,
context: None,
current: default.clone(),
default,
})
}
pub fn focus(&mut self, context: Option<Context>) -> Profile {
if let Some(context) = &context {
let prefix = format!("{}:", context.session);
self.stored
.windows
.retain(|key, _| key.starts_with(&prefix));
}
let profile = context
.as_ref()
.and_then(|c| {
self.stored
.windows
.get(&c.key())
.or_else(|| self.stored.applications.get(&c.app_id))
})
.unwrap_or(&self.default)
.clone();
self.context = context;
self.current = profile.clone();
// Pin even the initial default. A second window of the same application
// can change its mode without changing this already-open window.
if let Some(context) = &self.context {
self.dirty |= !self.stored.windows.contains_key(&context.key());
self.stored
.windows
.entry(context.key())
.or_insert_with(|| profile.clone());
}
profile
}
pub fn remember(&mut self, profile: Profile) -> Result<(), String> {
if self.current == profile {
return self.flush();
}
self.current = profile.clone();
let Some(context) = &self.context else {
return Ok(());
};
self.stored.windows.insert(context.key(), profile.clone());
if !context.app_id.is_empty() {
self.stored
.applications
.insert(context.app_id.clone(), profile);
}
self.dirty = true;
self.flush()
}
pub fn flush(&mut self) -> Result<(), String> {
if self.dirty {
self.save()?;
self.dirty = false;
}
Ok(())
}
fn save(&self) -> Result<(), String> {
let parent = self.path.parent().ok_or("preferences path has no parent")?;
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
let mut temp = tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?;
temp.as_file()
.set_permissions(std::fs::Permissions::from_mode(0o600))
.map_err(|e| e.to_string())?;
serde_json::to_writer_pretty(&mut temp, &self.stored).map_err(|e| e.to_string())?;
temp.write_all(b"\n")
.and_then(|()| temp.as_file().sync_all())
.map_err(|e| e.to_string())?;
temp.persist(&self.path).map_err(|e| e.to_string())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn context(window: u64, app: &str) -> Option<Context> {
Some(Context {
session: "one".into(),
window,
app_id: app.into(),
})
}
#[test]
fn isolates_windows_restores_closed_apps_and_survives_service_restart() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("preferences.json");
let rime = Profile {
schema: "luna_pinyin_simp".into(),
ascii: false,
};
let english = Profile {
ascii: true,
..rime.clone()
};
let other = Profile {
schema: "luna_pinyin".into(),
ascii: false,
};
let mut p = Profiles::open(&path, rime.schema.clone()).unwrap();
assert_eq!(p.focus(context(1, "QQ")), rime);
p.remember(english.clone()).unwrap();
assert_eq!(p.focus(context(2, "QQ")), english);
p.remember(other.clone()).unwrap();
assert_eq!(p.focus(context(1, "QQ")), english);
assert_eq!(p.focus(context(3, "teams")), rime);
drop(p);
let mut p = Profiles::open(&path, rime.schema.clone()).unwrap();
assert_eq!(p.focus(context(1, "QQ")), english);
assert_eq!(p.focus(context(2, "QQ")), other);
assert_eq!(p.focus(context(4, "QQ")), other);
let mut fresh = context(1, "QQ").unwrap();
fresh.session = "two".into();
assert_eq!(p.focus(Some(fresh)), other);
assert_eq!(p.focus(None), rime);
assert_eq!(
std::fs::metadata(path).unwrap().permissions().mode() & 0o777,
0o600
);
}
}
+9 -2
View File
@@ -5,7 +5,7 @@
/* Keep the versioned librime ABI in this small, header-checked boundary. */
typedef struct {
int accepted, ascii_mode, cursor, highlighted, page_no, last_page, count;
char *commit, *preedit;
char *commit, *preedit, *schema;
char *texts[9], *comments[9];
} FereseRimeResult;
@@ -57,15 +57,22 @@ void ferese_rime_snapshot(int key, int mask, FereseRimeResult *out) {
RIME_STRUCT(RimeStatus, status);
if (api->get_status(session, &status)) {
out->ascii_mode = status.is_ascii_mode;
out->schema = strdup(status.schema_id ? status.schema_id : "");
api->free_status(&status);
}
}
void ferese_rime_result_free(FereseRimeResult *out) {
free(out->commit); free(out->preedit);
free(out->commit); free(out->preedit); free(out->schema);
for (int n = 0; n < out->count; n++) { free(out->texts[n]); free(out->comments[n]); }
}
void ferese_rime_clear(void) { if (session) api->clear_composition(session); }
int ferese_rime_profile(const char *schema, int ascii_mode) {
if (!api->select_schema(session, schema)) return 0;
api->clear_composition(session);
api->set_option(session, "ascii_mode", ascii_mode);
return 1;
}
void ferese_rime_close(void) {
if (session) api->destroy_session(session);
if (api) api->finalize();
+39
View File
@@ -0,0 +1,39 @@
//! Heartbeat from the input-processing loop: a blocked engine cannot fake health.
use std::os::linux::net::SocketAddrExt;
use std::os::unix::net::{SocketAddr, UnixDatagram};
use std::time::{Duration, Instant};
pub struct Watchdog {
socket: UnixDatagram,
interval: Duration,
next: Instant,
}
impl Watchdog {
pub fn from_environment() -> Option<Self> {
let path = std::env::var("NOTIFY_SOCKET").ok()?;
let micros = std::env::var("WATCHDOG_USEC").ok()?.parse::<u64>().ok()?;
if micros == 0 {
return None;
}
let address = if let Some(name) = path.strip_prefix('@') {
SocketAddr::from_abstract_name(name.as_bytes()).ok()?
} else {
SocketAddr::from_pathname(path).ok()?
};
let socket = UnixDatagram::unbound().ok()?;
socket.connect_addr(&address).ok()?;
socket.set_nonblocking(true).ok()?;
socket.send(b"READY=1\nWATCHDOG=1").ok()?;
Some(Self {
socket,
interval: Duration::from_micros((micros / 3).max(1)),
next: Instant::now(),
})
}
pub fn tick(&mut self) {
if Instant::now() >= self.next {
let _ = self.socket.send(b"WATCHDOG=1");
self.next = Instant::now() + self.interval;
}
}
}