Golang Build: add the Run variant to the build system

This change alters how the go subprocess is killed to ensure that
the temporary binary executed during "go run" is also killed so
that users can cancel the Run variant like the other variants.

Additionally, there is support for a file path to the run variant
so that users can create a project setting that specifies the file
to always pass to "go run". The file path may be relative to
$GOPATH/src/, for multi-user or cross-platform development.
The relative file path functionality supports multiple entries in
$GOPATH.

Change-Id: I7cf3b3e9ff89665ea0cd776fe6facb164575fecb
Reviewed-on: https://go-review.googlesource.com/16441
Reviewed-by: Glenn Lewis <gmlewis@google.com>
Reviewed-by: Jason Buberel <jbuberel@google.com>
diff --git a/Go.sublime-build b/Go.sublime-build
index 006cbf1..f6d23f6 100644
--- a/Go.sublime-build
+++ b/Go.sublime-build
@@ -4,6 +4,10 @@
 
     "variants": [
         {
+            "name": "Run",
+            "task": "run"
+        },
+        {
             "name": "Test",
             "task": "test"
         },
diff --git a/dev/go_projects/src/runnable/main.go b/dev/go_projects/src/runnable/main.go
new file mode 100644
index 0000000..91cca4f
--- /dev/null
+++ b/dev/go_projects/src/runnable/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello, world.")
+}
diff --git a/dev/go_projects2/bin/.git-keep b/dev/go_projects2/bin/.git-keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dev/go_projects2/bin/.git-keep
diff --git a/dev/go_projects2/pkg/.git-keep b/dev/go_projects2/pkg/.git-keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dev/go_projects2/pkg/.git-keep
diff --git a/dev/go_projects2/src/runnable2/main.go b/dev/go_projects2/src/runnable2/main.go
new file mode 100644
index 0000000..f67c3ea
--- /dev/null
+++ b/dev/go_projects2/src/runnable2/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Goodbye!")
+}
diff --git a/dev/tests.py b/dev/tests.py
index b37e1b0..87e4b94 100644
--- a/dev/tests.py
+++ b/dev/tests.py
@@ -24,6 +24,7 @@
 
 
 TEST_GOPATH = path.join(path.dirname(__file__), 'go_projects')
