All --wait-cmd

pull/335/head
Philipp Heckel 2022-06-20 23:03:16 -04:00
parent fec4864771
commit 0080ea5a20
4 changed files with 118 additions and 107 deletions

View File

@ -11,13 +11,12 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
) )
func init() { func init() {
commands = append(commands, cmdPublish, cmdDone) commands = append(commands, cmdPublish)
} }
var flagsPublish = append( var flagsPublish = append(
@ -35,7 +34,8 @@ var flagsPublish = append(
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"}, &cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"}, &cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"}, &cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
&cli.IntFlag{Name: "pid", Aliases: []string{"done", "w"}, EnvVars: []string{"NTFY_PID"}, Usage: "monitor process with given PID and publish when it exists"}, &cli.IntFlag{Name: "wait-pid", Aliases: []string{"pid"}, EnvVars: []string{"NTFY_WAIT_PID"}, Usage: "wait until PID exits before publishing"},
&cli.BoolFlag{Name: "wait-cmd", Aliases: []string{"cmd", "done"}, EnvVars: []string{"NTFY_WAIT_CMD"}, Usage: "run and wait until command finishes before publishing"},
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"}, &cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"}, &cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
&cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"}, &cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"},
@ -46,7 +46,9 @@ var cmdPublish = &cli.Command{
Name: "publish", Name: "publish",
Aliases: []string{"pub", "send", "trigger"}, Aliases: []string{"pub", "send", "trigger"},
Usage: "Send message via a ntfy server", Usage: "Send message via a ntfy server",
UsageText: "ntfy publish [OPTIONS..] TOPIC [MESSAGE]\nNTFY_TOPIC=.. ntfy publish [OPTIONS..] -P [MESSAGE]", UsageText: `ntfy publish [OPTIONS..] TOPIC [MESSAGE...]
ntfy publish [OPTIONS..] --wait-cmd -P COMMAND...
NTFY_TOPIC=.. ntfy publish [OPTIONS..] -P [MESSAGE...]`,
Action: execPublish, Action: execPublish,
Category: categoryClient, Category: categoryClient,
Flags: flagsPublish, Flags: flagsPublish,
@ -65,8 +67,10 @@ Examples:
ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment
ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment
ntfy pub -u phil:mypass secret Psst # Publish with username/password ntfy pub -u phil:mypass secret Psst # Publish with username/password
ntfy pub --wait-pid 1234 mytopic # Wait for process 1234 to exit before publishing
ntfy pub --wait-cmd mytopic rsync -av ./ /tmp/a # Run command and publish after it completes
NTFY_USER=phil:mypass ntfy pub secret Psst # Use env variables to set username/password NTFY_USER=phil:mypass ntfy pub secret Psst # Use env variables to set username/password
NTFY_TOPIC=mytopic ntfy pub -P "some message"" # Use NTFY_TOPIC variable as topic NTFY_TOPIC=mytopic ntfy pub -P "some message" # Use NTFY_TOPIC variable as topic
cat flower.jpg | ntfy pub --file=- flowers 'Nice!' # Same as above, send image.jpg as attachment cat flower.jpg | ntfy pub --file=- flowers 'Nice!' # Same as above, send image.jpg as attachment
ntfy trigger mywebhook # Sending without message, useful for webhooks ntfy trigger mywebhook # Sending without message, useful for webhooks
@ -76,78 +80,7 @@ it has incredibly useful information: https://ntfy.sh/docs/publish/.
` + clientCommandDescriptionSuffix, ` + clientCommandDescriptionSuffix,
} }
var cmdDone = &cli.Command{
Name: "done",
Usage: "xxx",
UsageText: "xxx",
Action: execDone,
Category: categoryClient,
Flags: flagsPublish,
Before: initLogFunc,
Description: `xxx
` + clientCommandDescriptionSuffix,
}
func execDone(c *cli.Context) error {
return execPublishInternal(c, true)
}
func execPublish(c *cli.Context) error { func execPublish(c *cli.Context) error {
return execPublishInternal(c, false)
}
func parseTopicMessageCommand(c *cli.Context, isDoneCommand bool) (topic string, message string, command []string, err error) {
// 1. ntfy done <topic> <command>
// 2. ntfy done --pid <pid> <topic> [<message>]
// 3. NTFY_TOPIC=.. ntfy done <command>
// 4. NTFY_TOPIC=.. ntfy done --pid <pid> [<message>]
// 5. ntfy publish <topic> [<message>]
// 6. NTFY_TOPIC=.. ntfy publish [<message>]
var args []string
topic, args, err = parseTopicAndArgs(c)
if err != nil {
return
}
if isDoneCommand {
if c.Int("pid") > 0 {
message = strings.Join(args, " ")
} else if len(args) > 0 {
command = args
} else {
err = errors.New("must either specify --pid or a command")
}
} else {
message = strings.Join(args, " ")
}
if c.String("message") != "" {
message = c.String("message")
}
return
}
func parseTopicAndArgs(c *cli.Context) (topic string, args []string, err error) {
envTopic := c.Bool("env-topic")
if envTopic {
topic = os.Getenv("NTFY_TOPIC")
if topic == "" {
return "", nil, errors.New("if --env-topic is passed, must define NTFY_TOPIC environment variable")
}
return topic, remainingArgs(c, 0), nil
}
if c.NArg() < 1 {
return "", nil, errors.New("must specify topic")
}
return c.Args().Get(0), remainingArgs(c, 1), nil
}
func remainingArgs(c *cli.Context, fromIndex int) []string {
if c.NArg() > fromIndex {
return c.Args().Slice()[fromIndex:]
}
return []string{}
}
func execPublishInternal(c *cli.Context, doneCmd bool) error {
conf, err := loadConfig(c) conf, err := loadConfig(c)
if err != nil { if err != nil {
return err return err
@ -166,8 +99,8 @@ func execPublishInternal(c *cli.Context, doneCmd bool) error {
noCache := c.Bool("no-cache") noCache := c.Bool("no-cache")
noFirebase := c.Bool("no-firebase") noFirebase := c.Bool("no-firebase")
quiet := c.Bool("quiet") quiet := c.Bool("quiet")
pid := c.Int("pid") pid := c.Int("wait-pid")
topic, message, command, err := parseTopicMessageCommand(c, doneCmd) topic, message, command, err := parseTopicMessageCommand(c)
if err != nil { if err != nil {
return err return err
} }
@ -226,6 +159,9 @@ func execPublishInternal(c *cli.Context, doneCmd bool) error {
if err := waitForProcess(pid); err != nil { if err := waitForProcess(pid); err != nil {
return err return err
} }
if message == "" {
message = fmt.Sprintf("process with PID %d exited", pid)
}
} else if len(command) > 0 { } else if len(command) > 0 {
cmdResultMessage, err := runAndWaitForCommand(command) cmdResultMessage, err := runAndWaitForCommand(command)
if err != nil { if err != nil {
@ -267,6 +203,54 @@ func execPublishInternal(c *cli.Context, doneCmd bool) error {
return nil return nil
} }
func parseTopicMessageCommand(c *cli.Context) (topic string, message string, command []string, err error) {
// 1. ntfy publish --wait-cmd <topic> <command>
// 2. NTFY_TOPIC=.. ntfy publish --wait-cmd <command>
// 3. ntfy publish <topic> [<message>]
// 4. NTFY_TOPIC=.. ntfy publish [<message>]
var args []string
topic, args, err = parseTopicAndArgs(c)
if err != nil {
return
}
if c.Bool("wait-cmd") {
if len(args) == 0 {
err = errors.New("must specify command when --wait-cmd is passed, type 'ntfy publish --help' for help")
return
}
command = args
} else {
message = strings.Join(args, " ")
}
if c.String("message") != "" {
message = c.String("message")
}
return
}
func parseTopicAndArgs(c *cli.Context) (topic string, args []string, err error) {
envTopic := c.Bool("env-topic")
if envTopic {
fmt.Fprintln(c.App.ErrWriter, "\x1b[1;33mDeprecation notice: The --env-topic/-P flag will be removed in July 2022, see https://ntfy.sh/docs/deprecations/ for details.\x1b[0m")
topic = os.Getenv("NTFY_TOPIC")
if topic == "" {
return "", nil, errors.New("when --env-topic is passed, must define NTFY_TOPIC environment variable")
}
return topic, remainingArgs(c, 0), nil
}
if c.NArg() < 1 {
return "", nil, errors.New("must specify topic, type 'ntfy publish --help' for help")
}
return c.Args().Get(0), remainingArgs(c, 1), nil
}
func remainingArgs(c *cli.Context, fromIndex int) []string {
if c.NArg() > fromIndex {
return c.Args().Slice()[fromIndex:]
}
return []string{}
}
func waitForProcess(pid int) error { func waitForProcess(pid int) error {
if !processExists(pid) { if !processExists(pid) {
return fmt.Errorf("process with PID %d not running", pid) return fmt.Errorf("process with PID %d not running", pid)
@ -280,7 +264,7 @@ func waitForProcess(pid int) error {
} }
func runAndWaitForCommand(command []string) (message string, err error) { func runAndWaitForCommand(command []string) (message string, err error) {
prettyCmd := formatCommand(command) prettyCmd := util.QuoteCommand(command)
log.Debug("Running command: %s", prettyCmd) log.Debug("Running command: %s", prettyCmd)
cmd := exec.Command(command[0], command[1:]...) cmd := exec.Command(command[0], command[1:]...)
if log.IsTrace() { if log.IsTrace() {
@ -299,16 +283,3 @@ func runAndWaitForCommand(command []string) (message string, err error) {
log.Debug(message) log.Debug(message)
return message, nil return message, nil
} }
func formatCommand(command []string) string {
quoted := []string{command[0]}
noQuotesRegex := regexp.MustCompile(`^[-_./a-z0-9]+$`)
for _, c := range command[1:] {
if noQuotesRegex.MatchString(c) {
quoted = append(quoted, c)
} else {
quoted = append(quoted, fmt.Sprintf(`"%s"`, c))
}
}
return strings.Join(quoted, " ")
}

View File

@ -1,21 +1,35 @@
# Deprecation notices # Deprecation notices
This page is used to list deprecation notices for ntfy. Deprecated commands and options will be This page is used to list deprecation notices for ntfy. Deprecated commands and options will be
**removed after ~3 months** from the time they were deprecated. **removed after 1-3 months** from the time they were deprecated. How long the feature is deprecated
before the behavior is changed depends on the severity of the change, and how prominent the feature is.
## Active deprecations ## Active deprecations
### Android app: WebSockets will become the default connection protocol ### ntfy CLI: `ntfy publish --env-topic` will be removed
> Active since 2022-03-13, behavior will change in **June 2022** > Active since 2022-06-20, behavior will change end of **July 2022**
In future versions of the Android app, instant delivery connections and connections to self-hosted servers will The `ntfy publish --env-topic` option will be removed. It'll still be possible to specify a topic via the
be using the WebSockets protocol. This potentially requires [configuration changes in your proxy](https://ntfy.sh/docs/config/#nginxapache2caddy). `NTFY_TOPIC` environment variable, but it won't be necessary anymore to specify the `--env-topic` flag.
Due to [reports of varying battery consumption](https://github.com/binwiederhier/ntfy/issues/190) (which entirely === "Before"
seems to depend on the phone), JSON HTTP stream support will not be removed. Instead, I'll just flip the default to ```
WebSocket in June. $ NTFY_TOPIC=mytopic ntfy publish --env-topic "this is the message"
```
=== "After"
```
$ NTFY_TOPIC=mytopic ntfy publish "this is the message"
```
## Previous deprecations ## Previous deprecations
### <del>Android app: WebSockets will become the default connection protocol</del>
> Active since 2022-03-13, behavior will not change (deprecation removed 2022-06-20)
Instant delivery connections and connections to self-hosted servers in the Android app were going to switch
to use the WebSockets protocol by default. It was decided to keep JSON stream as the most compatible default
and add a notice banner in the Android app instead.
### Android app: Using `since=<timestamp>` instead of `since=<id>` ### Android app: Using `since=<timestamp>` instead of `since=<id>`
> Active since 2022-02-27, behavior changed with v1.14.0 > Active since 2022-02-27, behavior changed with v1.14.0

View File

@ -26,6 +26,7 @@ var (
randomMutex = sync.Mutex{} randomMutex = sync.Mutex{}
sizeStrRegex = regexp.MustCompile(`(?i)^(\d+)([gmkb])?$`) sizeStrRegex = regexp.MustCompile(`(?i)^(\d+)([gmkb])?$`)
errInvalidPriority = errors.New("invalid priority") errInvalidPriority = errors.New("invalid priority")
noQuotesRegex = regexp.MustCompile(`^[-_./:@a-zA-Z0-9]+$`)
) )
// FileExists checks if a file exists, and returns true if it does // FileExists checks if a file exists, and returns true if it does
@ -286,3 +287,22 @@ func MaybeMarshalJSON(v interface{}) string {
} }
return string(jsonBytes) return string(jsonBytes)
} }
// QuoteCommand combines a command array to a string, quoting arguments that need quoting.
// This function is naive, and sometimes wrong. It is only meant for lo pretty-printing a command.
//
// Warning: Never use this function with the intent to run the resulting command.
//
// Example:
// []string{"ls", "-al", "Document Folder"} -> ls -al "Document Folder"
func QuoteCommand(command []string) string {
var quoted []string
for _, c := range command {
if noQuotesRegex.MatchString(c) {
quoted = append(quoted, c)
} else {
quoted = append(quoted, fmt.Sprintf(`"%s"`, c))
}
}
return strings.Join(quoted, " ")
}

View File

@ -162,3 +162,9 @@ func TestLastString(t *testing.T) {
require.Equal(t, "last", LastString([]string{"first", "second", "last"}, "default")) require.Equal(t, "last", LastString([]string{"first", "second", "last"}, "default"))
require.Equal(t, "default", LastString([]string{}, "default")) require.Equal(t, "default", LastString([]string{}, "default"))
} }
func TestQuoteCommand(t *testing.T) {
require.Equal(t, `ls -al "Document Folder"`, QuoteCommand([]string{"ls", "-al", "Document Folder"}))
require.Equal(t, `rsync -av /home/phil/ root@example.com:/home/phil/`, QuoteCommand([]string{"rsync", "-av", "/home/phil/", "root@example.com:/home/phil/"}))
require.Equal(t, `/home/sweet/home "Äöü this is a test" "\a\b"`, QuoteCommand([]string{"/home/sweet/home", "Äöü this is a test", "\\a\\b"}))
}