A fresh profile has never checked for updates, so Betterbird checked at the test's start, found a newer build published and opened its About window on the operator's screen instead of leaving the reminder to show. The test's own preferences now also come last in its user.js, so nothing declared or carried overrides them.
161 lines
7.1 KiB
Go
161 lines
7.1 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.
|
|
//
|
|
// betterbird.update.checkIntervalDays 0 is Betterbird's own switch for its update check (MailGlue): a
|
|
// fresh profile has never checked, so Betterbird checks at its start and, when betterbird.eu publishes a
|
|
// newer build than the installed one, opens the About window on the operator's screen and gives it the
|
|
// focus (seen on the workstation 2026-10-12, novox/hq issue 345). The test is about the reminder only.
|
|
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("betterbird.update.checkIntervalDays", 0);`,
|
|
`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
|
|
}
|
|
// The module's user.js first, then what is carried, then the test's own preferences: the last
|
|
// statement wins, so neither can take the test's calendar or its quiet start away.
|
|
user := ""
|
|
if mine, err := readBounded(m.home(declared)); err == nil && validUserJS(string(mine)) == nil {
|
|
user = string(mine) + "\n"
|
|
}
|
|
user += strings.Join(carried, "\n") + "\n" + testPrefs(strings.TrimPrefix(calendar, m.Root))
|
|
if err := os.WriteFile(filepath.Join(dir, "profile", "user.js"), []byte(user), 0o600); err != nil {
|
|
return a, err
|
|
}
|
|
// The unit removes the profile when it ends, however it ends: at its 10 minutes, on stop, or when the
|
|
// test Betterbird is closed.
|
|
call := []string{"--user", "--collect", "--quiet", "--unit=" + testUnit, "-p", "RuntimeMaxSec=600",
|
|
"-p", "ExecStopPost=/usr/bin/rm -rf " + strings.TrimPrefix(dir, m.Root), "--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
|
|
}
|