178 lines
4.8 KiB
Go
178 lines
4.8 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"syscall"
|
|
|
|
"github.com/juju/errors"
|
|
|
|
"github.com/localhots/cmdui/backend/commands"
|
|
"github.com/localhots/cmdui/backend/db"
|
|
"github.com/localhots/cmdui/backend/runner"
|
|
)
|
|
|
|
func init() {
|
|
router.GET("/api/jobs", protectedEndpoint(jobsIndexHandler))
|
|
router.GET("/api/jobs/:job_id", protectedEndpoint(jobShowHandler))
|
|
router.PUT("/api/jobs/:job_id", protectedEndpoint(jobActionHandler))
|
|
router.GET("/api/jobs/:job_id/log", protectedEndpoint(jobLogHandler))
|
|
router.GET("/api/commands/:cmd/jobs", protectedEndpoint(jobsIndexHandler))
|
|
router.GET("/api/users/:user_id/jobs", protectedEndpoint(jobsIndexHandler))
|
|
}
|
|
|
|
// jobLogHandler returns job's log. If the command is still running, than the
|
|
// client would be attached to a log file and receive updates as well as few
|
|
// previous lines of it. If the command is completed, the entire log would be
|
|
// returned. If a `full` parameter is provided and the command is still running,
|
|
// the client would receive all existing log contents and future updates.
|
|
// GET /api/jobs/:job_id/log
|
|
// FIXME: What the fuck is this function
|
|
func jobLogHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
|
id := param(ctx, "job_id")
|
|
proc := runner.FindProcess(id)
|
|
if proc != nil {
|
|
var done <-chan struct{}
|
|
var err error
|
|
if r.FormValue("full") != "" {
|
|
done, err = runner.ReadFullLog(ctx, proc, unbufferedWriter{w})
|
|
} else {
|
|
done, err = runner.ReadLogUpdates(ctx, proc, unbufferedWriter{w})
|
|
}
|
|
if err != nil {
|
|
renderError(w, err, http.StatusInternalServerError, "Failed to tail a log")
|
|
}
|
|
<-done
|
|
return
|
|
}
|
|
|
|
proc = &runner.Process{ID: id}
|
|
done, err := runner.ReadFullLog(ctx, proc, unbufferedWriter{w})
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
<-done
|
|
}
|
|
|
|
// jobsIndexHandler returns a list of jobs for a given criteria.
|
|
// GET /api/jobs
|
|
// GET /api/commands/:cmd/jobs
|
|
// GET /api/users/:user_id/jobs
|
|
func jobsIndexHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
|
renderJobs := func(jobs []db.Job, err error) {
|
|
if err != nil {
|
|
renderError(w, err, http.StatusInternalServerError, "Failed to find jobs")
|
|
} else {
|
|
renderJSON(w, jobs)
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case param(ctx, "user_id") != "":
|
|
id := param(ctx, "user_id")
|
|
u, err := db.FindUser(id)
|
|
if err != nil {
|
|
renderError(w, err, http.StatusInternalServerError, "Failed to find a user")
|
|
}
|
|
if u == nil {
|
|
err := fmt.Errorf("User not found: %s", id)
|
|
renderError(w, err, http.StatusNotFound, "User not found")
|
|
return
|
|
}
|
|
|
|
renderJobs(db.FindUserJobs(id, requestedPage(r)))
|
|
case param(ctx, "cmd") != "":
|
|
cmdName := param(ctx, "cmd")
|
|
if _, ok := commands.Map()[cmdName]; ok {
|
|
renderJobs(db.FindCommandJobs(cmdName, requestedPage(r)))
|
|
} else {
|
|
err := fmt.Errorf("Command not found: %s", cmdName)
|
|
renderError(w, err, http.StatusNotFound, "Command not found")
|
|
}
|
|
default:
|
|
renderJobs(db.FindAllJobs(requestedPage(r)))
|
|
}
|
|
}
|
|
|
|
// jobShowHandler returns job details.
|
|
// GET /api/jobs/:job_id
|
|
func jobShowHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
|
id := param(ctx, "job_id")
|
|
job, err := db.FindJob(id)
|
|
if err != nil {
|
|
renderError(w, err, http.StatusInternalServerError, "Failed to find job")
|
|
return
|
|
}
|
|
if job != nil {
|
|
renderJSON(w, job)
|
|
} else {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
// jobActionHandler performs certain actions on a job.
|
|
// PUT /api/jobs/:job_id
|
|
func jobActionHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
renderError(w, err, http.StatusBadRequest, "Failed to parse form")
|
|
return
|
|
}
|
|
|
|
id := r.PostForm.Get("id")
|
|
if id == "" {
|
|
err := errors.New("Job ID is required")
|
|
renderError(w, err, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
proc := runner.FindProcess(id)
|
|
if proc == nil {
|
|
err := fmt.Errorf("Job %q was not found", id)
|
|
renderError(w, err, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
|
|
sigName := r.PostForm.Get("signal")
|
|
if sigName != "" {
|
|
sig, err := signalFromName(sigName)
|
|
if err != nil {
|
|
renderError(w, err, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
err = proc.Signal(sig)
|
|
if err != nil {
|
|
renderError(w, err, http.StatusInternalServerError, "Failed to send signal to a process")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func signalFromName(name string) (syscall.Signal, error) {
|
|
switch name {
|
|
case "SIGHUP":
|
|
return syscall.SIGHUP, nil
|
|
case "SIGINT":
|
|
return syscall.SIGINT, nil
|
|
case "SIGKILL":
|
|
return syscall.SIGKILL, nil
|
|
case "SIGQUIT":
|
|
return syscall.SIGQUIT, nil
|
|
case "SIGTERM":
|
|
return syscall.SIGTERM, nil
|
|
case "SIGTTIN":
|
|
return syscall.SIGTTIN, nil
|
|
case "SIGTTOU":
|
|
return syscall.SIGTTOU, nil
|
|
case "SIGUSR1":
|
|
return syscall.SIGUSR1, nil
|
|
case "SIGUSR2":
|
|
return syscall.SIGUSR2, nil
|
|
default:
|
|
return 0, fmt.Errorf("Signal not supported: %s", name)
|
|
}
|
|
}
|