wide/conf/wide.go

498 lines
12 KiB
Go

// Copyright (c) 2014-2019, b3log.org & hacpai.com
//
// 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
//
// https://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 conf includes configurations related manipulations.
package conf
import (
"encoding/json"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"text/template"
"time"
"github.com/b3log/wide/event"
"github.com/b3log/wide/log"
"github.com/b3log/wide/util"
)
const (
// PathSeparator holds the OS-specific path separator.
PathSeparator = string(os.PathSeparator)
// PathListSeparator holds the OS-specific path list separator.
PathListSeparator = string(os.PathListSeparator)
// WideVersion holds the current Wide's version.
WideVersion = "1.5.3"
// CodeMirrorVer holds the current editor version.
CodeMirrorVer = "5.1"
HelloWorld = `package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
`
)
// Configuration.
type conf struct {
IP string // server ip, ${ip}
Port string // server port
Context string // server context
Server string // server host and port ({IP}:{Port})
StaticServer string // static resources server scheme, host and port (http://{IP}:{Port})
LogLevel string // logging level: trace/debug/info/warn/error
Channel string // channel (ws://{IP}:{Port})
HTTPSessionMaxAge int // HTTP session max age (in seciond)
StaticResourceVersion string // version of static resources
MaxProcs int // Go max procs
RuntimeMode string // runtime mode (dev/prod)
WD string // current working direcitory, ${pwd}
Locale string // default locale
Playground string // playground directory
UsersWorkspaces string // users' workspaces directory (admin defaults to ${GOPATH}, others using this)
AllowRegister bool // allow register or not
Autocomplete bool // default autocomplete
}
// Logger.
var logger = log.NewLogger(os.Stdout)
// Wide configurations.
var Wide *conf
// configurations of users.
var Users []*User
// Indicates whether Docker is available.
var Docker bool
// Load loads the Wide configurations from wide.json and users' configurations from users/{username}.json.
func Load(confPath, confUsers, confIP, confPort, confServer, confLogLevel, confStaticServer, confContext, confChannel, confPlayground string, confUsersWorkspaces string) {
initWide(confPath, confIP, confPort, confServer, confLogLevel, confStaticServer, confContext, confChannel, confPlayground, confUsersWorkspaces)
initUsers(confUsers)
cmd := exec.Command("docker", "version")
_, err := cmd.CombinedOutput()
if nil != err {
logger.Warn("Not found 'docker' installed, running user's code will cause security problem")
}
}
func initUsers(confUsers string) {
f, err := os.Open(confUsers)
if nil != err {
logger.Error(err)
os.Exit(-1)
}
names, err := f.Readdirnames(-1)
if nil != err {
logger.Error(err)
os.Exit(-1)
}
f.Close()
for _, name := range names {
if strings.HasPrefix(name, ".") { // hiden files that not be created by Wide
continue
}
if ".json" != filepath.Ext(name) { // such as backup (*.json~) not be created by Wide
continue
}
user := &User{}
bytes, _ := ioutil.ReadFile("conf/users/" + name)
err := json.Unmarshal(bytes, user)
if err != nil {
logger.Errorf("Parses [%s] error: %v, skip loading this user", name, err)
continue
}
// Compatibility upgrade (1.3.0): https://github.com/b3log/wide/issues/83
if "" == user.Keymap {
user.Keymap = "wide"
}
// Compatibility upgrade (1.5.3): https://github.com/b3log/wide/issues/308
if "" == user.GoBuildArgsForLinux {
user.GoBuildArgsForLinux = "-i"
}
if "" == user.GoBuildArgsForWindows {
user.GoBuildArgsForWindows = "-i"
}
if "" == user.GoBuildArgsForDarwin {
user.GoBuildArgsForDarwin = "-i"
}
Users = append(Users, user)
}
initWorkspaceDirs()
initCustomizedConfs()
}
func initWide(confPath, confIP, confPort, confServer, confLogLevel, confStaticServer, confContext, confChannel, confPlayground string, confUsersWorkspaces string) {
bytes, err := ioutil.ReadFile(confPath)
if nil != err {
logger.Error(err)
os.Exit(-1)
}
Wide = &conf{}
err = json.Unmarshal(bytes, Wide)
if err != nil {
logger.Error("Parses [wide.json] error: ", err)
os.Exit(-1)
}
// Logging Level
log.SetLevel(Wide.LogLevel)
if "" != confLogLevel {
Wide.LogLevel = confLogLevel
log.SetLevel(confLogLevel)
}
logger.Debug("Conf: \n" + string(bytes))
// Working Directory
Wide.WD = util.OS.Pwd()
logger.Debugf("${pwd} [%s]", Wide.WD)
// User Home
home, err := util.OS.Home()
if nil != err {
logger.Error("Can't get user's home, please report this issue to developer", err)
os.Exit(-1)
}
logger.Debugf("${user.home} [%s]", home)
// Playground Directory
Wide.Playground = strings.Replace(Wide.Playground, "${home}", home, 1)
if "" != confPlayground {
Wide.Playground = confPlayground
}
// Users' workspaces Directory
Wide.UsersWorkspaces = strings.Replace(Wide.UsersWorkspaces, "${WD}", Wide.WD, 1)
Wide.UsersWorkspaces = strings.Replace(Wide.UsersWorkspaces, "${home}", home, 1)
if "" != confUsersWorkspaces {
Wide.UsersWorkspaces = confUsersWorkspaces
}
Wide.UsersWorkspaces = filepath.Clean(Wide.UsersWorkspaces)
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
if "" != confIP {
Wide.IP = confIP
} else {
ip, err := util.Net.LocalIP()
if nil != err {
logger.Error(err)
os.Exit(-1)
}
logger.Debugf("${ip} [%s]", ip)
Wide.IP = strings.Replace(Wide.IP, "${ip}", ip, 1)
}
if "" != confPort {
Wide.Port = confPort
}
// Server
Wide.Server = strings.Replace(Wide.Server, "{IP}", Wide.IP, 1)
Wide.Server = strings.Replace(Wide.Server, "{Port}", Wide.Port, 1)
if "" != confServer {
Wide.Server = confServer
}
// Static Server
Wide.StaticServer = strings.Replace(Wide.StaticServer, "{IP}", Wide.IP, 1)
Wide.StaticServer = strings.Replace(Wide.StaticServer, "{Port}", Wide.Port, 1)
if "" != confStaticServer {
Wide.StaticServer = confStaticServer
}
// Context
if "" != confContext {
Wide.Context = confContext
}
time := strconv.FormatInt(time.Now().UnixNano(), 10)
logger.Debugf("${time} [%s]", time)
Wide.StaticResourceVersion = strings.Replace(Wide.StaticResourceVersion, "${time}", time, 1)
// Channel
Wide.Channel = strings.Replace(Wide.Channel, "{IP}", Wide.IP, 1)
Wide.Channel = strings.Replace(Wide.Channel, "{Port}", Wide.Port, 1)
if "" != confChannel {
Wide.Channel = confChannel
}
}
// FixedTimeCheckEnv checks Wide runtime enviorment periodically (7 minutes).
//
// Exits process if found fatal issues (such as not found $GOPATH),
// Notifies user by notification queue if found warning issues (such as not found gocode).
func FixedTimeCheckEnv() {
checkEnv() // check immediately
go func() {
for _ = range time.Tick(time.Minute*7) {
checkEnv()
}
}()
}
func checkEnv() {
defer util.Recover()
cmd := exec.Command("go", "version")
buf, err := cmd.CombinedOutput()
if nil != err {
logger.Error("Not found 'go' command, please make sure Go has been installed correctly")
os.Exit(-1)
}
logger.Trace(string(buf))
if "" == os.Getenv("GOPATH") {
logger.Error("Not found $GOPATH, please configure it before running Wide")
os.Exit(-1)
}
gocode := util.Go.GetExecutableInGOBIN("gocode")
cmd = exec.Command(gocode)
_, err = cmd.Output()
if nil != err {
event.EventQueue <- &event.Event{Code: event.EvtCodeGocodeNotFound}
logger.Warnf("Not found gocode [%s], please install it with this command: go get github.com/nsf/gocode", gocode)
}
ideStub := util.Go.GetExecutableInGOBIN("gotools")
cmd = exec.Command(ideStub, "version")
_, err = cmd.Output()
if nil != err {
event.EventQueue <- &event.Event{Code: event.EvtCodeIDEStubNotFound}
logger.Warnf("Not found gotools [%s], please install it with this command: go get github.com/visualfc/gotools", ideStub)
}
}
// GetUserWorkspace gets workspace path with the specified username, returns "" if not found.
func GetUserWorkspace(username string) string {
for _, user := range Users {
if user.Name == username {
return user.WorkspacePath()
}
}
return ""
}
// GetGoFmt gets the path of Go format tool, returns "gofmt" if not found "goimports".
func GetGoFmt(username string) string {
for _, user := range Users {
if user.Name == username {
switch user.GoFormat {
case "gofmt":
return "gofmt"
case "goimports":
return util.Go.GetExecutableInGOBIN("goimports")
default:
logger.Errorf("Unsupported Go Format tool [%s]", user.GoFormat)
return "gofmt"
}
}
}
return "gofmt"
}
// GetUser gets configuration of the user specified by the given username, returns nil if not found.
func GetUser(username string) *User {
if "playground" == username { // reserved user for Playground
// mock it
return NewUser("playground", "", "", "")
}
for _, user := range Users {
if user.Name == username {
return user
}
}
return nil
}
// initCustomizedConfs initializes the user customized configurations.
func initCustomizedConfs() {
for _, user := range Users {
UpdateCustomizedConf(user.Name)
}
}
// UpdateCustomizedConf creates (if not exists) or updates user customized configuration files.
//
// 1. /static/user/{username}/style.css
func UpdateCustomizedConf(username string) {
var u *User
for _, user := range Users { // maybe it is a beauty of the trade-off of the another world between design and implementation
if user.Name == username {
u = user
}
}
if nil == u {
return
}
model := map[string]interface{}{"user": u}
t, err := template.ParseFiles("static/user/style.css.tmpl")
if nil != err {
logger.Error(err)
os.Exit(-1)
}
wd := util.OS.Pwd()
dir := filepath.Clean(wd + "/static/user/" + u.Name)
if err := os.MkdirAll(dir, 0755); nil != err {
logger.Error(err)
os.Exit(-1)
}
fout, err := os.Create(dir + PathSeparator + "style.css")
if nil != err {
logger.Error(err)
os.Exit(-1)
}
defer fout.Close()
if err := t.Execute(fout, model); nil != err {
logger.Error(err)
os.Exit(-1)
}
}
// initWorkspaceDirs initializes the directories of users' workspaces.
//
// Creates directories if not found on path of workspace.
func initWorkspaceDirs() {
paths := []string{}
for _, user := range Users {
paths = append(paths, filepath.SplitList(user.WorkspacePath())...)
}
for _, path := range paths {
CreateWorkspaceDir(path)
}
}
// CreateWorkspaceDir creates (if not exists) directories on the path.
//
// 1. root directory:{path}
// 2. src directory: {path}/src
// 3. package directory: {path}/pkg
// 4. binary directory: {path}/bin
func CreateWorkspaceDir(path string) {
createDir(path)
createDir(path + PathSeparator + "src")
createDir(path + PathSeparator + "pkg")
createDir(path + PathSeparator + "bin")
}
// createDir creates a directory on the path if it not exists.
func createDir(path string) {
if !util.File.IsExist(path) {
if err := os.MkdirAll(path, 0775); nil != err {
logger.Error(err)
os.Exit(-1)
}
logger.Tracef("Created a dir [%s]", path)
}
}
// GetEditorThemes gets the names of editor themes.
func GetEditorThemes() []string {
ret := []string{}
f, _ := os.Open("static/js/overwrite/codemirror" + "/theme")
names, _ := f.Readdirnames(-1)
f.Close()
for _, name := range names {
ret = append(ret, name[:strings.LastIndex(name, ".")])
}
sort.Strings(ret)
return ret
}
// GetThemes gets the names of themes.
func GetThemes() []string {
ret := []string{}
f, _ := os.Open("static/css/themes")
names, _ := f.Readdirnames(-1)
f.Close()
for _, name := range names {
ret = append(ret, name[:strings.LastIndex(name, ".")])
}
sort.Strings(ret)
return ret
}