mesh/merge-gate head 1835f0efad0f merged into trunk 39e7a06e6293 — pass: builds betterbird, sent nowhere; no bus step; 2 wait(s) for a person; every machin…
mesh/repo-check head 1835f0efad0f merged into trunk 39e7a06e6293 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
The module owned only the accounts' server fields, so identities, signatures, calendars, address books, filters and tags could differ between desktop nodes unseen, and the operator's own preferences were shipped in the catalogue. prefs becomes one entry per preference (a node's layer overrides one key) and the defaults file keeps no value of the operator's; identities, calendars, address-books, filters and tags become settings, adopted like accounts, filters owned whole per account, and what Betterbird rewrites written only while it is closed. New tools list each kind, derive the layers from every desktop node and propose one at a time, and show drift; every answer passes a credential check.
667 lines
24 KiB
Go
667 lines
24 KiB
Go
package main
|
|
|
|
// The configuration of Betterbird's preferences (novox/hq issue 345, issue 586), 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.
|
|
// - DECLARED: the setting prefs, the operator's setup: mesh-wide, with a node's layer over it key by key. The
|
|
// node-engine writes it to ~/.config/betterbird/prefs.json and the tools place it in the profile's user.js,
|
|
// where it wins over prefs.js at every start.
|
|
// - BETTERBIRD'S DEFAULT: what Betterbird starts with. The module's defaults file
|
|
// (/opt/betterbird/defaults/pref/mesh-defaults.js) holds no value of the operator's any more (issue 586): it
|
|
// names the preferences the module keeps at Betterbird's own default, where a value the profile saved is drift.
|
|
//
|
|
// 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 of the setting
|
|
// prefs 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), declared (the setting prefs), default (a value of 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"`
|
|
// Declared is what the setting prefs says on this machine: mesh-wide, with the node's layer over it.
|
|
Declared string `json:"declared,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"`
|
|
// PrefsProblem says why the setting prefs on this machine is refused.
|
|
PrefsProblem string `json:"prefs_problem,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)
|
|
node, _, nerr := m.readDeclaredPrefs()
|
|
if nerr != nil {
|
|
a.PrefsProblem = nerr.Error()
|
|
}
|
|
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)
|
|
listed := map[string]bool{}
|
|
for _, n := range names {
|
|
listed[n] = true
|
|
}
|
|
for _, n := range append(append([]string{}, sh.order...), sortedKeys(node)...) {
|
|
if !listed[n] {
|
|
listed[n] = true
|
|
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, Declared: 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 c.Declared != "":
|
|
c.Layer, c.module = "declared", c.Declared
|
|
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, placed[n]); why != "" {
|
|
c.Drift = why
|
|
switch {
|
|
case n == themePref:
|
|
c.Remedy = themeRemedy
|
|
case c.Layer == "declared":
|
|
c.Remedy = "betterbird_user_js with apply places the setting in the profile's user.js; Betterbird reads it " +
|
|
"at its next start (betterbird_restart)"
|
|
}
|
|
a.Drifting = append(a.Drifting, n)
|
|
}
|
|
c.Declared, c.Profile, c.Effective = m.answer(n, c.Declared), 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 differs from the module's configuration, or "": for a declared preference, the
|
|
// profile's user.js not holding the declared value (so Betterbird does not start with it); for a preference kept at
|
|
// Betterbird's default, a value in prefs.js that is not it. A lock holds whatever the profile says.
|
|
func drift(c ConfigPref, placed string) string {
|
|
if c.Locked != "" {
|
|
return ""
|
|
}
|
|
if c.Declared != "" {
|
|
if placed == c.Declared {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("the setting prefs says %s, and the profile's user.js does not hold it yet", c.Declared)
|
|
}
|
|
if 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 the setting prefs does not say 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 declared value the profile's user.js does not hold yet (betterbird_user_js with apply), or a " +
|
|
"value the profile saved where the module keeps Betterbird's default: betterbird_config_apply clears that one " +
|
|
"(Betterbird quit), or keep proposes a node layer of the setting prefs 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]
|
|
}
|
|
node := nodeName()
|
|
if node == "" {
|
|
return a, errors.New("nothing was proposed: this runtime does not say which machine it serves (MESH_NODE)")
|
|
}
|
|
// A layer is replaced whole, so the proposal carries this node's layer as it stands, with the kept values in
|
|
// the setting prefs beside what it held (novox/hq issue 586: one entry per preference).
|
|
layer, err := readLayer(node)
|
|
if err != nil {
|
|
return a, fmt.Errorf("nothing was proposed: %v", err)
|
|
}
|
|
prefs, _ := layer["prefs"].(map[string]any)
|
|
if prefs == nil {
|
|
prefs = map[string]any{}
|
|
}
|
|
for _, n := range keep {
|
|
v, ok := prefValue(kept[n])
|
|
if !ok {
|
|
return a, fmt.Errorf("nothing was proposed: the profile's value of %s is not one the setting can hold", n)
|
|
}
|
|
prefs[n] = v
|
|
}
|
|
layer["prefs"] = prefs
|
|
said, err := proposeLayer(node, layer, false)
|
|
a.Proposed = m.maskValue(said)
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
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 this node's layer as it stands, with the kept preferences in the setting prefs"
|
|
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()
|
|
}
|