118 lines
3.6 KiB
Go
118 lines
3.6 KiB
Go
// Fenrir console screen reader.
|
|
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify it
|
|
// under the terms of the GNU General Public License as published by the Free
|
|
// Software Foundation, either version 3 of the License, or (at your option)
|
|
// any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
// more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along with
|
|
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
|
|
|
|
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/fenrir/internal/i18n"
|
|
"git.stormux.org/storm/fenrir/internal/presentation"
|
|
"git.stormux.org/storm/fenrir/internal/scripts"
|
|
)
|
|
|
|
// script_username supplies Python's current-user argument without changing
|
|
// process credentials, HOME, cwd or any inherited environment variable.
|
|
// Session lookup runs in a script worker, never in the input dispatcher.
|
|
func script_username(terminalMode bool, terminal int) string {
|
|
if terminalMode {
|
|
for _, name := range []string{os.Getenv("LOGNAME"), os.Getenv("USER")} {
|
|
if name != "" {
|
|
return name
|
|
}
|
|
}
|
|
if current, err := user.Current(); err == nil {
|
|
return current.Username
|
|
}
|
|
return ""
|
|
}
|
|
// Read logind/elogind's local session metadata, matching Python's active-VT
|
|
// lookup without adding a synchronous D-Bus round trip to keyboard handling.
|
|
for _, directory := range []string{"/run/systemd/sessions", "/run/elogind/sessions"} {
|
|
entries, _ := os.ReadDir(directory)
|
|
for _, entry := range entries {
|
|
data, err := os.ReadFile(filepath.Join(directory, entry.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
values := map[string]string{}
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
if key, value, ok := strings.Cut(line, "="); ok {
|
|
values[key] = value
|
|
}
|
|
}
|
|
if values["VTNR"] != strconv.Itoa(terminal) && values["TTY"] != fmt.Sprintf("tty%d", terminal) {
|
|
continue
|
|
}
|
|
if current, err := user.LookupId(values["UID"]); err == nil {
|
|
return current.Username
|
|
}
|
|
}
|
|
}
|
|
return "" // Python also leaves this empty when no session was found.
|
|
}
|
|
|
|
func remote_authorized(uid, terminal int, terminalMode bool) bool {
|
|
if uid == 0 || uid == os.Geteuid() {
|
|
return true
|
|
}
|
|
if terminalMode || os.Geteuid() != 0 || terminal < 1 {
|
|
return false
|
|
}
|
|
info, err := os.Stat(fmt.Sprintf("/dev/tty%d", terminal))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
|
return ok && int(stat.Uid) == uid
|
|
}
|
|
|
|
func (a *application) script_result(result scripts.Result) {
|
|
if result.Generation != a.scriptGeneration || result.Terminal != a.lastTerminal || !a.lastActive {
|
|
return
|
|
}
|
|
if result.Err != nil {
|
|
slog.Warn("script invocation failed", "error", result.Err, "at", time.Now())
|
|
text := "Script failed"
|
|
switch result.Err.Error() {
|
|
case "Script file not found", "Script source is not a valid file", "Script file is not executable":
|
|
text = result.Err.Error()
|
|
}
|
|
a.present(i18n.T(text), presentation.Error)
|
|
return
|
|
}
|
|
if result.Truncated {
|
|
slog.Warn("script output truncated", "at", time.Now())
|
|
}
|
|
// Match Python's stderr-before-stdout result delivery. Speech still
|
|
// follows this reader's shared queue and mute policy.
|
|
if result.Stderr != "" {
|
|
a.present(result.Stderr, "")
|
|
}
|
|
if result.Stdout != "" {
|
|
a.present(result.Stdout, "")
|
|
}
|
|
}
|