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 } // 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 }