~jcc/swaybar-commander

68b686e2f69ab98bed7f5ea3b63a3de876308cf9 — Jason Cox 9 months ago ca6b653 master
Enable reloading config while running

Use SIGUSR1 or the helper command `swaybar-commander reload` to trigger
a config reload.
M block/block.go => block/block.go +7 -4
@@ 2,6 2,7 @@
package block

import (
	"context"
	"encoding/json"
	"fmt"
	"os"


@@ 16,7 17,7 @@ import (
	"git.sr.ht/~jcc/swaybar-commander/proto"
)

var execCommand = exec.Command // allow mocking in tests
var execCommandContext = exec.CommandContext // allow mocking in tests

// Block represents a single block in the swaybar.
type Block struct {


@@ 41,7 42,7 @@ func New(conf config.Block) Block {

// Run runs a Block indefinitely, sending updated swaybar protocol blocks on its
// UpdateChan whenever there is a change.
func (b *Block) Run(updateRequestSig syscall.Signal) {
func (b *Block) Run(ctx context.Context, updateRequestSig syscall.Signal) {
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, updateRequestSig)



@@ 53,7 54,7 @@ func (b *Block) Run(updateRequestSig syscall.Signal) {
			strconv.Itoa(int(updateRequestSig)),
		)

		updateRequesterCmd := execCommand(name, args...)
		updateRequesterCmd := execCommandContext(ctx, name, args...)
		err := updateRequesterCmd.Start()
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed to start update requester for block %v: %v\n", *b, err)


@@ 67,7 68,7 @@ func (b *Block) Run(updateRequestSig syscall.Signal) {
	lastOutput := ""

	for {
		cmd := execCommand(b.cmd[0], b.cmd[1:]...)
		cmd := execCommandContext(ctx, b.cmd[0], b.cmd[1:]...)
		outBytes, err := cmd.Output()
		if err != nil {
			fmt.Fprintf(os.Stderr, "error running command %v: %v", b.cmd, err)


@@ 92,6 93,8 @@ func (b *Block) Run(updateRequestSig syscall.Signal) {
		}

		select {
		case <-ctx.Done():
			return
		case <-b.ticker.C:
		case <-sigChan:
			b.ticker.Reset(b.pollFreq)

M block/block_test.go => block/block_test.go +10 -7
@@ 1,6 1,7 @@
package block

import (
	"context"
	"fmt"
	"os"
	"os/exec"


@@ 105,7 106,7 @@ func TestRun(t *testing.T) {
		})
		block.pollFreq = 25 * time.Millisecond

		execCommand = makeFakeExecCommand(
		execCommandContext = makeFakeExecCommand(
			t,
			c.stdouts,
			c.exitStatuses,


@@ 115,7 116,8 @@ func TestRun(t *testing.T) {

		actual := []proto.Block{}

		go block.Run(syscall.Signal(sigrtmin+1))
		ctx := context.Background()
		go block.Run(ctx, syscall.Signal(sigrtmin+1))

		for i := 0; i < len(c.expected); i++ {
			actual = append(actual, <-block.UpdateChan)


@@ 135,7 137,7 @@ func TestRunUpdateRequest(t *testing.T) {
		PollSecs: 100,
	})

	execCommand = makeFakeExecCommand(
	execCommandContext = makeFakeExecCommand(
		t,
		[]string{"a", "b"},
		[]int{0, 0},


@@ 161,7 163,8 @@ func TestRunUpdateRequest(t *testing.T) {
		}
	}

	go block.Run(sig)
	ctx := context.Background()
	go block.Run(ctx, sig)

	first := <-block.UpdateChan
	if first.FullText != "a" {


@@ 193,10 196,10 @@ func makeFakeExecCommand(
	exitStatuses []int,
	expectedName string,
	expectedArgs ...string,
) func(string, ...string) *exec.Cmd {
) func(context.Context, string, ...string) *exec.Cmd {
	i := 0

	return func(name string, args ...string) *exec.Cmd {
	return func(ctx context.Context, name string, args ...string) *exec.Cmd {
		if name != expectedName || !reflect.DeepEqual(args, expectedArgs) {
			t.Fatalf("incorrect exec.Command args: want %v, %v; got %v, %v", expectedName, expectedArgs, name, args)
			return nil


@@ 204,7 207,7 @@ func makeFakeExecCommand(

		cs := []string{"-test.run=TestHelperProcess", "--", name}
		cs = append(cs, args...)
		cmd := exec.Command(os.Args[0], cs...)
		cmd := exec.CommandContext(ctx, os.Args[0], cs...)

		if i >= len(stdouts) || i >= len(exitStatuses) {
			<-make(chan int) // block forever and wait for test execution to end

M commander.go => commander.go +11 -1
@@ 21,6 21,16 @@ func main() {
		return
	}

	if len(os.Args) == 2 && os.Args[1] == "reload" {
		err := commands.Reload()
		if err != nil {
			fmt.Fprintln(os.Stderr, err)
			os.Exit(1)
		}

		return
	}

	if len(os.Args) >= 3 && os.Args[1] == "update" {
		var instance string
		if len(os.Args) == 4 {


@@ 36,6 46,6 @@ func main() {
		return
	}

	fmt.Fprintf(os.Stderr, "USAGE: %s [update <index>]|[update <name> [<instance>]]\n", os.Args[0])
	fmt.Fprintf(os.Stderr, "USAGE: %s [reload]|[update <index>]|[update <name> [<instance>]]\n", os.Args[0])
	os.Exit(1)
}

M commands/commander.go => commands/commander.go +37 -4
@@ 2,7 2,10 @@
package commands

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"


@@ 14,9 17,33 @@ import (
)

const sigrtmin = 34
const reloadSignal = syscall.SIGUSR1

// Commander runs the swaybar-commander.
func Commander() error {
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, reloadSignal)
	firstRun := true

	for {
		ctx, cancel := context.WithCancel(context.Background())

		go func() {
			<-sigChan
			fmt.Fprintln(os.Stderr, "reloading")
			cancel()
		}()

		err := runCommander(ctx, firstRun)
		if err != nil {
			return err
		}

		firstRun = false
	}
}

func runCommander(ctx context.Context, init bool) error {
	conf, err := config.Load()
	if err != nil {
		return fmt.Errorf("failed to load config: %v", err)


@@ 37,7 64,9 @@ func Commander() error {
		}
	}

	proto.Init()
	if init {
		proto.Init()
	}

	debounce := debounce.New(100 * time.Millisecond)
	wg := sync.WaitGroup{}


@@ 47,11 76,15 @@ func Commander() error {

		go func(i int) {
			defer wg.Done()
			go blocks[i].Run(blockRuntimes[i].Signal)
			go blocks[i].Run(ctx, blockRuntimes[i].Signal)

			for {
				latestPBlocks[i] = <-blocks[i].UpdateChan
				debounce(func() { proto.Send(latestPBlocks) })
				select {
				case <-ctx.Done():
					return
				case latestPBlocks[i] = <-blocks[i].UpdateChan:
					debounce(func() { proto.Send(latestPBlocks) })
				}
			}
		}(i)
	}

A commands/reload.go => commands/reload.go +36 -0
@@ 0,0 1,36 @@
package commands

import (
	"fmt"
)

// Reload requests that all running instances of swaybar-commander reload the
// config file and restart.
func Reload() error {
	runtimePaths := getRuntimeFilePaths()
	if len(runtimePaths) == 0 {
		return fmt.Errorf("no instances of swaybar-commander appear to be running")
	}

	var signalled bool

	for _, path := range runtimePaths {
		process, _, err := readRuntimeFile(path)
		if err != nil {
			return fmt.Errorf("failed to read runtime file %s: %v", path, err)
		}

		if process == nil {
			continue
		}

		process.Signal(reloadSignal)
		signalled = true
	}

	if !signalled {
		return fmt.Errorf("no running instance found")
	}

	return nil
}

M swaybar-commander.1.scd => swaybar-commander.1.scd +4 -0
@@ 8,6 8,7 @@ swaybar-commander - swaybar status command for *sway*(1) using the
# SYNOPSIS

*swaybar-commander*++
*swaybar-commander* reload++
*swaybar-commander* update <index>++
*swaybar-commander* update <name> [<instance>]



@@ 46,6 47,9 @@ $XDG_CONFIG_HOME/swaybar-commander/config.toml. $XDG_CONFIG_HOME defaults to
The config file contains an optional *Defaults* table as well as an array of
*Blocks*.

To make swaybar-commander reload its configuration, use *swaybar-commander
reload* or send _SIGUSR1_ to the *swaybar-commander* process.

## DEFAULTS

The optional *Defaults* table contains default values for the body properties