An agent meets the mesh's words most often in tool descriptions, and nothing compared them with the glossary: several still said "the host" for the node-engine and the forge's pull request comment was headed "Change plan", a word retired twice over. retired-words is the copy of the words the glossary retires for the tools, and checks/words fails the repository check when any string a module's code can show, or any manifest description, uses one. Those found are reworded here.
679 lines
22 KiB
Go
679 lines
22 KiB
Go
package main
|
|
|
|
// Slack as the tools see it. Slack is one Electron process tree: the main process (comm slack, no
|
|
// --type=) is both the window and the tray icon, and its helpers (--type=zygote, gpu-process,
|
|
// renderer, utility) are its children. The tools read only:
|
|
// - the account's processes, and of Slack's main process where its output goes (fd 1 and 2);
|
|
// - Slack's settings file, ~/.config/Slack/storage/root-state.json, of which only switches are
|
|
// answered (launch on login, start hidden, run from the tray, notification method, hardware
|
|
// acceleration), and local-settings.json for the Electron version;
|
|
// - Slack's own log, ~/.config/Slack/logs/default/browser*.log, from which the signed-in accounts and
|
|
// workspaces are counted (counted only: no name, no id) and which slack_log answers masked;
|
|
// - the X window tree (xwininfo), for the tray icon; the session bus, for who shows notifications.
|
|
//
|
|
// Never read: Cookies, Local Storage, IndexedDB, the os_crypt key in Local State. Those are the
|
|
// sessions' credentials, and no tool needs them.
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Where Slack keeps things, under the operator's home, and how it is started.
|
|
const (
|
|
slackComm = "slack"
|
|
slackBin = "/usr/bin/slack"
|
|
packageFor = "slack-desktop"
|
|
restartAs = "mesh-slack"
|
|
entryName = "slack.desktop"
|
|
stateFile = ".config/Slack/storage/root-state.json"
|
|
localFile = ".config/Slack/local-settings.json"
|
|
logDir = ".config/Slack/logs/default"
|
|
i3Main = ".config/i3/config"
|
|
i3DropIns = ".config/i3/config.d"
|
|
notifyName = "org.freedesktop.Notifications"
|
|
)
|
|
|
|
// startArgs are the package's own desktop entry's arguments: GTK 3, and -s, started to the tray.
|
|
var startArgs = []string{"--gtk-version=3", "-s"}
|
|
|
|
// ruleMark is how the module's window rules are recognised wherever they are found.
|
|
const ruleMark = `for_window [class="(?i)^slack$"`
|
|
|
|
// readLink is how a process's file descriptors are read; tests replace it, so no test makes a link.
|
|
var readLink = os.Readlink
|
|
|
|
// isLink says whether a path is a symbolic link itself; tests replace it for the same reason.
|
|
var isLink = func(p string) bool {
|
|
fi, err := os.Lstat(p)
|
|
return err == nil && fi.Mode()&os.ModeSymlink != 0
|
|
}
|
|
|
|
// mainProcs are Slack's main processes: comm slack without --type=.
|
|
func (m *Machine) mainProcs() []Proc {
|
|
var out []Proc
|
|
for _, p := range m.procs(slackComm) {
|
|
if !strings.Contains(p.Command, "--type=") {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Stream is where a process's standard output or error goes.
|
|
type Stream struct {
|
|
FD int `json:"fd"`
|
|
Goes string `json:"goes"`
|
|
// Dead is a pipe no process of the account reads: a write there fails with EPIPE.
|
|
Dead bool `json:"dead,omitempty"`
|
|
}
|
|
|
|
// streams says where fd 1 and 2 of a process go.
|
|
func (m *Machine) streams(pid int) []Stream {
|
|
var out []Stream
|
|
for _, fd := range []int{1, 2} {
|
|
target, err := readLink(m.path(fmt.Sprintf("/proc/%d/fd/%d", pid, fd)))
|
|
s := Stream{FD: fd}
|
|
switch {
|
|
case err != nil:
|
|
s.Goes = "unreadable"
|
|
case target == "/dev/null":
|
|
s.Goes = "discarded (/dev/null)"
|
|
case strings.HasPrefix(target, "socket:"):
|
|
s.Goes = "a socket (the journal, when the session's output is sent there)"
|
|
case strings.HasPrefix(target, "pipe:"):
|
|
if r := m.pipeReaders(target, pid); len(r) > 0 {
|
|
s.Goes = "a pipe read by " + strings.Join(r, ", ")
|
|
} else {
|
|
s.Goes, s.Dead = "a pipe nobody reads", true
|
|
}
|
|
default:
|
|
s.Goes = m.tilde(target)
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// pipeReaders are the account's processes holding the pipe open for reading (O_RDONLY in fdinfo).
|
|
func (m *Machine) pipeReaders(pipe string, except int) []string {
|
|
entries, _ := os.ReadDir(m.path("/proc"))
|
|
seen := map[string]bool{}
|
|
var out []string
|
|
for _, e := range entries {
|
|
pid, err := strconv.Atoi(e.Name())
|
|
if err != nil || pid == except {
|
|
continue
|
|
}
|
|
dir := m.path(filepath.Join("/proc", e.Name()))
|
|
if m.uidOf(dir) != m.UID {
|
|
continue
|
|
}
|
|
fds, _ := os.ReadDir(filepath.Join(dir, "fd"))
|
|
for _, fd := range fds {
|
|
if t, err := readLink(filepath.Join(dir, "fd", fd.Name())); err != nil || t != pipe {
|
|
continue
|
|
}
|
|
if readOnly(readTrimmed(filepath.Join(dir, "fdinfo", fd.Name()))) {
|
|
name := fmt.Sprintf("%s (pid %d)", readTrimmed(filepath.Join(dir, "comm")), pid)
|
|
if !seen[name] {
|
|
seen[name] = true
|
|
out = append(out, name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// readOnly reads an fdinfo's flags (octal): the access mode is its low two bits, 0 for reading.
|
|
func readOnly(fdinfo string) bool {
|
|
for _, l := range strings.Split(fdinfo, "\n") {
|
|
if f := strings.Fields(l); len(f) == 2 && f[0] == "flags:" {
|
|
n, err := strconv.ParseInt(f[1], 8, 64)
|
|
return err == nil && n&3 == 0
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Settings are the switches of Slack's settings the tools answer. Every other key stays unread.
|
|
type Settings struct {
|
|
Found bool `json:"found"`
|
|
LaunchOnLogin *bool `json:"launch_on_login,omitempty"`
|
|
HideOnStartup *bool `json:"hide_on_startup,omitempty"`
|
|
RunFromTray *bool `json:"run_from_tray,omitempty"`
|
|
NotificationMethod string `json:"notification_method,omitempty"`
|
|
HardwareAccel *bool `json:"hardware_acceleration,omitempty"`
|
|
Electron string `json:"electron,omitempty"`
|
|
}
|
|
|
|
func (m *Machine) settings() Settings {
|
|
var s Settings
|
|
raw, err := readBounded(m.home(stateFile))
|
|
if err == nil {
|
|
var doc struct {
|
|
Settings struct {
|
|
LaunchOnStartup *bool `json:"launchOnStartup"`
|
|
HideOnStartup *bool `json:"hideOnStartup"`
|
|
RunFromTray *bool `json:"runFromTray"`
|
|
NotificationMethod *string `json:"notificationMethod"`
|
|
UseHwAcceleration *bool `json:"useHwAcceleration"`
|
|
} `json:"settings"`
|
|
}
|
|
if json.Unmarshal(raw, &doc) == nil {
|
|
s.Found = true
|
|
s.LaunchOnLogin, s.HideOnStartup, s.RunFromTray = doc.Settings.LaunchOnStartup, doc.Settings.HideOnStartup, doc.Settings.RunFromTray
|
|
s.HardwareAccel = doc.Settings.UseHwAcceleration
|
|
s.NotificationMethod = "Slack's default"
|
|
if doc.Settings.NotificationMethod != nil && *doc.Settings.NotificationMethod != "" {
|
|
s.NotificationMethod = *doc.Settings.NotificationMethod
|
|
}
|
|
}
|
|
}
|
|
if raw, err := readBounded(m.home(localFile)); err == nil {
|
|
var doc struct {
|
|
Electron string `json:"lastElectronVersionLaunched"`
|
|
}
|
|
if json.Unmarshal(raw, &doc) == nil {
|
|
s.Electron = doc.Electron
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Workspaces is how many accounts Slack is signed in to, and how many workspaces they open: counted
|
|
// from the last report Slack logged of each account, since its last start. Never a name or an id.
|
|
type Workspaces struct {
|
|
Accounts int `json:"accounts"`
|
|
Workspaces int `json:"workspaces"`
|
|
AsOf string `json:"as_of,omitempty"`
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
var logStamp = regexp.MustCompile(`^\[(\d\d/\d\d/\d\d, \d\d:\d\d:\d\d:\d+)\] (\w+): `)
|
|
|
|
// browserLogs are Slack's main-process logs, oldest first (browser2.log, browser1.log, browser.log).
|
|
func (m *Machine) browserLogs() []string {
|
|
files, _ := filepath.Glob(m.home(logDir, "browser*.log"))
|
|
sort.Slice(files, func(i, j int) bool {
|
|
a, _ := os.Stat(files[i])
|
|
b, _ := os.Stat(files[j])
|
|
if a == nil || b == nil {
|
|
return files[i] < files[j]
|
|
}
|
|
return a.ModTime().Before(b.ModTime())
|
|
})
|
|
if len(files) > 3 {
|
|
files = files[len(files)-3:]
|
|
}
|
|
return files
|
|
}
|
|
|
|
func (m *Machine) workspaces() Workspaces {
|
|
var lines []string
|
|
for _, f := range m.browserLogs() {
|
|
l, err := tailLines(f, 1<<20)
|
|
if err == nil {
|
|
lines = append(lines, l...)
|
|
}
|
|
}
|
|
start := 0
|
|
for i := len(lines) - 1; i >= 0; i-- {
|
|
if strings.Contains(lines[i], "] info: Store: INITIALIZE") {
|
|
start = i
|
|
break
|
|
}
|
|
}
|
|
accounts := map[string][]string{}
|
|
var asOf string
|
|
for i := start; i < len(lines); i++ {
|
|
if !strings.Contains(lines[i], "Store: STORE_USER_WORKSPACES") {
|
|
continue
|
|
}
|
|
var block strings.Builder
|
|
j := i + 1
|
|
for ; j < len(lines) && !logStamp.MatchString(lines[j]); j++ {
|
|
block.WriteString(lines[j])
|
|
}
|
|
var report struct {
|
|
User string `json:"userTeamId"`
|
|
Workspaces []string `json:"workspaceIds"`
|
|
}
|
|
if json.Unmarshal([]byte(block.String()), &report) == nil && report.User != "" {
|
|
accounts[report.User] = report.Workspaces
|
|
if mm := logStamp.FindStringSubmatch(lines[i]); mm != nil {
|
|
asOf = mm[1]
|
|
}
|
|
}
|
|
i = j - 1
|
|
}
|
|
w := Workspaces{Accounts: len(accounts), AsOf: asOf}
|
|
all := map[string]bool{}
|
|
for _, ws := range accounts {
|
|
for _, id := range ws {
|
|
all[id] = true
|
|
}
|
|
}
|
|
w.Workspaces = len(all)
|
|
if len(accounts) == 0 {
|
|
w.Note = "Slack has logged no account since its last start: signed out, or not started yet"
|
|
}
|
|
return w
|
|
}
|
|
|
|
// Tray is whether Slack's icon sits in a tray: a window of class "Slack" reparented into another
|
|
// program's window (i3bar's tray), found in the X window tree.
|
|
type Tray struct {
|
|
Present bool `json:"present"`
|
|
In string `json:"in,omitempty"`
|
|
Unknown string `json:"unknown,omitempty"`
|
|
}
|
|
|
|
var treeLine = regexp.MustCompile(`^(\s*)0x[0-9a-f]+ (?:"[^"]*"|\(has no name\)): \("([^"]*)" "([^"]*)"\)`)
|
|
|
|
// trayIn reads `xwininfo -root -tree`: each window is indented under its parent.
|
|
func trayIn(tree string) (bool, string) {
|
|
type frame struct {
|
|
indent int
|
|
class string
|
|
}
|
|
var stack []frame
|
|
for _, l := range strings.Split(tree, "\n") {
|
|
trimmed := strings.TrimLeft(l, " ")
|
|
if !strings.HasPrefix(trimmed, "0x") {
|
|
continue
|
|
}
|
|
indent := len(l) - len(trimmed)
|
|
for len(stack) > 0 && stack[len(stack)-1].indent >= indent {
|
|
stack = stack[:len(stack)-1]
|
|
}
|
|
mm := treeLine.FindStringSubmatch(l)
|
|
class := ""
|
|
if mm != nil {
|
|
class = mm[3]
|
|
if mm[2] == "slack" && class == "Slack" {
|
|
for k := len(stack) - 1; k >= 0; k-- {
|
|
if c := stack[k].class; c != "" && !strings.EqualFold(c, "slack") {
|
|
return true, c
|
|
}
|
|
}
|
|
}
|
|
}
|
|
stack = append(stack, frame{indent, class})
|
|
}
|
|
return false, ""
|
|
}
|
|
|
|
func (m *Machine) tray(s Session) Tray {
|
|
o := m.cmd(5*time.Second, s.Env(), "xwininfo", "-root", "-tree")
|
|
if err := failed(o, "xwininfo", "-root", "-tree"); err != nil {
|
|
if o.Err == ErrNotInstalled {
|
|
return Tray{Unknown: "xwininfo (package xorg-xwininfo) is not installed, so the window tree cannot be read"}
|
|
}
|
|
return Tray{Unknown: err.Error()}
|
|
}
|
|
ok, in := trayIn(o.Stdout)
|
|
return Tray{Present: ok, In: in}
|
|
}
|
|
|
|
// Notifier is who shows Slack's notifications: the owner of the session bus's notification name.
|
|
type Notifier struct {
|
|
Owner string `json:"owner,omitempty"`
|
|
PID int `json:"pid,omitempty"`
|
|
// Activatable is a notifier that is not running but that the bus starts on the first notification.
|
|
Activatable bool `json:"activatable,omitempty"`
|
|
Unknown string `json:"unknown,omitempty"`
|
|
}
|
|
|
|
func (m *Machine) notifier() Notifier {
|
|
o := m.cmd(5*time.Second, m.bus(), "busctl", "--user", "list", "--no-legend", "--no-pager")
|
|
if err := failed(o, "busctl", "--user", "list"); err != nil {
|
|
return Notifier{Unknown: err.Error()}
|
|
}
|
|
for _, l := range strings.Split(o.Stdout, "\n") {
|
|
f := strings.Fields(l)
|
|
if len(f) < 3 || f[0] != notifyName {
|
|
continue
|
|
}
|
|
if pid, err := strconv.Atoi(f[1]); err == nil {
|
|
return Notifier{Owner: f[2], PID: pid}
|
|
}
|
|
return Notifier{Activatable: true}
|
|
}
|
|
return Notifier{}
|
|
}
|
|
|
|
// Status is what slack_status answers.
|
|
type Status struct {
|
|
Installed string `json:"installed,omitempty"`
|
|
Foreign bool `json:"outside_the_official_repositories,omitempty"`
|
|
Running []Proc `json:"running"`
|
|
Helpers int `json:"helper_processes"`
|
|
Output []Stream `json:"output,omitempty"`
|
|
Tray *Tray `json:"tray,omitempty"`
|
|
Notifier Notifier `json:"notifications_to"`
|
|
Settings Settings `json:"settings"`
|
|
Workspaces Workspaces `json:"signed_in"`
|
|
Starts []string `json:"starts"`
|
|
}
|
|
|
|
// foreign says whether pacman counts the package as foreign: installed, but in no sync repository.
|
|
func (m *Machine) foreign(pkg string) bool {
|
|
o := m.cmd(0, nil, "pacman", "-Qmq", pkg)
|
|
return o.Err == nil && o.Code == 0 && strings.TrimSpace(o.Stdout) == pkg
|
|
}
|
|
|
|
func (m *Machine) Status() (Status, error) {
|
|
s := Status{Running: m.mainProcs(), Settings: m.settings(), Workspaces: m.workspaces(), Starts: []string{}}
|
|
if s.Running == nil {
|
|
s.Running = []Proc{}
|
|
}
|
|
s.Helpers = len(m.procs(slackComm)) - len(s.Running)
|
|
if v, err := m.installed(packageFor); err == nil && v != "" {
|
|
s.Installed, s.Foreign = v, m.foreign(packageFor)
|
|
}
|
|
if len(s.Running) > 0 {
|
|
s.Output = m.streams(s.Running[0].PID)
|
|
if sess, err := m.session(); err == nil {
|
|
t := m.tray(sess)
|
|
s.Tray = &t
|
|
}
|
|
}
|
|
s.Notifier = m.notifier()
|
|
for _, st := range m.starts() {
|
|
s.Starts = append(s.Starts, st.What)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// Start is one thing that starts Slack at login; Mine is the module's own.
|
|
type Start struct {
|
|
What string
|
|
Mine bool
|
|
Do string
|
|
}
|
|
|
|
// starts are every start of Slack at login the tools can see: window-manager exec lines (the module's
|
|
// in i3's main file, any other in the operator's drop-ins) and XDG autostart entries.
|
|
func (m *Machine) starts() []Start {
|
|
var out []Start
|
|
main := m.tilde(filepath.Join(m.Home, i3Main)) + ":"
|
|
for _, l := range m.i3Starts(slackComm) {
|
|
if strings.HasPrefix(l, main) {
|
|
out = append(out, Start{What: "window manager (this module's line): " + l, Mine: true})
|
|
} else {
|
|
out = append(out, Start{What: "window manager: " + l, Do: "remove the line: the module's line in i3's configuration is Slack's one start"})
|
|
}
|
|
}
|
|
if a := m.autostart(entryName); a.Starts {
|
|
st := Start{What: "XDG autostart: " + a.From + " (" + a.Exec + ")"}
|
|
user := m.home(".config", "autostart", entryName)
|
|
if isLink(user) {
|
|
st.Do = "untick 'Launch app on login' in Slack's preferences: Slack made this link for that setting and removes it when unticked"
|
|
} else if strings.HasPrefix(a.From, "~") {
|
|
st.Do = "delete " + a.From + " (the predecessor's): Slack then reads 'Launch app on login' as off, and the module's line is the one start"
|
|
} else {
|
|
st.Do = "hide it with an entry of the account (Hidden=true): the module's line is Slack's one start"
|
|
}
|
|
out = append(out, st)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// LogAnswer is what slack_log answers.
|
|
type LogAnswer struct {
|
|
Source string `json:"source"`
|
|
Files []string `json:"files"`
|
|
Lines []string `json:"lines"`
|
|
Cut bool `json:"cut,omitempty"`
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
const mostAnswer = 256 << 10
|
|
|
|
// The masks. A token or a cookie is hidden wherever it appears; a notification's or a message's text,
|
|
// and the names of people, channels and workspaces, are hidden by their key.
|
|
var (
|
|
slackToken = regexp.MustCompile(`xox[a-z]-[A-Za-z0-9-]+|xapp-[A-Za-z0-9-]+`)
|
|
credential = regexp.MustCompile(`(?i)\b(authorization|bearer|token|password|secret|cookie|set-cookie)(["']?\s*[:=]\s*["']?|\s+)(?:bearer\s+)?[^\s"',;&]+`)
|
|
cookieD = regexp.MustCompile(`\bd=[A-Za-z0-9%/+=._-]{20,}`)
|
|
privateKey = regexp.MustCompile(`"(title|subtitle|content|body|text|authorName|userName|channelName|workspaceName|teamName|name|email|realName|displayName)": "(?:[^"\\]|\\.)*"`)
|
|
)
|
|
|
|
func mask(s string) string {
|
|
s = slackToken.ReplaceAllString(s, "<hidden>")
|
|
s = cookieD.ReplaceAllString(s, "d=<hidden>")
|
|
s = credential.ReplaceAllString(s, "${1}${2}<hidden>")
|
|
return privateKey.ReplaceAllString(s, `"${1}": "<hidden>"`)
|
|
}
|
|
|
|
// Log answers the last n lines of Slack's main-process log (source browser) or of its web app's
|
|
// console log (source webapp), masked; problems keeps the error and warning entries.
|
|
func (m *Machine) Log(source string, n int, problems bool) (LogAnswer, error) {
|
|
a := LogAnswer{Source: source, Files: []string{}, Lines: []string{}}
|
|
var files []string
|
|
switch source {
|
|
case "browser":
|
|
files = m.browserLogs()
|
|
case "webapp":
|
|
files, _ = filepath.Glob(m.home(logDir, "webapp-console*.log"))
|
|
sort.Slice(files, func(i, j int) bool {
|
|
a, _ := os.Stat(files[i])
|
|
b, _ := os.Stat(files[j])
|
|
return a != nil && b != nil && a.ModTime().Before(b.ModTime())
|
|
})
|
|
default:
|
|
return a, fmt.Errorf("source is browser or webapp, not %q", source)
|
|
}
|
|
if len(files) == 0 {
|
|
a.Note = "Slack keeps no such log in ~/" + logDir
|
|
return a, nil
|
|
}
|
|
var got []string
|
|
for i := len(files) - 1; i >= 0 && len(got) < n && len(a.Files) < 3; i-- {
|
|
lines, err := tailLines(files[i], 1<<20)
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
if problems {
|
|
var keep []string
|
|
for _, l := range lines {
|
|
if mm := logStamp.FindStringSubmatch(l); mm != nil && (mm[2] == "error" || mm[2] == "warn") {
|
|
keep = append(keep, l)
|
|
}
|
|
}
|
|
lines = keep
|
|
}
|
|
if len(lines) > n-len(got) {
|
|
lines = lines[len(lines)-(n-len(got)):]
|
|
}
|
|
got = append(lines, got...)
|
|
a.Files = append(a.Files, m.tilde(strings.TrimPrefix(files[i], m.Root)))
|
|
}
|
|
size := 0
|
|
for i := len(got) - 1; i >= 0; i-- {
|
|
l := mask(got[i])
|
|
if size+len(l) > mostAnswer {
|
|
a.Cut = true
|
|
got = got[i+1:]
|
|
break
|
|
}
|
|
size += len(l) + 1
|
|
got[i] = l
|
|
}
|
|
if got == nil {
|
|
got = []string{}
|
|
}
|
|
a.Lines = got
|
|
return a, nil
|
|
}
|
|
|
|
// tailLines answers the last n lines of a file, read from its end to at most MostRead.
|
|
func tailLines(path string, n int) ([]string, error) {
|
|
h, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer h.Close()
|
|
if st, err := h.Stat(); err == nil && st.Size() > MostRead {
|
|
if _, err := h.Seek(st.Size()-MostRead, io.SeekStart); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
var all []string
|
|
s := bufio.NewScanner(io.LimitReader(h, MostRead))
|
|
s.Buffer(make([]byte, 64<<10), 4<<20)
|
|
for s.Scan() {
|
|
all = append(all, string(bytes.TrimRight(s.Bytes(), "\r")))
|
|
}
|
|
if len(all) > n {
|
|
all = all[len(all)-n:]
|
|
}
|
|
return all, s.Err()
|
|
}
|
|
|
|
// RestartAnswer is what slack_restart answers.
|
|
type RestartAnswer struct {
|
|
Ended []int `json:"ended"`
|
|
Killed []int `json:"killed,omitempty"`
|
|
Running []Proc `json:"running"`
|
|
Session Session `json:"session"`
|
|
Unit string `json:"unit"`
|
|
}
|
|
|
|
// Restart ends Slack (the main process and its helpers) and starts it again to the tray, in the
|
|
// operator's session, under the account's service manager: its output then goes to the journal.
|
|
func (m *Machine) Restart() (RestartAnswer, error) {
|
|
s, err := m.session()
|
|
if err != nil {
|
|
return RestartAnswer{}, err
|
|
}
|
|
a := RestartAnswer{Session: s, Unit: restartAs + ".service"}
|
|
a.Ended, a.Killed = m.stop(8*time.Second, slackComm)
|
|
if err := m.detach(s, restartAs, append([]string{slackBin}, startArgs...)...); err != nil {
|
|
return a, err
|
|
}
|
|
m.waitFor(slackComm, 6*time.Second)
|
|
a.Running = m.mainProcs()
|
|
if a.Running == nil {
|
|
a.Running = []Proc{}
|
|
}
|
|
if len(a.Running) == 0 {
|
|
return a, fmt.Errorf("Slack was started as %s but no main process appeared within 6 s: "+
|
|
"see `journalctl --user -u %s`", a.Unit, a.Unit)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// CheckAnswer is what slack_check answers.
|
|
type CheckAnswer struct {
|
|
OK bool `json:"ok"`
|
|
Findings []Finding `json:"findings"`
|
|
Starts []string `json:"starts"`
|
|
}
|
|
|
|
// Check verifies what the module promises: Slack installed, one start (the module's line), the window
|
|
// rules once, one Slack running with its output read, its icon in the tray, and a notifier.
|
|
func (m *Machine) Check() (CheckAnswer, error) {
|
|
a := CheckAnswer{Findings: []Finding{}, Starts: []string{}}
|
|
add := func(what, do string) { a.Findings = append(a.Findings, Finding{what, do}) }
|
|
v, err := m.installed(packageFor)
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
if v == "" {
|
|
add("Slack ("+packageFor+") is not installed", "install it from the AUR: the module does not install it, because the node-engine installs packages from the official repositories only")
|
|
}
|
|
mine := 0
|
|
for _, st := range m.starts() {
|
|
a.Starts = append(a.Starts, st.What)
|
|
if st.Mine {
|
|
mine++
|
|
} else {
|
|
add("a second start: "+st.What, st.Do)
|
|
}
|
|
}
|
|
if mine == 0 {
|
|
add("i3's configuration has not got the module's start of Slack", "assign the i3 module, which places this module's lines, and push the node")
|
|
} else if mine > 1 {
|
|
add(fmt.Sprintf("i3's configuration starts Slack %d times", mine), "push the node: the module's lines are placed once")
|
|
}
|
|
rules, operators := m.windowRules()
|
|
if rules == 0 {
|
|
add("i3's configuration has not got the module's window rules for Slack", "assign the i3 module and push the node")
|
|
}
|
|
for _, f := range operators {
|
|
add("the operator's "+f+" repeats Slack's window rules, which are this module's now", "delete "+f+": i3 reads it after the module's lines")
|
|
}
|
|
running := m.mainProcs()
|
|
sess, serr := m.session()
|
|
if serr == nil {
|
|
switch {
|
|
case len(running) == 0:
|
|
add("Slack does not run in the desktop session", "slack_restart")
|
|
case len(running) > 1:
|
|
add(fmt.Sprintf("%d Slack main processes run", len(running)), "slack_restart ends them all and starts one")
|
|
}
|
|
}
|
|
if len(running) > 0 {
|
|
for _, st := range m.streams(running[0].PID) {
|
|
if st.Dead {
|
|
add(fmt.Sprintf("Slack's fd %d is a pipe nobody reads (a session started before its output went to the journal): "+
|
|
"its logger silences the EPIPE it gets there, any other write it makes fails", st.FD),
|
|
"slack_restart: its output then goes to the journal. Every later login sends the session's output there (the i3 module's login entry)")
|
|
}
|
|
}
|
|
if serr == nil {
|
|
if t := m.tray(sess); !t.Present && t.Unknown == "" {
|
|
add("Slack's icon is in no tray: closing its window leaves it unreachable", "give the bar a tray (i3bar's tray_output), then slack_restart")
|
|
}
|
|
}
|
|
}
|
|
if n := m.notifier(); n.Unknown == "" && n.Owner == "" && !n.Activatable {
|
|
add("nobody shows notifications on the session bus ("+notifyName+"): Slack's go nowhere", "assign dunst, the node's notifier")
|
|
}
|
|
a.OK = len(a.Findings) == 0
|
|
return a, nil
|
|
}
|
|
|
|
// windowRules counts the module's rule lines in i3's main file, and names the operator's drop-ins that
|
|
// carry Slack's rules too.
|
|
func (m *Machine) windowRules() (int, []string) {
|
|
n := 0
|
|
if raw, err := readBounded(m.home(i3Main)); err == nil {
|
|
for _, l := range strings.Split(string(raw), "\n") {
|
|
if strings.HasPrefix(strings.TrimSpace(l), ruleMark) {
|
|
n++
|
|
}
|
|
}
|
|
}
|
|
var files []string
|
|
more, _ := filepath.Glob(m.home(i3DropIns, "*.conf"))
|
|
for _, f := range more {
|
|
raw, err := readBounded(f)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, l := range strings.Split(string(raw), "\n") {
|
|
t := strings.ToLower(strings.TrimSpace(l))
|
|
if strings.HasPrefix(t, "for_window") && strings.Contains(t, "slack") {
|
|
files = append(files, m.tilde(strings.TrimPrefix(f, m.Root)))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return n, files
|
|
}
|