A calendar reminder showed as a transparent, empty window on shanks and no tool could say which window it was or which preferences Betterbird ran with. The module owns one file, the user.js its setting prefs fills, which its tools place whole into the profile Betterbird uses and never over an operator's own; it reads named preferences without credentials or accounts, restarts Betterbird under the user manager so its output reaches the journal, checks what it promises, and shows a reminder on demand from a throw-away profile so a fix can be proved without the operator's calendar. No package: betterbird-bin is the AUR's.
151 lines
6.3 KiB
Go
151 lines
6.3 KiB
Go
package main
|
|
|
|
// A reminder on demand, without the operator's calendar: a second Betterbird, in a profile of its own
|
|
// under the account's runtime directory, with one local calendar file holding one event whose reminder
|
|
// is due a few seconds after the start. The running Betterbird, its profile, its accounts and its
|
|
// calendars are not touched: the second one starts with --no-remote, so it neither hands over to the
|
|
// first nor is handed to.
|
|
//
|
|
// The test profile gets the module's user.js too, so the reminder is drawn with the preferences the
|
|
// module declares. A theme or another preference of the operator's profile can be carried over by
|
|
// name (carry), within the same limits as betterbird_prefs: never a credential, never an account.
|
|
//
|
|
// It lives for at most ten minutes (the unit's RuntimeMaxSec), and stop ends it early and removes the
|
|
// profile. The reminder window is then found with xorg_windows (class Calendar, role AlarmWindow) and
|
|
// looked at with xorg_screenshot.
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// testDir is the test's profile and calendar, under the account's runtime directory.
|
|
func (m *Machine) testDir() string {
|
|
return m.path(fmt.Sprintf("/run/user/%d/%s", m.UID, testUnit))
|
|
}
|
|
|
|
// carryName is a preference name the test may carry over from the operator's profile.
|
|
var carryName = regexp.MustCompile(`^[A-Za-z0-9_.\-]+$`)
|
|
|
|
// ReminderAnswer is what betterbird_reminder_test answers.
|
|
type ReminderAnswer struct {
|
|
Unit string `json:"unit"`
|
|
Profile string `json:"profile"`
|
|
Event string `json:"event"`
|
|
Due string `json:"reminder_due"`
|
|
Carried []string `json:"carried,omitempty"`
|
|
Withheld []string `json:"withheld,omitempty"`
|
|
Look string `json:"look"`
|
|
Stopped bool `json:"stopped,omitempty"`
|
|
}
|
|
|
|
// ics is the test calendar: one event, its reminder `after` from now.
|
|
func ics(now time.Time, after time.Duration, stamp string) string {
|
|
f := func(t time.Time) string { return t.UTC().Format("20060102T150405Z") }
|
|
start := now.Add(after + 10*time.Minute)
|
|
return strings.Join([]string{"BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//novox mesh//betterbird reminder test//EN",
|
|
"BEGIN:VEVENT", "UID:mesh-reminder-test-" + stamp + "@mesh", "DTSTAMP:" + f(now), "DTSTART:" + f(start),
|
|
"DTEND:" + f(start.Add(30*time.Minute)), "SUMMARY:Mesh reminder test", "BEGIN:VALARM", "ACTION:DISPLAY",
|
|
"DESCRIPTION:Mesh reminder test", "TRIGGER:-PT10M", "END:VALARM", "END:VEVENT", "END:VCALENDAR", ""}, "\r\n")
|
|
}
|
|
|
|
// testPrefs are the test profile's own preferences: its one calendar, and no first-run questions.
|
|
func testPrefs(calendar string) string {
|
|
return strings.Join([]string{
|
|
`user_pref("calendar.registry.meshtest.type", "ics");`,
|
|
`user_pref("calendar.registry.meshtest.uri", "file://` + calendar + `");`,
|
|
`user_pref("calendar.registry.meshtest.name", "Mesh reminder test");`,
|
|
`user_pref("calendar.registry.meshtest.calendar-main-in-composite", true);`,
|
|
`user_pref("calendar.registry.meshtest.suppressAlarms", false);`,
|
|
`user_pref("calendar.list.sortOrder", "meshtest");`,
|
|
`user_pref("mail.provider.suppress_dialog_on_startup", true);`,
|
|
`user_pref("mail.shell.checkDefaultClient", false);`,
|
|
`user_pref("datareporting.policy.dataSubmissionEnabled", false);`,
|
|
`user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);`,
|
|
`user_pref("app.update.auto", false);`,
|
|
`user_pref("mail.rights.version", 1);`,
|
|
"",
|
|
}, "\n")
|
|
}
|
|
|
|
// ReminderTest starts the test Betterbird, or with stop ends it and removes its profile.
|
|
func (m *Machine) ReminderTest(after time.Duration, carry []string, stop bool) (ReminderAnswer, error) {
|
|
dir := m.testDir()
|
|
a := ReminderAnswer{Unit: testUnit + ".service", Profile: strings.TrimPrefix(filepath.Join(dir, "profile"), m.Root),
|
|
Look: "xorg_windows with class Calendar answers the reminder window (role AlarmWindow, its depth and map state); " +
|
|
"xorg_screenshot with that window shows it as the screen does"}
|
|
if stop {
|
|
_ = m.cmd(5*time.Second, m.bus(), "systemctl", "--user", "stop", testUnit+".service")
|
|
if err := os.RemoveAll(dir); err != nil {
|
|
return a, err
|
|
}
|
|
a.Stopped, a.Look = true, ""
|
|
return a, nil
|
|
}
|
|
s, err := m.session()
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
var carried []string
|
|
if len(carry) > 0 {
|
|
p, err := m.profile()
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
prefsJS, _ := readBounded(filepath.Join(p.path, "prefs.js"))
|
|
have := readPrefs(prefsJS)
|
|
for _, n := range carry {
|
|
switch {
|
|
case !carryName.MatchString(n):
|
|
return a, fmt.Errorf("carry holds preference names, not %q", n)
|
|
case withheld(n) != "":
|
|
a.Withheld = append(a.Withheld, n+": "+withheld(n))
|
|
case have[n] == "":
|
|
a.Withheld = append(a.Withheld, n+": not set in the operator's profile")
|
|
default:
|
|
carried = append(carried, fmt.Sprintf("user_pref(%s, %s);", strconv.Quote(n), have[n]))
|
|
a.Carried = append(a.Carried, n)
|
|
}
|
|
}
|
|
}
|
|
_ = m.cmd(5*time.Second, s.Env(), "systemctl", "--user", "stop", testUnit+".service")
|
|
if err := os.RemoveAll(dir); err != nil {
|
|
return a, err
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(dir, "profile"), 0o700); err != nil {
|
|
return a, err
|
|
}
|
|
now := m.Now()
|
|
calendar := filepath.Join(dir, "test.ics")
|
|
if err := os.WriteFile(calendar, []byte(ics(now, after, strconv.FormatInt(now.Unix(), 10))), 0o600); err != nil {
|
|
return a, err
|
|
}
|
|
user := testPrefs(strings.TrimPrefix(calendar, m.Root))
|
|
if mine, err := readBounded(m.home(declared)); err == nil && validUserJS(string(mine)) == nil {
|
|
user = string(mine) + "\n" + user
|
|
}
|
|
user += strings.Join(carried, "\n") + "\n"
|
|
if err := os.WriteFile(filepath.Join(dir, "profile", "user.js"), []byte(user), 0o600); err != nil {
|
|
return a, err
|
|
}
|
|
call := []string{"--user", "--collect", "--quiet", "--unit=" + testUnit, "-p", "RuntimeMaxSec=600",
|
|
"--setenv=MOZ_NO_REMOTE=1"}
|
|
for _, kv := range [][2]string{{"DISPLAY", s.Display}, {"XAUTHORITY", s.XAuthority}} {
|
|
if kv[1] != "" {
|
|
call = append(call, "--setenv="+kv[0]+"="+kv[1])
|
|
}
|
|
}
|
|
call = append(call, "--", startBin, "--no-remote", "--profile", strings.TrimPrefix(filepath.Join(dir, "profile"), m.Root))
|
|
if err := failed(m.cmd(8*time.Second, s.Env(), "systemd-run", call...), "systemd-run", call...); err != nil {
|
|
return a, err
|
|
}
|
|
a.Event = "Mesh reminder test, in a calendar file of the test profile only"
|
|
a.Due = now.Add(after).UTC().Format(time.RFC3339)
|
|
return a, nil
|
|
}
|