mirror of
https://github.com/turtlemonvh/tnnlr.git
synced 2021-09-26 15:22:24 +03:00
A whole lot of changes to make this thing usable. Fixes to http server, starting point for background process, logging.
This commit is contained in:
57
README.md
57
README.md
@@ -1,18 +1,22 @@
|
|||||||
# Tunnlr
|
# Tunnlr
|
||||||
|
|
||||||
Tnnlr is a simple utility to managing ssh tunnels. It is currently a very ugly work in progress.
|
Tnnlr is a simple utility to managing ssh tunnels. It is currently a very ugly work in progress, but it does work
|
||||||
|
|
||||||
|
* keep tunnels open
|
||||||
|
* compose new tunnels using a helpful ui
|
||||||
|
* reload a whole set of tunnels quickly
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Get the tool
|
# Get the tool
|
||||||
go get github.com/turtlemonvh/tnnlr
|
$ go get github.com/turtlemonvh/tnnlr
|
||||||
|
|
||||||
# Install binary
|
# Install binary
|
||||||
go install $GOPATH/src/github.com/turtlemonvh/tnnlr/tnnlr
|
$ go install $GOPATH/src/github.com/turtlemonvh/tnnlr/tnnlr
|
||||||
|
|
||||||
# Create a config file with tunnels for this project
|
# Create a config file with tunnels for this project
|
||||||
cat > $GOPATH/src/github.com/turtlemonvh/tnnlr/.tunnlr << EOF
|
$ cat > $GOPATH/src/github.com/turtlemonvh/tnnlr/.tunnlr << EOF
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"defaultUrl": "/",
|
"defaultUrl": "/",
|
||||||
@@ -32,20 +36,55 @@ cat > $GOPATH/src/github.com/turtlemonvh/tnnlr/.tunnlr << EOF
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start up the web ui on localhost:8080
|
# Start up the web ui on localhost:8080
|
||||||
tnnlr
|
$ tnnlr
|
||||||
|
|
||||||
# Go to localhost:8080 and click the "Reload Tunnels from File" button.
|
# Go to localhost:8080 and click the "Reload Tunnels from File" button.
|
||||||
|
|
||||||
|
# Check help docs for more information
|
||||||
|
$ tnnlr -h
|
||||||
|
APP:
|
||||||
|
cli
|
||||||
|
|
||||||
|
COMMAND:
|
||||||
|
tnnlr
|
||||||
|
|
||||||
|
|
||||||
|
AVAILABLE SUBCOMMANDS:
|
||||||
|
ls : List running tunnels
|
||||||
|
help : Print this help message
|
||||||
|
|
||||||
|
PARSING ORDER: (set values will override in this order)
|
||||||
|
CLI Flag > Environment
|
||||||
|
|
||||||
|
VARIABLES:
|
||||||
|
+-------------+---------+----------+-----------+----------------------------------------+
|
||||||
|
| FLAG | DEFAULT | REQUIRED | ENV NAME | DESCRIPTION |
|
||||||
|
+-------------+---------+----------+-----------+----------------------------------------+
|
||||||
|
| --log-level | info | No | LOG_LEVEL | Logging levels. Options are: |
|
||||||
|
| | | | | [panic,fatal,error,warning,info,debug] |
|
||||||
|
| --tunnels | .tnnlr | No | TUNNELS | Configuration file listing |
|
||||||
|
| | | | | tunnels to load. |
|
||||||
|
| --ssh-exec | ssh | No | SSH_EXEC | The executable process to use |
|
||||||
|
| | | | | for ssh. Can be a full path or |
|
||||||
|
| | | | | just a cmd name. |
|
||||||
|
| --port | 8080 | No | PORT | The port to run the webserver |
|
||||||
|
| | | | | on. |
|
||||||
|
+-------------+---------+----------+-----------+----------------------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
Authors:
|
||||||
|
Timothy Van Heest (timothy@ionic.com)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- Run tunnels in a goroutine
|
- Serve logfiles through UI
|
||||||
|
- Options to let tunnels continue running on shutdown
|
||||||
- Option for https default url
|
- Option for https default url
|
||||||
- Updates and cleanup to configuration
|
|
||||||
- Option to load whole sets of tunnels at a time easily, via file select in browser
|
- Option to load whole sets of tunnels at a time easily, via file select in browser
|
||||||
- Less ugly code
|
- Less ugly code
|
||||||
- Less ugly UI
|
- Less ugly UI
|
||||||
- Command line interface
|
- Command line interface
|
||||||
- Poll in the background and keep tunnels open
|
- Make `ls` command do something useful
|
||||||
- Kill tunnels on shutdown
|
|
||||||
|
|||||||
34
opt.go
Normal file
34
opt.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package tnnlr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Bookkeeping operations for tnnlr.
|
||||||
|
Logging and pid tracking for launched processes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var baseDir = "~/.tnnlr"
|
||||||
|
|
||||||
|
var relProc = "proc"
|
||||||
|
var relLog = "log"
|
||||||
|
|
||||||
|
func getRelativePath(subdir string) (string, error) {
|
||||||
|
basePath, err := homedir.Expand(baseDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(basePath, subdir), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRelDir(subdir string) error {
|
||||||
|
procDir, err := getRelativePath(subdir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.MkdirAll(procDir, os.ModePerm)
|
||||||
|
}
|
||||||
23
templates.go
23
templates.go
@@ -5,6 +5,13 @@ var homePage string = `
|
|||||||
<head>
|
<head>
|
||||||
<title>Tnnlr</title>
|
<title>Tnnlr</title>
|
||||||
<style>
|
<style>
|
||||||
|
#tips {
|
||||||
|
float: left;
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
#tips > h2 {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
table {
|
table {
|
||||||
float: left;
|
float: left;
|
||||||
clear: left;
|
clear: left;
|
||||||
@@ -39,7 +46,7 @@ var homePage string = `
|
|||||||
{{ if $.HasMessages }}
|
{{ if $.HasMessages }}
|
||||||
<h2>Messages</h2>
|
<h2>Messages</h2>
|
||||||
{{range $nmsg, $msg := $.Messages }}
|
{{range $nmsg, $msg := $.Messages }}
|
||||||
<p class="msg">{{ $msg }}</p>
|
<p class="msg">{{ $msg.String }}</p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
@@ -113,5 +120,19 @@ var homePage string = `
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div id="tips">
|
||||||
|
<hr>
|
||||||
|
<h2>Tips</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
The process may be marked "not alive" because of a network timeout. Try reloading this page to check status again.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Running "reload" both re-loads the definition of a process disk and restarts that process. Be sure to save any edited process state to disk before reloading.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
`
|
`
|
||||||
|
|||||||
157
tnnlr.go
157
tnnlr.go
@@ -7,6 +7,8 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ type Tnnlr struct {
|
|||||||
SshExec string // path to ssh executable
|
SshExec string // path to ssh executable
|
||||||
LogLevel string
|
LogLevel string
|
||||||
TunnelReloadFile string
|
TunnelReloadFile string
|
||||||
|
Port int
|
||||||
msgs chan Message
|
msgs chan Message
|
||||||
tunnels map[string]*Tunnel
|
tunnels map[string]*Tunnel
|
||||||
}
|
}
|
||||||
@@ -40,6 +43,16 @@ func (t *Tnnlr) Init() {
|
|||||||
var err error
|
var err error
|
||||||
var level log.Level
|
var level log.Level
|
||||||
|
|
||||||
|
// Create bookkeeping directories
|
||||||
|
for _, dir := range []string{relProc, relLog} {
|
||||||
|
if err = createRelDir(dir); err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
"dir": dir,
|
||||||
|
}).Fatal("Unable to create bookkeeping directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parse template
|
// Parse template
|
||||||
t.Template, err = template.New("Homepage").Parse(homePage)
|
t.Template, err = template.New("Homepage").Parse(homePage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -57,6 +70,7 @@ func (t *Tnnlr) Init() {
|
|||||||
log.SetLevel(level)
|
log.SetLevel(level)
|
||||||
|
|
||||||
// A generously buffered channel
|
// A generously buffered channel
|
||||||
|
// FIXME: Use a list for messages instead so app doesn't lock when queue is full
|
||||||
t.msgs = make(chan Message, 100)
|
t.msgs = make(chan Message, 100)
|
||||||
t.tunnels = make(map[string]*Tunnel)
|
t.tunnels = make(map[string]*Tunnel)
|
||||||
|
|
||||||
@@ -78,6 +92,8 @@ func (t *Tnnlr) AddMessage(msg string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tnnlr) Run() {
|
func (t *Tnnlr) Run() {
|
||||||
|
go t.CleanBookkeepingDirs()
|
||||||
|
|
||||||
if log.GetLevel() != log.DebugLevel {
|
if log.GetLevel() != log.DebugLevel {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
@@ -90,7 +106,7 @@ func (t *Tnnlr) Run() {
|
|||||||
r.GET("/reload/:id", t.ReloadOne)
|
r.GET("/reload/:id", t.ReloadOne)
|
||||||
r.GET("/bash_command/:id", t.ShowCommand)
|
r.GET("/bash_command/:id", t.ShowCommand)
|
||||||
r.GET("/status/:id", t.ReloadOne)
|
r.GET("/status/:id", t.ReloadOne)
|
||||||
r.Run()
|
r.Run(fmt.Sprintf(":%d", t.Port))
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP views
|
// HTTP views
|
||||||
@@ -241,6 +257,7 @@ func (t *Tnnlr) ReloadOne(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Errors are handled in the functions themselves
|
// Errors are handled in the functions themselves
|
||||||
|
// Stop process if it is running now
|
||||||
t.RemoveTunnel(rTnnlId)
|
t.RemoveTunnel(rTnnlId)
|
||||||
t.AddTunnel(foundTnnl)
|
t.AddTunnel(foundTnnl)
|
||||||
|
|
||||||
@@ -283,6 +300,11 @@ func (t *Tnnlr) Add(c *gin.Context) {
|
|||||||
func (t *Tnnlr) ShowCommand(c *gin.Context) {
|
func (t *Tnnlr) ShowCommand(c *gin.Context) {
|
||||||
rTnnlId := c.Param("id")
|
rTnnlId := c.Param("id")
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"id": rTnnlId,
|
||||||
|
"tunnels": t.tunnels,
|
||||||
|
}).Info("Showing command for tunnel")
|
||||||
|
|
||||||
tnnl, ok := t.tunnels[rTnnlId]
|
tnnl, ok := t.tunnels[rTnnlId]
|
||||||
if !ok {
|
if !ok {
|
||||||
message := "Failed to find tunnel with the requested id"
|
message := "Failed to find tunnel with the requested id"
|
||||||
@@ -294,9 +316,7 @@ func (t *Tnnlr) ShowCommand(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.String(200, tnnl.getCommand())
|
||||||
"cmd": tnnl.getCommand(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load from disk
|
// Load from disk
|
||||||
@@ -370,3 +390,132 @@ func (t *Tnnlr) KillAllTunnels() {
|
|||||||
}
|
}
|
||||||
t.Unlock()
|
t.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tnnlr) ManagedTunnels() map[string]bool {
|
||||||
|
t.Lock()
|
||||||
|
var tunnelIds = make(map[string]bool)
|
||||||
|
for tnnlId, _ := range t.tunnels {
|
||||||
|
tunnelIds[tnnlId] = true
|
||||||
|
}
|
||||||
|
t.Unlock()
|
||||||
|
return tunnelIds
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Background cleanup and management of jobs
|
||||||
|
|
||||||
|
TODO
|
||||||
|
- check cmd and pid information directly to see if process is running instead of checking port
|
||||||
|
- option to leave process running when tnnlr shuts down
|
||||||
|
|
||||||
|
*/
|
||||||
|
func (t *Tnnlr) CleanBookkeepingDirs() {
|
||||||
|
|
||||||
|
procDir, err := getRelativePath(relProc)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("Unable to check proc dir to reap processes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logDir, err := getRelativePath(relLog)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("Unable to check log dir to clean up logs")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Loop every 10 seconds
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
// Based on scanning pid dir
|
||||||
|
runningProcesses := make(map[string]bool)
|
||||||
|
// From the state of the program
|
||||||
|
managedProcesses := t.ManagedTunnels()
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"dir": procDir,
|
||||||
|
}).Debug("Scanning pid dir for changed files")
|
||||||
|
|
||||||
|
// Load all tunnels
|
||||||
|
// Clean up any pid files associated with tunnels that are not currently running and are not known to the current live process
|
||||||
|
pidFiles, err := filepath.Glob(fmt.Sprintf("%s/*.pid", procDir))
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("Error listing files in proc dir")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pf := range pidFiles {
|
||||||
|
c, e := ioutil.ReadFile(pf)
|
||||||
|
if e != nil {
|
||||||
|
os.Remove(pf)
|
||||||
|
}
|
||||||
|
var tnnl Tunnel
|
||||||
|
err = json.Unmarshal(c, &tnnl)
|
||||||
|
if e != nil {
|
||||||
|
os.Remove(pf)
|
||||||
|
}
|
||||||
|
// Check if it this process is running
|
||||||
|
if !tnnl.PortInUse() {
|
||||||
|
// The process is dead.
|
||||||
|
// Check if we should be running this and restart.
|
||||||
|
if managedProcesses[tnnl.Id] {
|
||||||
|
// Should restart
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"id": tnnl.Id,
|
||||||
|
"name": tnnl.Name,
|
||||||
|
"cmd": tnnl.getCommand(),
|
||||||
|
}).Info("Found dead process, restarting")
|
||||||
|
tnnl.Run(t.SshExec)
|
||||||
|
runningProcesses[tnnl.Id] = true
|
||||||
|
} else {
|
||||||
|
// Cleanup
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"id": tnnl.Id,
|
||||||
|
"name": tnnl.Name,
|
||||||
|
"cmd": tnnl.getCommand(),
|
||||||
|
}).Info("Found dead process, cleaning up")
|
||||||
|
tnnl.Stop()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"id": tnnl.Id,
|
||||||
|
"name": tnnl.Name,
|
||||||
|
"cmd": tnnl.getCommand(),
|
||||||
|
}).Debug("Found running process in pid dir")
|
||||||
|
runningProcesses[tnnl.Id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all logfiles
|
||||||
|
// Clean up any not associated with a process that is live or being restarted
|
||||||
|
logFiles, err := filepath.Glob(fmt.Sprintf("%s/*.log", logDir))
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("Error listing files in log dir")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, lf := range logFiles {
|
||||||
|
// Get the id from the filename
|
||||||
|
pts := strings.Split(lf, "/")
|
||||||
|
lpt := pts[len(pts)-1]
|
||||||
|
id := strings.Split(lpt, ".log")[0]
|
||||||
|
|
||||||
|
// If this isn't running, delete it
|
||||||
|
if !runningProcesses[id] {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"logFile": lf,
|
||||||
|
}).Info("Removing logfile for dead process")
|
||||||
|
os.Remove(lf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/timjchin/unpuzzled"
|
"github.com/timjchin/unpuzzled"
|
||||||
"github.com/turtlemonvh/tnnlr"
|
"github.com/turtlemonvh/tnnlr"
|
||||||
)
|
)
|
||||||
@@ -10,6 +13,11 @@ import (
|
|||||||
// Config
|
// Config
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
logLevels := make([]string, len(logrus.AllLevels))
|
||||||
|
for il, l := range logrus.AllLevels {
|
||||||
|
logLevels[il] = l.String()
|
||||||
|
}
|
||||||
|
|
||||||
myTnnlr := &tnnlr.Tnnlr{}
|
myTnnlr := &tnnlr.Tnnlr{}
|
||||||
app := unpuzzled.NewApp()
|
app := unpuzzled.NewApp()
|
||||||
app.Command = &unpuzzled.Command{
|
app.Command = &unpuzzled.Command{
|
||||||
@@ -18,8 +26,27 @@ func main() {
|
|||||||
&unpuzzled.StringVariable{
|
&unpuzzled.StringVariable{
|
||||||
Name: "log-level",
|
Name: "log-level",
|
||||||
Destination: &(myTnnlr.LogLevel),
|
Destination: &(myTnnlr.LogLevel),
|
||||||
|
Description: fmt.Sprintf("Logging levels. Options are: [%s]", strings.Join(logLevels, ",")),
|
||||||
Default: "info",
|
Default: "info",
|
||||||
},
|
},
|
||||||
|
&unpuzzled.StringVariable{
|
||||||
|
Name: "tunnels",
|
||||||
|
Destination: &(myTnnlr.TunnelReloadFile),
|
||||||
|
Description: "Configuration file listing tunnels to load.",
|
||||||
|
Default: ".tnnlr",
|
||||||
|
},
|
||||||
|
&unpuzzled.StringVariable{
|
||||||
|
Name: "ssh-exec",
|
||||||
|
Destination: &(myTnnlr.SshExec),
|
||||||
|
Description: "The executable process to use for ssh. Can be a full path or just a cmd name.",
|
||||||
|
Default: "ssh",
|
||||||
|
},
|
||||||
|
&unpuzzled.IntVariable{
|
||||||
|
Name: "port",
|
||||||
|
Destination: &(myTnnlr.Port),
|
||||||
|
Description: "The port to run the webserver on.",
|
||||||
|
Default: 8080,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: func() {
|
Action: func() {
|
||||||
// Run website
|
// Run website
|
||||||
@@ -38,5 +65,15 @@ func main() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
app.Authors = []unpuzzled.Author{
|
||||||
|
{
|
||||||
|
Name: "Timothy Van Heest",
|
||||||
|
Email: "timothy@ionic.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.ParsingOrder = []unpuzzled.ParsingType{
|
||||||
|
unpuzzled.EnvironmentVariables,
|
||||||
|
unpuzzled.CliFlags,
|
||||||
|
}
|
||||||
app.Run(os.Args)
|
app.Run(os.Args)
|
||||||
}
|
}
|
||||||
|
|||||||
73
tunnel.go
73
tunnel.go
@@ -1,10 +1,16 @@
|
|||||||
package tnnlr
|
package tnnlr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tunnels
|
// Tunnels
|
||||||
@@ -16,6 +22,7 @@ type Tunnel struct {
|
|||||||
Username string `form:"username" json:"userName"` // can be ""
|
Username string `form:"username" json:"userName"` // can be ""
|
||||||
LocalPort int32 `form:"localPort" json:"localPort" binding:"required"`
|
LocalPort int32 `form:"localPort" json:"localPort" binding:"required"`
|
||||||
RemotePort int32 `form:"remotePort" json:"remotePort" binding:"required"`
|
RemotePort int32 `form:"remotePort" json:"remotePort" binding:"required"`
|
||||||
|
Pid int `json:"pid"` // not set until after process starts
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +31,7 @@ func (t *Tunnel) getCommand() string {
|
|||||||
if t.Username != "" {
|
if t.Username != "" {
|
||||||
remote = fmt.Sprintf("%s@%s", t.Username, remote)
|
remote = fmt.Sprintf("%s@%s", t.Username, remote)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(`ssh -L %d:localhost:%d %s -N`,
|
return fmt.Sprintf(`ssh -v -L %d:localhost:%d %s -N`,
|
||||||
t.LocalPort,
|
t.LocalPort,
|
||||||
t.RemotePort,
|
t.RemotePort,
|
||||||
remote,
|
remote,
|
||||||
@@ -34,6 +41,10 @@ func (t *Tunnel) getCommand() string {
|
|||||||
// Check if process running the tunnel is alive
|
// Check if process running the tunnel is alive
|
||||||
// FIXME: Seems to return true even if process has exited
|
// FIXME: Seems to return true even if process has exited
|
||||||
func (t *Tunnel) IsAlive() bool {
|
func (t *Tunnel) IsAlive() bool {
|
||||||
|
if !t.PortInUse() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if t.cmd == nil {
|
if t.cmd == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -59,7 +70,30 @@ func (t *Tunnel) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tunnel) LogPath() (string, error) {
|
||||||
|
return getRelativePath(filepath.Join(relLog, fmt.Sprintf("%s.log", t.Id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tunnel) PidPath() (string, error) {
|
||||||
|
return getRelativePath(filepath.Join(relProc, fmt.Sprintf("%s.pid", t.Id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tunnel) PortInUse() bool {
|
||||||
|
// Check if this port is already in use
|
||||||
|
// https://stackoverflow.com/questions/40296483/continuously-check-if-tcp-port-is-in-use
|
||||||
|
conn, _ := net.DialTimeout("tcp", net.JoinHostPort("", fmt.Sprintf("%d", t.LocalPort)), time.Duration(1*time.Millisecond))
|
||||||
|
if conn != nil {
|
||||||
|
conn.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Run the cmd and set the active process
|
// Run the cmd and set the active process
|
||||||
|
/*
|
||||||
|
Writes process information into ~/.tnnl/proc/XXX.pid
|
||||||
|
Writes log information into ~/.tnnl/log/XXX.log
|
||||||
|
*/
|
||||||
func (t *Tunnel) Run(sshExec string) error {
|
func (t *Tunnel) Run(sshExec string) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -68,13 +102,45 @@ func (t *Tunnel) Run(sshExec string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.PortInUse() {
|
||||||
|
return fmt.Errorf("Port %d is already in use", t.LocalPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up logging and launch task
|
||||||
|
// FIXME: Allow this to live beyond the life of the process
|
||||||
|
logPath, err := t.LogPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logOut, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
cmdParts := strings.Split(t.getCommand(), " ")
|
cmdParts := strings.Split(t.getCommand(), " ")
|
||||||
cmd := exec.Command(sshExec, cmdParts[1:]...)
|
cmd := exec.Command(sshExec, cmdParts[1:]...)
|
||||||
|
cmd.Stdout = logOut
|
||||||
|
cmd.Stderr = logOut
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.cmd = cmd
|
t.cmd = cmd
|
||||||
|
t.Pid = cmd.Process.Pid
|
||||||
|
|
||||||
|
// Write JSON representation of task to pid file
|
||||||
|
pidPath, err := t.PidPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tJSON, err := json.Marshal(t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(pidPath, tJSON, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,5 +151,10 @@ func (t *Tunnel) Stop() error {
|
|||||||
return t.cmd.Process.Kill()
|
return t.cmd.Process.Kill()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Clear pid file path
|
||||||
|
p, err := t.PidPath()
|
||||||
|
if err == nil {
|
||||||
|
os.Remove(p)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user