Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
lepsch committed Sep 30, 2021
0 parents commit 4d08e7b
Show file tree
Hide file tree
Showing 16 changed files with 497 additions and 0 deletions.
63 changes: 63 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Set the workflow name.
name: CI

# Execute the workflow on pushes and pull requests.
on: [push, pull_request]

jobs:
linux:
name: Linux
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '^1.17'
- run: docker version
- run: go version
- run: ./build.sh
- uses: actions/upload-artifact@v2
with:
name: binaries
path: mutagen-ssh-wrapper.tar.gz
versioning:
name: Versioning
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '^1.17'
- run: go version
- name: "Analyze version information and release status"
id: analyze
run: |
# Determine whether or not this is a release build.
RELEASE="${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}"
# Determine version target information for Go. If this is a release,
# then we'll use the tag, otherwise we'll use the raw commit identifier.
if [ "${RELEASE}" = "true" ]; then
TARGET="${GITHUB_REF#refs/tags/}"
else
TARGET="${GITHUB_SHA}"
fi
# Set outputs.
echo ::set-output name=release::${RELEASE}
echo ::set-output name=target::${TARGET}
outputs:
release: ${{ steps.analyze.outputs.release }}
target: ${{ steps.analyze.outputs.target }}
release:
name: Release
runs-on: ubuntu-latest
needs: [linux, versioning]
if: ${{ needs.versioning.outputs.release == 'true' }}
steps:
- uses: actions/download-artifact@v2
with:
name: binaries
path: .
- uses: softprops/action-gh-release@v1
with:
files: |
mutagen-ssh-wrapper.tar.gz
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# IDE specific
/.vscode

# Output dir
/out

# Bundle
*.tar.gz
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Duckly

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# mutagen-ssh-wrapper
Mutagen SSH wrapper for Windows
7 changes: 7 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh
set -e

GOOS=windows GOARCH=386 go build -o ./out/ ./cmd/ssh
GOOS=windows GOARCH=386 go build -o ./out/ ./cmd/scp
GOOS=windows GOARCH=386 go build -o ./out/ ./cmd/grun
tar czf mutagen-ssh-wrapper.tar.gz -C ./out scp.exe ssh.exe grun.exe
60 changes: 60 additions & 0 deletions cmd/grun/grun.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"context"
"fmt"
"os"
"os/exec"
"strconv"

"golang.org/x/sync/errgroup"
"ssh-wrapper/cmd"
)

func main() {
if len(os.Args) < 2 {
if len(os.Args) == 1 {
fmt.Fprintln(os.Stderr, "Error: missing arguments: parent PID and command")
} else {
fmt.Fprintln(os.Stderr, "Error: missing command argument")
}
os.Exit(1)
}

commandName, args := os.Args[2], os.Args[3:]

parentPid, err := strconv.Atoi(os.Args[1])
if err != nil {
fmt.Fprintln(os.Stderr, "Error: parent PID is not a number:", parentPid)
os.Exit(1)
}

parentProcess, err := os.FindProcess(parentPid)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: cannot find process PID %d: %v", parentPid, err)
os.Exit(1)
}

g, ctx := errgroup.WithContext(context.Background())

command := exec.CommandContext(ctx, commandName, args...)
command.Stdin = os.Stdin
command.Stdout = os.Stdout
command.Stderr = os.Stderr

// Wait for the child process to finish
g.Go(func() error {
cmd.WrapRunAndExit(command)
return nil
})

// Wait for the parent process to finish. If so, kill the child process
g.Go(func() error {
_, err := parentProcess.Wait()
command.Process.Kill()
return err
})

g.Wait()
os.Exit(1)
}
70 changes: 70 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cmd

import (
"context"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"

"ssh-wrapper/pkg/process"
)

const (
MUTAGEN_SSH_CONFIG = "MUTAGEN_SSH_CONFIG"
MUTAGEN_CONNECT_TIMEOUT_IN_SECONDS = 20
)

