You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@openwhisk.apache.org by GitBox <gi...@apache.org> on 2018/03/25 03:57:24 UTC

[GitHub] sciabarracom closed pull request #4: Submissions

sciabarracom closed pull request #4: Submissions
URL: https://github.com/apache/incubator-openwhisk-runtime-go/pull/4
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/README.md b/README.md
index 70d2067..d0681d2 100644
--- a/README.md
+++ b/README.md
@@ -7,4 +7,3 @@ Local development
 `./gradle :docker:distDocker`
 
 This will produce the image `whisk/golang`
-
diff --git a/main/proxy.go b/main/proxy.go
index 8def703..fe8c0bc 100644
--- a/main/proxy.go
+++ b/main/proxy.go
@@ -17,9 +17,20 @@
 package main
 
 import (
-	"fmt"
+	"flag"
+	"io/ioutil"
+	"log"
+
+	"github.com/sciabarracom/incubator-openwhisk-runtime-go/openwhisk"
 )
 
+// disable stderr except when debugging
+var debug = flag.Bool("debug", false, "enable debug output")
+
 func main() {
-	fmt.Println("Just a placeholder")
+	flag.Parse()
+	if !*debug {
+		log.SetOutput(ioutil.Discard)
+	}
+	openwhisk.Start()
 }
