Get Process Ports

This commit is contained in:
Berger Eugene 2023-07-10 23:42:19 +03:00
parent 43ac3cc86f
commit c51b8a7d39
18 changed files with 231 additions and 39 deletions

4
go.mod
View File

@ -5,6 +5,7 @@ go 1.20
require (
github.com/InVisionApp/go-health/v2 v2.1.3
github.com/adrg/xdg v0.4.0
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5
github.com/f1bonacc1/glippy v0.0.0-20230203184233-82c6562cecd1
github.com/fatih/color v1.15.0
github.com/gdamore/tcell/v2 v2.6.0
@ -16,10 +17,13 @@ require (
github.com/spf13/cobra v1.7.0
github.com/swaggo/swag v1.16.1
gopkg.in/yaml.v2 v2.4.0
//github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5
)
replace github.com/InVisionApp/go-health/v2 => github.com/f1bonacc1/go-health/v2 v2.1.3
replace github.com/cakturk/go-netstat => github.com/mololab/netstat v0.0.0-20221129160431-27c9226a45b1
require (
github.com/InVisionApp/go-logger v1.0.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect

6
go.sum
View File

@ -4,6 +4,8 @@ github.com/InVisionApp/go-logger v1.0.1 h1:WFL19PViM1mHUmUWfsv5zMo379KSWj2MRmBlz
github.com/InVisionApp/go-logger v1.0.1/go.mod h1:+cGTDSn+P8105aZkeOfIhdd7vFO5X1afUHcjvanY0L8=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
@ -14,6 +16,8 @@ github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQ
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwPhhyuFkbINB+2a1xATwk8SNDWnJiD41g=
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
@ -128,6 +132,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mololab/netstat v0.0.0-20221129160431-27c9226a45b1 h1:3k8Fbl7pFlde2/847ox7l/Wqql9Ww644JsQoPaY3/1o=
github.com/mololab/netstat v0.0.0-20221129160431-27c9226a45b1/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=

View File

@ -84,36 +84,8 @@ processes:
success_threshold: 1
failure_threshold: 3
# nginx:
# command: "docker run -d --rm -p80:80 --name nginx_test nginx"
# # availability:
# # restart: on_failure
# is_daemon: true
# shutdown:
# command: "docker stop nginx_test"
# signal: 15
# timeout_seconds: 5
# liveness_probe:
# exec:
# command: "[ $(docker inspect -f '{{.State.Running}}' nginx_test) = 'true' ]"
# initial_delay_seconds: 5
# period_seconds: 2
# timeout_seconds: 5
# success_threshold: 1
# failure_threshold: 3
# readiness_probe:
# http_get:
# host: 127.0.0.1
# path: "/"
# port: 80
# initial_delay_seconds: 5
# period_seconds: 10
# timeout_seconds: 5
# success_threshold: 1
# failure_threshold: 3
# availability:
# restart: "always"
# backoff_seconds: 2
server:
command: "python3 -m http.server 4040"
kcalc:
command: "kcalc"

View File

@ -215,3 +215,23 @@ func (api *PcApi) GetHostName(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"name": name})
}
// @Schemes
// @Description Retrieves process open ports
// @Tags Process
// @Summary Get process ports
// @Produce json
// @Param name path string true "Process Name"
// @Success 200 {object} object "Process Ports"
// @Router /process/ports/{name} [get]
func (api *PcApi) GetProcessPorts(c *gin.Context) {
name := c.Param("name")
ports, err := api.project.GetProcessPorts(name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, ports)
}

View File