func ProcessArgs() []string {
sshConfig := os.Getenv(MUTAGEN_SSH_CONFIG)
if sshConfig == "" {
fmt.Fprintln(os.Stderr, "Error: MUTAGEN_SSH_CONFIG environment variable is not set")
os.Exit(1)
}

// Increase the default timeout
args := os.Args[1:]
for i, arg := range args {
if strings.HasPrefix(arg, "-oConnectTimeout=") {
args[i] = fmt.Sprintf("-oConnectTimeout=%d", MUTAGEN_CONNECT_TIMEOUT_IN_SECONDS)
break
}
}

return append([]string{fmt.Sprintf("-F%s", sshConfig)}, args...)
}

func WrapRunAndExit(command *exec.Cmd) {
command.Stdin = os.Stdin
command.Stdout = os.Stdout
command.Stderr = os.Stderr

if err := command.Run(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
os.Exit(status.ExitStatus())
}
}
os.Exit(1)
}
os.Exit(0)
}

func Grun(commandName string, args ...string) {
args = append([]string{strconv.Itoa(os.Getpid()), commandName}, args...)

self, err := os.Executable()
if err != nil {
fmt.Fprintln(os.Stderr, "Error: cannot get current executable path")
os.Exit(1)
}

grun := path.Join(filepath.Dir(self), process.ExecutableName("grun", runtime.GOOS))
command := exec.CommandContext(context.Background(), grun, args...)
WrapRunAndExit(command)
}
21 changes: 21 additions & 0 deletions cmd/scp/scp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"fmt"
"os"

"ssh-wrapper/pkg/ssh"
"ssh-wrapper/cmd"
)

func main() {
args := cmd.ProcessArgs()

commandName, err := ssh.ScpCommandPath()
if err != nil {
fmt.Fprintln(os.Stderr, "Error: unable to set up SCP invocation:", err)
os.Exit(1)
}

cmd.Grun(commandName, args...)
}
21 changes: 21 additions & 0 deletions cmd/ssh/ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"fmt"
"os"

"ssh-wrapper/cmd"
"ssh-wrapper/pkg/ssh"
)

func main() {
args := cmd.ProcessArgs()

commandName, err := ssh.SshCommandPath()
if err != nil {
fmt.Fprintln(os.Stderr, "Error: unable to set up SSH invocation:", err)
os.Exit(1)
}

cmd.Grun(commandName, args...)
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module ssh-wrapper

go 1.17

require golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
39 changes: 39 additions & 0 deletions pkg/process/find.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package process

import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
)

// FindCommand searches for a command with the specified name within the
// specified list of directories. It's similar to os/exec.LookPath, except that
// it allows one to manually specify paths, and it uses a slightly simpler
// lookup mechanism.
func FindCommand(name string, paths []string) (string, error) {
// Iterate through the directories.
for _, path := range paths {
// Compute the target name.
target := filepath.Join(path, ExecutableName(name, runtime.GOOS))

// Check if the target exists and has the correct type.
// TODO: Should we do more extensive (and platform-specific) testing on
// the resulting metadata? See, e.g., the implementation of
// os/exec.LookPath.
if metadata, err := os.Stat(target); err != nil {
if os.IsNotExist(err) {
continue
}
return "", fmt.Errorf("unable to query file metadata: %w", err)
} else if metadata.Mode()&os.ModeType != 0 {
continue
} else {
return target, nil
}
}

// Failure.
return "", errors.New("unable to locate command")
}
13 changes: 13 additions & 0 deletions pkg/process/name.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package process

// ExecutableName computes the name for an executable for a given base name on a
// specified operating system.
func ExecutableName(base, goos string) string {
// If we're on Windows, append ".exe".
if goos == "windows" {
return base + ".exe"
}

// Otherwise return the base name unmodified.
return base
}
Loading

0 comments on commit 4d08e7b

Please sign in to comment.