Skip to content

Commit 32a4966

Browse files
authored
add restart (#13)
* no need to pass env to sudo ; use pkexec when not launched by terminal * let desktop pattern define env vars to pass along * add --config and use it to point viper at correct file instead of setting home * allow only one menu process running * let menu listen for hup to accept new restart request * move pidpaths under menu/wait and make them consistent and only set once
1 parent 98abd3a commit 32a4966

11 files changed

Lines changed: 358 additions & 142 deletions

File tree

cmd/get.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ var getCmd = &cobra.Command{
1414
RunE: func(cmd *cobra.Command, args []string) error {
1515
pattern := patterns.GetRunning()
1616
if pattern == nil {
17-
if pidPath.IsRunning() && !pidPath.IsOurs() {
17+
if waitPidPath.IsRunning() && !waitPidPath.IsOurs() {
1818
return sendViaIPC(cmd)
1919
}
2020
} else {

cmd/menu.go

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,59 @@ import (
44
"fmt"
55
"os"
66
"os/exec"
7+
"os/signal"
8+
"path/filepath"
79
"strings"
10+
"syscall"
811
"time"
912

1013
"github.com/BitPonyLLC/huekeys/buildinfo"
1114
"github.com/BitPonyLLC/huekeys/internal/menu"
15+
"github.com/BitPonyLLC/huekeys/pkg/patterns"
16+
"github.com/BitPonyLLC/huekeys/pkg/pidpath"
17+
"github.com/BitPonyLLC/huekeys/pkg/util"
1218

1319
"github.com/rs/zerolog/log"
1420
"github.com/spf13/cobra"
1521
"github.com/spf13/viper"
1622
)
1723

1824
var patternName string
25+
var menuPidPath *pidpath.PidPath
26+
var restarting = false
1927

2028
func init() {
2129
menuCmd.Flags().StringVarP(&patternName, "pattern", "p", patternName, "name of pattern to run at start")
2230
viper.BindPFlag("menu.pattern", menuCmd.Flags().Lookup("pattern"))
31+
32+
defaultPidPath := filepath.Join(os.TempDir(), buildinfo.App.Name+"-menu.pid")
33+
menuCmd.Flags().String("pidpath", defaultPidPath, "pathname of the menu pidfile")
34+
viper.BindPFlag("menu.pidpath", menuCmd.Flags().Lookup("pidpath"))
35+
2336
rootCmd.AddCommand(menuCmd)
2437
}
2538

2639
var menuCmd = &cobra.Command{
27-
Use: "menu",
28-
Short: "Display a menu in the system tray",
29-
PreRunE: ensureWaitRunning,
40+
Use: "menu",
41+
Short: "Display a menu in the system tray",
42+
PreRunE: func(cmd *cobra.Command, _ []string) error {
43+
err := menuPidPath.CheckAndSet()
44+
if err != nil {
45+
return err
46+
}
47+
48+
return ensureWaitRunning(cmd)
49+
},
3050
RunE: func(cmd *cobra.Command, _ []string) error {
51+
restart := make(chan os.Signal, 1)
52+
signal.Notify(restart, syscall.SIGHUP)
53+
go func() {
54+
sig := <-restart
55+
restarting = true
56+
log.Info().Str("signal", sig.String()).Msg("restarting")
57+
cancelFunc()
58+
}()
59+
3160
args := []string{}
3261
for c := runCmd; c != rootCmd; c = c.Parent() {
3362
args = append([]string{c.Name()}, args...)
@@ -45,7 +74,24 @@ var menuCmd = &cobra.Command{
4574
}
4675
}
4776

48-
return menu.Show(cmd.Context(), &log.Logger, viper.GetString("sockpath"))
77+
return menu.Show(cmd.Context(), &log.Logger, waitSockPath())
78+
},
79+
PostRun: func(_ *cobra.Command, _ []string) {
80+
if menuPidPath != nil {
81+
menuPidPath.Release()
82+
}
83+
84+
if !restarting {
85+
return
86+
}
87+
88+
mCmd := exec.Command(os.Args[0], os.Args[1:]...)
89+
err := mCmd.Start()
90+
if err != nil {
91+
log.Error().Err(err).Msg("failed to restart menu")
92+
}
93+
94+
log.Info().Str("cmd", mCmd.String()).Interface("menu", mCmd.Process).Msg("new menu process started")
4995
},
5096
Args: func(cmd *cobra.Command, args []string) error {
5197
if patternName == "" {
@@ -62,8 +108,8 @@ var menuCmd = &cobra.Command{
62108
},
63109
}
64110

65-
func ensureWaitRunning(cmd *cobra.Command, args []string) error {
66-
if !pidPath.IsOurs() && pidPath.IsRunning() {
111+
func ensureWaitRunning(cmd *cobra.Command) error {
112+
if !waitPidPath.IsOurs() && waitPidPath.IsRunning() {
67113
// wait is already executing in the background
68114
return nil
69115
}
@@ -73,19 +119,43 @@ func ensureWaitRunning(cmd *cobra.Command, args []string) error {
73119
return fmt.Errorf("unable to determine executable pathname: %w", err)
74120
}
75121

76-
// use sh exec to remove sudo parent processes hanging around
77-
hkCmd := "exec " + exe + " run wait &"
78-
execArgs := []string{"sudo", "-E", "sh", "-c", hkCmd}
79-
execStr := strings.Join(execArgs, " ")
122+
dpEnv, err := patterns.DesktopPatternEnv()
123+
if err != nil {
124+
if util.IsTTY(os.Stderr) {
125+
cmd.PrintErrln(err)
126+
} else {
127+
log.Warn().Err(err).Msg("")
128+
}
129+
}
130+
131+
var execName string
132+
var execArgs []string
133+
134+
// checking only stdin isn't enough: it's attached when launched from gnome!
135+
if util.IsTTY(os.Stdin) && util.IsTTY(os.Stdout) {
136+
execName = "sudo"
137+
execArgs = []string{}
138+
} else {
139+
// need to open a dialog for permission...
140+
execName = "pkexec"
141+
execArgs = []string{"--user", "root"}
142+
}
143+
144+
// use sh exec to let parent processes exit
145+
hkCmd := fmt.Sprint("export ", dpEnv, "; exec ", exe,
146+
" --config ", viper.GetViper().ConfigFileUsed(), " run wait &")
147+
execArgs = append(execArgs, "sh", "-c", hkCmd)
148+
149+
execStr := fmt.Sprint(execName, " ", strings.Join(execArgs, " "))
80150
log.Debug().Str("cmd", execStr).Msg("")
81151

82-
err = exec.Command("sudo", "-E", "sh", "-c", hkCmd).Run()
152+
err = exec.Command(execName, execArgs...).Run()
83153
if err != nil {
84154
return fmt.Errorf("unable to run %s: %w", execStr, err)
85155
}
86156

87157
// wait a second for socket to be ready...
88-
sockPath := viper.GetString("sockpath")
158+
sockPath := waitSockPath()
89159
for i := 0; i < 10; i += 1 {
90160
time.Sleep(50 * time.Millisecond)
91161
_, err := os.Stat(sockPath)

cmd/pattern.go

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package cmd
22

33
import (
4+
"os"
5+
"path/filepath"
6+
7+
"github.com/BitPonyLLC/huekeys/buildinfo"
48
"github.com/BitPonyLLC/huekeys/pkg/patterns"
9+
"github.com/BitPonyLLC/huekeys/pkg/pidpath"
510
"github.com/BitPonyLLC/huekeys/pkg/util"
611

712
"github.com/rs/zerolog/log"
813
"github.com/spf13/cobra"
914
"github.com/spf13/viper"
1015
)
1116

17+
var waitPidPath *pidpath.PidPath
18+
1219
var runCmd = &cobra.Command{
1320
Use: "run",
1421
Short: "runs a backlight pattern",
@@ -20,20 +27,45 @@ func init() {
2027
rootCmd.AddCommand(runCmd)
2128

2229
//----------------------------------------
23-
addPatternCmd("wait for remote commands", patterns.Get("wait"))
2430
addPatternCmd("pulse the keyboard brightness up and down", patterns.Get("pulse"))
2531
addPatternCmd("loop through all the colors of the rainbow", patterns.Get("rainbow"))
2632
addPatternCmd("constantly change the color to a random selection", patterns.Get("random"))
2733
addPatternCmd("change the color according to CPU utilization (cold to hot)", patterns.Get("cpu"))
2834
addPatternCmd("monitor the desktop picture and change the keyboard color to match", patterns.Get("desktop"))
2935

36+
//----------------------------------------
37+
waitCmd := addPatternCmd("wait for remote commands", patterns.Get("wait"))
38+
// wait needs to manage the pidpath and start the IPC server...
39+
waitCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
40+
if err := commonPreRunE(cmd, args); err != nil {
41+
return err
42+
}
43+
if err := waitPidPath.CheckAndSet(); err != nil {
44+
return fail(11, err)
45+
}
46+
return ipcServer.Start(cmd.Context(), &log.Logger, waitSockPath(), rootCmd)
47+
}
48+
waitCmd.PostRun = func(cmd *cobra.Command, args []string) {
49+
if waitPidPath != nil {
50+
waitPidPath.Release()
51+
}
52+
}
53+
54+
defaultPidPath := filepath.Join(os.TempDir(), buildinfo.App.Name+"-wait.pid")
55+
waitCmd.Flags().String("pidpath", defaultPidPath, "pathname of the wait pidfile")
56+
viper.BindPFlag("wait.pidpath", waitCmd.Flags().Lookup("pidpath"))
57+
58+
defaultSockPath := filepath.Join(os.TempDir(), buildinfo.App.Name+"-wait.sock")
59+
waitCmd.Flags().String("sockpath", defaultSockPath, "pathname of the wait sockfile")
60+
viper.BindPFlag("wait.sockpath", waitCmd.Flags().Lookup("sockpath"))
61+
3062
//----------------------------------------
3163
watchPattern := patterns.Get("watch").(*patterns.WatchPattern)
3264
watchCmd := addPatternCmd("watch and report color, brightness, and pattern changes", watchPattern)
3365
// watch needs to behave differently from others when run...
3466
watchCmd.RunE = func(cmd *cobra.Command, _ []string) error {
35-
if pidPath.IsRunning() && !pidPath.IsOurs() {
36-
return sendViaIPCForeground(cmd, true)
67+
if waitPidPath.IsRunning() && !waitPidPath.IsOurs() {
68+
return sendViaIPCForeground(cmd, true, "")
3769
}
3870
// there may be multiple watch patterns running (i.e. multiple watch
3971
// clients) so each one needs to maintain its own Out writer!
@@ -63,33 +95,14 @@ func init() {
6395
}
6496

6597
func addPatternCmd(short string, pattern patterns.Pattern) *cobra.Command {
66-
priority := viper.GetInt("nice")
6798
basePattern := pattern.GetBase()
6899

69100
cmd := &cobra.Command{
70-
Use: pattern.GetBase().Name,
71-
Short: short,
72-
PreRunE: func(cmd *cobra.Command, _ []string) error {
73-
if err := pidPath.CheckAndSet(); err != nil {
74-
if pidPath.IsRunning() {
75-
if cmd.Name() == "wait" {
76-
return err
77-
}
78-
log.Debug().Err(err).Msg("ignoring")
79-
return nil
80-
}
81-
return fail(11, err)
82-
}
83-
if err := util.BeNice(priority); err != nil {
84-
return fail(12, err)
85-
}
86-
if _, ok := pattern.(*patterns.WaitPattern); ok {
87-
return ipcServer.Start(cmd.Context(), &log.Logger, viper.GetString("sockpath"), rootCmd)
88-
}
89-
return nil
90-
},
101+
Use: pattern.GetBase().Name,
102+
Short: short,
103+
PreRunE: commonPreRunE,
91104
RunE: func(cmd *cobra.Command, _ []string) error {
92-
if pidPath.IsRunning() && !pidPath.IsOurs() {
105+
if waitPidPath.IsRunning() && !waitPidPath.IsOurs() {
93106
return sendViaIPC(cmd)
94107
}
95108
return pattern.Run(cmd.Context(), &log.Logger)
@@ -106,3 +119,11 @@ func addPatternCmd(short string, pattern patterns.Pattern) *cobra.Command {
106119
runCmd.AddCommand(cmd)
107120
return cmd
108121
}
122+
123+
func commonPreRunE(cmd *cobra.Command, _ []string) error {
124+
return util.BeNice(viper.GetInt("nice"))
125+
}
126+
127+
func waitSockPath() string {
128+
return viper.GetString("wait.sockpath")
129+
}

cmd/quit.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ var quitCmd = &cobra.Command{
1111
Use: "quit",
1212
Short: "Tells remote process to quit",
1313
RunE: func(cmd *cobra.Command, args []string) error {
14-
if pidPath.IsRunning() {
15-
if pidPath.IsOurs() {
14+
if waitPidPath.IsRunning() {
15+
if waitPidPath.IsOurs() {
1616
log.Info().Msg("received request to quit")
1717
cancelFunc()
1818
return nil

cmd/remote_cmd.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,22 @@ import (
88

99
"github.com/rs/zerolog/log"
1010
"github.com/spf13/cobra"
11-
"github.com/spf13/viper"
1211
)
1312

1413
func sendViaIPC(cmd *cobra.Command) error {
15-
return sendViaIPCForeground(cmd, false)
14+
return sendViaIPCForeground(cmd, false, "")
1615
}
1716

18-
func sendViaIPCForeground(cmd *cobra.Command, foreground bool) error {
19-
msg := strings.Join(os.Args[1:], " ")
20-
log.Debug().Int("pid", pidPath.Getpid()).Str("cmd", msg).Msg("sending")
17+
func sendMsgViaIPC(cmd *cobra.Command, msg string) error {
18+
return sendViaIPCForeground(cmd, false, msg)
19+
}
20+
21+
func sendViaIPCForeground(cmd *cobra.Command, foreground bool, msg string) error {
22+
if msg == "" {
23+
msg = strings.Join(os.Args[1:], " ")
24+
}
25+
26+
log.Debug().Int("pid", waitPidPath.Getpid()).Str("cmd", msg).Msg("sending")
2127

2228
client := &ipc.Client{
2329
Foreground: foreground,
@@ -34,7 +40,7 @@ func sendViaIPCForeground(cmd *cobra.Command, foreground bool) error {
3440
}()
3541
}
3642

37-
err := client.Send(viper.GetString("sockpath"), msg)
43+
err := client.Send(waitSockPath(), msg)
3844
if err != nil {
3945
return err
4046
}

cmd/restart.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"syscall"
7+
8+
"github.com/BitPonyLLC/huekeys/pkg/pidpath"
9+
10+
"github.com/rs/zerolog/log"
11+
"github.com/spf13/cobra"
12+
"github.com/spf13/viper"
13+
)
14+
15+
var restartCmd = &cobra.Command{
16+
Use: "restart",
17+
Short: "Tells remote process to restart",
18+
RunE: func(cmd *cobra.Command, args []string) error {
19+
if !waitPidPath.IsRunning() {
20+
return errors.New("no remote process found")
21+
}
22+
23+
if waitPidPath.IsOurs() {
24+
log.Info().Msg("received request to restart")
25+
cancelFunc()
26+
return nil
27+
}
28+
29+
menuPidPath = pidpath.NewPidPath(viper.GetString("menu.pidpath"), 0666)
30+
if !menuPidPath.IsRunning() {
31+
return sendMsgViaIPC(cmd, "quit")
32+
}
33+
34+
log.Info().Str("menu", menuPidPath.String()).Msg("sending restart signal to menu")
35+
err := syscall.Kill(menuPidPath.Getpid(), syscall.SIGHUP)
36+
if err != nil {
37+
return fmt.Errorf("unable to kill menu process: %w", err)
38+
}
39+
40+
return nil
41+
},
42+
}
43+
44+
func init() {
45+
rootCmd.AddCommand(restartCmd)
46+
}

0 commit comments

Comments
 (0)