diff --git a/README.md b/README.md
index 25a5220..8894380 100755
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ Process Compose is a simple and flexible scheduler and orchestrator to manage no
- Forking (services or daemons) processes
- REST API (OpenAPI a.k.a Swagger)
- Logs caching
+- Functions as both server and client
It is heavily inspired by [docker-compose](https://github.com/docker/compose), but without the need for containers. The configuration syntax tries to follow the docker-compose specifications, with a few minor additions and lots of subtractions.
@@ -394,10 +395,54 @@ Default environment variables:
#### ✅ REST API
-A convenient Swagger API is provided: http://localhost:8080/swagger/index.html
+A convenient Swagger API is provided: http://localhost:8080
+Default port is 8080. Specify your own port:
+
+```shell
+process-compose -p PORT
+```
+
+#### ✅ Client Mode
+
+Process compose can also connect to itself as client. Available commands:
+
+##### Processes List
+
+```shell
+process-compose process list #lists available processes
+```
+
+##### Process Start
+
+```shell
+process-compose process start [PROCESS] #starts one of the available non running processes
+```
+
+##### Process Stop
+
+```shell
+process-compose process stop [PROCESS] #stops one of the running processes
+```
+
+##### Process Restart
+
+```shell
+process-compose process start [PROCESS] #restarts one of the available processes
+```
+
+Restart will wait `process.availability.backoff_seconds` seconds between `stop` and `start` of the process. If not configured the default value is 1s.
+
+By default the client will try to use the default port `8080` and default address `localhost` to connect to the locally running instance of process-compose. You can provide deferent values:
+
+```shell
+process-compose -p PORT process -a ADDRESS list
+```
+
+
+
#### ✅ Configuration
##### ✅ Support .env file
diff --git a/default.nix b/default.nix
index c059917..ebf309a 100644
--- a/default.nix
+++ b/default.nix
@@ -6,7 +6,7 @@ pkgs.buildGoModule rec {
src = ./.;
ldflags = [ "-X main.version=v${version}" ];
- vendorSha256 = "bJXym+JTAIbEh/dIkY22HdNwgYbdCGgUIts0KkS0GYk=";
+ vendorSha256 = "RqPH8gm8K8sLuRl4FTpGeitS++t3ygAgZ6OMvBCsCB8=";
postInstall = "mv $out/bin/{src,process-compose}";
diff --git a/go.mod b/go.mod
index ce507f4..f797eca 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
github.com/gin-gonic/gin v1.8.1
github.com/joho/godotenv v1.4.0
github.com/rivo/tview v0.0.0-20220916081518-2e69b7385a37
- github.com/swaggo/swag v1.8.6
+ github.com/swaggo/swag v1.8.7
gopkg.in/yaml.v2 v2.4.0
)
@@ -18,7 +18,9 @@ replace github.com/InVisionApp/go-health/v2 => github.com/f1bonacc1/go-health/v2
require (
github.com/InVisionApp/go-logger v1.0.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
+ github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
@@ -28,6 +30,7 @@ require (
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/goccy/go-json v0.9.11 // indirect
+ github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
@@ -38,7 +41,11 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/rivo/uniseg v0.4.2 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/spf13/cobra v1.6.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
+ github.com/urfave/cli/v2 v2.3.0 // indirect
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect
golang.org/x/net v0.0.0-20220926192436-02166a98028e // indirect
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
diff --git a/go.sum b/go.sum
index 9d4061f..a4b8eea 100644
--- a/go.sum
+++ b/go.sum
@@ -18,6 +18,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -32,6 +34,7 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
github.com/gdamore/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0=
github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
@@ -77,6 +80,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
+github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -150,12 +155,18 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
+github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -174,10 +185,13 @@ github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg=
github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
+github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU=
+github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
+github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
diff --git a/src/api/pc_api.go b/src/api/pc_api.go
index 3dd315c..a9396d6 100644
--- a/src/api/pc_api.go
+++ b/src/api/pc_api.go
@@ -97,3 +97,22 @@ func StartProcess(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"name": name})
}
+
+// @Schemes
+// @Description Restarts the process
+// @Tags Process
+// @Summary Restart a process
+// @Produce json
+// @Param name path string true "Process Name"
+// @Success 200 {string} string "Restarted Process Name"
+// @Router /process/restart/{name} [post]
+func RestartProcess(c *gin.Context) {
+ name := c.Param("name")
+ err := app.PROJ.RestartProcess(name)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"name": name})
+}
diff --git a/src/api/routes.go b/src/api/routes.go
index 0a6a205..f1d3c05 100644
--- a/src/api/routes.go
+++ b/src/api/routes.go
@@ -5,6 +5,8 @@ import (
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
+ "net/http"
+ "net/url"
)
// @title Process Compose API
@@ -32,10 +34,16 @@ func InitRoutes(useLogger bool) *gin.Engine {
//url := ginSwagger.URL("http://localhost:8080/swagger/doc.json") // The url pointing to API definition
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
+ r.GET("/", func(c *gin.Context) {
+ location := url.URL{Path: "/swagger/index.html"}
+ c.Redirect(http.StatusFound, location.RequestURI())
+ })
+
r.GET("/processes", GetProcesses)
r.GET("/process/logs/:name/:endOffset/:limit", GetProcessLogs)
r.PATCH("/process/stop/:name", StopProcess)
r.POST("/process/start/:name", StartProcess)
+ r.POST("/process/restart/:name", RestartProcess)
return r
}
diff --git a/src/app/config.go b/src/app/config.go
index e8e7ad0..2a60142 100644
--- a/src/app/config.go
+++ b/src/app/config.go
@@ -49,6 +49,10 @@ type ProcessState struct {
Pid int `json:"pid"`
}
+type ProcessStates struct {
+ States []ProcessState `json:"data"`
+}
+
func (p ProcessConfig) GetDependencies() []string {
dependencies := make([]string, len(p.DependsOn))
diff --git a/src/app/process.go b/src/app/process.go
index 7e72943..6d2cd58 100644
--- a/src/app/process.go
+++ b/src/app/process.go
@@ -13,7 +13,7 @@ import (
"syscall"
"time"
- "github.com/f1bonacc1/process-compose/src/cmd"
+ "github.com/f1bonacc1/process-compose/src/command"
"github.com/f1bonacc1/process-compose/src/health"
"github.com/f1bonacc1/process-compose/src/pclog"
@@ -85,7 +85,7 @@ func (p *Process) run() error {
}
for {
starter := func() error {
- p.command = cmd.BuildCommand(p.getCommand())
+ p.command = command.BuildCommand(p.getCommand())
p.command.Env = p.getProcessEnvironment()
p.setProcArgs()
stdout, _ := p.command.StdoutPipe()
@@ -116,7 +116,7 @@ func (p *Process) run() error {
p.waitForDaemonCompletion()
}
- if !p.isRestartable(p.procState.ExitCode) {
+ if !p.isRestartable() {
break
}
p.setState(ProcessStateRestarting)
@@ -149,7 +149,8 @@ func (p *Process) getProcessEnvironment() []string {
return env
}
-func (p *Process) isRestartable(exitCode int) bool {
+func (p *Process) isRestartable() bool {
+ exitCode := p.procState.ExitCode
if p.procConf.RestartPolicy.Restart == RestartPolicyNo ||
p.procConf.RestartPolicy.Restart == "" {
return false
@@ -223,10 +224,10 @@ func (p *Process) doConfiguredStop(params ShutDownParams) error {
defer cancel()
defer p.notifyDaemonStopped()
- command := cmd.BuildCommandContext(ctx, params.ShutDownCommand)
- command.Env = p.getProcessEnvironment()
+ cmd := command.BuildCommandContext(ctx, params.ShutDownCommand)
+ cmd.Env = p.getProcessEnvironment()
- if err := command.Run(); err != nil {
+ if err := cmd.Run(); err != nil {
// the process termination timedout and it will be killed
log.Error().Msgf("terminating %s with timeout %d failed - %s", p.getName(), timeout, err.Error())
return p.stop(int(syscall.SIGKILL))
@@ -393,7 +394,7 @@ func (p *Process) stopProbes() {
}
}
-func (p *Process) onLivenessCheckEnd(isOk, isFatal bool, err string) {
+func (p *Process) onLivenessCheckEnd(_, isFatal bool, err string) {
if isFatal {
log.Info().Msgf("%s is not alive anymore - %s", p.getName(), err)
p.logBuffer.Write("Error: liveness check fail - " + err)
diff --git a/src/app/project.go b/src/app/project.go
index e4d8251..39e2488 100644
--- a/src/app/project.go
+++ b/src/app/project.go
@@ -3,13 +3,12 @@ package app
import (
"errors"
"fmt"
- "io/ioutil"
+ "github.com/f1bonacc1/process-compose/src/pclog"
"os"
"path/filepath"
"sort"
"strings"
-
- "github.com/f1bonacc1/process-compose/src/pclog"
+ "time"
"github.com/joho/godotenv"
"github.com/rs/zerolog"
@@ -170,9 +169,9 @@ func (p *Project) StartProcess(name string) error {
log.Error().Msgf("Process %s is already running", name)
return fmt.Errorf("process %s is already running", name)
}
- if proc, ok := p.Processes[name]; ok {
- proc.Name = name
- p.runProcess(proc)
+ if processConfig, ok := p.Processes[name]; ok {
+ processConfig.Name = name
+ p.runProcess(processConfig)
} else {
return fmt.Errorf("no such process: %s", name)
}
@@ -190,6 +189,25 @@ func (p *Project) StopProcess(name string) error {
return nil
}
+func (p *Project) RestartProcess(name string) error {
+ proc := p.getRunningProcess(name)
+ if proc != nil {
+ _ = proc.shutDown()
+ if proc.isRestartable() {
+ return nil
+ }
+ time.Sleep(proc.getBackoff())
+ }
+
+ if processConfig, ok := p.Processes[name]; ok {
+ processConfig.Name = name
+ p.runProcess(processConfig)
+ } else {
+ return fmt.Errorf("no such process: %s", name)
+ }
+ return nil
+}
+
func (p *Project) ShutDownProject() {
p.mapMutex.Lock()
runProc := p.runningProcesses
@@ -331,7 +349,7 @@ func (p *Project) GetLexicographicProcessNames() []string {
}
func CreateProject(inputFile string) *Project {
- yamlFile, err := ioutil.ReadFile(inputFile)
+ yamlFile, err := os.ReadFile(inputFile)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
diff --git a/src/client/common.go b/src/client/common.go
new file mode 100644
index 0000000..4d7c972
--- /dev/null
+++ b/src/client/common.go
@@ -0,0 +1,5 @@
+package client
+
+type pcError struct {
+ Error string `json:"error"`
+}
diff --git a/src/client/processes.go b/src/client/processes.go
new file mode 100644
index 0000000..42b1f95
--- /dev/null
+++ b/src/client/processes.go
@@ -0,0 +1,29 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/f1bonacc1/process-compose/src/app"
+ "net/http"
+)
+
+func GetProcessesName(address string, port int) ([]string, error) {
+ url := fmt.Sprintf("http://%s:%d/processes", address, port)
+ resp, err := http.Get(url)
+ if err != nil {
+ return []string{}, err
+ }
+ defer resp.Body.Close()
+ //Create a variable of the same type as our model
+ var sResp app.ProcessStates
+
+ //Decode the data
+ if err := json.NewDecoder(resp.Body).Decode(&sResp); err != nil {
+ return []string{}, err
+ }
+ procs := make([]string, len(sResp.States))
+ for i, proc := range sResp.States {
+ procs[i] = proc.Name
+ }
+ return procs, nil
+}
diff --git a/src/client/restart.go b/src/client/restart.go
new file mode 100644
index 0000000..b7d1a6a
--- /dev/null
+++ b/src/client/restart.go
@@ -0,0 +1,26 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/rs/zerolog/log"
+ "net/http"
+)
+
+func RestartProcesses(address string, port int, name string) error {
+ url := fmt.Sprintf("http://%s:%d/process/restart/%s", address, port, name)
+ resp, err := http.Post(url, "application/json", nil)
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode == http.StatusOK {
+ return nil
+ }
+ defer resp.Body.Close()
+ var respErr pcError
+ if err = json.NewDecoder(resp.Body).Decode(&respErr); err != nil {
+ log.Error().Msgf("failed to decode restart process %s response: %v", name, err)
+ return err
+ }
+ return fmt.Errorf(respErr.Error)
+}
diff --git a/src/client/start.go b/src/client/start.go
new file mode 100644
index 0000000..a83ac9e
--- /dev/null
+++ b/src/client/start.go
@@ -0,0 +1,26 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/rs/zerolog/log"
+ "net/http"
+)
+
+func StartProcesses(address string, port int, name string) error {
+ url := fmt.Sprintf("http://%s:%d/process/start/%s", address, port, name)
+ resp, err := http.Post(url, "application/json", nil)
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode == http.StatusOK {
+ return nil
+ }
+ defer resp.Body.Close()
+ var respErr pcError
+ if err = json.NewDecoder(resp.Body).Decode(&respErr); err != nil {
+ log.Error().Msgf("failed to decode start process %s response: %v", name, err)
+ return err
+ }
+ return fmt.Errorf(respErr.Error)
+}
diff --git a/src/client/stop.go b/src/client/stop.go
new file mode 100644
index 0000000..037d4df
--- /dev/null
+++ b/src/client/stop.go
@@ -0,0 +1,31 @@
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/rs/zerolog/log"
+ "net/http"
+)
+
+func StopProcesses(address string, port int, name string) error {
+ url := fmt.Sprintf("http://%s:%d/process/stop/%s", address, port, name)
+ client := &http.Client{}
+ req, err := http.NewRequest(http.MethodPatch, url, nil)
+ if err != nil {
+ return err
+ }
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ if resp.StatusCode == http.StatusOK {
+ return nil
+ }
+ defer resp.Body.Close()
+ var respErr pcError
+ if err = json.NewDecoder(resp.Body).Decode(&respErr); err != nil {
+ log.Error().Msgf("failed to decode stop process %s response: %v", name, err)
+ return err
+ }
+ return fmt.Errorf(respErr.Error)
+}
diff --git a/src/cmd/list.go b/src/cmd/list.go
new file mode 100644
index 0000000..6aae7e5
--- /dev/null
+++ b/src/cmd/list.go
@@ -0,0 +1,30 @@
+package cmd
+
+import (
+ "fmt"
+ "github.com/f1bonacc1/process-compose/src/client"
+
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+)
+
+// listCmd represents the list command
+var listCmd = &cobra.Command{
+ Use: "list",
+ Short: "List available processes",
+ Aliases: []string{"ls"},
+ Run: func(cmd *cobra.Command, args []string) {
+ processNames, err := client.GetProcessesName(pcAddress, port)
+ if err != nil {
+ log.Error().Msgf("Failed to get processes names %v", err)
+ return
+ }
+ for _, proc := range processNames {
+ fmt.Println(proc)
+ }
+ },
+}
+
+func init() {
+ processCmd.AddCommand(listCmd)
+}
diff --git a/src/cmd/process.go b/src/cmd/process.go
new file mode 100644
index 0000000..50025b7
--- /dev/null
+++ b/src/cmd/process.go
@@ -0,0 +1,21 @@
+package cmd
+
+import (
+ "github.com/spf13/cobra"
+)
+
+var (
+ pcAddress string
+)
+
+// processCmd represents the process command
+var processCmd = &cobra.Command{
+ Use: "process",
+ Short: "Execute operations on available processes",
+ Args: cobra.MinimumNArgs(1),
+}
+
+func init() {
+ rootCmd.AddCommand(processCmd)
+ processCmd.PersistentFlags().StringVarP(&pcAddress, "address", "a", "localhost", "address of a running process compose server")
+}
diff --git a/src/cmd/restart.go b/src/cmd/restart.go
new file mode 100644
index 0000000..ae1b8a2
--- /dev/null
+++ b/src/cmd/restart.go
@@ -0,0 +1,27 @@
+package cmd
+
+import (
+ "github.com/f1bonacc1/process-compose/src/client"
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+)
+
+// restartCmd represents the restart command
+var restartCmd = &cobra.Command{
+ Use: "restart [PROCESS]",
+ Short: "Restart a process",
+ Args: cobra.ExactArgs(1),
+ Run: func(cmd *cobra.Command, args []string) {
+ name := args[0]
+ err := client.RestartProcesses(pcAddress, port, name)
+ if err != nil {
+ log.Error().Msgf("Failed to restart processes %s: %v", name, err)
+ return
+ }
+ log.Info().Msgf("Process %s restarted", name)
+ },
+}
+
+func init() {
+ processCmd.AddCommand(restartCmd)
+}
diff --git a/src/cmd/root.go b/src/cmd/root.go
new file mode 100644
index 0000000..a60c3a6
--- /dev/null
+++ b/src/cmd/root.go
@@ -0,0 +1,126 @@
+package cmd
+
+import (
+ "fmt"
+ "github.com/f1bonacc1/process-compose/src/api"
+ "github.com/f1bonacc1/process-compose/src/app"
+ "github.com/f1bonacc1/process-compose/src/tui"
+ "github.com/gin-gonic/gin"
+ "github.com/rs/zerolog"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+)
+
+const EnvDebugMode = "PC_DEBUG_MODE"
+
+var (
+ fileName string
+ port int
+ isTui bool
+ version string
+
+ // rootCmd represents the base command when called without any subcommands
+ rootCmd = &cobra.Command{
+ Use: "process-compose",
+ Short: "Processes scheduler and orchestrator",
+ // Uncomment the following line if your bare application
+ // has an action associated with it:
+ Run: func(cmd *cobra.Command, args []string) {
+ fmt.Println(fileName)
+ if !cmd.Flags().Changed("config") {
+
+ pwd, err := os.Getwd()
+ if err != nil {
+ log.Fatal().Msg(err.Error())
+ }
+ file, err := app.AutoDiscoverComposeFile(pwd)
+ if err != nil {
+ log.Fatal().Msg(err.Error())
+ }
+ fileName = file
+ }
+
+ if os.Getenv(EnvDebugMode) == "" {
+ gin.SetMode(gin.ReleaseMode)
+ }
+
+ routersInit := api.InitRoutes(!isTui)
+ readTimeout := time.Duration(60) * time.Second
+ writeTimeout := time.Duration(60) * time.Second
+ endPoint := fmt.Sprintf(":%d", port)
+ maxHeaderBytes := 1 << 20
+
+ server := &http.Server{
+ Addr: endPoint,
+ Handler: routersInit,
+ ReadTimeout: readTimeout,
+ WriteTimeout: writeTimeout,
+ MaxHeaderBytes: maxHeaderBytes,
+ }
+
+ log.Info().Msgf("start http server listening %s", endPoint)
+
+ go server.ListenAndServe()
+
+ project := app.CreateProject(fileName)
+
+ if isTui {
+ defer quiet()()
+ go project.Run()
+ tui.SetupTui(version, project.LogLength)
+ } else {
+ runHeadless(project)
+ }
+
+ log.Info().Msg("Thank you for using proccess-compose")
+ },
+ }
+)
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute(ver string) {
+ version = ver
+ err := rootCmd.Execute()
+ if err != nil {
+ os.Exit(1)
+ }
+}
+
+func init() {
+
+ rootCmd.Flags().StringVarP(&fileName, "config", "f", app.DefaultFileNames[0], "path to config file to load")
+ rootCmd.Flags().BoolVarP(&isTui, "tui", "t", true, "disable tui (-t=false)")
+ rootCmd.PersistentFlags().IntVarP(&port, "port", "p", 8080, "port number")
+}
+
+func runHeadless(project *app.Project) {
+ cancelChan := make(chan os.Signal, 1)
+ // catch SIGTERM or SIGINTERRUPT
+ signal.Notify(cancelChan, syscall.SIGTERM, syscall.SIGINT)
+ go project.Run()
+ sig := <-cancelChan
+ log.Info().Msgf("Caught %v - Shutting down the running processes...", sig)
+ project.ShutDownProject()
+}
+
+func quiet() func() {
+ null, _ := os.Open(os.DevNull)
+ sout := os.Stdout
+ serr := os.Stderr
+ os.Stdout = null
+ os.Stderr = null
+ zerolog.SetGlobalLevel(zerolog.Disabled)
+ return func() {
+ defer null.Close()
+ os.Stdout = sout
+ os.Stderr = serr
+ zerolog.SetGlobalLevel(zerolog.DebugLevel)
+ }
+}
diff --git a/src/cmd/start.go b/src/cmd/start.go
new file mode 100644
index 0000000..6ca2292
--- /dev/null
+++ b/src/cmd/start.go
@@ -0,0 +1,31 @@
+/*
+Copyright © 2022 NAME HERE
+*/
+package cmd
+
+import (
+ "github.com/f1bonacc1/process-compose/src/client"
+ "github.com/rs/zerolog/log"
+
+ "github.com/spf13/cobra"
+)
+
+// startCmd represents the start command
+var startCmd = &cobra.Command{
+ Use: "start [PROCESS]",
+ Short: "Start a process",
+ Args: cobra.ExactArgs(1),
+ Run: func(cmd *cobra.Command, args []string) {
+ name := args[0]
+ err := client.StartProcesses(pcAddress, port, name)
+ if err != nil {
+ log.Error().Msgf("Failed to start processes %s: %v", name, err)
+ return
+ }
+ log.Info().Msgf("Process %s started", name)
+ },
+}
+
+func init() {
+ processCmd.AddCommand(startCmd)
+}
diff --git a/src/cmd/stop.go b/src/cmd/stop.go
new file mode 100644
index 0000000..87c1740
--- /dev/null
+++ b/src/cmd/stop.go
@@ -0,0 +1,30 @@
+/*
+Copyright © 2022 NAME HERE
+*/
+package cmd
+
+import (
+ "github.com/f1bonacc1/process-compose/src/client"
+ "github.com/rs/zerolog/log"
+ "github.com/spf13/cobra"
+)
+
+// stopCmd represents the stop command
+var stopCmd = &cobra.Command{
+ Use: "stop [PROCESS]",
+ Short: "Stop a running process",
+ Args: cobra.ExactArgs(1),
+ Run: func(cmd *cobra.Command, args []string) {
+ name := args[0]
+ err := client.StopProcesses(pcAddress, port, name)
+ if err != nil {
+ log.Error().Msgf("Failed to stop processes %s: %v", name, err)
+ return
+ }
+ log.Info().Msgf("Process %s stopped", name)
+ },
+}
+
+func init() {
+ processCmd.AddCommand(stopCmd)
+}
diff --git a/src/cmd/command.go b/src/command/command.go
similarity index 97%
rename from src/cmd/command.go
rename to src/command/command.go
index 682419d..d0c63ce 100644
--- a/src/cmd/command.go
+++ b/src/command/command.go
@@ -1,4 +1,4 @@
-package cmd
+package command
import (
"context"
diff --git a/src/docs/docs.go b/src/docs/docs.go
index 61d83d5..5c6c494 100644
--- a/src/docs/docs.go
+++ b/src/docs/docs.go
@@ -59,6 +59,35 @@ const docTemplate = `{
}
}
},
+ "/process/restart/{name}": {
+ "post": {
+ "description": "Restarts the process",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Process"
+ ],
+ "summary": "Restart a process",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Process Name",
+ "name": "name",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Restarted Process Name",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
"/process/start/{name}": {
"post": {
"description": "Starts the process if the state is not 'running' or 'pending'",
diff --git a/src/docs/swagger.json b/src/docs/swagger.json
index d401d31..6425873 100644
--- a/src/docs/swagger.json
+++ b/src/docs/swagger.json
@@ -47,6 +47,35 @@
}
}
},
+ "/process/restart/{name}": {
+ "post": {
+ "description": "Restarts the process",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Process"
+ ],
+ "summary": "Restart a process",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Process Name",
+ "name": "name",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Restarted Process Name",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
"/process/start/{name}": {
"post": {
"description": "Starts the process if the state is not 'running' or 'pending'",
diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml
index 3d5e335..eef1759 100644
--- a/src/docs/swagger.yaml
+++ b/src/docs/swagger.yaml
@@ -30,6 +30,25 @@ paths:
summary: Get process logs
tags:
- Process
+ /process/restart/{name}:
+ post:
+ description: Restarts the process
+ parameters:
+ - description: Process Name
+ in: path
+ name: name
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Restarted Process Name
+ schema:
+ type: string
+ summary: Restart a process
+ tags:
+ - Process
/process/start/{name}:
post:
description: Starts the process if the state is not 'running' or 'pending'
diff --git a/src/health/exec_checker.go b/src/health/exec_checker.go
index 46eed79..9ead695 100644
--- a/src/health/exec_checker.go
+++ b/src/health/exec_checker.go
@@ -2,9 +2,8 @@ package health
import (
"context"
+ "github.com/f1bonacc1/process-compose/src/command"
"time"
-
- "github.com/f1bonacc1/process-compose/src/cmd"
)
type execChecker struct {
@@ -16,7 +15,7 @@ func (c *execChecker) Status() (interface{}, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.timeout)*time.Second)
defer cancel()
- cmd := cmd.BuildCommandContext(ctx, c.command)
+ cmd := command.BuildCommandContext(ctx, c.command)
if err := cmd.Run(); err != nil {
return nil, err
diff --git a/src/main.go b/src/main.go
index b733986..78108ff 100644
--- a/src/main.go
+++ b/src/main.go
@@ -1,25 +1,12 @@
package main
import (
- "flag"
- "fmt"
- "net/http"
- "os/signal"
- "syscall"
- "time"
-
- "os"
-
- "github.com/f1bonacc1/process-compose/src/api"
- "github.com/f1bonacc1/process-compose/src/app"
- "github.com/f1bonacc1/process-compose/src/tui"
- "github.com/gin-gonic/gin"
+ "github.com/f1bonacc1/process-compose/src/cmd"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
+ "os"
)
-const EnvDebugMode = "PC_DEBUG_MODE"
-
var version = "undefined"
func setupLogger() {
@@ -31,99 +18,10 @@ func setupLogger() {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
-func isFlagPassed(name string) bool {
- found := false
- flag.Visit(func(f *flag.Flag) {
- if f.Name == name {
- found = true
- }
- })
- return found
-}
-
func init() {
setupLogger()
}
-func quiet() func() {
- null, _ := os.Open(os.DevNull)
- sout := os.Stdout
- serr := os.Stderr
- os.Stdout = null
- os.Stderr = null
- zerolog.SetGlobalLevel(zerolog.Disabled)
- return func() {
- defer null.Close()
- os.Stdout = sout
- os.Stderr = serr
- zerolog.SetGlobalLevel(zerolog.DebugLevel)
- }
-}
-
-func runHeadless(project *app.Project) {
- cancelChan := make(chan os.Signal, 1)
- // catch SIGTERM or SIGINTERRUPT
- signal.Notify(cancelChan, syscall.SIGTERM, syscall.SIGINT)
- go func() {
- project.Run()
- }()
- sig := <-cancelChan
- log.Info().Msgf("Caught %v - Shutting down the running processes...", sig)
- project.ShutDownProject()
-}
-
func main() {
- fileName := ""
- port := 8080
- isTui := true
- flag.StringVar(&fileName, "f", app.DefaultFileNames[0], "path to file to load")
- flag.IntVar(&port, "p", port, "port number")
- flag.BoolVar(&isTui, "t", isTui, "disable tui (-t=false)")
- flag.Parse()
- if !isFlagPassed("f") {
- pwd, err := os.Getwd()
- if err != nil {
- log.Fatal().Msg(err.Error())
- }
- file, err := app.AutoDiscoverComposeFile(pwd)
- if err != nil {
- log.Fatal().Msg(err.Error())
- }
- fileName = file
- }
-
- if os.Getenv(EnvDebugMode) == "" {
- gin.SetMode(gin.ReleaseMode)
- }
-
- routersInit := api.InitRoutes(!isTui)
- readTimeout := time.Duration(60) * time.Second
- writeTimeout := time.Duration(60) * time.Second
- endPoint := fmt.Sprintf(":%d", port)
- maxHeaderBytes := 1 << 20
-
- server := &http.Server{
- Addr: endPoint,
- Handler: routersInit,
- ReadTimeout: readTimeout,
- WriteTimeout: writeTimeout,
- MaxHeaderBytes: maxHeaderBytes,
- }
-
- log.Info().Msgf("start http server listening %s", endPoint)
-
- go server.ListenAndServe()
-
- project := app.CreateProject(fileName)
-
- if isTui {
- defer quiet()()
- go project.Run()
- tui.SetupTui(version, project.LogLength)
- } else {
- runHeadless(project)
- }
-
- log.Info().Msg("Thank you for using proccess-compose")
-
+ cmd.Execute(version)
}
diff --git a/src/tui/view.go b/src/tui/view.go
index af8641f..831498c 100644
--- a/src/tui/view.go
+++ b/src/tui/view.go
@@ -216,6 +216,9 @@ func (pv *pcView) createProcTable() *tview.Table {
case tcell.KeyF7:
name := pv.getSelectedProcName()
app.PROJ.StartProcess(name)
+ case tcell.KeyCtrlR:
+ name := pv.getSelectedProcName()
+ app.PROJ.RestartProcess(name)
}
return event
})
@@ -294,6 +297,7 @@ func (pv *pcView) updateHelpTextView() {
fmt.Fprintf(pv.helpText, "%s ", "F7[black:green]Start[-:-:-]")
fmt.Fprintf(pv.helpText, "%s%s%s ", "F8[black:green]", procScr, " Screen[-:-:-]")
fmt.Fprintf(pv.helpText, "%s ", "F9[black:green]Kill[-:-:-]")
+ fmt.Fprintf(pv.helpText, "%s ", "CTRL+R[black:green]Restart[-:-:-]")
fmt.Fprintf(pv.helpText, "%s ", "F10[black:green]Quit[-:-:-]")
}