@ -43,6 +43,7 @@ func InitRoutes(useLogger bool, handler *PcApi) *gin.Engine {
r.GET("/processes", handler.GetProcesses)
r.GET("/process/:name", handler.GetProcess)
r.GET("/process/info/:name", handler.GetProcessInfo)
r.GET("/process/ports/:name", handler.GetProcessPorts)
r.GET("/process/logs/:name/:endOffset/:limit", handler.GetProcessLogs)
r.PATCH("/process/stop/:name", handler.StopProcess)
r.POST("/process/start/:name", handler.StartProcess)

View File

@ -4,6 +4,7 @@ import (
"bufio"
"context"
"fmt"
"github.com/cakturk/go-netstat/netstat"
"github.com/f1bonacc1/process-compose/src/types"
"io"
"math/rand"
@ -160,7 +161,7 @@ func (p *Process) getBackoff() time.Duration {
func (p *Process) getProcessEnvironment() []string {
env := []string{
"PC_PROC_NAME=" + p.getName(),
"PC_PROC_NAME=" + p.procConf.Name,
"PC_REPLICA_NUM=" + strconv.Itoa(p.procConf.ReplicaNum),
}
env = append(env, os.Environ()...)
@ -528,3 +529,20 @@ func (p *Process) validateProcess() error {
}
return nil
}
func (p *Process) getOpenPorts(ports *types.ProcessPorts) error {
socks, err := netstat.TCPSocks(func(s *netstat.SockTabEntry) bool {
return s.State == netstat.Listen
})
if err != nil {
log.Err(err).Msgf("failed to get open ports for %s", p.getName())
return err
}
for _, e := range socks {
if e.Process != nil && e.Process.Pid == p.procState.Pid {
log.Debug().Msgf("%s is listening on %d", p.getName(), e.LocalAddr.Port)
ports.TcpPorts = append(ports.TcpPorts, e.LocalAddr.Port)
}
}
return nil
}

View File

@ -25,4 +25,5 @@ type IProject interface {
StartProcess(name string) error
RestartProcess(name string) error
ScaleProcess(name string, scale int) error
GetProcessPorts(name string) (*types.ProcessPorts, error)
}

View File

@ -260,6 +260,24 @@ func (p *ProjectRunner) GetProcessInfo(name string) (*types.ProcessConfig, error
}
}
func (p *ProjectRunner) GetProcessPorts(name string) (*types.ProcessPorts, error) {
proc := p.getRunningProcess(name)
if proc == nil {
return nil, fmt.Errorf("can't get ports: process %s is not running", name)
}
ports := &types.ProcessPorts{
Name: name,
TcpPorts: make([]uint16, 0),
UdpPorts: make([]uint16, 0),
}
err := proc.getOpenPorts(ports)
if err != nil {
return nil, err
}
return ports, nil
}
func (p *ProjectRunner) ShutDownProject() {
p.runProcMutex.Lock()
defer p.runProcMutex.Unlock()

View File

@ -72,8 +72,11 @@ func (p *PcClient) GetLexicographicProcessNames() ([]string, error) {
}
func (p *PcClient) GetProcessInfo(name string) (*types.ProcessConfig, error) {
config, err := GetProcessInfo(p.address, p.port, name)
return config, err
return GetProcessInfo(p.address, p.port, name)
}
func (p *PcClient) GetProcessPorts(name string) (*types.ProcessPorts, error) {
return GetProcessPorts(p.address, p.port, name)
}
func (p *PcClient) GetProcessState(name string) (*types.ProcessState, error) {

View File

@ -65,7 +65,6 @@ func GetProcessInfo(address string, port int, name string) (*types.ProcessConfig
return nil, err
}
defer resp.Body.Close()
//Create a variable of the same type as our model
var sResp types.ProcessConfig
//Decode the data
@ -76,3 +75,21 @@ func GetProcessInfo(address string, port int, name string) (*types.ProcessConfig
return &sResp, nil
}
func GetProcessPorts(address string, port int, name string) (*types.ProcessPorts, error) {
url := fmt.Sprintf("http://%s:%d/process/ports/%s", address, port, name)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var sResp types.ProcessPorts
//Decode the data
if err := json.NewDecoder(resp.Body).Decode(&sResp); err != nil {
log.Err(err).Msgf("what I got: %s", err.Error())
return nil, err
}
return &sResp, nil
}

30
src/cmd/ports.go Normal file
View File

@ -0,0 +1,30 @@
package cmd
import (
"fmt"
"github.com/f1bonacc1/process-compose/src/client"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
// portsCmd represents the ports command
var portsCmd = &cobra.Command{
Use: "ports [PROCESS]",
Short: "Get the ports that a process is listening on",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
ports, err := client.GetProcessPorts(pcAddress, port, name)
if err != nil {
logFatal(err, "failed to get process %s ports", name)
return
}
log.Info().Msgf("Process %s TCP ports: %v", name, ports.TcpPorts)
fmt.Printf("Process %s TCP ports: %v\n", name, ports.TcpPorts)
},
}
func init() {
processCmd.AddCommand(portsCmd)
}

View File

@ -1,6 +1,7 @@
package cmd
import (
"fmt"
"github.com/f1bonacc1/process-compose/src/api"
"github.com/f1bonacc1/process-compose/src/loader"
"github.com/rs/zerolog/log"
@ -79,3 +80,10 @@ func getConfigDefault() []string {
}
return []string{}
}
func logFatal(err error, format string, args ...interface{}) {
fmt.Printf(format, args...)
fmt.Printf(": %v\n", err)
log.Err(err).Msgf(format, args...)
os.Exit(1)
}

View File

@ -122,6 +122,35 @@ const docTemplate = `{
}
}
},
"/process/ports/{name}": {
"get": {
"description": "Retrieves process open ports",
"produces": [
"application/json"
],
"tags": [
"Process"
],
"summary": "Get process ports",
"parameters": [
{
"type": "string",
"description": "Process Name",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Process Ports",
"schema": {
"type": "object"
}
}
}
}
},
"/process/restart/{name}": {
"post": {
"description": "Restarts the process",

View File

@ -110,6 +110,35 @@
}
}
},
"/process/ports/{name}": {
"get": {
"description": "Retrieves process open ports",
"produces": [
"application/json"
],
"tags": [
"Process"
],
"summary": "Get process ports",
"parameters": [
{
"type": "string",
"description": "Process Name",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Process Ports",
"schema": {
"type": "object"
}
}
}
}
},
"/process/restart/{name}": {
"post": {
"description": "Restarts the process",

View File

@ -90,6 +90,25 @@ paths:
summary: Get process logs
tags:
- Process
/process/ports/{name}:
get:
description: Retrieves process open ports
parameters:
- description: Process Name
in: path
name: name
required: true
type: string
produces:
- application/json
responses:
"200":
description: Process Ports
schema:
type: object
summary: Get process ports
tags:
- Process
/process/restart/{name}:
post:
description: Restarts the process

View File

@ -8,7 +8,7 @@ import (
"strings"
)
func (pv *pcView) createProcInfoForm(info *types.ProcessConfig) *tview.Form {
func (pv *pcView) createProcInfoForm(info *types.ProcessConfig, ports *types.ProcessPorts) *tview.Form {
f := tview.NewForm()
f.SetCancelFunc(func() {
pv.pages.RemovePage(PageDialog)
@ -23,8 +23,11 @@ func (pv *pcView) createProcInfoForm(info *types.ProcessConfig) *tview.Form {
addStringIfNotEmpty("Working Directory:", info.WorkingDir, f)
addStringIfNotEmpty("Log Location:", info.LogLocation, f)
f.AddInputField("Replica:", fmt.Sprintf("%d/%d", info.ReplicaNum+1, info.Replicas), 0, nil, nil)
addSliceIfNotEmpty("Environment:", info.Environment, f)
addSliceIfNotEmpty("Depends On:", mapKeysToSlice(info.DependsOn), f)
addDropDownIfNotEmpty("Environment:", info.Environment, f)
addCSVIfNotEmpty("Depends On:", mapKeysToSlice(info.DependsOn), f)
if ports != nil {
addCSVIfNotEmpty("TCP Ports:", ports.TcpPorts, f)
}
f.AddCheckbox("Is Disabled:", info.Disabled, nil)
f.AddCheckbox("Is Daemon:", info.IsDaemon, nil)
f.AddButton("Close", func() {
@ -40,12 +43,19 @@ func addStringIfNotEmpty(label, value string, f *tview.Form) {
}
}
func addSliceIfNotEmpty(label string, value []string, f *tview.Form) {
func addDropDownIfNotEmpty(label string, value []string, f *tview.Form) {
if len(value) > 0 {
f.AddDropDown(label, value, 0, nil)
}
}
func addCSVIfNotEmpty[K comparable](label string, value []K, f *tview.Form) {
if len(value) > 0 {
csvPorts := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(value)), ":"), "[]")
f.AddInputField(label, csvPorts, 0, nil, nil)
}
}
// mapKeysToSlice extract keys of map as slice,
func mapKeysToSlice[K comparable, V any](m map[K]V) []K {
keys := make([]K, len(m))

View File

@ -246,7 +246,8 @@ func (pv *pcView) showInfo() {
pv.showError(err.Error())
return
}
form := pv.createProcInfoForm(info)
ports, _ := pv.project.GetProcessPorts(name)
form := pv.createProcInfoForm(info, ports)
pv.showDialog(form, 0, 0)
}

View File

@ -83,6 +83,12 @@ type ProcessState struct {
IsRunning bool
}
type ProcessPorts struct {
Name string `json:"name"`
TcpPorts []uint16 `json:"tcp_ports"`
UdpPorts []uint16 `json:"udp_ports"`
}
type ProcessesState struct {
States []ProcessState `json:"data"`
}