diff --git a/cmd/publish.go b/cmd/publish.go index 5acbd46b..1edb24db 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -47,7 +47,7 @@ var cmdPublish = &cli.Command{ Aliases: []string{"pub", "send", "trigger"}, Usage: "Send message via a ntfy server", UsageText: `ntfy publish [OPTIONS..] TOPIC [MESSAGE...] -ntfy publish [OPTIONS..] --wait-cmd -P COMMAND... +ntfy publish [OPTIONS..] --wait-cmd COMMAND... NTFY_TOPIC=.. ntfy publish [OPTIONS..] -P [MESSAGE...]`, Action: execPublish, Category: categoryClient, @@ -156,18 +156,18 @@ func execPublish(c *cli.Context) error { options = append(options, client.WithBasicAuth(user, pass)) } if pid > 0 { - if err := waitForProcess(pid); err != nil { - return err - } - if message == "" { - message = fmt.Sprintf("process with PID %d exited", pid) - } - } else if len(command) > 0 { - cmdResultMessage, err := runAndWaitForCommand(command) + newMessage, err := waitForProcess(pid) if err != nil { return err } else if message == "" { - message = cmdResultMessage + message = newMessage + } + } else if len(command) > 0 { + newMessage, err := runAndWaitForCommand(command) + if err != nil { + return err + } else if message == "" { + message = newMessage } } var body io.Reader @@ -203,11 +203,14 @@ func execPublish(c *cli.Context) error { return nil } +// parseTopicMessageCommand reads the topic and the remaining arguments from the context. + +// There are a few cases to consider: +// ntfy publish [] +// ntfy publish --wait-cmd +// NTFY_TOPIC=.. ntfy publish [] +// NTFY_TOPIC=.. ntfy publish --wait-cmd func parseTopicMessageCommand(c *cli.Context) (topic string, message string, command []string, err error) { - // 1. ntfy publish --wait-cmd - // 2. NTFY_TOPIC=.. ntfy publish --wait-cmd - // 3. ntfy publish [] - // 4. NTFY_TOPIC=.. ntfy publish [] var args []string topic, args, err = parseTopicAndArgs(c) if err != nil { @@ -251,35 +254,39 @@ func remainingArgs(c *cli.Context, fromIndex int) []string { return []string{} } -func waitForProcess(pid int) error { +func waitForProcess(pid int) (message string, err error) { if !processExists(pid) { - return fmt.Errorf("process with PID %d not running", pid) + return "", fmt.Errorf("process with PID %d not running", pid) } + start := time.Now() log.Debug("Waiting for process with PID %d to exit", pid) for processExists(pid) { time.Sleep(500 * time.Millisecond) } - log.Debug("Process with PID %d exited", pid) - return nil + runtime := time.Since(start).Round(time.Millisecond) + log.Debug("Process with PID %d exited after %s", pid, runtime) + return fmt.Sprintf("Process with PID %d exited after %s", pid, runtime), nil } func runAndWaitForCommand(command []string) (message string, err error) { prettyCmd := util.QuoteCommand(command) log.Debug("Running command: %s", prettyCmd) + start := time.Now() cmd := exec.Command(command[0], command[1:]...) if log.IsTrace() { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr } - if err := cmd.Run(); err != nil { + err = cmd.Run() + runtime := time.Since(start).Round(time.Millisecond) + if err != nil { if exitError, ok := err.(*exec.ExitError); ok { - message = fmt.Sprintf("Command failed (exit code %d): %s", exitError.ExitCode(), prettyCmd) - } else { - message = fmt.Sprintf("Command failed: %s, error: %s", prettyCmd, err.Error()) + log.Debug("Command failed after %s (exit code %d): %s", runtime, exitError.ExitCode(), prettyCmd) + return fmt.Sprintf("Command failed after %s (exit code %d): %s", runtime, exitError.ExitCode(), prettyCmd), nil } - } else { - message = fmt.Sprintf("Command done: %s", prettyCmd) + // Hard fail when command does not exist or could not be properly launched + return "", fmt.Errorf("command failed: %s, error: %s", prettyCmd, err.Error()) } - log.Debug(message) - return message, nil + log.Debug("Command succeeded after %s: %s", runtime, prettyCmd) + return fmt.Sprintf("Command succeeded after %s: %s", runtime, prettyCmd), nil } diff --git a/cmd/publish_test.go b/cmd/publish_test.go index 23d2d36d..2b9ad3fc 100644 --- a/cmd/publish_test.go +++ b/cmd/publish_test.go @@ -5,7 +5,11 @@ import ( "github.com/stretchr/testify/require" "heckel.io/ntfy/test" "heckel.io/ntfy/util" + "os" + "os/exec" + "strconv" "testing" + "time" ) func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) { @@ -70,3 +74,66 @@ func TestCLI_Publish_All_The_Things(t *testing.T) { require.Equal(t, int64(0), m.Attachment.Expires) require.Equal(t, "", m.Attachment.Type) } + +func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) { + s, port := test.StartServer(t) + defer test.StopServer(t, s, port) + topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port) + + // Test: sleep 0.5 + sleep := exec.Command("sleep", "0.5") + require.Nil(t, sleep.Start()) + go sleep.Wait() // Must be called to release resources + start := time.Now() + app, _, stdout, _ := newTestApp() + require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-pid", strconv.Itoa(sleep.Process.Pid), topic})) + m := toMessage(t, stdout.String()) + require.True(t, time.Since(start) >= 500*time.Millisecond) + require.Regexp(t, `Process with PID \d+ exited after `, m.Message) + + // Test: PID does not exist + app, _, _, _ = newTestApp() + err := app.Run([]string{"ntfy", "publish", "--wait-pid", "1234567", topic}) + require.Error(t, err) + require.Equal(t, "process with PID 1234567 not running", err.Error()) + + // Test: Successful command (exit 0) + start = time.Now() + app, _, stdout, _ = newTestApp() + require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "sleep", "0.5"})) + m = toMessage(t, stdout.String()) + require.True(t, time.Since(start) >= 500*time.Millisecond) + require.Contains(t, m.Message, `Command succeeded after `) + require.Contains(t, m.Message, `: sleep 0.5`) + + // Test: Failing command (exit 1) + app, _, stdout, _ = newTestApp() + require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "/bin/false", "false doesn't care about its args"})) + m = toMessage(t, stdout.String()) + require.Contains(t, m.Message, `Command failed after `) + require.Contains(t, m.Message, `(exit code 1): /bin/false "false doesn't care about its args"`, m.Message) + + // Test: Non-existing command (hard fail!) + app, _, _, _ = newTestApp() + err = app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "does-not-exist-no-really", "really though"}) + require.Error(t, err) + require.Equal(t, `command failed: does-not-exist-no-really "really though", error: exec: "does-not-exist-no-really": executable file not found in $PATH`, err.Error()) + + // Tests with NTFY_TOPIC set //// + require.Nil(t, os.Setenv("NTFY_TOPIC", topic)) + + // Test: Successful command with NTFY_TOPIC + app, _, stdout, _ = newTestApp() + require.Nil(t, app.Run([]string{"ntfy", "publish", "--env-topic", "--cmd", "echo", "hi there"})) + m = toMessage(t, stdout.String()) + require.Equal(t, "mytopic", m.Topic) + + // Test: Successful --wait-pid with NTFY_TOPIC + sleep = exec.Command("sleep", "0.2") + require.Nil(t, sleep.Start()) + go sleep.Wait() // Must be called to release resources + app, _, stdout, _ = newTestApp() + require.Nil(t, app.Run([]string{"ntfy", "publish", "--env-topic", "--wait-pid", strconv.Itoa(sleep.Process.Pid)})) + m = toMessage(t, stdout.String()) + require.Regexp(t, `Process with PID \d+ exited after .+ms`, m.Message) +} diff --git a/util/util.go b/util/util.go index ca65c8fc..dab291ac 100644 --- a/util/util.go +++ b/util/util.go @@ -121,38 +121,6 @@ func ValidRandomString(s string, length int) bool { return true } -// DurationToHuman converts a duration to a human-readable format -func DurationToHuman(d time.Duration) (str string) { - if d == 0 { - return "0" - } - - d = d.Round(time.Second) - days := d / time.Hour / 24 - if days > 0 { - str += fmt.Sprintf("%dd", days) - } - d -= days * time.Hour * 24 - - hours := d / time.Hour - if hours > 0 { - str += fmt.Sprintf("%dh", hours) - } - d -= hours * time.Hour - - minutes := d / time.Minute - if minutes > 0 { - str += fmt.Sprintf("%dm", minutes) - } - d -= minutes * time.Minute - - seconds := d / time.Second - if seconds > 0 { - str += fmt.Sprintf("%ds", seconds) - } - return -} - // ParsePriority parses a priority string into its equivalent integer value func ParsePriority(priority string) (int, error) { switch strings.TrimSpace(strings.ToLower(priority)) { diff --git a/util/util_test.go b/util/util_test.go index a448eebd..9b716a35 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -5,33 +5,8 @@ import ( "io/ioutil" "path/filepath" "testing" - "time" ) -func TestDurationToHuman_SevenDays(t *testing.T) { - d := 7 * 24 * time.Hour - require.Equal(t, "7d", DurationToHuman(d)) -} - -func TestDurationToHuman_MoreThanOneDay(t *testing.T) { - d := 49 * time.Hour - require.Equal(t, "2d1h", DurationToHuman(d)) -} - -func TestDurationToHuman_LessThanOneDay(t *testing.T) { - d := 17*time.Hour + 15*time.Minute - require.Equal(t, "17h15m", DurationToHuman(d)) -} - -func TestDurationToHuman_TenOfThings(t *testing.T) { - d := 10*time.Hour + 10*time.Minute + 10*time.Second - require.Equal(t, "10h10m10s", DurationToHuman(d)) -} - -func TestDurationToHuman_Zero(t *testing.T) { - require.Equal(t, "0", DurationToHuman(0)) -} - func TestRandomString(t *testing.T) { s1 := RandomString(10) s2 := RandomString(10)