+TEST_GOPATH2 = path.join(path.dirname(__file__), 'go_projects2')
 VIEW_SETTINGS = {
     'GOPATH': TEST_GOPATH,
     'GOOS': None,
@@ -39,16 +40,21 @@
 class GolangBuildTests(unittest.TestCase):
 
     def setUp(self):
-        for subdir in ('pkg', 'bin', 'src'):
-            full_path = path.join(TEST_GOPATH, subdir)
-            for entry in os.listdir(full_path):
-                if entry in set(['.git-keep', 'good', 'bad']):
-                    continue
-                entry_path = path.join(full_path, entry)
-                if path.isdir(entry_path):
-                    shutil.rmtree(entry_path)
-                else:
-                    os.remove(entry_path)
+        skip_entries = {}
+        skip_entries[TEST_GOPATH] = set(['.git-keep', 'good', 'bad', 'runnable'])
+        skip_entries[TEST_GOPATH2] = set(['.git-keep', 'runnable2'])
+
+        for gopath in (TEST_GOPATH, TEST_GOPATH2):
+            for subdir in ('pkg', 'bin', 'src'):
+                full_path = path.join(gopath, subdir)
+                for entry in os.listdir(full_path):
+                    if entry in skip_entries[gopath]:
+                        continue
+                    entry_path = path.join(full_path, entry)
+                    if path.isdir(entry_path):
+                        shutil.rmtree(entry_path)
+                    else:
+                        os.remove(entry_path)
 
     def test_build(self):
         ensure_not_ui_thread()
@@ -132,6 +138,68 @@
         self.assertEqual('success', result)
         self.assertTrue(confirm_user('Did "go test" succeed?'))
 
+    def test_run(self):
+        ensure_not_ui_thread()
+
+        file_path = path.join(TEST_GOPATH, 'src', 'runnable', 'main.go')
+
+        def _run_build(view, result_queue):
+            view.window().run_command('golang_build', {'task': 'run'})
+
+        result_queue = open_file(file_path, VIEW_SETTINGS, _run_build)
+        result = wait_build(result_queue)
+        self.assertEqual('success', result)
+        self.assertTrue(confirm_user('Did "go run" succeed?'))
+
+    def test_run_with_file_path_flag_absolute(self):
+        ensure_not_ui_thread()
+
+        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+        def _run_build(view, result_queue):
+            view.window().run_command('golang_build', {'task': 'run'})
+
+        custom_view_settings = VIEW_SETTINGS.copy()
+        custom_view_settings['run:flags'] = [os.path.join(TEST_GOPATH, 'src', 'runnable', 'main.go')]
+
+        result_queue = open_file(file_path, custom_view_settings, _run_build)
+        result = wait_build(result_queue)
+        self.assertEqual('success', result)
+        self.assertTrue(confirm_user('Did "go run" succeed for runnable/main.go?'))
+
+    def test_run_with_file_path_flag_relative(self):
+        ensure_not_ui_thread()
+
+        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+        def _run_build(view, result_queue):
+            view.window().run_command('golang_build', {'task': 'run'})
+
+        custom_view_settings = VIEW_SETTINGS.copy()
+        custom_view_settings['run:flags'] = ['runnable/main.go']
+
+        result_queue = open_file(file_path, custom_view_settings, _run_build)
+        result = wait_build(result_queue)
+        self.assertEqual('success', result)
+        self.assertTrue(confirm_user('Did "go run" succeed for runnable/main.go?'))
+
+    def test_run_with_file_path_flag_relative_multiple_gopath(self):
+        ensure_not_ui_thread()
+
+        file_path = path.join(TEST_GOPATH, 'src', 'good', 'rune_len.go')
+
+        def _run_build(view, result_queue):
+            view.window().run_command('golang_build', {'task': 'run'})
+
+        custom_view_settings = VIEW_SETTINGS.copy()
+        custom_view_settings['GOPATH'] = os.pathsep.join([TEST_GOPATH, TEST_GOPATH2])
+        custom_view_settings['run:flags'] = ['runnable2/main.go']
+
+        result_queue = open_file(file_path, custom_view_settings, _run_build)
+        result = wait_build(result_queue)
+        self.assertEqual('success', result)
+        self.assertTrue(confirm_user('Did "go run" succeed for runnable2/main.go?'))
+
     def test_install(self):
         ensure_not_ui_thread()
 
diff --git a/docs/commands.md b/docs/commands.md
index fc35a57..0c4c010 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -19,6 +19,7 @@
 
  - `task`: A string of the build task to perform. Accepts the following values:
    - `"build"`: executes `go build -v`
+   - `"run"`: executes `go run -v {current_filename}`
    - `"test"`: executes `go test -v`
    - `"install"`: executes `go install -v`
    - `"clean"`: executes `go clean -v`
diff --git a/docs/configuration.md b/docs/configuration.md
index ab5ed93..61b614c 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -15,7 +15,7 @@
  - [Command Flags](#command-flags)
    - [Formatting Command Flag Settings](#formatting-command-flag-settings)
    - [Command Flag Setting Locations](#command-flag-setting-locations)
-   - [Using Command Flags to Specify Build and Install Targets](#using-command-flags-to-specify-build-and-install-targets)
+   - [Using Command Flags to Specify Build, Run and Install Targets](#using-command-flags-to-specify-build-run-and-install-targets)
 
 ## Environment Autodetection
 
@@ -55,9 +55,12 @@
 
 Settings are placed in a json structure. Common settings include:
 
- - `PATH` - a list of directories to search for executables within. On Windows
-   these are separated by `;`. OS X and Linux use `:` as a directory separator.
- - `GOPATH` - a string of the path to the root of your Go environment
+ - `PATH` - a string containing a list of directories to search for executables
+   within. On Windows these are separated by `;`. OS X and Linux use `:` as a
+   directory separator.
+ - `GOPATH` - a string containing one or more Go workspace paths. Like `PATH`,
+   on Windows multiple paths are separated by `;`, on OS X and Linux they are
+   separated by `:`.
 
 Other Go environment variables will be used if set. Examples include: `GOOS`,
 `GOARCH`, `GOROOT` and `GORACE`. The
@@ -153,6 +156,7 @@
 have its flags customized. The settings names are:
 
  - `build:flags` for "go build"
+ - `run:flags` for "go run"
  - `test:flags` for "go test"
  - `install:flags` for "go install"
  - `clean:flags` for "go clean"
@@ -257,7 +261,7 @@
 project settings. Instead, any settings in a more specific location will
 override those in a less specific location.
 
-### Using Command Flags to Specify Build and Install Targets
+### Using Command Flags to Specify Build, Run and Install Targets
 
 The default behavior of the build commands is to execute the `go` tool in the
 directory containing the file currently being edited. This behavior is
@@ -295,7 +299,7 @@
     "folders":
     [
         {
-            "path": "/Users/jbuberel/workspace/src"
+            "path": "/Users/jsmith/workspace/src"
         }
     ],
     "settings": {
@@ -311,11 +315,35 @@
 
 ```
 > Environment:
->   GOPATH=/Users/jbuberel/workspace-shared:/Users/jbuberel/workspace
-> Directory: /Users/jbuberel
-> Command: /Users/jbuberel/go15/bin/go install -v github.com/myusername/myprojectname/mycommand
+>   GOPATH=/Users/jsmith/workspace-shared:/Users/jsmith/workspace
+> Directory: /Users/jsmith
+> Command: /Users/jsmith/go15/bin/go install -v github.com/myusername/myprojectname/mycommand
 > Output:
 github.com/myusername/myprojectname/mycommand
 > Elapsed: 0.955s
 > Result: Success
 ```
+
+Another common command to use a custom flag with is `Go: Run`. With the Run
+build variant, the file to execute may be specified as either an absolute file
+path, or relative to the `$GOPATH/src/` directory.
+
+```json
+{
+    "folders":
+    [
+        {
+            "path": "/Users/jsmith/workspace/src"
+        }
+    ],
+    "settings": {
+        "golang": {
+            "run:flags": ["-v", "github.com/myusername/myproject/main.go"]
+        }
+    }
+}
+```
+
+If the file path is relative to `$GOPATH/src/`, it will be automatically
+expanded so the `go` tool will process it properly. In the case that `$GOPATH`
+has multiple entries, the first with a matching filename will be used.
diff --git a/docs/readme.md b/docs/readme.md
index dfb1ffe..ea7beb4 100644
--- a/docs/readme.md
+++ b/docs/readme.md
@@ -7,6 +7,7 @@
 
  - A Sublime Text build system for executing:
    - `go build`
+   - `go run`
    - `go install`
    - `go test`
    - `go clean`
diff --git a/docs/usage.md b/docs/usage.md
index 46db044..829fdde 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -18,6 +18,7 @@
 The variants included with the build system include:
 
  - **Build**, which executes `go build`
+ - **Run**, which executes `go run` with the current filepath
  - **Test**, which executes `go test`
  - **Install**, which executes `go install`
  - **Cross-Compile (Interactive)**, which executes `go build` with `GOOS` and
@@ -30,6 +31,7 @@
 On Sublime Text 3, the command palette entries will be:
 
  - `Build with: Go`
+ - `Build with: Go - Run`
  - `Build with: Go - Test`
  - `Build with: Go - Install`
  - `Build with: Go - Cross-Compile (Interactive)`
@@ -38,6 +40,7 @@
 On Sublime Text 2, the command palette entries will be:
 
  - `Build: Build`
+ - `Build: Run`
  - `Build: Test`
  - `Build: Install`
  - `Build: Cross-Compile (Interactive)`
@@ -70,6 +73,6 @@
 
 ## Commands
 
-For information on the available commands, their arguments and example key
+For information on the available commands, their arguments, example key
 bindings and command palette entries, please read the
 [commands documentation](commands.md).
diff --git a/golang_build.py b/golang_build.py
index 19d27a8..613c918 100644
--- a/golang_build.py
+++ b/golang_build.py
@@ -10,10 +10,14 @@
 import textwrap
 import collections
 
+import signal
+
 if sys.version_info < (3,):
     import Queue as queue
+    str_cls = unicode  # noqa
 else:
     import queue
+    str_cls = str
 
 import sublime
 import sublime_plugin
@@ -100,6 +104,43 @@
         if flags is None:
             flags = ['-v']
 
+        if task == 'run':
+            # Allow the user to set a file path into the flags settings,
+            # thus requiring that the flags be checked to ensure a second
+            # filename is not added
+            found_filename = False
+
+            # Allow users to call "run" with a src-relative file path. Because
+            # of that, flags may be rewritten since the "go run" does not
+            # accept such file paths.
+            use_new_flags = False
+            new_flags = []
+
+            gopaths = env['GOPATH'].split(os.pathsep)
+
+            for flag in flags:
+                if flag.endswith('.go'):
+                    absolute_path = flag
+                    if os.path.isfile(absolute_path):
+                        found_filename = True
+                        break
+
+                    # If the file path is src-relative, rewrite the flag
+                    for gopath in gopaths:
+                        gopath_relative = os.path.join(gopath, 'src', flag)
+                        if os.path.isfile(gopath_relative):
+                            found_filename = True
+                            flag = gopath_relative
+                            use_new_flags = True
+                            break
+                new_flags.append(flag)
+
+            if use_new_flags:
+                flags = new_flags
+
+            if not found_filename:
+                flags.append(self.window.active_view().file_name())
+
         if task == 'cross_compile':
             _task_cross_compile(
                 self,
@@ -568,9 +609,15 @@
         self.env = env
 
         startupinfo = None
+        preexec_fn = None
         if sys.platform == 'win32':
             startupinfo = subprocess.STARTUPINFO()
             startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+        else:
+            # On posix platforms we create a new process group by executing
+            # os.setsid() after the fork before the go binary is executed. This
+            # allows us to use os.killpg() to kill the whole process group.
+            preexec_fn = os.setsid
 
         self._cleanup_lock = threading.Lock()
         self.started = time.time()
@@ -580,7 +627,8 @@
             stderr=subprocess.PIPE,
             cwd=cwd,
             env=env,
-            startupinfo=startupinfo
+            startupinfo=startupinfo,
+            preexec_fn=preexec_fn
         )
         self.finished = False
 
@@ -625,7 +673,27 @@
         try:
             if not self.proc:
                 return
-            self.proc.terminate()
+
+            if sys.platform != 'win32':
+                # On posix platforms we send SIGTERM to the whole process
+                # group to ensure both go and the compiled temporary binary
+                # are killed.
+                os.killpg(os.getpgid(self.proc.pid), signal.SIGTERM)
+            else:
+                # On Windows, there is no API to get the child processes
+                # of a process and send signals to them all. Attempted to use
+                # startupinfo.dwFlags with CREATE_NEW_PROCESS_GROUP and then
+                # calling self.proc.send_signal(signal.CTRL_BREAK_EVENT),
+                # however that did not kill the temporary binary. taskkill is
+                # part of Windows XP and newer, so we use that.
+                startupinfo = subprocess.STARTUPINFO()
+                startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+                kill_proc = subprocess.Popen(
+                    ['taskkill', '/F', '/T', '/PID', str_cls(self.proc.pid)],
+                    startupinfo=startupinfo
+                )
+                kill_proc.wait()
+
             self.result = 'cancelled'
             self.finished = time.time()
             self.proc = None
diff --git a/readme.md b/readme.md
index 5ab1173..3a93eca 100644
--- a/readme.md
+++ b/readme.md
@@ -7,6 +7,7 @@
 
  - A Sublime Text build system for executing:
    - `go build`
+   - `go run`
    - `go install`
    - `go test`
    - `go clean`