diff --git a/openwhisk/_test/bc.sh b/openwhisk/_test/bc.sh
new file mode 100755
index 0000000..ff78878
--- /dev/null
+++ b/openwhisk/_test/bc.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+bc -q >&3
+
diff --git a/openwhisk/_test/exec b/openwhisk/_test/exec
new file mode 100755
index 0000000..67aee49
Binary files /dev/null and b/openwhisk/_test/exec differ
diff --git a/openwhisk/_test/exec.zip b/openwhisk/_test/exec.zip
new file mode 100644
index 0000000..bb7a22a
Binary files /dev/null and b/openwhisk/_test/exec.zip differ
diff --git a/openwhisk/_test/hello.sh b/openwhisk/_test/hello.sh
new file mode 100755
index 0000000..cc38507
--- /dev/null
+++ b/openwhisk/_test/hello.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+while read line
+do
+   name="$(echo $line | jq -r .name)" 
+   if [ "$name" == "*" ]
+   then echo "Goodbye!" >&2 
+        exit 0
+   fi
+   echo msg="hello $name"
+   echo '{"hello": "'$name'"}' >&3
+done
+
diff --git a/openwhisk/actionProxy.go b/openwhisk/actionProxy.go
new file mode 100644
index 0000000..5889b01
--- /dev/null
+++ b/openwhisk/actionProxy.go
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package openwhisk
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+)
+
+// theServer is the current server
+var theServer http.Server
+
+// theChannel is the channel communicating with the action
+var theExecutor *Executor
+
+// StartLatestAction tries to start
+// the more recently uplodaded
+// action if valid, otherwise remove it
+// and fallback to the previous, if any
+func StartLatestAction() error {
+
+	// find the action if any
+	highestDir := highestDir("./action")
+	if highestDir == 0 {
+		log.Println("no action found")
+		theExecutor = nil
+		return fmt.Errorf("no valid actions available")
+	}
+
+	// save the current executor
+	curExecutor := theExecutor
+
+	// try to launch the action
+	executable := fmt.Sprintf("./action/%d/exec", highestDir)
+	newExecutor := NewExecutor(executable)
+	log.Printf("starting %s", executable)
+	err := newExecutor.Start()
+	if err == nil {
+		theExecutor = newExecutor
+		if curExecutor != nil {
+			log.Println("stopping old executor")
+			curExecutor.Stop()
+		}
+		return nil
+	}
+
+	// cannot start, removing the action
+	// and leaving the current executor running
+
+	exeDir := fmt.Sprintf("./action/%d/", highestDir)
+	log.Printf("removing the failed action in %s", exeDir)
+	os.RemoveAll(exeDir)
+	return err
+}
+
+// Start creates a proxy to execute actions
+func Start() {
+	// handle initialization
+	http.HandleFunc("/init", initHandler)
+	// handle execution
+	http.HandleFunc("/run", runHandler)
+
+	// start
+	log.Println("Started!")
+	theServer.Addr = ":8080"
+	log.Fatal(theServer.ListenAndServe())
+}
diff --git a/openwhisk/actionProxy_test.go b/openwhisk/actionProxy_test.go
new file mode 100644
index 0000000..b46ca3d
--- /dev/null
+++ b/openwhisk/actionProxy_test.go
@@ -0,0 +1,44 @@
+package openwhisk
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestStartLatestAction(t *testing.T) {
+
+	// cleanup
+	os.RemoveAll("./action")
+	theExecutor = nil
+
+	// start an action that terminate immediately
+	buf := []byte("#!/bin/sh\ntrue\n")
+	extractAction(&buf, true)
+	StartLatestAction()
+	assert.Nil(t, theExecutor)
+
+	// start the action that emits 1
+	buf = []byte("#!/bin/sh\nwhile read a; do echo 1 >&3 ; done\n")
+	extractAction(&buf, true)
+	StartLatestAction()
+	theExecutor.io <- "x"
+	assert.Equal(t, <-theExecutor.io, "1")
+
+	// now start an action that terminate immediately
+	buf = []byte("#!/bin/sh\ntrue\n")
+	extractAction(&buf, true)
+	StartLatestAction()
+	theExecutor.io <- "y"
+	assert.Equal(t, <-theExecutor.io, "1")
+
+	// start the action that emits 2
+	buf = []byte("#!/bin/sh\nwhile read a; do echo 2 >&3 ; done\n")
+	extractAction(&buf, true)
+	StartLatestAction()
+	theExecutor.io <- "z"
+	assert.Equal(t, <-theExecutor.io, "2")
+	/**/
+	theExecutor.Stop()
+}
diff --git a/openwhisk/executor.go b/openwhisk/executor.go
new file mode 100644
index 0000000..6a48f4b
--- /dev/null
+++ b/openwhisk/executor.go
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package openwhisk
+
+import (
+	"bufio"
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"runtime"
+	"time"
+)
+
+// TIMEOUT to wait for process to start
+// and log to be produced
+const TIMEOUT = 5 * time.Millisecond
+
+// Executor is the container and the guardian  of a child process
+// It starts a command, feeds input and output, read logs and control its termination
+type Executor struct {
+	io      chan string
+	log     chan bool
+	exit    chan error
+	_cmd    *exec.Cmd
+	_input  *bufio.Writer
+	_output *bufio.Scanner
+	_logout *bufio.Scanner
+	_logerr *bufio.Scanner
+}
+
+// NewExecutor creates a child subprocess using the provided command line.
+// You can then start it getting a communication channel
+func NewExecutor(command string, args ...string) (proc *Executor) {
+	cmd := exec.Command(command, args...)
+
+	stdin, err := cmd.StdinPipe()
+	if err != nil {
+		return nil
+	}
+
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil
+	}
+
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		return nil
+	}
+
+	pipeOut, pipeIn, err := os.Pipe()
+	if err != nil {
+		return nil
+	}
+	cmd.ExtraFiles = []*os.File{pipeIn}
+
+	return &Executor{
+		make(chan string),
+		make(chan bool),
+		make(chan error),
+		cmd,
+		bufio.NewWriter(stdin),
+		bufio.NewScanner(pipeOut),
+		bufio.NewScanner(stdout),
+		bufio.NewScanner(stderr),
+	}
+}
+
+// collect log from a stream
+func _collect(ch chan string, scan *bufio.Scanner) {
+	for scan.Scan() {
+		ch <- scan.Text()
+	}
+}
+
+// loop over the command executing
+// returning when the command exits
+func (proc *Executor) run() {
+	log.Println("run: start")
+	err := proc._cmd.Start()
+	if err != nil {
+		proc.exit <- err
+		log.Println("run: early exit")
+		proc._cmd = nil // do not kill
+		return
+	}
+	// wait for the exit
+	proc.exit <- proc._cmd.Wait()
+	proc._cmd = nil // do not kill
+	log.Println("run: end")
+}
+
+// manage copying stdout and stder in output
+// with log guards
+func (proc *Executor) logger() {
+	log.Println("logger: start")
+	// poll stdout and stderr
+	chOut := make(chan string)
+	go _collect(chOut, proc._logout)
+	chErr := make(chan string)
+	go _collect(chErr, proc._logerr)
+
+	// wait for the signal
+	for <-proc.log {
+		// flush stdout
+		runtime.Gosched()
+		for loop := true; loop; {
+			select {
+			case buf := <-chOut:
+				fmt.Println(buf)
+			case <-time.After(TIMEOUT):
+				loop = false
+			}
+		}
+		fmt.Println("XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX")
+
+		// flush stderr
+		runtime.Gosched()
+		for loop := true; loop; {
+			select {
+			case buf := <-chErr:
+				fmt.Println(buf)
+			case <-time.After(TIMEOUT):
+				loop = false
+			}
+		}
+		fmt.Println("XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX")
+	}
+	log.Printf("logger: end")
+}
+
+// main service function
+// writing in input
+// and reading in output
+// using the provide channels
+func (proc *Executor) service() {
+	log.Println("service: start")
+	for {
+		in := <-proc.io
+		if in == "" {
+			log.Println("terminated upon request")
+			break
+		}
+		// input/output with the process
+		log.Printf(">>>%s\n", in)
+		proc._input.WriteString(in + "\n")
+		proc._input.Flush()
+		if proc._output.Scan() {
+			out := proc._output.Text()
+			log.Printf("<<<%s\n", out)
+			proc.io <- out
+			if out == "" {
+				break
+			}
+		}
+	}
+	log.Printf("service: end")
+}
+
+// Start execution of the command
+// returns an error if the program fails
+func (proc *Executor) Start() error {
+	// start the underlying executable
+	// check if died
+	go proc.run()
+	select {
+	case <-proc.exit:
+		// oops, it died
+		return fmt.Errorf("command exited")
+	case <-time.After(TIMEOUT):
+		// ok let's process it
+		go proc.service()
+		go proc.logger()
+	}
+	return nil
+}
+
+// Stop will kill the process
+// and close the channels
+func (proc *Executor) Stop() {
+	log.Println("stopping")
+	if proc._cmd != nil {
+		proc.log <- false
+		proc.io <- ""
+		proc._cmd.Process.Kill()
+		<-proc.exit
+		proc._cmd = nil
+	}
+	close(proc.io)
+	close(proc.exit)
+	close(proc.log)
+}
diff --git a/openwhisk/executor_test.go b/openwhisk/executor_test.go
new file mode 100644
index 0000000..b904cb3
--- /dev/null
+++ b/openwhisk/executor_test.go
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package openwhisk
+
+import (
+	"fmt"
+	"time"
+)
+
+func ExampleNewExecutor_failed() {
+	proc := NewExecutor("true")
+	err := proc.Start()
+	fmt.Println(err)
+	proc.Stop()
+	proc = NewExecutor("/bin/pwd")
+	err = proc.Start()
+	fmt.Println(err)
+	proc.Stop()
+	proc = NewExecutor("donotexist")
+	err = proc.Start()
+	fmt.Println(err)
+	proc.Stop()
+	proc = NewExecutor("/etc/passwd")
+	err = proc.Start()
+	fmt.Println(err)
+	proc.Stop()
+	// Output:
+	// command exited
+	// command exited
+	// command exited
+	// command exited
+}
+
+func ExampleNewExecutor_bc() {
+	proc := NewExecutor("_test/bc.sh")
+	err := proc.Start()
+	fmt.Println(err)
+	//proc.log <- true
+	proc.io <- "2+2"
+	fmt.Println(<-proc.io)
+	// and now, exit detection
+	proc.io <- "quit"
+	proc.log <- true
+	select {
+	case in := <-proc.io:
+		fmt.Println(in)
+	case <-proc.exit:
+		fmt.Println("exit")
+	}
+	time.Sleep(100 * time.Millisecond)
+	proc.Stop()
+	// Output:
+	// <nil>
+	// 4
+	// exit
+	// XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX
+	// XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX
+}
+
+func ExampleNewExecutor_hello() {
+	proc := NewExecutor("_test/hello.sh")
+	err := proc.Start()
+	fmt.Println(err)
+	proc.io <- `{"name":"Mike"}`
+	fmt.Println(<-proc.io)
+	proc.log <- true
+	time.Sleep(100 * time.Millisecond)
+	proc.Stop()
+	time.Sleep(100 * time.Millisecond)
+	_, ok := <-proc.io
+	fmt.Printf("io %v\n", ok)
+	// Unordered output:
+	// <nil>
+	// {"hello": "Mike"}
+	// msg=hello Mike
+	// XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX
+	// XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX
+	// io false
+}
+
+func ExampleNewExecutor_term() {
+	proc := NewExecutor("_test/hello.sh")
+	err := proc.Start()
+	fmt.Println(err)
+	proc.io <- `{"name":"*"}`
+	var exited bool
+	select {
+	case <-proc.io:
+		exited = false
+	case <-proc.exit:
+		exited = true
+	}
+	proc.log <- true
+	fmt.Printf("exit %v\n", exited)
+	time.Sleep(100 * time.Millisecond)
+	proc.Stop()
+	time.Sleep(100 * time.Millisecond)
+	_, ok := <-proc.io
+	fmt.Printf("io %v\n", ok)
+	// Unordered output:
+	// <nil>
+	// exit true
+	// XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX
+	// Goodbye!
+	// XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX
+	// io false
+}
diff --git a/openwhisk/extractor.go b/openwhisk/extractor.go
new file mode 100644
index 0000000..17965bb
--- /dev/null
+++ b/openwhisk/extractor.go
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package openwhisk
+
+import (
+	"archive/zip"
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"strconv"
+
+	"github.com/h2non/filetype"
+)
+
+func unzip(src []byte, dest string) error {
+	reader := bytes.NewReader(src)
+	r, err := zip.NewReader(reader, int64(len(src)))
+	if err != nil {
+		return err
+	}
+
+	os.MkdirAll(dest, 0755)
+
+	// Closure to address file descriptors issue with all the deferred .Close() methods
+	extractAndWriteFile := func(f *zip.File) error {
+		rc, err := f.Open()
+		if err != nil {
+			return err
+		}
+		defer func() {
+			rc.Close()
+		}()
+
+		path := filepath.Join(dest, f.Name)
+
+		if f.FileInfo().IsDir() {
+			os.MkdirAll(path, f.Mode())
+		} else {
+			os.MkdirAll(filepath.Dir(path), f.Mode())
+			f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
+			if err != nil {
+				return err
+			}
+			defer func() {
+				f.Close()
+			}()
+
+			_, err = io.Copy(f, rc)
+			if err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+
+	for _, f := range r.File {
+		err := extractAndWriteFile(f)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// higherDir will find the highest numeric name a sub directory has
+// 0 if no numeric dir names found
+func highestDir(dir string) int {
+	files, err := ioutil.ReadDir(dir)
+	if err != nil {
+		return 0
+	}
+	max := 0
+	for _, file := range files {
+		n, err := strconv.Atoi(file.Name())
+		if err == nil {
+			if n > max {
+				max = n
+			}
+		}
+	}
+	return max
+}
+
+var currentDir = highestDir("./action")
+
+// extractAction accept a byte array write it to a file
+func extractAction(buf *[]byte, isScript bool) error {
+	if buf == nil || len(*buf) == 0 {
+		return fmt.Errorf("no file")
+	}
+	currentDir++
+	newDir := fmt.Sprintf("./action/%d", currentDir)
+	os.MkdirAll(newDir, 0755)
+	kind, err := filetype.Match(*buf)
+	if err != nil {
+		return err
+	}
+	if kind.Extension == "zip" {
+		log.Println("Extract Action, assuming a zip")
+		return unzip(*buf, newDir)
+	}
+	if kind.Extension == "elf" || isScript {
+		if isScript {
+			log.Println("Extract Action, assuming a script")
+		} else {
+			log.Println("Extract Action, assuming a binary")
+		}
+		return ioutil.WriteFile(newDir+"/exec", *buf, 0755)
+	}
+	log.Println("No valid action found")
+	return fmt.Errorf("unknown filetype %s", kind)
+}
diff --git a/openwhisk/extractor_test.go b/openwhisk/extractor_test.go
new file mode 100644
index 0000000..90db00a
--- /dev/null
+++ b/openwhisk/extractor_test.go
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package openwhisk
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"testing"
+
+	"github.com/h2non/filetype"
+	"github.com/stretchr/testify/assert"
+)
+
+func sys(cli string) {
+	cmd := exec.Command(cli)
+	out, err := cmd.CombinedOutput()
+	if err == nil {
+		fmt.Printf(">>>%s\n%s<<<\n", cli, string(out))
+	} else {
+		fmt.Println("KO")
+		log.Print(err)
+	}
+}
+
+func TestExtractActionTest_exec(t *testing.T) {
+	//sys("pwd")
+	// cleanup
+	assert.Nil(t, os.RemoveAll("./action"))
+	file, _ := ioutil.ReadFile("_test/exec")
+	extractAction(&file, false)
+	assert.Nil(t, exists("./action", "exec"))
+}
+
+func exists(dir, filename string) error {
+	path := fmt.Sprintf("%s/%d/%s", dir, highestDir(dir), filename)
+	_, err := os.Stat(path)
+	return err
+}
+
+func detect(dir, filename string) string {
+	path := fmt.Sprintf("%s/%d/%s", dir, highestDir(dir), filename)
+	file, _ := ioutil.ReadFile(path)
+	kind, _ := filetype.Match(file)
+	return kind.Extension
+}
+func TestExtractActionTest_exe(t *testing.T) {
+	//sys("pwd")
+	// cleanup
+	assert.Nil(t, os.RemoveAll("./action"))
+	// match  exe
+	file, _ := ioutil.ReadFile("_test/exec")
+	extractAction(&file, false)
+	assert.Equal(t, detect("./action", "exec"), "elf")
+}
+
+func TestExtractActionTest_zip(t *testing.T) {
+	//sys("pwd")
+	// cleanup
+	assert.Nil(t, os.RemoveAll("./action"))
+	// match  exe
+	file, _ := ioutil.ReadFile("_test/exec.zip")
+	extractAction(&file, false)
+	assert.Equal(t, detect("./action", "exec"), "elf")
+	assert.Nil(t, exists("./action", "etc"))
+	assert.Nil(t, exists("./action", "dir/etc"))
+}
+
+func TestExtractAction_script(t *testing.T) {
+	buf := []byte("#!/bin/sh\necho ok")
+	assert.NotNil(t, extractAction(&buf, false))
+	assert.Nil(t, extractAction(&buf, true))
+}
+
+func TestHighestDir(t *testing.T) {
+	assert.Equal(t, highestDir("./_test"), 0)
+	assert.Equal(t, highestDir("./_test/first"), 3)
+	assert.Equal(t, highestDir("./_test/second"), 17)
+}
diff --git a/openwhisk/initHandler.go b/openwhisk/initHandler.go
new file mode 100644
index 0000000..59dce64
--- /dev/null
+++ b/openwhisk/initHandler.go
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package openwhisk
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+)
+
+type initRequest struct {
+	Value struct {
+		Code   string `json:",omitempty"`
+		Binary bool   `json:",omitempty"`
+	} `json:",omitempty"`
+}
+
+func sendOK(w http.ResponseWriter) {
+	// answer OK
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Content-Length", "12")
+	w.Write([]byte("{\"ok\":true}\n"))
+	if f, ok := w.(http.Flusher); ok {
+		f.Flush()
+	}
+}
+
+func initHandler(w http.ResponseWriter, r *http.Request) {
+
+	// read body of the request
+	// log.Println("init: reading")
+	body, err := ioutil.ReadAll(r.Body)
+	defer r.Body.Close()
+	if err != nil {
+		sendError(w, http.StatusBadRequest, fmt.Sprintf("%v", err))
+	}
+
+	// decode request parameters
+	//log.Println("init: decoding")
+	var request initRequest
+	err = json.Unmarshal(body, &request)
+
+	if err != nil {
+		sendError(w, http.StatusBadRequest, fmt.Sprintf("Error unmarshaling request: %v", err))
+		return
+	}
+
+	if request.Value.Code == "" {
+		sendOK(w)
+		return
+	}
+
+	// check if it is a binary
+	if request.Value.Binary {
+		var decoded []byte
+		decoded, err = base64.StdEncoding.DecodeString(request.Value.Code)
+		if err != nil {
+			sendError(w, http.StatusBadRequest, "cannot decode the request: "+err.Error())
+			return
+		}
+		// extract the replacement, stopping and then starting the action
+		err = extractAction(&decoded, false)
+	} else {
+		buf := []byte(request.Value.Code)
+		err = extractAction(&buf, true)
+	}
+	if err != nil {
+		sendError(w, http.StatusBadRequest, "invalid action: "+err.Error())
+		return
+	}
+
+	// stop and start
+	err = StartLatestAction()
+	if err != nil {
+		sendError(w, http.StatusBadRequest, "cannot start action: "+err.Error())
+		return
+	}
+	sendOK(w)
+}
diff --git a/openwhisk/runHandler.go b/openwhisk/runHandler.go
new file mode 100644
index 0000000..4aee1a6
--- /dev/null
+++ b/openwhisk/runHandler.go
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package openwhisk
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"strings"
+)
+
+// Params are the parameteres sent to the action
+type Params struct {
+	Value json.RawMessage `json:"value"`
+}
+
+// ErrResponse is the response when there are errors
+type ErrResponse struct {
+	Error string `json:"error"`
+}
+
+func sendError(w http.ResponseWriter, code int, cause string) {
+	errResponse := ErrResponse{Error: cause}
+	b, err := json.Marshal(errResponse)
+	if err != nil {
+		fmt.Println("error marshalling error response:", err)
+	}
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(code)
+	w.Write(b)
+	w.Write([]byte("\n"))
+}
+
+func runHandler(w http.ResponseWriter, r *http.Request) {
+
+	// parse the request
+	params := Params{}
+	body, err := ioutil.ReadAll(r.Body)
+	defer r.Body.Close()
+	if err != nil {
+		sendError(w, http.StatusBadRequest, fmt.Sprintf("Error reading request body: %v", err))
+		return
+	}
+
+	// decode request parameters
+	err = json.Unmarshal(body, &params)
+	if err != nil {
+		sendError(w, http.StatusBadRequest, fmt.Sprintf("Error unmarshaling request: %v", err))
+		return
+	}
+
+	// check if you have an action
+	if theExecutor == nil {
+		sendError(w, http.StatusBadRequest, fmt.Sprintf("no action defined yet"))
+		return
+	}
+
+	// execute the action
+	// and check for early termination
+	theExecutor.io <- string(params.Value)
+	var response string
+	var exited bool
+	select {
+	case response = <-theExecutor.io:
+		exited = false
+	case err = <-theExecutor.exit:
+		exited = true
+	}
+
+	// check for early termination
+	if exited {
+		theExecutor = nil
+		sendError(w, http.StatusBadRequest, fmt.Sprintf("command exited"))
+		return
+	}
+
+	// flush the logs sending the activation message at the end
+	theExecutor.log <- true
+
+	// check response
+	if response == "" {
+		sendError(w, http.StatusBadRequest, fmt.Sprintf("%v", err))
+		return
+	}
+
+	// return the response
+	if !strings.HasSuffix(response, "\n") {
+		response = response + "\n"
+	}
+	log.Print(response)
+	w.Header().Set("Content-Type", "application/json")
+	numBytesWritten, err := w.Write([]byte(response))
+
+	// flush output
+	if f, ok := w.(http.Flusher); ok {
+		f.Flush()
+	}
+
+	// diagnostic when writing problems
+	if err != nil {
+		sendError(w, http.StatusInternalServerError, fmt.Sprintf("Error writing response: %v", err))
+		return
+	}
+	if numBytesWritten != len(response) {
+		sendError(w, http.StatusInternalServerError, fmt.Sprintf("Only wrote %d of %d bytes to response", numBytesWritten, len(response)))
+		return
+	}
+}
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644
index 0000000..4499418
--- /dev/null
+++ b/test/.gitignore
@@ -0,0 +1,4 @@
+action/
+*.json
+*.err
+
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 0000000..3a601c9
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,9 @@
+# Test
+
+How to run tests:
+
+- ensure you are running them under linux !
+- install `cram`: `sudo pip install cram`
+- start the tester: `cd test ; ./start.sh`
+- in another terminal `cd test ; cram test.t`
+
diff --git a/test/bin/.gitignore b/test/bin/.gitignore
new file mode 100644
index 0000000..b374f99
--- /dev/null
+++ b/test/bin/.gitignore
@@ -0,0 +1,3 @@
+hello_*
+empty
+hi
diff --git a/test/bin/build.sh b/test/bin/build.sh
new file mode 100755
index 0000000..4ed5b0a
--- /dev/null
+++ b/test/bin/build.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+FILE=${1:?go file}
+OUT=$(basename $FILE)
+BIN=${OUT%%.go}
+ZIP=${BIN}.zip
+go build -i -o bin/$BIN $FILE
+GOOS=linux GOARCH=amd64 go build -o exec $FILE
+zip zip/$ZIP exec
+rm exec
+echo "built bin/$BIN zip/$ZIP"
diff --git a/test/bin/init.sh b/test/bin/init.sh
new file mode 100755
index 0000000..e700bb4
--- /dev/null
+++ b/test/bin/init.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+FILE=${1:?file}
+JSON=${2:-/tmp/json$$}
+if file -i $FILE | grep text/ >/dev/null
+then echo '{"value":{"main":"main","code":'$(cat $FILE | jq -R -s .)'}}' >/tmp/json$$
+else echo '{"value":{"binary":true,"code":"'$(base64 -w 0 $FILE)'"}}' >/tmp/json$$
+fi
+#cat $JSON | jq .
+curl -H "Content-Type: application/json" -XPOST -w "%{http_code}\n" http://localhost:${PORT:-8080}/init -d @$JSON 2>/dev/null
+rm /tmp/json$$ 2>/dev/null
diff --git a/test/bin/post.sh b/test/bin/post.sh
new file mode 100755
index 0000000..6255f39
--- /dev/null
+++ b/test/bin/post.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+FILE=${1:?file}
+curl -H "Content-Type: application/json" -XPOST -w "%{http_code}\n" http://localhost:${PORT:-8080}/init -d @$FILE 2>/dev/null
diff --git a/test/bin/run.sh b/test/bin/run.sh
new file mode 100755
index 0000000..0df821e
--- /dev/null
+++ b/test/bin/run.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+DEFAULT='{"name": "Mike"}'
+JSON=${1:-$DEFAULT}
+DATA='{"value":'$JSON'}'
+curl -H "Content-Type: application/json" -w "%{http_code}\n" -XPOST http://localhost:${PORT:-8080}/run -d "$DATA" 2>/dev/null
+
diff --git a/test/etc/hello.js b/test/etc/hello.js
new file mode 100644
index 0000000..a1bf768
--- /dev/null
+++ b/test/etc/hello.js
@@ -0,0 +1,5 @@
+function main(args) {
+  return {
+     "result": "Hello, "+args.name
+  }
+}
diff --git a/test/etc/hello.sh b/test/etc/hello.sh
new file mode 100755
index 0000000..9b7f318
--- /dev/null
+++ b/test/etc/hello.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+while read line
+do
+   name="$(echo $line | jq -r .name)"
+   if test "$name" == ""
+   then exit
+   fi
+   echo "name=$name" 
+   hello="Hello, $name"
+   echo '{"hello":"'$hello'"}' >&3
+done
+
diff --git a/test/src/empty.go b/test/src/empty.go
new file mode 100644
index 0000000..35a2611
--- /dev/null
+++ b/test/src/empty.go
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+func main() {}
diff --git a/test/src/hello/hello.go b/test/src/hello/hello.go
new file mode 100644
index 0000000..b0592d0
--- /dev/null
+++ b/test/src/hello/hello.go
@@ -0,0 +1,30 @@
+package hello
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+)
+
+// Hello receive an event in format
+// { "name": "Mike"}
+// and returns a greeting in format
+// { "greetings": "Hello, Mike"}
+func Hello(event json.RawMessage) (json.RawMessage, error) {
+	// input and output
+	var input struct {
+		Name string
+	}
+	var output struct {
+		Greetings string `json:"greetings"`
+	}
+	// read the input event
+	json.Unmarshal(event, &input)
+	if input.Name != "" {
+		// handle the event
+		output.Greetings = "Hello, " + input.Name
+		log.Println(output.Greetings)
+		return json.Marshal(output)
+	}
+	return nil, fmt.Errorf("no name specified")
+}
diff --git a/test/src/hello/hello_test.go b/test/src/hello/hello_test.go
new file mode 100644
index 0000000..0624cad
--- /dev/null
+++ b/test/src/hello/hello_test.go
@@ -0,0 +1,28 @@
+package hello
+
+import (
+	"fmt"
+)
+
+func ExampleHello() {
+	name := []byte(`{ "name": "Mike"}`)
+	data, _ := Hello(name)
+	fmt.Printf("%s", data)
+	// Output:
+	// {"greetings":"Hello, Mike"}
+}
+
+func ExampleHello_noName() {
+	name := []byte(`{ "noname": "Mike"}`)
+	_, err := Hello(name)
+	fmt.Print(err)
+	// Output:
+	// no name specified
+}
+func ExampleHello_badJson() {
+	name := []byte(`{{`)
+	_, err := Hello(name)
+	fmt.Print(err)
+	// Output:
+	// no name specified
+}
diff --git a/test/src/hello_greeting.go b/test/src/hello_greeting.go
new file mode 100644
index 0000000..f70bd2c
--- /dev/null
+++ b/test/src/hello_greeting.go
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package main
+
+import (
+	"log"
+	"os"
+
+	"github.com/apache/incubator-openwhisk-client-go/whisk"
+	"github.com/sciabarracom/incubator-openwhisk-runtime-go/test/src/hello"
+)
+
+func main() {
+	log.SetPrefix("hello_greeting: ")
+	whisk.StartWithArgs(hello.Hello, os.Args[1:])
+}
diff --git a/test/src/hello_message.go b/test/src/hello_message.go
new file mode 100644
index 0000000..c29170d
--- /dev/null
+++ b/test/src/hello_message.go
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package main
+
+import (
+	"encoding/json"
+	"log"
+	"os"
+
+	"github.com/apache/incubator-openwhisk-client-go/whisk"
+)
+
+func sayHello(event json.RawMessage) (json.RawMessage, error) {
+	var obj map[string]interface{}
+	json.Unmarshal(event, &obj)
+	name, ok := obj["name"].(string)
+	if !ok {
+		name = "Stranger"
+	}
+	log.Printf("name=%s\n", name)
+	msg := map[string]string{"message": ("Hello, " + name + "!")}
+	return json.Marshal(msg)
+}
+
+func main() {
+	log.SetPrefix("hello_message: ")
+	whisk.StartWithArgs(sayHello, os.Args[1:])
+}
diff --git a/test/src/hi.go b/test/src/hi.go
new file mode 100644
index 0000000..6a07be8
--- /dev/null
+++ b/test/src/hi.go
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("hi")
+}
diff --git a/test/start.sh b/test/start.sh
new file mode 100755
index 0000000..5310d7f
--- /dev/null
+++ b/test/start.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+cd "$(dirname $0)"
+bin/build.sh src/hello_greeting.go 
+bin/build.sh src/hello_message.go 
+bin/build.sh src/empty.go
+bin/build.sh src/hi.go
+rm -Rvf action
+go run ../main/proxy.go -debug
diff --git a/test/test.t b/test/test.t
new file mode 100644
index 0000000..a85b86c
--- /dev/null
+++ b/test/test.t
@@ -0,0 +1,93 @@
+  $ export T=$TESTDIR
+
+  $ $T/bin/run.sh 
+  {"error":"no action defined yet"}
+  400
+
+  $ $T/bin/post.sh $T/etc/empty.json
+  {"ok":true}
+  200
+
+  $ $T/bin/init.sh $T/test.t
+  {"error":"cannot start action: command exited"}
+  400
+
+  $ $T/bin/init.sh $T/bin/empty
+  {"error":"cannot start action: command exited"}
+  400
+
+  $ $T/bin/init.sh $T/bin/hi
+  {"error":"cannot start action: command exited"}
+  400
+
+  $ $T/bin/run.sh 
+  {"error":"no action defined yet"}
+  400
+
+  $ $T/bin/init.sh $T/bin/hello_message
+  {"ok":true}
+  200
+
+  $ $T/bin/run.sh 
+  {"message":"Hello, Mike!"}
+  200
+
+  $ $T/bin/init.sh $T/bin/hello_greeting
+  {"ok":true}
+  200
+
+  $ $T/bin/run.sh 
+  {"greetings":"Hello, Mike"}
+  200
+
+  $ $T/bin/init.sh $T/zip/hello_message.zip
+  {"ok":true}
+  200
+
+  $ $T/bin/run.sh 
+  {"message":"Hello, Mike!"}
+  200
+
+  $ $T/bin/init.sh $T/zip/hello_greeting.zip
+  {"ok":true}
+  200
+
+  $ $T/bin/run.sh 
+  {"greetings":"Hello, Mike"}
+  200
+
+  $ $T/bin/init.sh $T/test.t
+  {"error":"cannot start action: command exited"}
+  400
+
+  $ $T/bin/run.sh 
+  {"greetings":"Hello, Mike"}
+  200
+
+  $ $T/bin/init.sh $T/bin/empty
+  {"error":"cannot start action: command exited"}
+  400
+
+  $ $T/bin/run.sh 
+  {"greetings":"Hello, Mike"}
+  200
+
+  $ $T/bin/init.sh $T/bin/hi
+  {"error":"cannot start action: command exited"}
+  400
+
+  $ $T/bin/init.sh $T/etc/hello.sh
+  {"ok":true}
+  200
+
+  $ $T/bin/run.sh
+  {"hello":"Hello, Mike"}
+  200
+
+  $ $T/bin/run.sh '{"name": ""}'
+  {"error":"command exited"}
+  400
+
+  $ $T/bin/run.sh
+  {"error":"no action defined yet"}
+  400
\ No newline at end of file
diff --git a/test/zip/.gitignore b/test/zip/.gitignore
new file mode 100644
index 0000000..c4c4ffc
--- /dev/null
+++ b/test/zip/.gitignore
@@ -0,0 +1 @@
+*.zip


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services