#!/usr/bin/env bash set -euo pipefail session_target_started=0 shell_pid= agent_pid= autostart_pid= rime_unit= manage_session=0 if [[ ${FERESE_SESSION_MODE:-desktop} == desktop && ${FERESE_SESSION_IMPORT_ENV:-0} == 1 ]]; then manage_session=1 fi cleanup() { trap - EXIT INT TERM if [[ -n $autostart_pid ]]; then kill "$autostart_pid" 2>/dev/null || true wait "$autostart_pid" 2>/dev/null || true fi if [[ -n $shell_pid ]]; then kill "$shell_pid" 2>/dev/null || true; fi if [[ -n $agent_pid ]]; then kill "$agent_pid" 2>/dev/null || true wait "$agent_pid" 2>/dev/null || true fi if [[ -n $rime_unit ]]; then timeout --kill-after=1s 6s systemctl --user stop "$rime_unit" || true fi if ((session_target_started)); then systemctl --user stop ferese-session.target || true fi } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM # The compositor supplies its new Wayland socket before this helper starts. # Nested previews must never replace the host session's activation environment. if ((manage_session)); then # Shell control uses a private socket FD. Activation must use the public # socket so portal and other desktop services connect as ordinary clients. public_display=${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}} # Empty assignments replace stale values; D-Bus cannot remove activation variables. activation_vars=( "WAYLAND_DISPLAY=$public_display" "DISPLAY=${DISPLAY:-}" "XAUTHORITY=${XAUTHORITY:-}" "FERESE_SOCKET=${FERESE_SOCKET:-}" XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE ) if [[ ${FERESE_ENABLE_INPUT_METHOD:-0} == 1 ]]; then activation_vars+=("GTK_IM_MODULE=" QT_IM_MODULE QT_IM_MODULES IBUS_ADDRESS FERESE_RIME_CONTROL "SDL_IM_MODULE=" "XMODIFIERS=${XMODIFIERS:-}") fi if command -v dbus-update-activation-environment >/dev/null; then if ! env WAYLAND_DISPLAY="$public_display" \ dbus-update-activation-environment --systemd "${activation_vars[@]}"; then env WAYLAND_DISPLAY="$public_display" \ dbus-update-activation-environment "${activation_vars[@]}" || true fi fi # User buses can outlive logout and may predate a newly installed service directory. if command -v dbus-send >/dev/null; then dbus-send --session --type=method_call --print-reply \ --dest=org.freedesktop.DBus /org/freedesktop/DBus \ org.freedesktop.DBus.ReloadConfig >/dev/null || true fi if command -v systemctl >/dev/null; then systemctl --user daemon-reload || true if systemctl --user start ferese-session.target; then session_target_started=1 # A user manager can retain the target across session transitions. # Refresh an already-running portal after importing this session's # environment so it uses the installed release and current display. systemctl --user try-restart xdg-desktop-portal-ferese.service || true else echo 'Ferese: graphical session target could not start; portal activation may be unavailable.' >&2 fi fi fi release_dir=$(dirname -- "$(readlink -f -- "$0")") # Start the native input method on the public socket, without shell privileges. # Pin the executable to this release and let systemd recover both crashes and a # stalled input loop. A failed IME must leave the compositor and shell usable. start_rime() { local user_data=${FERESE_RIME_USER_DATA:-${XDG_DATA_HOME:-$HOME/.local/share}/ferese/rime} local shared_data=${FERESE_RIME_SHARED_DATA:-/usr/share/rime-data} local identity=${FERESE_SOCKET:-${XDG_RUNTIME_DIR:-}/$public_display} local name variable load_state deadline local -a environment=() name=$(systemd-escape --template=ferese-rime-session@.service "$identity") || return 1 [[ -x $release_dir/ferese-rime ]] || return 1 [[ -r $shared_data/luna_pinyin_simp.schema.yaml ]] || { echo 'Ferese: install the Rime Luna Pinyin schemas before starting native input.' >&2 return 1 } mkdir -p -- "$user_data" || return 1 # noclobber preserves existing customization, including a concurrent setup. if [[ ! -e $user_data/default.custom.yaml && ! -L $user_data/default.custom.yaml ]]; then (set -o noclobber; cat -- "$release_dir/rime/default.custom.yaml" > "$user_data/default.custom.yaml") || return 1 fi # SIGKILL cannot run the previous helper's trap. The compositor control socket # survives helper recovery, so replace only its old IME instance. Bound all # manager calls and wait for the transient unit to be collected before reuse. load_state=$(timeout --kill-after=1s 2s systemctl --user show "$name" --property=LoadState --value) || return 1 if [[ $load_state != not-found ]]; then timeout --kill-after=1s 6s systemctl --user stop "$name" || return 1 deadline=$((SECONDS + 3)) while [[ $load_state != not-found ]]; do ((SECONDS < deadline)) || return 1 load_state=$(timeout --kill-after=1s 1s systemctl --user show "$name" --property=LoadState --value) || return 1 if [[ $load_state != not-found ]]; then sleep .05; fi done fi for variable in HOME PATH LANG XDG_RUNTIME_DIR XDG_CONFIG_HOME XDG_CACHE_HOME XDG_DATA_HOME XDG_STATE_HOME \ DBUS_SESSION_BUS_ADDRESS XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE \ FERESE_SOCKET FERESE_RIME_CONTROL; do if [[ -v $variable ]]; then environment+=("--setenv=$variable=${!variable}"); fi done if timeout --kill-after=1s 6s systemd-run --user --collect --quiet --no-block --unit="$name" --service-type=notify \ --property=Restart=on-failure --property=RestartSec=1 \ --property=StartLimitIntervalSec=60 --property=StartLimitBurst=5 \ --property=WatchdogSec=10 --property=WatchdogSignal=SIGKILL \ --property=TimeoutStartSec=45 --property=TimeoutStopSec=3 \ --property=BindsTo=ferese-session.target --property=PartOf=ferese-session.target \ "${environment[@]}" \ /usr/bin/env -u WAYLAND_SOCKET -u FERESE_SHELL_CONTROL_SOCKET -u FERESE_PRIVATE_X11_COOKIE \ WAYLAND_DISPLAY="$public_display" "$release_dir/ferese-rime" \ --user-data "$user_data" --shared-data "$shared_data"; then rime_unit=$name else return 1 fi } # Agent failures must not take down a healthy shell. The compositor supervises # this helper (and creates fresh private connections) when the shell itself exits. supervise_agent() { local child= delay=1 started trap 'trap - TERM INT; if [[ -n $child ]]; then kill "$child" 2>/dev/null || true; wait "$child" 2>/dev/null || true; fi; exit 0' TERM INT while true; do started=$SECONDS env -u WAYLAND_SOCKET -u FERESE_SHELL_CONTROL_SOCKET \ WAYLAND_DISPLAY="${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}}" \ "$release_dir/ferese-polkit-agent" & child=$! wait "$child" || true child= if ((SECONDS - started >= 60)); then delay=1; fi sleep "$delay" & child=$! wait "$child" || true child= if ((delay < 30)); then delay=$((delay * 2)); fi if ((delay > 30)); then delay=30; fi done } supervise_agent & agent_pid=$! "$release_dir/ferese-shell" & shell_pid=$! if ((manage_session && session_target_started)) && [[ ${FERESE_ENABLE_INPUT_METHOD:-0} == 1 ]]; then if ! start_rime; then echo 'Ferese: native Rime could not start; the desktop remains available.' >&2 fi fi if ((manage_session)) && [[ ${FERESE_CLIENT_RESTART:-0} != 1 && -x $release_dir/feresectl ]]; then env WAYLAND_DISPLAY="${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}}" \ "$release_dir/feresectl" autostart & autostart_pid=$! fi if wait "$shell_pid"; then status=0 else status=$? fi exit "$status"