2

I want to run a command in the Bash shell every one minute, and serve the output via http, on http://localhost:8080/feed

package main

import (
    "fmt"
    "os/exec"
)

func main() {
    cmd := `<a piped command>`
    out, err := exec.Command("bash", "-c", cmd).Output()
    if err != nil {
        fmt.Sprintf("Failed to execute command: %s", cmd)
    }
    fmt.Println(string(out))
}

UPDATE :

package main

import (
    "fmt"
    "log"
    "net/http"
    "os/exec"
)

func handler(w http.ResponseWriter, r *http.Request) {
    cmd := `<a piped command>`
    out, err := exec.Command("bash", "-c", cmd).Output()
    if err != nil {
        fmt.Sprintf("Failed to execute command: %s", cmd)
    }
    fmt.Fprintf(w, string(out))
}

func main() {
    http.HandleFunc("/feed", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

With the above code, the command is run every time http://localhost:8080/feed is accessed. How do I make it cache(?) the output of the command for one minute, and then run the command again only after the cache time is expired?

doraqwe
  • 47
  • 3
  • What's your question? – Peter Oct 13 '19 at 06:25
  • @Peter I want the `cmd` command as declared in the code above, to run every one minute, and the output of the command to be accessible on `http://localhost:8080/feed`. How do I go about doing that? – doraqwe Oct 13 '19 at 06:30

3 Answers3

1

If output is not too big, you can keep it in memory variable. I took approach to wait for result in case that script is executed (using mutex):

package main

import (
  "fmt"
  "net/http"
  "os/exec"
  "sync"
  "time"
)   

var LoopDelay = 60*time.Second

type Output struct {
  sync.Mutex
  content string
}   


func main() {

  var output *Output = new(Output)

  go updateResult(output)

  http.HandleFunc("/feed", initHandle(output))
  err := http.ListenAndServe(":8080", nil)
  if err != nil {
    fmt.Println("ERROR", err)
  }   
}   

func initHandle(output *Output) func(http.ResponseWriter, *http.Request) {
  return func(respw http.ResponseWriter, req *http.Request) {
    output.Lock()
    defer output.Unlock()
    _, err := respw.Write([]byte(output.content))
    if err != nil {
        fmt.Println("ERROR: Unable to write response: ", err)
    }
  }
}

func updateResult(output *Output) {
  var execFn = func() { /* Extracted so that 'defer' executes at the end of loop iteration */
    output.Lock()
    defer output.Unlock()
    command := exec.Command("bash", "-c", "date | nl ")
    output1, err := command.CombinedOutput()
    if err != nil {
      output.content = err.Error()
    } else {
      output.content = string(output1)
    }   

  }   
  for {
    execFn()
    time.Sleep(LoopDelay)
  }
}

Executing

date; curl http://localhost:8080/feed

gives output (on multiple calls):

 1  dimanche 13 octobre 2019, 09:41:40 (UTC+0200)
http-server-cmd-output> date; curl http://localhost:8080/feed

dimanche 13 octobre 2019, 09:42:05 (UTC+0200)
 1  dimanche 13 octobre 2019, 09:41:40 (UTC+0200)

Few things to consider:
- Used 'date | nl' as command with pipe example
- If output too big, write to file
- Very likely it is good idea to keep mutex only for content update (no need to wait during script execution) - you can try to improve it
- Go routine may have variable (channel) to exit on signal (ex: when program ends)

EDIT: variable moved to main function

igr
  • 3,409
  • 1
  • 20
  • 25
  • 2
    you should not be locking during command exec, but only during variable assignments.. –  Oct 13 '19 at 09:22
  • it is pointless to assign content to `err.Error()`, if the program fails during execution for internal logical reason, the returned go error will contain something about the exit code being != 0, and all the interesting information located in the command output will be lost. This assignment is useful only if the binary can not be started at all. –  Oct 13 '19 at 09:32
  • it is a little weird to use output both as a function parameter and a package variable. –  Oct 13 '19 at 09:35
  • Sure, that are things that can be improved – igr Oct 13 '19 at 10:04
  • @mh-cbon It depends who will use output of the script. I assumed in this example, that they would be interested in error if it occurs (ex: you have web interface for process customized by user input) – igr Oct 13 '19 at 10:45
  • @igr The updated code doesn't work. I get the following error : `.\test.go:21:19: undefined: output .\test.go:23:39: undefined: output` which are these lines - `go updateResult(output) http.HandleFunc("/feed", initHandle(output)` – doraqwe Oct 14 '19 at 02:36
  • @doraqwe it should be fixed now – igr Oct 14 '19 at 15:55
1

How do I make it cache(?) the output of the command for one minute, and then run the command again only after the cache time is expired?

In below solution two goroutines are declared.

  • First goroutine loop until the context is done to execute the command at regular interval and send a copy to the second go routine.
  • The second routine, tries to get from the first goroutine, or distribute its value to other goroutines.
  • The http handler, third goroutine, only queries the value from the getter and does something with it.

The reason to use three routines instead of two in this example is to prevent blocking the http routines if the command is being executed. With that additional routines, http requests only wait for the synchronization to occur.

type state is a dummy struct to transport the values within channels.

We prevent race conditions because of two facts, the state is passed by value, cmd.Output() allocates a new buffer each time it runs.

To retrieve original command in the http handler, OP should build a custom error type and attach those information to the recorded error, within the http handler, OP should type assert the error to its custom type and get the specific details from there.

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os/exec"
    "time"
)

type state struct {
    out []byte
    err error
}

func main() {

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    set := make(chan state, 1)
    go func() {
        ticker := time.NewTicker(2 * time.Second)
        cmd := []string{"date"}
        s := state{}
        s.out, s.err = exec.Command(cmd[0], cmd[1:]...).Output()
        set <- s
        for {
            select {
            case <-ctx.Done():
                return
            case <-ticker.C:
                s.out, s.err = exec.Command(cmd[0], cmd[1:]...).Output()
                set <- s
            }
        }
    }()

    get := make(chan state)
    go func() {
        s := state{}
        for {
            select {
            case <-ctx.Done():
                return
            case s = <-set: // get the fresh value, if any
            case get <- s: // distribute your copy
            }
        }
    }()

    http.HandleFunc("/feed", func(w http.ResponseWriter, r *http.Request) {
        state := <-get
        if state.err != nil {
            fmt.Printf("Failed to execute command: %v", state.err)
        }
        fmt.Fprintf(w, string(state.out))
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}
-1

You could:

That way, any time you go to your server, you will get the last command output.

How do I make it cache(?) the output of the command for one minute, and then run the command again only after the cache time is expired?

By keeping the execution of the command separate from the command output being served.

That is why you must kept the two asynchronous, typically through a goroutine, a a time.NewTicker.
See "Is there a way to do repetitive tasks at intervals?"

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • @doraqwe I have updated the answer accordingly: don't serve directly the output. Serve a file in which you periodically write the output. – VonC Oct 13 '19 at 07:14
  • Unless the output is too big to keep in memory, getting the filesystem involved just slows the whole thing down. – Adrian Oct 14 '19 at 13:14
  • @Adrian There is no obvious performance issue here, and that keeps the all system more resilient to stop/restart events: it can read and publish the last recorded command output (as written in the file) – VonC Oct 14 '19 at 13:18
  • Not particularly, since it's producing its own data, it can do so immediately on startup. Also, not all applications would prefer to serve stale content - what if that file is half an hour old when the app starts? – Adrian Oct 14 '19 at 13:26
  • @Adrian It is a log with a timestamp in its message: if it is an hour old, you will see it right away. – VonC Oct 14 '19 at 13:29