This commit is contained in:
Liang Ding 2015-02-13 09:59:51 +08:00
parent 7521c8b6d4
commit 36a8bb4d50
16 changed files with 907 additions and 28 deletions

View File

@ -20,6 +20,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"os/user"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv" "strconv"
@ -42,6 +43,15 @@ const (
WideVersion = "1.1.0" WideVersion = "1.1.0"
// CodeMirrorVer holds the current editor version. // CodeMirrorVer holds the current editor version.
CodeMirrorVer = "4.10" CodeMirrorVer = "4.10"
HelloWorld = `package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
`
) )
// Configuration. // Configuration.
@ -59,6 +69,7 @@ type conf struct {
RuntimeMode string // runtime mode (dev/prod) RuntimeMode string // runtime mode (dev/prod)
WD string // current working direcitory, ${pwd} WD string // current working direcitory, ${pwd}
Locale string // default locale Locale string // default locale
Playground string // playground directory
} }
// Logger. // Logger.
@ -74,8 +85,12 @@ var Users []*User
var Docker bool var Docker bool
// Load loads the Wide configurations from wide.json and users' configurations from users/{username}.json. // Load loads the Wide configurations from wide.json and users' configurations from users/{username}.json.
func Load(confPath, confIP, confPort, confServer, confLogLevel, confStaticServer, confContext, confChannel string, confDocker bool) { func Load(confPath, confIP, confPort, confServer, confLogLevel, confStaticServer, confContext, confChannel,
initWide(confPath, confIP, confPort, confServer, confLogLevel, confStaticServer, confContext, confChannel, confDocker) confPlayground string, confDocker bool) {
// XXX: ugly args list....
initWide(confPath, confIP, confPort, confServer, confLogLevel, confStaticServer, confContext, confChannel,
confPlayground, confDocker)
initUsers() initUsers()
} }
@ -114,7 +129,8 @@ func initUsers() {
initCustomizedConfs() initCustomizedConfs()
} }
func initWide(confPath, confIP, confPort, confServer, confLogLevel, confStaticServer, confContext, confChannel string, confDocker bool) { func initWide(confPath, confIP, confPort, confServer, confLogLevel, confStaticServer, confContext, confChannel,
confPlayground string, confDocker bool) {
bytes, err := ioutil.ReadFile(confPath) bytes, err := ioutil.ReadFile(confPath)
if nil != err { if nil != err {
logger.Error(err) logger.Error(err)
@ -144,6 +160,31 @@ func initWide(confPath, confIP, confPort, confServer, confLogLevel, confStaticSe
Wide.WD = util.OS.Pwd() Wide.WD = util.OS.Pwd()
logger.Debugf("${pwd} [%s]", Wide.WD) logger.Debugf("${pwd} [%s]", Wide.WD)
// User Home
user, err := user.Current()
if nil != err {
logger.Error("Can't get user's home, please report this issue to developer")
os.Exit(-1)
}
userHome := user.HomeDir
logger.Debugf("${user.home} [%s]", userHome)
// Playground Directory
Wide.Playground = strings.Replace(Wide.Playground, "${home}", userHome, 1)
if "" != confPlayground {
Wide.Playground = confPlayground
}
if !util.File.IsExist(Wide.Playground) {
if err := os.Mkdir(Wide.Playground, 0775); nil != err {
logger.Errorf("Create Playground [%s] error", err)
os.Exit(-1)
}
}
// IP // IP
ip, err := util.Net.LocalIP() ip, err := util.Net.LocalIP()
if err != nil { if err != nil {

View File

@ -11,5 +11,6 @@
"MaxProcs": 4, "MaxProcs": 4,
"RuntimeMode": "dev", "RuntimeMode": "dev",
"WD": "${pwd}", "WD": "${pwd}",
"Locale": "en_US" "Locale": "en_US",
"Playground": "${home}/playground"
} }

26
main.go
View File

@ -37,8 +37,8 @@ import (
"github.com/b3log/wide/log" "github.com/b3log/wide/log"
"github.com/b3log/wide/notification" "github.com/b3log/wide/notification"
"github.com/b3log/wide/output" "github.com/b3log/wide/output"
"github.com/b3log/wide/playground"
"github.com/b3log/wide/session" "github.com/b3log/wide/session"
"github.com/b3log/wide/shell"
"github.com/b3log/wide/util" "github.com/b3log/wide/util"
) )
@ -57,6 +57,7 @@ func init() {
confChannel := flag.String("channel", "", "this will overwrite Wide.Channel if specified") confChannel := flag.String("channel", "", "this will overwrite Wide.Channel if specified")
confStat := flag.Bool("stat", false, "whether report statistics periodically") confStat := flag.Bool("stat", false, "whether report statistics periodically")
confDocker := flag.Bool("docker", false, "whether run in a docker container") confDocker := flag.Bool("docker", false, "whether run in a docker container")
confPlayground := flag.String("playground", "", "this will overwrite Wide.Playground if specified")
flag.Parse() flag.Parse()
@ -75,7 +76,7 @@ func init() {
event.Load() event.Load()
conf.Load(*confPath, *confIP, *confPort, *confServer, *confLogLevel, *confStaticServer, *confContext, *confChannel, conf.Load(*confPath, *confIP, *confPort, *confServer, *confLogLevel, *confStaticServer, *confContext, *confChannel,
*confDocker) *confPlayground, *confDocker)
conf.FixedTimeCheckEnv() conf.FixedTimeCheckEnv()
@ -151,8 +152,8 @@ func main() {
http.HandleFunc(conf.Wide.Context+"/find/usages", handlerWrapper(editor.FindUsagesHandler)) http.HandleFunc(conf.Wide.Context+"/find/usages", handlerWrapper(editor.FindUsagesHandler))
// shell // shell
http.HandleFunc(conf.Wide.Context+"/shell/ws", handlerWrapper(shell.WSHandler)) // http.HandleFunc(conf.Wide.Context+"/shell/ws", handlerWrapper(shell.WSHandler))
http.HandleFunc(conf.Wide.Context+"/shell", handlerWrapper(shell.IndexHandler)) // http.HandleFunc(conf.Wide.Context+"/shell", handlerWrapper(shell.IndexHandler))
// notification // notification
http.HandleFunc(conf.Wide.Context+"/notification/ws", handlerWrapper(notification.WSHandler)) http.HandleFunc(conf.Wide.Context+"/notification/ws", handlerWrapper(notification.WSHandler))
@ -163,6 +164,14 @@ func main() {
http.HandleFunc(conf.Wide.Context+"/signup", handlerWrapper(session.SignUpUser)) http.HandleFunc(conf.Wide.Context+"/signup", handlerWrapper(session.SignUpUser))
http.HandleFunc(conf.Wide.Context+"/preference", handlerWrapper(session.PreferenceHandler)) http.HandleFunc(conf.Wide.Context+"/preference", handlerWrapper(session.PreferenceHandler))
// playground
http.HandleFunc(conf.Wide.Context+"/playground", handlerWrapper(playground.IndexHandler))
http.HandleFunc(conf.Wide.Context+"/playground/ws", handlerWrapper(playground.WSHandler))
http.HandleFunc(conf.Wide.Context+"/playground/save", handlerWrapper(playground.SaveHandler))
http.HandleFunc(conf.Wide.Context+"/playground/build", handlerWrapper(playground.BuildHandler))
http.HandleFunc(conf.Wide.Context+"/playground/run", handlerWrapper(playground.RunHandler))
http.HandleFunc(conf.Wide.Context+"/playground/stop", handlerWrapper(playground.StopHandler))
logger.Infof("Wide is running [%s]", conf.Wide.Server+conf.Wide.Context) logger.Infof("Wide is running [%s]", conf.Wide.Server+conf.Wide.Context)
err := http.ListenAndServe(conf.Wide.Server, nil) err := http.ListenAndServe(conf.Wide.Server, nil)
@ -186,6 +195,14 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
username := httpSession.Values["username"].(string)
if "playground" == username { // reserved user for Playground
http.Redirect(w, r, conf.Wide.Context+"login", http.StatusFound)
return
}
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
if "" != conf.Wide.Context { if "" != conf.Wide.Context {
httpSession.Options.Path = conf.Wide.Context httpSession.Options.Path = conf.Wide.Context
@ -197,7 +214,6 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
sid := strconv.Itoa(rand.Int()) sid := strconv.Itoa(rand.Int())
wideSession := session.WideSessions.New(httpSession, sid) wideSession := session.WideSessions.New(httpSession, sid)
username := httpSession.Values["username"].(string)
user := conf.GetUser(username) user := conf.GetUser(username)
if nil == user { if nil == user {
logger.Warnf("Not found user [%s]", username) logger.Warnf("Not found user [%s]", username)

View File

@ -20,6 +20,6 @@ import (
"os/exec" "os/exec"
) )
func setNamespace(cmd *exec.Cmd) { func SetNamespace(cmd *exec.Cmd) {
// do nothing // do nothing
} }

View File

@ -19,7 +19,7 @@ import (
"syscall" "syscall"
) )
func setNamespace(cmd *exec.Cmd) { func SetNamespace(cmd *exec.Cmd) {
// XXX: keep move with Go 1.4 and later's // XXX: keep move with Go 1.4 and later's
cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr = &syscall.SysProcAttr{}

View File

@ -27,13 +27,13 @@ type procs map[string][]*os.Process
// Processse of all users. // Processse of all users.
// //
// <sid, []*os.Process> // <sid, []*os.Process>
var processes = procs{} var Processes = procs{}
// Exclusive lock. // Exclusive lock.
var mutex sync.Mutex var mutex sync.Mutex
// add adds the specified process to the user process set. // add adds the specified process to the user process set.
func (procs *procs) add(wSession *session.WideSession, proc *os.Process) { func (procs *procs) Add(wSession *session.WideSession, proc *os.Process) {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
@ -50,7 +50,7 @@ func (procs *procs) add(wSession *session.WideSession, proc *os.Process) {
} }
// remove removes the specified process from the user process set. // remove removes the specified process from the user process set.
func (procs *procs) remove(wSession *session.WideSession, proc *os.Process) { func (procs *procs) Remove(wSession *session.WideSession, proc *os.Process) {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
@ -75,7 +75,7 @@ func (procs *procs) remove(wSession *session.WideSession, proc *os.Process) {
} }
// kill kills a process specified by the given pid. // kill kills a process specified by the given pid.
func (procs *procs) kill(wSession *session.WideSession, pid int) { func (procs *procs) Kill(wSession *session.WideSession, pid int) {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()

View File

@ -64,7 +64,7 @@ func RunHandler(w http.ResponseWriter, r *http.Request) {
cmd.Dir = curDir cmd.Dir = curDir
if conf.Docker { if conf.Docker {
setNamespace(cmd) SetNamespace(cmd)
} }
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
@ -111,7 +111,7 @@ func RunHandler(w http.ResponseWriter, r *http.Request) {
channelRet["pid"] = cmd.Process.Pid channelRet["pid"] = cmd.Process.Pid
// add the process to user's process set // add the process to user's process set
processes.add(wSession, cmd.Process) Processes.Add(wSession, cmd.Process)
go func(runningId int) { go func(runningId int) {
defer util.Recover() defer util.Recover()
@ -151,7 +151,7 @@ func RunHandler(w http.ResponseWriter, r *http.Request) {
if nil != err { if nil != err {
// remove the exited process from user process set // remove the exited process from user process set
processes.remove(wSession, cmd.Process) Processes.Remove(wSession, cmd.Process)
logger.Tracef("User [%s, %s] 's running [id=%d, file=%s] has done [stdout %v], ", wSession.Username, sid, runningId, filePath, err) logger.Tracef("User [%s, %s] 's running [id=%d, file=%s] has done [stdout %v], ", wSession.Username, sid, runningId, filePath, err)
@ -253,5 +253,5 @@ func StopHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
processes.kill(wSession, pid) Processes.Kill(wSession, pid)
} }

73
playground/build.go Normal file
View File

@ -0,0 +1,73 @@
// Copyright (c) 2014-2015, b3log.org
//
// Licensed 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 playground
import (
"encoding/json"
"html/template"
"net/http"
"os/exec"
"path/filepath"
"strings"
"github.com/b3log/wide/conf"
"github.com/b3log/wide/session"
"github.com/b3log/wide/util"
)
// BuildHandler handles request of Playground building.
func BuildHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
httpSession, _ := session.HTTPSession.Get(r, "wide-session")
if httpSession.IsNew {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
data["succ"] = false
return
}
filePath := args["filePath"].(string)
suffix := ""
if util.OS.IsWindows() {
suffix = ".exe"
}
fileName := filepath.Base(filePath)
executable := filepath.Clean(conf.Wide.Playground + "/" + strings.Replace(fileName, ".go", suffix, -1))
cmd := exec.Command("go", "build", "-o", executable, filePath)
out, err := cmd.CombinedOutput()
data["output"] = template.HTML(string(out))
if nil != err {
logger.Error(err)
data["succ"] = false
return
}
data["executable"] = executable
}

113
playground/file.go Normal file
View File

@ -0,0 +1,113 @@
// Copyright (c) 2014-2015, b3log.org
//
// Licensed 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 playground
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"net/http"
"os"
"os/exec"
"path/filepath"
"github.com/b3log/wide/session"
"github.com/b3log/wide/util"
"github.com/b3log/wide/conf"
)
// SaveHandler handles request of Playground code save.
func SaveHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
session, _ := session.HTTPSession.Get(r, "wide-session")
if session.IsNew {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
data["succ"] = false
return
}
code := args["code"].(string)
// generate file name
hasher := md5.New()
hasher.Write([]byte(code))
fileName := hex.EncodeToString(hasher.Sum(nil))
fileName += ".go"
filePath := filepath.Clean(conf.Wide.Playground + "/" + fileName)
fout, err := os.Create(filePath)
if nil != err {
logger.Error(err)
data["succ"] = false
return
}
fout.WriteString(code)
if err := fout.Close(); nil != err {
logger.Error(err)
data["succ"] = false
return
}
data["filePath"] = filePath
data["url"] = filepath.ToSlash(filePath)
argv := []string{filePath}
cmd := exec.Command("gofmt", argv...)
bytes, _ := cmd.Output()
output := string(bytes)
if "" == output {
// format error, returns the original content
data["succ"] = true
data["code"] = code
return
}
code = string(output)
data["code"] = code
// generate file name
hasher = md5.New()
hasher.Write([]byte(code))
fileName = hex.EncodeToString(hasher.Sum(nil))
fileName += ".go"
filePath = filepath.Clean(conf.Wide.Playground + "/" + fileName)
data["filePath"] = filePath
data["url"] = filepath.ToSlash(filePath)
fout, err = os.Create(filePath)
fout.WriteString(code)
if err := fout.Close(); nil != err {
logger.Error(err)
data["succ"] = false
return
}
}

99
playground/playgrounds.go Normal file
View File

@ -0,0 +1,99 @@
// Copyright (c) 2014-2015, b3log.org
//
// Licensed 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 shell include playground related mainipulations.
package playground
import (
"html/template"
"math/rand"
"net/http"
"os"
"strconv"
"time"
"github.com/b3log/wide/conf"
"github.com/b3log/wide/i18n"
"github.com/b3log/wide/log"
"github.com/b3log/wide/session"
"github.com/b3log/wide/util"
"github.com/gorilla/websocket"
)
// Logger.
var logger = log.NewLogger(os.Stdout)
// IndexHandler handles request of Playground index.
func IndexHandler(w http.ResponseWriter, r *http.Request) {
username := "playground"
// create a HTTP session
httpSession, _ := session.HTTPSession.Get(r, "wide-session")
httpSession.Values["username"] = username
if httpSession.IsNew {
httpSession.Values["id"] = strconv.Itoa(rand.Int())
}
httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
if "" != conf.Wide.Context {
httpSession.Options.Path = conf.Wide.Context
}
httpSession.Save(r, w)
// create a wide session
rand.Seed(time.Now().UnixNano())
sid := strconv.Itoa(rand.Int())
wideSession := session.WideSessions.New(httpSession, sid)
locale := conf.Wide.Locale
code := conf.HelloWorld
model := map[string]interface{}{"conf": conf.Wide, "i18n": i18n.GetAll(locale), "locale": locale,
"session": wideSession, "pathSeparator": conf.PathSeparator, "codeMirrorVer": conf.CodeMirrorVer,
"code": template.HTML(code)}
wideSessions := session.WideSessions.GetByUsername(username)
logger.Tracef("User [%s] has [%d] sessions", username, len(wideSessions))
t, err := template.ParseFiles("views/playground/index.html")
if nil != err {
logger.Error(err)
http.Error(w, err.Error(), 500)
return
}
t.Execute(w, model)
}
// WSHandler handles request of creating Playground channel.
func WSHandler(w http.ResponseWriter, r *http.Request) {
sid := r.URL.Query()["sid"][0]
conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024)
wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()}
ret := map[string]interface{}{"output": "Playground initialized", "cmd": "init-playground"}
err := wsChan.WriteJSON(&ret)
if nil != err {
return
}
session.PlaygroundWS[sid] = &wsChan
logger.Tracef("Open a new [PlaygroundWS] with session [%s], %d", sid, len(session.PlaygroundWS))
}

253
playground/run.go Normal file
View File

@ -0,0 +1,253 @@
// Copyright (c) 2014-2015, b3log.org
//
// Licensed 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 playground
import (
"bufio"
"encoding/json"
"math/rand"
"net/http"
"os/exec"
"strings"
"time"
"github.com/b3log/wide/conf"
"github.com/b3log/wide/output"
"github.com/b3log/wide/session"
"github.com/b3log/wide/util"
)
const (
outputBufMax = 128 // 128 string(rune)
outputTimeout = 100 // 100ms
)
type outputBuf struct {
content string
millisecond int64
}
// RunHandler handles request of executing a binary file.
func RunHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
data["succ"] = false
}
sid := args["sid"].(string)
wSession := session.WideSessions.Get(sid)
if nil == wSession {
data["succ"] = false
}
filePath := args["executable"].(string)
cmd := exec.Command(filePath)
if conf.Docker {
output.SetNamespace(cmd)
}
stdout, err := cmd.StdoutPipe()
if nil != err {
logger.Error(err)
data["succ"] = false
}
stderr, err := cmd.StderrPipe()
if nil != err {
logger.Error(err)
data["succ"] = false
}
outReader := bufio.NewReader(stdout)
errReader := bufio.NewReader(stderr)
if err := cmd.Start(); nil != err {
logger.Error(err)
data["succ"] = false
}
wsChannel := session.PlaygroundWS[sid]
channelRet := map[string]interface{}{}
if !data["succ"].(bool) {
if nil != wsChannel {
channelRet["cmd"] = "run-done"
channelRet["output"] = ""
err := wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Error(err)
return
}
wsChannel.Refresh()
}
return
}
channelRet["pid"] = cmd.Process.Pid
// add the process to user's process set
output.Processes.Add(wSession, cmd.Process)
go func(runningId int) {
defer util.Recover()
defer cmd.Wait()
// push once for front-end to get the 'run' state and pid
if nil != wsChannel {
channelRet["cmd"] = "run"
channelRet["output"] = ""
err := wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Error(err)
return
}
wsChannel.Refresh()
}
go func() {
buf := outputBuf{}
for {
wsChannel := session.PlaygroundWS[sid]
if nil == wsChannel {
break
}
r, _, err := outReader.ReadRune()
oneRuneStr := string(r)
oneRuneStr = strings.Replace(oneRuneStr, "<", "&lt;", -1)
oneRuneStr = strings.Replace(oneRuneStr, ">", "&gt;", -1)
buf.content += oneRuneStr
if nil != err {
// remove the exited process from user process set
output.Processes.Remove(wSession, cmd.Process)
logger.Tracef("User [%s, %s] 's running [id=%d, file=%s] has done [stdout %v], ", wSession.Username, sid, runningId, filePath, err)
channelRet["cmd"] = "run-done"
channelRet["output"] = buf.content
err := wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Error(err)
break
}
wsChannel.Refresh()
break
}
now := time.Now().UnixNano() / int64(time.Millisecond)
if 0 == buf.millisecond {
buf.millisecond = now
}
if now-outputTimeout >= buf.millisecond || len(buf.content) > outputBufMax || oneRuneStr == "\n" {
channelRet["cmd"] = "run"
channelRet["output"] = buf.content
buf = outputBuf{} // a new buffer
err = wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Error(err)
break
}
wsChannel.Refresh()
}
}
}()
buf := outputBuf{}
for {
r, _, err := errReader.ReadRune()
wsChannel := session.PlaygroundWS[sid]
if nil != err || nil == wsChannel {
break
}
oneRuneStr := string(r)
oneRuneStr = strings.Replace(oneRuneStr, "<", "&lt;", -1)
oneRuneStr = strings.Replace(oneRuneStr, ">", "&gt;", -1)
buf.content += oneRuneStr
now := time.Now().UnixNano() / int64(time.Millisecond)
if 0 == buf.millisecond {
buf.millisecond = now
}
if now-outputTimeout >= buf.millisecond || len(buf.content) > outputBufMax || oneRuneStr == "\n" {
channelRet["cmd"] = "run"
channelRet["output"] = "<span class='stderr'>" + buf.content + "</span>"
buf = outputBuf{} // a new buffer
err = wsChannel.WriteJSON(&channelRet)
if nil != err {
logger.Error(err)
break
}
wsChannel.Refresh()
}
}
}(rand.Int())
}
// StopHandler handles request of stoping a running process.
func StopHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{"succ": true}
defer util.RetJSON(w, r, data)
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
logger.Error(err)
data["succ"] = false
return
}
sid := args["sid"].(string)
pid := int(args["pid"].(float64))
wSession := session.WideSessions.Get(sid)
if nil == wSession {
data["succ"] = false
return
}
output.Processes.Kill(wSession, pid)
}

View File

@ -60,6 +60,9 @@ var (
// NotificationWS holds all notification channels. <sid, *util.WSChannel> // NotificationWS holds all notification channels. <sid, *util.WSChannel>
NotificationWS = map[string]*util.WSChannel{} NotificationWS = map[string]*util.WSChannel{}
// PlaygroundWS holds all playground channels. <sid, *util.WSChannel>
PlaygroundWS = map[string]*util.WSChannel{}
) )
// HTTP session store. // HTTP session store.
@ -371,6 +374,11 @@ func (sessions *wSessions) Remove(sid string) {
delete(SessionWS, sid) delete(SessionWS, sid)
} }
if ws, ok := PlaygroundWS[sid]; ok {
ws.Close()
delete(PlaygroundWS, sid)
}
cnt := 0 // count wide sessions associated with HTTP session cnt := 0 // count wide sessions associated with HTTP session
for _, ses := range *sessions { for _, ses := range *sessions {
if ses.HTTPSession.Values["id"] == s.HTTPSession.Values["id"] { if ses.HTTPSession.Values["id"] == s.HTTPSession.Values["id"] {

View File

@ -315,6 +315,10 @@ func getOnlineUsers() []*conf.User {
for _, username := range usernames { for _, username := range usernames {
u := conf.GetUser(username) u := conf.GetUser(username)
if "playground" == username { // user [playground] is a reserved mock user
continue
}
if nil == u { if nil == u {
logger.Warnf("Not found user [%s]", username) logger.Warnf("Not found user [%s]", username)
@ -333,7 +337,13 @@ func getOnlineUsers() []*conf.User {
// 2. generate 'Hello, 世界' demo code in the workspace (a console version and a http version) // 2. generate 'Hello, 世界' demo code in the workspace (a console version and a http version)
// 3. update the user customized configurations, such as style.css // 3. update the user customized configurations, such as style.css
// 4. serve files of the user's workspace via HTTP // 4. serve files of the user's workspace via HTTP
//
// Note: user [playground] is a reserved mock user
func addUser(username, password, email string) string { func addUser(username, password, email string) string {
if "playground" == username {
return userExists
}
addUserMutex.Lock() addUserMutex.Lock()
defer addUserMutex.Unlock() defer addUserMutex.Unlock()
@ -393,14 +403,7 @@ func consoleHello(workspace string) {
return return
} }
fout.WriteString(`package main fout.WriteString(conf.HelloWorld)
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
`)
fout.Close() fout.Close()
} }

196
static/js/playground.js Normal file
View File

@ -0,0 +1,196 @@
/*
* Copyright (c) 2014-2015, b3log.org
*
* Licensed 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.
*/
var playground = {
editor: undefined,
pid: undefined,
init: function () {
$("#editorDiv").append("<textarea id='editor'></textarea>");
var textArea = document.getElementById("editor");
textArea.value = code;
playground.editor = CodeMirror.fromTextArea(textArea, {
lineNumbers: true,
autofocus: true,
autoCloseBrackets: true,
matchBrackets: true,
highlightSelectionMatches: {showToken: /\w/},
rulers: [{color: "#ccc", column: 120, lineStyle: "dashed"}],
styleActiveLine: true,
theme: "wide",
tabSize: 4,
indentUnit: 4,
foldGutter: true,
cursorHeight: 1,
});
this._initWS();
},
_initWS: function () {
// Used for session retention, server will release all resources of the session if this channel closed
var sessionWS = new ReconnectingWebSocket(config.channel + '/session/ws?sid=' + config.wideSessionId);
sessionWS.onopen = function () {
console.log('[session onopen] connected');
};
sessionWS.onmessage = function (e) {
console.log('[session onmessage]' + e.data);
};
sessionWS.onclose = function (e) {
console.log('[session onclose] disconnected (' + e.code + ')');
};
sessionWS.onerror = function (e) {
console.log('[session onerror] ' + JSON.parse(e));
};
var playgroundWS = new ReconnectingWebSocket(config.channel + '/playground/ws?sid=' + config.wideSessionId);
playgroundWS.onopen = function () {
console.log('[playground onopen] connected');
};
playgroundWS.onmessage = function (e) {
console.log('[playground onmessage]' + e.data);
var data = JSON.parse(e.data);
playground.pid = data.pid;
var val = $("#output").val();
$("#output").val(val + data.output);
};
playgroundWS.onclose = function (e) {
console.log('[playground onclose] disconnected (' + e.code + ')');
};
playgroundWS.onerror = function (e) {
console.log('[playground onerror] ' + JSON.parse(e));
};
},
share: function () {
if (!playground.editor) {
return;
}
var request = newWideRequest();
request.pid = playground.pid;
var code = playground.editor.getValue();
var request = newWideRequest();
request.code = code;
$.ajax({
type: 'POST',
url: config.context + '/playground/save',
data: JSON.stringify(request),
dataType: "json",
success: function (data) {
playground.editor.setValue(data.code);
if (!data.succ) {
return;
}
var url = window.location.protocol + "//" + window.location.host + '/' + data.url;
var html = '<a href="' + url + '" target="_blank">'
+ url + "</a>";
$("#url").html(html);
}
});
},
stop: function () {
if (!playground.editor || !playground.pid) {
return;
}
var request = newWideRequest();
request.pid = playground.pid;
$.ajax({
type: 'POST',
url: config.context + '/playground/stop',
data: JSON.stringify(request),
dataType: "json"
});
},
run: function () {
if (!playground.editor) {
return;
}
var code = playground.editor.getValue();
// Step 1. save & format code
var request = newWideRequest();
request.code = code;
$("#output").val("");
$.ajax({
type: 'POST',
url: config.context + '/playground/save',
data: JSON.stringify(request),
dataType: "json",
success: function (data) {
// console.log(data);
playground.editor.setValue(data.code);
if (!data.succ) {
return;
}
// Step 2. compile code
var request = newWideRequest();
request.filePath = data.filePath;
$.ajax({
type: 'POST',
url: config.context + '/playground/build',
data: JSON.stringify(request),
dataType: "json",
success: function (data) {
// console.log(data);
$("#output").val(data.output);
if (!data.succ) {
return;
}
// Step 3. run the executable binary and handle its output
var request = newWideRequest();
request.executable = data.executable;
$.ajax({
type: 'POST',
url: config.context + '/playground/run',
data: JSON.stringify(request),
dataType: "json",
success: function (data) {
// console.log(data);
}
});
}
});
}
});
}
};
$(document).ready(function () {
playground.init();
});

View File

@ -121,7 +121,7 @@ var session = {
} }
}, },
_initWS: function () { _initWS: function () {
// 用于保持会话,如果该通道断开,则服务器端会销毁会话状态,回收相关资源. // Used for session retention, server will release all resources of the session if this channel closed
var sessionWS = new ReconnectingWebSocket(config.channel + '/session/ws?sid=' + config.wideSessionId); var sessionWS = new ReconnectingWebSocket(config.channel + '/session/ws?sid=' + config.wideSessionId);
sessionWS.onopen = function () { sessionWS.onopen = function () {

View File

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{.i18n.wide}} - {{.i18n.wide_title}}</title>
<meta name="keywords" content="Wide, Golang, IDE, Team, Cloud, B3log, Playground"/>
<meta name="description" content="A Web-based IDE for Teams using Golang, do your development anytime, anywhere."/>
<meta name="author" content="B3log">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/css/base.css?{{.conf.StaticResourceVersion}}">
<link rel="stylesheet" href="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/codemirror.css">
<link rel="stylesheet" href="{{$.conf.StaticServer}}/static/js/overwrite/codemirror/theme/wide.css">
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
</head>
<body>
<div>
<button id="run" onclick="playground.run();">Run</button>
<button id="stop" onclick="playground.stop();">Stop</button>
<button id="share" onclick="playground.share();">Share</button>
<span id="url"></span>
</div>
<div>
<div id="editorDiv">
</div>
<textarea id="output" rows="20" readonly="readonly" style="width: 100%;" ></textarea>
</div>
<script>
var config = {
"context": "{{.conf.Context}}",
"staticServer": "{{.conf.StaticServer}}",
"channel": "{{.conf.Channel}}",
"wideSessionId": "{{.session.ID}}"
};
var code = "{{.code}}";
function newWideRequest() {
var ret = {
sid: config.wideSessionId
};
return ret;
}
</script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/reconnecting-websocket.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/ztree/jquery.ztree.all-3.5.min.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/codemirror.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/selection/active-line.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/overwrite/codemirror/addon/hint/show-hint.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/hint/anyword-hint.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/display/rulers.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/edit/closebrackets.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/edit/matchbrackets.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/edit/closetag.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/search/searchcursor.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/search/search.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/dialog/dialog.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/search/match-highlighter.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/fold/foldcode.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/fold/foldgutter.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/fold/brace-fold.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/fold/comment-fold.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/mode/loadmode.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/addon/comment/comment.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/mode/meta.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/lib/codemirror-{{.codeMirrorVer}}/mode/go/go.js"></script>
<script type="text/javascript" src="{{.conf.StaticServer}}/static/js/playground.js?{{.conf.StaticResourceVersion}}"></script>
</body>
</html>