mesh/merge-gate head c4b4bac757c5 merged into trunk 0863ec2a059d — pass: builds betterbird, sent nowhere; no bus step; 1 wait(s) for a person; every machin…
mesh/repo-check head c4b4bac757c5 merged into trunk 0863ec2a059d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Betterbird keeps the active theme in its add-on database, and extensions.activeThemeID only mirrors it. So clearing the preference changed nothing, and the drift report then hid that. The theme is now read from extensions.json against default-theme@mozilla.org. Another theme is drift, and its remedy is the add-on manager: config_apply never clears it and keep refuses it. A credential's, an account's or a server's name in the defaults file is refused by code: it is never answered or cleared, and the check names it. Apply also refuses while the profile's lock is there, and syncs prefs.js before the rename. A saved default of a built-in preference is no longer drift.
653 lines
23 KiB
Go
653 lines
23 KiB
Go
package main
|
|
|
|
// The configuration the module ships (novox/hq issue 345), in three layers:
|
|
//
|
|
// - LOCKED: the enterprise policy (/opt/betterbird/distribution/policies.json). Its Preferences entries with
|
|
// Status locked win over everything, and Betterbird's settings show them greyed out.
|
|
// - DEFAULT: the module's defaults file (/opt/betterbird/defaults/pref/mesh-defaults.js), the operator's
|
|
// setup. Betterbird reads it as a default, so a value the profile saved in prefs.js wins over it: that is
|
|
// drift, unless a node layer says so.
|
|
// - NODE: the setting prefs, which the node-engine writes to ~/.config/betterbird/user.js and the tools place
|
|
// in the profile's user.js, where it wins over prefs.js at every start.
|
|
//
|
|
// betterbird_config shows each managed preference through the layers and flags the drift; betterbird_config_apply
|
|
// clears a drifting value from prefs.js while Betterbird is not running; keep proposes a node layer that holds the
|
|
// profile's value for this node instead, through the controller's settings verb, never by writing user.js.
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// prefDir is the installation's own defaults directory; the module places its files there.
|
|
prefDir = installDir + "/defaults/pref"
|
|
// meshDefaults is the module's DEFAULT layer.
|
|
meshDefaults = prefDir + "/mesh-defaults.js"
|
|
// omniJa is Betterbird's packed application, holding its own default preferences.
|
|
omniJa = installDir + "/omni.ja"
|
|
)
|
|
|
|
// moduleFiles are the module's files in the installation's defaults directory: their values are the module's
|
|
// defaults, not Betterbird's.
|
|
var moduleFiles = map[string]bool{"mesh-defaults.js": true, "vendor-prefs.js": true}
|
|
|
|
var (
|
|
// defaultLine is one pref(…) or sticky_pref(…) line of a defaults file; a comment may follow it.
|
|
defaultLine = regexp.MustCompile(`^\s*(?:sticky_)?pref\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*(.*?)\s*\)\s*;`)
|
|
// keepsDefault is a line of the module's defaults naming a preference it manages without a value.
|
|
keepsDefault = regexp.MustCompile(`^//\s*keeps Betterbird's default:\s*([A-Za-z0-9_.@{}\-]+)\s*$`)
|
|
)
|
|
|
|
// builtIn are the defaults compiled into Betterbird of the preferences the module keeps at Betterbird's default,
|
|
// which no defaults file gives: so a value saved equal to them is not drift.
|
|
var builtIn = map[string]string{
|
|
themePref: `"` + defaultTheme + `"`,
|
|
"browser.display.use_system_colors": "false",
|
|
"layout.css.always_underline_links": "false",
|
|
}
|
|
|
|
const (
|
|
themePref = "extensions.activeThemeID"
|
|
defaultTheme = "default-theme@mozilla.org"
|
|
// themeRemedy is how the theme comes back: Betterbird keeps the active theme in its add-on database
|
|
// (extensions.json), and the preference only mirrors it (XPIDatabase addonChanged), so clearing it changes
|
|
// nothing and betterbird_config_apply never touches it.
|
|
themeRemedy = "enable the default theme in Betterbird's add-on manager (Add-ons and Themes, Themes): the active " +
|
|
"theme is kept in the add-on database, and its preference only mirrors it, so clearing prefs.js would not change it"
|
|
)
|
|
|
|
// activeTheme is the theme the profile's add-on database marks active, or "" when it cannot be read.
|
|
func activeTheme(profile string) string {
|
|
raw, err := readBounded(filepath.Join(profile, "extensions.json"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var ext struct {
|
|
Addons []struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Active bool `json:"active"`
|
|
} `json:"addons"`
|
|
}
|
|
if json.Unmarshal(raw, &ext) != nil {
|
|
return ""
|
|
}
|
|
for _, a := range ext.Addons {
|
|
if a.Type == "theme" && a.Active {
|
|
return a.ID
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// unmanageable says why a name in the module's defaults is never managed by these tools, or "": a credential's,
|
|
// an account's or a server's preference, which betterbird_config never answers and betterbird_config_apply never
|
|
// clears, whatever the defaults file lists.
|
|
func unmanageable(name string) string {
|
|
if why := withheld(name); why != "" {
|
|
return why
|
|
}
|
|
for _, p := range accountOwned {
|
|
if strings.HasPrefix(name, p) {
|
|
return "an account's preference, the setting accounts' alone"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// answer is a value as these tools answer it: masked, but for the theme's id.
|
|
func (m *Machine) answer(name, v string) string {
|
|
if allowedExtensions[name] {
|
|
return v
|
|
}
|
|
return m.maskValue(v)
|
|
}
|
|
|
|
// nodeName is the machine this runtime serves, as the mesh names it: a variable so a test answers.
|
|
var nodeName = func() string { return os.Getenv("MESH_NODE") }
|
|
|
|
// shipped is the module's DEFAULT layer as placed on the machine.
|
|
type shipped struct {
|
|
values map[string]string
|
|
// keeps are the managed names whose module value is Betterbird's own default.
|
|
keeps map[string]bool
|
|
// order is every managed name, in the file's order.
|
|
order []string
|
|
}
|
|
|
|
// parseShipped reads the module's defaults file.
|
|
func parseShipped(raw []byte) shipped {
|
|
s := shipped{values: map[string]string{}, keeps: map[string]bool{}}
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
if mm := keepsDefault.FindStringSubmatch(strings.TrimSpace(line)); mm != nil {
|
|
if !s.keeps[mm[1]] {
|
|
s.keeps[mm[1]] = true
|
|
s.order = append(s.order, mm[1])
|
|
}
|
|
continue
|
|
}
|
|
if mm := defaultLine.FindStringSubmatch(line); mm != nil {
|
|
if _, seen := s.values[mm[1]]; !seen {
|
|
s.order = append(s.order, mm[1])
|
|
}
|
|
s.values[mm[1]] = mm[2]
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// shippedDefaults reads the module's defaults file from the installation.
|
|
func (m *Machine) shippedDefaults() (shipped, error) {
|
|
raw, err := readBounded(m.path(meshDefaults))
|
|
if err != nil {
|
|
return shipped{values: map[string]string{}, keeps: map[string]bool{}}, err
|
|
}
|
|
return parseShipped(raw), nil
|
|
}
|
|
|
|
// DefaultValue is a default preference and the file that gives it.
|
|
type DefaultValue struct {
|
|
Value string
|
|
From string
|
|
// Module says the module's files give it, not Betterbird's.
|
|
Module bool
|
|
}
|
|
|
|
// readDefaultsInto reads the pref(…) lines of the files of one place, in Gecko's order: it reads a place's
|
|
// files last-first by name (Preferences.cpp, pref_ReadDefaultPrefs), so of a name in several files the value
|
|
// of the file first by name holds. A later place overrides an earlier one.
|
|
func readDefaultsInto(out map[string]DefaultValue, files map[string][]byte, label func(string) (string, bool)) {
|
|
names := make([]string, 0, len(files))
|
|
for n := range files {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
place := map[string]DefaultValue{}
|
|
for i := len(names) - 1; i >= 0; i-- {
|
|
from, module := label(names[i])
|
|
for _, line := range strings.Split(string(files[names[i]]), "\n") {
|
|
if mm := defaultLine.FindStringSubmatch(line); mm != nil {
|
|
place[mm[1]] = DefaultValue{Value: mm[2], From: from, Module: module}
|
|
}
|
|
}
|
|
}
|
|
for k, v := range place {
|
|
out[k] = v
|
|
}
|
|
}
|
|
|
|
// mostDefaultsFile bounds one defaults file read from omni.ja.
|
|
const mostDefaultsFile = 4 << 20
|
|
|
|
// defaults reads every default preference Betterbird starts with: greprefs.js and defaults/pref/*.js in
|
|
// omni.ja, then the installation's defaults/pref/*.js, where the module's files are. A preference compiled into
|
|
// Betterbird (a static preference) is in no file, and is absent here. withModule false leaves the module's own
|
|
// files out, so what remains is Betterbird's. The error says what could not be read.
|
|
func (m *Machine) defaults(withModule bool) (map[string]DefaultValue, error) {
|
|
out := map[string]DefaultValue{}
|
|
var problems []string
|
|
if r, err := zip.OpenReader(m.path(omniJa)); err != nil {
|
|
problems = append(problems, omniJa+" is unreadable: "+err.Error())
|
|
} else {
|
|
gre, app := map[string][]byte{}, map[string][]byte{}
|
|
for _, f := range r.File {
|
|
switch {
|
|
case f.Name == "greprefs.js":
|
|
case strings.HasPrefix(f.Name, "defaults/pref/") && strings.HasSuffix(f.Name, ".js") && !strings.Contains(f.Name[len("defaults/pref/"):], "/"):
|
|
default:
|
|
continue
|
|
}
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
b, _ := io.ReadAll(io.LimitReader(rc, mostDefaultsFile))
|
|
rc.Close()
|
|
if f.Name == "greprefs.js" {
|
|
gre[f.Name] = b
|
|
} else {
|
|
app[filepath.Base(f.Name)] = b
|
|
}
|
|
}
|
|
r.Close()
|
|
readDefaultsInto(out, gre, func(n string) (string, bool) { return "Betterbird's default (omni.ja " + n + ")", false })
|
|
readDefaultsInto(out, app, func(n string) (string, bool) {
|
|
return "Betterbird's default (omni.ja defaults/pref/" + n + ")", false
|
|
})
|
|
}
|
|
installed := map[string][]byte{}
|
|
entries, _ := os.ReadDir(m.path(prefDir))
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".js") || (moduleFiles[e.Name()] && !withModule) {
|
|
continue
|
|
}
|
|
if b, err := readBounded(m.path(filepath.Join(prefDir, e.Name()))); err == nil {
|
|
installed[e.Name()] = b
|
|
}
|
|
}
|
|
readDefaultsInto(out, installed, func(n string) (string, bool) {
|
|
if moduleFiles[n] {
|
|
return "the module's default (" + prefDir + "/" + n + ")", true
|
|
}
|
|
return "Betterbird's default (" + prefDir + "/" + n + ")", false
|
|
})
|
|
if len(problems) > 0 {
|
|
return out, errors.New(strings.Join(problems, "; "))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// policyPrefs are the Preferences entries of the placed policy: name to its value as a preference file writes
|
|
// it, and whether it is locked. An unreadable policy has none.
|
|
type policyPref struct {
|
|
Value string
|
|
Locked bool
|
|
}
|
|
|
|
func policyPrefsOf(raw []byte) map[string]policyPref {
|
|
var p struct {
|
|
Policies struct {
|
|
Preferences map[string]struct {
|
|
Value any `json:"Value"`
|
|
Status string `json:"Status"`
|
|
} `json:"Preferences"`
|
|
} `json:"policies"`
|
|
}
|
|
out := map[string]policyPref{}
|
|
if json.Unmarshal(raw, &p) != nil {
|
|
return out
|
|
}
|
|
for name, e := range p.Policies.Preferences {
|
|
out[name] = policyPref{Value: literal(e.Value), Locked: e.Status == "locked"}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// locks are the preferences the placed policy locks: name to value.
|
|
func (m *Machine) locks() map[string]string {
|
|
raw, err := readBounded(m.path(policyFile))
|
|
if err != nil {
|
|
return map[string]string{}
|
|
}
|
|
out := map[string]string{}
|
|
for name, p := range policyPrefsOf(raw) {
|
|
if p.Locked {
|
|
out[name] = p.Value
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// literal is a JSON value as a preference file writes it.
|
|
func literal(v any) string {
|
|
switch x := v.(type) {
|
|
case bool:
|
|
return strconv.FormatBool(x)
|
|
case float64:
|
|
return strconv.FormatFloat(x, 'f', -1, 64)
|
|
case string:
|
|
return quoted(x)
|
|
case nil:
|
|
return ""
|
|
}
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|
|
|
|
// statementsOf are the user_pref(…) statements of a user.js, several to a line allowed: name to value.
|
|
func statementsOf(raw []byte) map[string]string {
|
|
out := map[string]string{}
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
for _, st := range splitStatements(strings.TrimSpace(line)) {
|
|
if mm := prefLine.FindStringSubmatch(st); mm != nil {
|
|
out[mm[1]] = mm[2]
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ConfigPref is one managed preference through the layers.
|
|
type ConfigPref struct {
|
|
Name string `json:"name"`
|
|
// Layer is locked (the policy), default (the module's defaults file) or "Betterbird's default" (a managed
|
|
// preference whose module value is Betterbird's own).
|
|
Layer string `json:"layer"`
|
|
Locked string `json:"locked,omitempty"`
|
|
ModuleDefault string `json:"module_default,omitempty"`
|
|
BetterbirdDefault string `json:"betterbird_default,omitempty"`
|
|
// NodeLayer is what the setting prefs on this machine says (the module's user.js).
|
|
NodeLayer string `json:"node_layer,omitempty"`
|
|
// Profile is what the profile saved in its prefs.js.
|
|
Profile string `json:"profile,omitempty"`
|
|
Effective string `json:"effective"`
|
|
From string `json:"effective_from"`
|
|
// Drift says how the profile's own value beats the module's without a node layer saying so.
|
|
Drift string `json:"drift,omitempty"`
|
|
// Remedy is how drift that clearing prefs.js cannot fix is fixed (the theme).
|
|
Remedy string `json:"remedy,omitempty"`
|
|
// module is the value the module means: its default, or Betterbird's.
|
|
module string
|
|
}
|
|
|
|
// ConfigAnswer is what betterbird_config answers.
|
|
type ConfigAnswer struct {
|
|
Profile string `json:"profile"`
|
|
Defaults string `json:"module_defaults"`
|
|
Prefs []ConfigPref `json:"prefs"`
|
|
// Drifting are the names that drift.
|
|
Drifting []string `json:"drifting"`
|
|
// Refused are names the module's defaults list that the tools never manage: an account's or a credential's.
|
|
Refused []string `json:"refused,omitempty"`
|
|
// Proposed is the controller's answer to a node layer proposed with keep.
|
|
Proposed string `json:"proposed,omitempty"`
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
// config reads every managed preference through the layers: the policy's locks, the module's defaults, the
|
|
// node layer (the declared user.js), the profile's prefs.js and user.js.
|
|
func (m *Machine) config() (ConfigAnswer, Profile, error) {
|
|
a := ConfigAnswer{Prefs: []ConfigPref{}, Drifting: []string{}}
|
|
p, err := m.profile()
|
|
if err != nil {
|
|
return a, p, err
|
|
}
|
|
a.Profile = p.Dir
|
|
sh, serr := m.shippedDefaults()
|
|
if serr != nil {
|
|
a.Defaults = "not placed: " + meshDefaults + " is missing; the module's push places it"
|
|
} else {
|
|
a.Defaults = fmt.Sprintf("placed: %s, %d preference(s) with a value and %d kept at Betterbird's default",
|
|
meshDefaults, len(sh.values), len(sh.keeps))
|
|
}
|
|
locked := m.locks()
|
|
own, _ := m.defaults(false)
|
|
declaredRaw, _ := readBounded(m.home(declared))
|
|
node := statementsOf(declaredRaw)
|
|
prefsRaw, _ := readBounded(filepath.Join(p.path, "prefs.js"))
|
|
saved := readPrefs(prefsRaw)
|
|
userRaw, _ := readBounded(filepath.Join(p.path, "user.js"))
|
|
placed := statementsOf(userRaw)
|
|
|
|
var names []string
|
|
for n := range locked {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
for _, n := range sh.order {
|
|
if _, isLocked := locked[n]; !isLocked {
|
|
names = append(names, n)
|
|
}
|
|
}
|
|
theme := activeTheme(p.path)
|
|
for _, n := range names {
|
|
if why := unmanageable(n); why != "" {
|
|
a.Refused = append(a.Refused, n+": "+why)
|
|
continue
|
|
}
|
|
c := ConfigPref{Name: n, NodeLayer: node[n], Profile: saved[n]}
|
|
if n == themePref && theme != "" {
|
|
// The add-on database says which theme is active; the preference only mirrors it.
|
|
c.Profile = quoted(theme)
|
|
}
|
|
bb, hasBB := own[n]
|
|
if hasBB {
|
|
c.BetterbirdDefault = bb.Value
|
|
} else if v, ok := builtIn[n]; ok {
|
|
c.BetterbirdDefault = v
|
|
bb.From = "Betterbird's built-in default"
|
|
}
|
|
switch v, isLocked := locked[n]; {
|
|
case isLocked:
|
|
c.Layer, c.Locked, c.module = "locked", v, v
|
|
case sh.keeps[n]:
|
|
c.Layer = "Betterbird's default"
|
|
c.module = c.BetterbirdDefault
|
|
default:
|
|
c.Layer, c.ModuleDefault, c.module = "default", sh.values[n], sh.values[n]
|
|
}
|
|
switch {
|
|
case c.Locked != "":
|
|
c.Effective, c.From = c.Locked, "locked by the module's policy"
|
|
case placed[n] != "":
|
|
c.Effective, c.From = placed[n], "the profile's user.js (wins at the start)"
|
|
case c.Profile != "":
|
|
c.Effective, c.From = c.Profile, "the profile's prefs.js"
|
|
case c.ModuleDefault != "":
|
|
c.Effective, c.From = c.ModuleDefault, "the module's default"
|
|
case c.BetterbirdDefault != "":
|
|
c.Effective, c.From = c.BetterbirdDefault, bb.From
|
|
default:
|
|
c.From = "Betterbird's built-in default (in no defaults file)"
|
|
}
|
|
if why := drift(c); why != "" {
|
|
c.Drift = why
|
|
if n == themePref {
|
|
c.Remedy = themeRemedy
|
|
}
|
|
a.Drifting = append(a.Drifting, n)
|
|
}
|
|
c.NodeLayer, c.Profile, c.Effective = m.answer(n, c.NodeLayer), m.answer(n, c.Profile), m.answer(n, c.Effective)
|
|
a.Prefs = append(a.Prefs, c)
|
|
}
|
|
return a, p, nil
|
|
}
|
|
|
|
// drift says how the profile's saved value beats the module's, or "": a value in prefs.js that is not the
|
|
// module's, where no lock holds and no node layer names the preference.
|
|
func drift(c ConfigPref) string {
|
|
if c.Locked != "" || c.NodeLayer != "" || c.Profile == "" || c.Profile == c.module {
|
|
return ""
|
|
}
|
|
want := c.module
|
|
if want == "" {
|
|
want = "Betterbird's built-in default"
|
|
}
|
|
where := "the profile's prefs.js holds"
|
|
if c.Name == themePref {
|
|
where = "the profile's active theme is"
|
|
}
|
|
return fmt.Sprintf("%s %s, which beats the module's %s, and no node layer says so", where, c.Profile, want)
|
|
}
|
|
|
|
// Config is what betterbird_config answers; with keep it proposes a node layer of the setting prefs that holds
|
|
// the profile's own values of the named managed preferences on this machine (novox/hq issue 345). The module
|
|
// never writes user.js for it: the operator approves the layer, and the next push writes it.
|
|
func (m *Machine) Config(keep []string) (ConfigAnswer, error) {
|
|
a, p, err := m.config()
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
a.Note = "drift is a value the profile saved that beats the module's default without a node layer saying so: " +
|
|
"betterbird_config_apply clears it (Betterbird quit), or keep proposes a node layer that holds it for this machine"
|
|
if len(keep) == 0 {
|
|
return a, nil
|
|
}
|
|
by := map[string]ConfigPref{}
|
|
for _, c := range a.Prefs {
|
|
by[c.Name] = c
|
|
}
|
|
prefsRaw, _ := readBounded(filepath.Join(p.path, "prefs.js"))
|
|
saved := readPrefs(prefsRaw)
|
|
kept := map[string]string{}
|
|
for _, n := range keep {
|
|
c, managed := by[n]
|
|
switch {
|
|
case !managed:
|
|
return a, fmt.Errorf("nothing was proposed: %s is not a preference the module manages (betterbird_config lists them)", n)
|
|
case c.Locked != "":
|
|
return a, fmt.Errorf("nothing was proposed: %s is locked by the module's policy; a lock changes only in the catalogue", n)
|
|
case n == themePref:
|
|
return a, fmt.Errorf("nothing was proposed: a node layer cannot choose the theme; the add-on manager does (%s)", themeRemedy)
|
|
case saved[n] == "":
|
|
return a, fmt.Errorf("nothing was proposed: the profile saved no value of its own for %s", n)
|
|
}
|
|
kept[n] = saved[n]
|
|
}
|
|
declaredRaw, err := readBounded(m.home(declared))
|
|
if err != nil {
|
|
return a, fmt.Errorf("nothing was proposed: the module's user.js is not on this machine (~/%s)", declared)
|
|
}
|
|
node := nodeName()
|
|
if node == "" {
|
|
return a, errors.New("nothing was proposed: this runtime does not say which machine it serves (MESH_NODE)")
|
|
}
|
|
// The setting is one string, so a node layer replaces it whole: it carries what the setting says on this
|
|
// machine today, with the kept values in place of any the same names had.
|
|
var parts []string
|
|
for _, line := range strings.Split(string(declaredRaw), "\n") {
|
|
for _, st := range splitStatements(strings.TrimSpace(line)) {
|
|
if mm := prefLine.FindStringSubmatch(st); mm != nil {
|
|
if _, replaced := kept[mm[1]]; !replaced {
|
|
parts = append(parts, fmt.Sprintf("user_pref(%s, %s);", quoted(mm[1]), mm[2]))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, n := range keep {
|
|
if v, ok := kept[n]; ok {
|
|
parts = append(parts, fmt.Sprintf("user_pref(%s, %s);", quoted(n), v))
|
|
delete(kept, n)
|
|
}
|
|
}
|
|
value := strings.Join(parts, " ")
|
|
if err := validUserJS(mark + ")\n" + value + "\n"); err != nil {
|
|
return a, fmt.Errorf("nothing was proposed: the layer would be refused: %v", err)
|
|
}
|
|
body, _ := json.Marshal(map[string]string{"prefs": value})
|
|
raw, err := asker("seat:mesh-controller.settings", map[string]any{"module": "betterbird", "node": node,
|
|
"values": string(body), "propose": "true"})
|
|
if err != nil {
|
|
return a, fmt.Errorf("the controller's settings verb did not answer: %v", err)
|
|
}
|
|
said, ok := verbSaid(raw)
|
|
a.Proposed = m.maskValue(said)
|
|
if !ok {
|
|
return a, fmt.Errorf("the controller did not take the proposal: %s", a.Proposed)
|
|
}
|
|
a.Note = "proposed for " + node + ": the operator approves it on a channel that proves who answers; the next " +
|
|
"push writes it, and it is placed at Betterbird's next start from the launcher (or betterbird_restart). " +
|
|
"The layer carries the whole setting prefs as it stands on this machine, since a layer replaces it whole"
|
|
return a, nil
|
|
}
|
|
|
|
// Cleared is one value betterbird_config_apply took out of prefs.js.
|
|
type Cleared struct {
|
|
Name string `json:"name"`
|
|
Was string `json:"was"`
|
|
// Now is the value that holds once Betterbird starts again.
|
|
Now string `json:"now"`
|
|
}
|
|
|
|
// ConfigApplyAnswer is what betterbird_config_apply answers.
|
|
type ConfigApplyAnswer struct {
|
|
Profile string `json:"profile"`
|
|
Applied bool `json:"applied"`
|
|
Clears []Cleared `json:"clears"`
|
|
// NotCleared is drift that clearing prefs.js cannot fix, each with how it is fixed.
|
|
NotCleared []string `json:"not_cleared,omitempty"`
|
|
Note string `json:"note"`
|
|
}
|
|
|
|
// ConfigApply takes the drifting values out of the profile's prefs.js, so the module's value holds at the next
|
|
// start. Without apply it lists them. It acts only while Betterbird is not running, because Betterbird
|
|
// rewrites prefs.js when it quits. Every other line of prefs.js is kept as it was.
|
|
func (m *Machine) ConfigApply(apply bool) (ConfigApplyAnswer, error) {
|
|
c, p, err := m.config()
|
|
a := ConfigApplyAnswer{Profile: c.Profile, Clears: []Cleared{}}
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
if strings.HasPrefix(c.Defaults, "not placed") {
|
|
return a, fmt.Errorf("the module's defaults are %s: nothing to bring the profile back to", c.Defaults)
|
|
}
|
|
drifting := map[string]bool{}
|
|
for _, cp := range c.Prefs {
|
|
if cp.Drift != "" && cp.Remedy != "" {
|
|
a.NotCleared = append(a.NotCleared, cp.Name+": "+cp.Remedy)
|
|
continue
|
|
}
|
|
if cp.Drift != "" {
|
|
drifting[cp.Name] = true
|
|
now := m.answer(cp.Name, cp.module)
|
|
if now == "" {
|
|
now = "Betterbird's built-in default"
|
|
}
|
|
a.Clears = append(a.Clears, Cleared{Name: cp.Name, Was: cp.Profile, Now: now})
|
|
}
|
|
}
|
|
if !apply {
|
|
a.Note = "a dry run: these values would be taken out of prefs.js. Quit Betterbird, then call with apply"
|
|
return a, nil
|
|
}
|
|
if ps := m.mainProcs(); len(ps) > 0 {
|
|
return a, fmt.Errorf("Betterbird is running (pid %d): it rewrites prefs.js when it quits, so nothing was cleared; quit it, then apply again", ps[0].PID)
|
|
}
|
|
// The profile's own lock is Gecko's sign that a Betterbird holds it, whichever installation started it.
|
|
if _, err := os.Lstat(filepath.Join(p.path, "lock")); err == nil {
|
|
return a, errors.New("the profile is locked (its lock file is there): a Betterbird holds it, or one ended " +
|
|
"without quitting; nothing was cleared. Quit Betterbird (or start and quit it once), then apply again")
|
|
}
|
|
if len(drifting) == 0 {
|
|
a.Applied, a.Note = true, "nothing in prefs.js drifts: it was left as it was"
|
|
return a, nil
|
|
}
|
|
target := filepath.Join(p.path, "prefs.js")
|
|
raw, err := readBounded(target)
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
st, err := os.Stat(target)
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
lines := strings.SplitAfter(string(raw), "\n")
|
|
var kept strings.Builder
|
|
for _, l := range lines {
|
|
if mm := prefLine.FindStringSubmatch(strings.TrimRight(l, "\n")); mm != nil && drifting[mm[1]] {
|
|
continue
|
|
}
|
|
kept.WriteString(l)
|
|
}
|
|
tmp := target + ".mesh-new"
|
|
if err := writeSynced(tmp, []byte(kept.String()), st.Mode().Perm()); err != nil {
|
|
os.Remove(tmp)
|
|
return a, err
|
|
}
|
|
if err := os.Rename(tmp, target); err != nil {
|
|
os.Remove(tmp)
|
|
return a, err
|
|
}
|
|
a.Applied = true
|
|
a.Note = fmt.Sprintf("cleared %d value(s) from prefs.js; the module's values hold from Betterbird's next start", len(a.Clears))
|
|
return a, nil
|
|
}
|
|
|
|
// writeSynced writes a file and syncs it to disk before it is renamed into place, so a crash never leaves an
|
|
// empty prefs.js.
|
|
func writeSynced(path string, data []byte, mode os.FileMode) error {
|
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := f.Write(data); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
if err := f.Sync(); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
return f.Close()
|
|
}
|