Toby Chui 2 lat temu
rodzic
commit
1efa953123

+ 8 - 2
mod/info/logviewer/logviewer.go

@@ -3,7 +3,6 @@ package logviewer
 import (
 	"encoding/json"
 	"errors"
-	"fmt"
 	"io/fs"
 	"net/http"
 	"os"
@@ -27,6 +26,7 @@ type LogFile struct {
 	Title    string
 	Filename string
 	Fullpath string
+	Filesize int64
 }
 
 func NewLogViewer(option *ViewerOption) *Viewer {
@@ -39,12 +39,12 @@ func NewLogViewer(option *ViewerOption) *Viewer {
 //List all the log files in the log folder. Return in map[string]LogFile format
 func (v *Viewer) HandleListLog(w http.ResponseWriter, r *http.Request) {
 	logFiles := v.ListLogFiles(false)
-	fmt.Println(logFiles)
 	js, _ := json.Marshal(logFiles)
 	utils.SendJSONResponse(w, string(js))
 }
 
 // Read log of a given catergory and filename
+//Require GET varaible: file and catergory
 func (v *Viewer) HandleReadLog(w http.ResponseWriter, r *http.Request) {
 	filename, err := utils.Mv(r, "file", false)
 	if err != nil {
@@ -87,10 +87,16 @@ func (v *Viewer) ListLogFiles(showFullpath bool) map[string][]*LogFile {
 				fullpath = ""
 			}
 
+			st, err := os.Stat(path)
+			if err != nil {
+				return nil
+			}
+
 			logList = append(logList, &LogFile{
 				Title:    strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)),
 				Filename: filepath.Base(path),
 				Fullpath: fullpath,
+				Filesize: st.Size(),
 			})
 
 			result[catergory] = logList

+ 611 - 611
mod/subservice/subservice.go

@@ -1,611 +1,611 @@
-package subservice
-
-import (
-	"encoding/json"
-	"errors"
-	"io/ioutil"
-	"log"
-	"net/http"
-	"net/url"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"runtime"
-	"sort"
-	"strconv"
-	"strings"
-	"time"
-
-	"imuslab.com/arozos/mod/info/logger"
-	modules "imuslab.com/arozos/mod/modules"
-	"imuslab.com/arozos/mod/network/reverseproxy"
-	"imuslab.com/arozos/mod/network/websocketproxy"
-	user "imuslab.com/arozos/mod/user"
-)
-
-/*
-	ArOZ Online System - Dynamic Subsystem loading services
-	author: tobychui
-
-	This module load in ArOZ Online Subservice using authorized reverse proxy channel.
-	Please see the demo subservice module for more information on implementing a subservice module.
-*/
-
-type SubService struct {
-	Port         int                        //Port that this subservice use
-	ServiceDir   string                     //The directory where the service is located
-	Path         string                     //Path that this subservice is located
-	RpEndpoint   string                     //Reverse Proxy Endpoint
-	ProxyHandler *reverseproxy.ReverseProxy //Reverse Proxy Object
-	Info         modules.ModuleInfo         //Module information for this subservice
-	Process      *exec.Cmd                  //The CMD runtime object of the process
-}
-
-type SubServiceRouter struct {
-	ReservePaths      []string
-	RunningSubService []SubService
-	BasePort          int
-
-	listenPort    int
-	userHandler   *user.UserHandler
-	moduleHandler *modules.ModuleHandler
-	logger        *logger.Logger
-}
-
-func NewSubServiceRouter(ReservePaths []string, basePort int, userHandler *user.UserHandler, moduleHandler *modules.ModuleHandler, parentPort int) *SubServiceRouter {
-	//Create a service logger
-	thisLogger, _ := logger.NewLogger("system", "system/logs/subservice", true)
-
-	return &SubServiceRouter{
-		ReservePaths:      ReservePaths,
-		RunningSubService: []SubService{},
-		BasePort:          basePort,
-
-		listenPort:    parentPort,
-		userHandler:   userHandler,
-		moduleHandler: moduleHandler,
-		logger:        thisLogger,
-	}
-}
-
-// Load and start all the subservices inside this rootpath
-func (sr *SubServiceRouter) LoadSubservicesFromRootPath(rootpath string) {
-	scanningPath := filepath.ToSlash(filepath.Clean(rootpath)) + "/*"
-
-	subservices, _ := filepath.Glob(scanningPath)
-	for _, servicePath := range subservices {
-		if !fileExists(servicePath + "/.disabled") {
-			//Only enable module with no suspended config file
-			err := sr.Launch(servicePath, true)
-			if err != nil {
-				sr.logger.PrintAndLog("Subservice", "Failed to start subservice: "+filepath.Base(servicePath)+" "+err.Error(), err)
-				//log.Println(err)
-			}
-		}
-
-	}
-}
-
-func (sr *SubServiceRouter) Launch(servicePath string, startupMode bool) error {
-	//Get the executable name from its path
-	servicePath = filepath.ToSlash(servicePath)
-	binaryname := filepath.Base(servicePath)
-	serviceRoot := filepath.Base(servicePath)
-	binaryExecPath := filepath.ToSlash(binaryname)
-	if runtime.GOOS == "windows" {
-		binaryExecPath = binaryExecPath + ".exe"
-	} else {
-		binaryExecPath = binaryExecPath + "_" + runtime.GOOS + "_" + runtime.GOARCH
-	}
-
-	//Check if startscript exists. If no, try to launch the binaries
-	if fileExists(servicePath + "/.startscript") {
-		//Launch from start.bat or start.sh
-		if !(fileExists(servicePath+"/start.sh") || fileExists(servicePath+"/start.bat")) {
-			//log.Println("Failed to load subservice: " + serviceRoot + ", .startscript flag is TRUE but no start script found")
-			return errors.New(".startscript flag is TRUE but no start script found")
-		}
-
-		startScriptName := "start.sh"
-		if runtime.GOOS == "windows" {
-			startScriptName = "start.bat"
-		}
-
-		binaryExecPath = startScriptName
-	} else {
-		//No startscript defined. Start from binary files if exists
-		if runtime.GOOS == "windows" && !fileExists(servicePath+"/"+binaryExecPath) {
-			if startupMode {
-				//log.Println("Failed to load subservice: " + serviceRoot)
-				return errors.New("Subservice executable not exists " + servicePath + "/" + binaryExecPath + ". Skipping this service")
-			} else {
-				return errors.New("Subservice executable " + servicePath + "/" + binaryExecPath + ". Skipping this service")
-			}
-
-		} else if runtime.GOOS == "linux" {
-			//Check if service installed using which
-			cmd := exec.Command("which", serviceRoot)
-			searchResults, _ := cmd.CombinedOutput()
-			if len(strings.TrimSpace(string(searchResults))) == 0 {
-				//This is not installed. Check if it exists as a binary (aka ./myservice)
-				if !fileExists(servicePath + "/" + binaryExecPath) {
-					if startupMode {
-						//log.Println("Package not installed. " + serviceRoot)
-						return errors.New("Package not installed")
-					} else {
-						return errors.New("Package not installed")
-					}
-				}
-			}
-		} else if runtime.GOOS == "darwin" {
-			//Skip the whereis approach that linux use
-			if !fileExists(servicePath + "/" + binaryExecPath) {
-				return errors.New("Subservice executable not exists " + servicePath + "/" + binaryExecPath + ". Skipping this service")
-			}
-		}
-	}
-
-	//Check if the suspend file exists. If yes, clear it
-	if fileExists(servicePath + "/.disabled") {
-		os.Remove(servicePath + "/.disabled")
-	}
-
-	//Check if there are config files that replace the -info tag. If yes, use it instead.
-	out := []byte{}
-	if fileExists(servicePath + "/moduleInfo.json") {
-		launchConfig, err := ioutil.ReadFile(servicePath + "/moduleInfo.json")
-		if err != nil {
-			if startupMode {
-				log.Fatal("Failed to read moduleInfo.json: "+binaryname, err)
-			} else {
-				return errors.New("Failed to read moduleInfo.json: " + binaryname)
-			}
-
-		}
-		out = launchConfig
-	} else {
-		infocmd := exec.Command(servicePath+"/"+binaryExecPath, "-info")
-		launchConfig, err := infocmd.CombinedOutput()
-		if err != nil {
-			sr.logger.PrintAndLog("Subservice", "Missing module startup info for "+servicePath, errors.New("Startup flag -info return no JSON string and moduleInfo.json does not exists for "+servicePath))
-			if startupMode {
-				log.Fatal("Unable to start service: "+binaryname, err)
-			} else {
-				return errors.New("Unable to start service: " + binaryname)
-			}
-
-		}
-		out = launchConfig
-	}
-
-	//Clean the module info and append it into the module list
-	serviceLaunchInfo := strings.TrimSpace(string(out))
-
-	thisModuleInfo := modules.ModuleInfo{}
-	err := json.Unmarshal([]byte(serviceLaunchInfo), &thisModuleInfo)
-	if err != nil {
-		if startupMode {
-			log.Fatal("Failed to load subservice: "+serviceRoot+"\n", err.Error())
-
-		} else {
-			return errors.New("Failed to load subservice: " + serviceRoot)
-		}
-	}
-
-	var thisSubService SubService
-	if fileExists(servicePath + "/.noproxy") {
-		//Adaptive mode. This is designed for modules that do not designed with ArOZ Online in mind.
-		//Ignore proxy setup and startup the application
-		absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
-		if fileExists(servicePath + "/.startscript") {
-			initPath := servicePath + "/start.sh"
-			if runtime.GOOS == "windows" {
-				initPath = servicePath + "/start.bat"
-			}
-
-			if !fileExists(initPath) {
-				if startupMode {
-					log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
-				} else {
-					return errors.New("start.sh not found. Unable to startup service " + serviceRoot)
-				}
-			}
-			absolutePath, _ = filepath.Abs(initPath)
-		}
-
-		cmd := exec.Command(absolutePath)
-		cmd.Stdout = os.Stdout
-		cmd.Stderr = os.Stderr
-		cmd.Dir = filepath.ToSlash(servicePath + "/")
-
-		//Spawn a new go routine to run this subservice
-		go func(cmdObject *exec.Cmd) {
-			if err := cmd.Start(); err != nil {
-				panic(err)
-			}
-		}(cmd)
-
-		//Create the servie object
-		thisSubService = SubService{
-			Path:       binaryExecPath,
-			Info:       thisModuleInfo,
-			ServiceDir: serviceRoot,
-			Process:    cmd,
-		}
-		sr.logger.PrintAndLog("Subservice", "Starting service "+serviceRoot+" in compatibility mode", nil)
-	} else {
-		//Create a proxy for this service
-		//Get proxy endpoint from startDir dir
-		rProxyEndpoint := filepath.Dir(thisModuleInfo.StartDir)
-		//Check if this path is reversed
-		if stringInSlice(rProxyEndpoint, sr.ReservePaths) || rProxyEndpoint == "" {
-			if startupMode {
-				log.Fatal(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
-			} else {
-				return errors.New(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
-			}
-		}
-
-		//Assign a port for this subservice
-		thisServicePort := sr.GetNextUsablePort()
-
-		//Run the subservice with the given port
-		absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
-
-		if fileExists(servicePath + "/.startscript") {
-			initPath := servicePath + "/start.sh"
-			if runtime.GOOS == "windows" {
-				initPath = servicePath + "/start.bat"
-			}
-
-			if !fileExists(initPath) {
-				if startupMode {
-					log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
-				} else {
-					return errors.New(serviceRoot + "start.sh not found. Unable to startup service " + serviceRoot)
-				}
-
-			}
-			absolutePath, _ = filepath.Abs(initPath)
-		}
-
-		servicePort := ":" + intToString(thisServicePort)
-		if fileExists(filepath.Join(servicePath, "/.intport")) {
-			servicePort = intToString(thisServicePort)
-		}
-
-		cmd := exec.Command(absolutePath, "-port", servicePort, "-rpt", "http://localhost:"+intToString(sr.listenPort)+"/api/ajgi/interface")
-		cmd.Stdout = os.Stdout
-		cmd.Stderr = os.Stderr
-		cmd.Dir = filepath.ToSlash(servicePath + "/")
-		//log.Println(cmd.Dir,binaryExecPath)
-
-		//Spawn a new go routine to run this subservice
-		go func(cmd *exec.Cmd) {
-			if err := cmd.Start(); err != nil {
-				panic(err)
-			}
-		}(cmd)
-
-		//Create a subservice object for this subservice
-		thisSubService = SubService{
-			Port:       thisServicePort,
-			Path:       binaryExecPath,
-			ServiceDir: serviceRoot,
-			RpEndpoint: rProxyEndpoint,
-			Info:       thisModuleInfo,
-			Process:    cmd,
-		}
-
-		sr.logger.PrintAndLog("Subservice", "Subservice Registered: "+thisModuleInfo.Name, nil)
-
-		//Create a new proxy object
-		path, _ := url.Parse("http://localhost:" + intToString(thisServicePort))
-		proxy := reverseproxy.NewReverseProxy(path)
-		thisSubService.ProxyHandler = proxy
-	}
-
-	//Append this subservice into the list
-	sr.RunningSubService = append(sr.RunningSubService, thisSubService)
-
-	//Append this module into the loaded module list
-	sr.moduleHandler.LoadedModule = append(sr.moduleHandler.LoadedModule, &thisModuleInfo)
-
-	return nil
-}
-
-func (sr *SubServiceRouter) HandleListing(w http.ResponseWriter, r *http.Request) {
-	//List all subservice running in the background
-	type visableInfo struct {
-		Port       int
-		ServiceDir string
-		Path       string
-		RpEndpoint string
-		ProcessID  int
-		Info       modules.ModuleInfo
-	}
-
-	type disabledServiceInfo struct {
-		ServiceDir string
-		Path       string
-	}
-
-	enabled := []visableInfo{}
-	disabled := []disabledServiceInfo{}
-	for _, thisSubservice := range sr.RunningSubService {
-		enabled = append(enabled, visableInfo{
-			Port:       thisSubservice.Port,
-			Path:       thisSubservice.Path,
-			ServiceDir: thisSubservice.ServiceDir,
-			RpEndpoint: thisSubservice.RpEndpoint,
-			ProcessID:  thisSubservice.Process.Process.Pid,
-			Info:       thisSubservice.Info,
-		})
-	}
-
-	disabledModules, _ := filepath.Glob("subservice/*/.disabled")
-	for _, modFile := range disabledModules {
-		thisdsi := new(disabledServiceInfo)
-		thisdsi.ServiceDir = filepath.Base(filepath.Dir(modFile))
-		thisdsi.Path = filepath.Base(filepath.Dir(modFile))
-		if runtime.GOOS == "windows" {
-			thisdsi.Path = thisdsi.Path + ".exe"
-		}
-		disabled = append(disabled, *thisdsi)
-
-	}
-
-	jsonString, err := json.Marshal(struct {
-		Enabled  []visableInfo
-		Disabled []disabledServiceInfo
-	}{
-		Enabled:  enabled,
-		Disabled: disabled,
-	})
-	if err != nil {
-		sr.logger.PrintAndLog("Subservice", "Unable to list subservice folder", err)
-	}
-	sendJSONResponse(w, string(jsonString))
-}
-
-// Kill the subservice that is currently running
-func (sr *SubServiceRouter) HandleKillSubService(w http.ResponseWriter, r *http.Request) {
-	userinfo, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
-	//Require admin permission
-	if !userinfo.IsAdmin() {
-		sendErrorResponse(w, "Permission denied")
-		return
-	}
-
-	//OK. Get paramters
-	serviceDir, _ := mv(r, "serviceDir", true)
-	//moduleName, _ := mv(r, "moduleName", true)
-
-	err := sr.KillSubService(serviceDir)
-	if err != nil {
-		sendErrorResponse(w, err.Error())
-	} else {
-		sendOK(w)
-	}
-
-}
-
-func (sr *SubServiceRouter) HandleStartSubService(w http.ResponseWriter, r *http.Request) {
-	userinfo, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
-
-	//Require admin permission
-	if !userinfo.IsAdmin() {
-		sendErrorResponse(w, "Permission denied")
-		return
-	}
-
-	//OK. Get which dir to start
-	serviceDir, _ := mv(r, "serviceDir", true)
-	err := sr.StartSubService(serviceDir)
-	if err != nil {
-		sendErrorResponse(w, err.Error())
-	} else {
-		sendOK(w)
-	}
-
-}
-
-// Check if the user has permission to access such proxy module
-func (sr *SubServiceRouter) CheckUserPermissionOnSubservice(ss *SubService, u *user.User) bool {
-	moduleName := ss.Info.Name
-	return u.GetModuleAccessPermission(moduleName)
-}
-
-// Check if the target is reverse proxy. If yes, return the proxy handler and the rewritten url in string
-func (sr *SubServiceRouter) CheckIfReverseProxyPath(r *http.Request) (bool, *reverseproxy.ReverseProxy, string, *SubService) {
-	requestURL := r.URL.Path
-
-	for _, subservice := range sr.RunningSubService {
-		thisServiceProxyEP := subservice.RpEndpoint
-		if thisServiceProxyEP != "" {
-			if len(requestURL) > len(thisServiceProxyEP)+1 && requestURL[1:len(thisServiceProxyEP)+1] == thisServiceProxyEP {
-				//This is a proxy path. Generate the rewrite URL
-				//Get all GET paramters from URL
-				values := r.URL.Query()
-				counter := 0
-				parsedGetTail := ""
-				for k, v := range values {
-					if counter == 0 {
-						parsedGetTail = "?" + k + "=" + url.QueryEscape(v[0])
-					} else {
-						parsedGetTail = parsedGetTail + "&" + k + "=" + url.QueryEscape(v[0])
-					}
-					counter++
-				}
-
-				return true, subservice.ProxyHandler, requestURL[len(thisServiceProxyEP)+1:] + parsedGetTail, &subservice
-			}
-		}
-	}
-	return false, nil, "", &SubService{}
-}
-
-func (sr *SubServiceRouter) Close() {
-	//Handle shutdown of subprocesses. Kill all of them
-	for _, subservice := range sr.RunningSubService {
-		cmd := subservice.Process
-		if cmd != nil {
-			if runtime.GOOS == "windows" {
-				//Force kill with the power of CMD
-				kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
-				//kill.Stderr = os.Stderr
-				//kill.Stdout = os.Stdout
-				kill.Run()
-			} else {
-				//Send sigkill to process
-				cmd.Process.Kill()
-			}
-		}
-	}
-}
-
-func (sr *SubServiceRouter) KillSubService(serviceDir string) error {
-	//Remove them from the system
-	ssi := -1
-	moduleName := ""
-	for i, ss := range sr.RunningSubService {
-		if ss.ServiceDir == serviceDir {
-			ssi = i
-			moduleName = ss.Info.Name
-			//Kill the module cmd
-			cmd := ss.Process
-			if cmd != nil {
-				if runtime.GOOS == "windows" {
-					//Force kill with the power of CMD
-					kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
-					kill.Run()
-				} else {
-					err := cmd.Process.Kill()
-					if err != nil {
-						return err
-					}
-				}
-			}
-
-			//Write a suspended file into the module
-			ioutil.WriteFile("subservice/"+ss.ServiceDir+"/.disabled", []byte(""), 0755)
-		}
-	}
-
-	//Pop this service from running Subservice
-	if ssi != -1 {
-		i := ssi
-		copy(sr.RunningSubService[i:], sr.RunningSubService[i+1:])
-		sr.RunningSubService = sr.RunningSubService[:len(sr.RunningSubService)-1]
-	}
-
-	//Pop the related module from the loadedModule list
-	mi := -1
-	for i, m := range sr.moduleHandler.LoadedModule {
-		if m.Name == moduleName {
-			mi = i
-		}
-	}
-	if mi != -1 {
-		i := mi
-		copy(sr.moduleHandler.LoadedModule[i:], sr.moduleHandler.LoadedModule[i+1:])
-		sr.moduleHandler.LoadedModule = sr.moduleHandler.LoadedModule[:len(sr.moduleHandler.LoadedModule)-1]
-	}
-	return nil
-}
-
-func (sr *SubServiceRouter) StartSubService(serviceDir string) error {
-	if fileExists("subservice/" + serviceDir) {
-		err := sr.Launch("subservice/"+serviceDir, false)
-		if err != nil {
-			return err
-		}
-	} else {
-		return errors.New("Subservice directory not exists.")
-	}
-
-	//Sort the list
-	sort.Slice(sr.moduleHandler.LoadedModule, func(i, j int) bool {
-		return sr.moduleHandler.LoadedModule[i].Name < sr.moduleHandler.LoadedModule[j].Name
-	})
-
-	sort.Slice(sr.RunningSubService, func(i, j int) bool {
-		return sr.RunningSubService[i].Info.Name < sr.RunningSubService[j].Info.Name
-	})
-
-	return nil
-}
-
-// Get a list of subservice roots in realpath
-func (sr *SubServiceRouter) GetSubserviceRoot() []string {
-	subserviceRoots := []string{}
-	for _, subService := range sr.RunningSubService {
-		subserviceRoots = append(subserviceRoots, subService.Path)
-	}
-
-	return subserviceRoots
-}
-
-// Scan and get the next avaible port for subservice from its basePort
-func (sr *SubServiceRouter) GetNextUsablePort() int {
-	basePort := sr.BasePort
-	for sr.CheckIfPortInUse(basePort) {
-		basePort++
-	}
-	return basePort
-}
-
-func (sr *SubServiceRouter) CheckIfPortInUse(port int) bool {
-	for _, service := range sr.RunningSubService {
-		if service.Port == port {
-			return true
-		}
-	}
-	return false
-}
-
-func (sr *SubServiceRouter) HandleRoutingRequest(w http.ResponseWriter, r *http.Request, proxy *reverseproxy.ReverseProxy, subserviceObject *SubService, rewriteURL string) {
-	u, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
-	if !sr.CheckUserPermissionOnSubservice(subserviceObject, u) {
-		//Permission denied
-		http.NotFound(w, r)
-		return
-	}
-	//Perform reverse proxy serving
-	r.URL, _ = url.Parse(rewriteURL)
-	token, _ := sr.userHandler.GetAuthAgent().NewTokenFromRequest(w, r)
-	r.Header.Set("aouser", u.Username)
-	r.Header.Set("aotoken", token)
-	r.Header.Set("X-Forwarded-Host", r.Host)
-	if r.Header["Upgrade"] != nil && r.Header["Upgrade"][0] == "websocket" {
-		//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
-		r.Header.Set("A-Upgrade", "websocket")
-		u, _ := url.Parse("ws://localhost:" + strconv.Itoa(subserviceObject.Port) + r.URL.String())
-		wspHandler := websocketproxy.NewProxy(u)
-		wspHandler.ServeHTTP(w, r)
-		return
-	}
-
-	r.Host = r.URL.Host
-	err := proxy.ServeHTTP(w, r)
-	if err != nil {
-		//Check if it is cancelling events.
-		if !strings.Contains(err.Error(), "cancel") {
-			sr.logger.PrintAndLog("Subservice", subserviceObject.Info.Name+" IS NOT RESPONDING!", err)
-			sr.RestartSubService(subserviceObject)
-		}
-
-	}
-}
-
-// Handle fail start over when the remote target is not responding
-func (sr *SubServiceRouter) RestartSubService(ss *SubService) {
-	go func(ss *SubService) {
-		//Kill the original subservice
-		sr.KillSubService(ss.ServiceDir)
-		sr.logger.PrintAndLog("Subservice", "RESTARTING SUBSERVICE "+ss.Info.Name+" IN 10 SECOUNDS", nil)
-		time.Sleep(10000 * time.Millisecond)
-		sr.StartSubService(ss.ServiceDir)
-		sr.logger.PrintAndLog("Subservice", "SUBSERVICE "+ss.Info.Name+" RESTARTED", nil)
-	}(ss)
-}
+package subservice
+
+import (
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+
+	"imuslab.com/arozos/mod/info/logger"
+	modules "imuslab.com/arozos/mod/modules"
+	"imuslab.com/arozos/mod/network/reverseproxy"
+	"imuslab.com/arozos/mod/network/websocketproxy"
+	user "imuslab.com/arozos/mod/user"
+)
+
+/*
+	ArOZ Online System - Dynamic Subsystem loading services
+	author: tobychui
+
+	This module load in ArOZ Online Subservice using authorized reverse proxy channel.
+	Please see the demo subservice module for more information on implementing a subservice module.
+*/
+
+type SubService struct {
+	Port         int                        //Port that this subservice use
+	ServiceDir   string                     //The directory where the service is located
+	Path         string                     //Path that this subservice is located
+	RpEndpoint   string                     //Reverse Proxy Endpoint
+	ProxyHandler *reverseproxy.ReverseProxy //Reverse Proxy Object
+	Info         modules.ModuleInfo         //Module information for this subservice
+	Process      *exec.Cmd                  //The CMD runtime object of the process
+}
+
+type SubServiceRouter struct {
+	ReservePaths      []string
+	RunningSubService []SubService
+	BasePort          int
+
+	listenPort    int
+	userHandler   *user.UserHandler
+	moduleHandler *modules.ModuleHandler
+	logger        *logger.Logger
+}
+
+func NewSubServiceRouter(ReservePaths []string, basePort int, userHandler *user.UserHandler, moduleHandler *modules.ModuleHandler, parentPort int) *SubServiceRouter {
+	//Create a service logger
+	thisLogger, _ := logger.NewLogger("subserv", "system/logs/subservice", true)
+
+	return &SubServiceRouter{
+		ReservePaths:      ReservePaths,
+		RunningSubService: []SubService{},
+		BasePort:          basePort,
+
+		listenPort:    parentPort,
+		userHandler:   userHandler,
+		moduleHandler: moduleHandler,
+		logger:        thisLogger,
+	}
+}
+
+// Load and start all the subservices inside this rootpath
+func (sr *SubServiceRouter) LoadSubservicesFromRootPath(rootpath string) {
+	scanningPath := filepath.ToSlash(filepath.Clean(rootpath)) + "/*"
+
+	subservices, _ := filepath.Glob(scanningPath)
+	for _, servicePath := range subservices {
+		if !fileExists(servicePath + "/.disabled") {
+			//Only enable module with no suspended config file
+			err := sr.Launch(servicePath, true)
+			if err != nil {
+				sr.logger.PrintAndLog("Subservice", "Failed to start subservice: "+filepath.Base(servicePath)+" "+err.Error(), err)
+				//log.Println(err)
+			}
+		}
+
+	}
+}
+
+func (sr *SubServiceRouter) Launch(servicePath string, startupMode bool) error {
+	//Get the executable name from its path
+	servicePath = filepath.ToSlash(servicePath)
+	binaryname := filepath.Base(servicePath)
+	serviceRoot := filepath.Base(servicePath)
+	binaryExecPath := filepath.ToSlash(binaryname)
+	if runtime.GOOS == "windows" {
+		binaryExecPath = binaryExecPath + ".exe"
+	} else {
+		binaryExecPath = binaryExecPath + "_" + runtime.GOOS + "_" + runtime.GOARCH
+	}
+
+	//Check if startscript exists. If no, try to launch the binaries
+	if fileExists(servicePath + "/.startscript") {
+		//Launch from start.bat or start.sh
+		if !(fileExists(servicePath+"/start.sh") || fileExists(servicePath+"/start.bat")) {
+			//log.Println("Failed to load subservice: " + serviceRoot + ", .startscript flag is TRUE but no start script found")
+			return errors.New(".startscript flag is TRUE but no start script found")
+		}
+
+		startScriptName := "start.sh"
+		if runtime.GOOS == "windows" {
+			startScriptName = "start.bat"
+		}
+
+		binaryExecPath = startScriptName
+	} else {
+		//No startscript defined. Start from binary files if exists
+		if runtime.GOOS == "windows" && !fileExists(servicePath+"/"+binaryExecPath) {
+			if startupMode {
+				//log.Println("Failed to load subservice: " + serviceRoot)
+				return errors.New("Subservice executable not exists " + servicePath + "/" + binaryExecPath + ". Skipping this service")
+			} else {
+				return errors.New("Subservice executable " + servicePath + "/" + binaryExecPath + ". Skipping this service")
+			}
+
+		} else if runtime.GOOS == "linux" {
+			//Check if service installed using which
+			cmd := exec.Command("which", serviceRoot)
+			searchResults, _ := cmd.CombinedOutput()
+			if len(strings.TrimSpace(string(searchResults))) == 0 {
+				//This is not installed. Check if it exists as a binary (aka ./myservice)
+				if !fileExists(servicePath + "/" + binaryExecPath) {
+					if startupMode {
+						//log.Println("Package not installed. " + serviceRoot)
+						return errors.New("Package not installed")
+					} else {
+						return errors.New("Package not installed")
+					}
+				}
+			}
+		} else if runtime.GOOS == "darwin" {
+			//Skip the whereis approach that linux use
+			if !fileExists(servicePath + "/" + binaryExecPath) {
+				return errors.New("Subservice executable not exists " + servicePath + "/" + binaryExecPath + ". Skipping this service")
+			}
+		}
+	}
+
+	//Check if the suspend file exists. If yes, clear it
+	if fileExists(servicePath + "/.disabled") {
+		os.Remove(servicePath + "/.disabled")
+	}
+
+	//Check if there are config files that replace the -info tag. If yes, use it instead.
+	out := []byte{}
+	if fileExists(servicePath + "/moduleInfo.json") {
+		launchConfig, err := ioutil.ReadFile(servicePath + "/moduleInfo.json")
+		if err != nil {
+			if startupMode {
+				log.Fatal("Failed to read moduleInfo.json: "+binaryname, err)
+			} else {
+				return errors.New("Failed to read moduleInfo.json: " + binaryname)
+			}
+
+		}
+		out = launchConfig
+	} else {
+		infocmd := exec.Command(servicePath+"/"+binaryExecPath, "-info")
+		launchConfig, err := infocmd.CombinedOutput()
+		if err != nil {
+			sr.logger.PrintAndLog("Subservice", "Missing module startup info for "+servicePath, errors.New("Startup flag -info return no JSON string and moduleInfo.json does not exists for "+servicePath))
+			if startupMode {
+				log.Fatal("Unable to start service: "+binaryname, err)
+			} else {
+				return errors.New("Unable to start service: " + binaryname)
+			}
+
+		}
+		out = launchConfig
+	}
+
+	//Clean the module info and append it into the module list
+	serviceLaunchInfo := strings.TrimSpace(string(out))
+
+	thisModuleInfo := modules.ModuleInfo{}
+	err := json.Unmarshal([]byte(serviceLaunchInfo), &thisModuleInfo)
+	if err != nil {
+		if startupMode {
+			log.Fatal("Failed to load subservice: "+serviceRoot+"\n", err.Error())
+
+		} else {
+			return errors.New("Failed to load subservice: " + serviceRoot)
+		}
+	}
+
+	var thisSubService SubService
+	if fileExists(servicePath + "/.noproxy") {
+		//Adaptive mode. This is designed for modules that do not designed with ArOZ Online in mind.
+		//Ignore proxy setup and startup the application
+		absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
+		if fileExists(servicePath + "/.startscript") {
+			initPath := servicePath + "/start.sh"
+			if runtime.GOOS == "windows" {
+				initPath = servicePath + "/start.bat"
+			}
+
+			if !fileExists(initPath) {
+				if startupMode {
+					log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
+				} else {
+					return errors.New("start.sh not found. Unable to startup service " + serviceRoot)
+				}
+			}
+			absolutePath, _ = filepath.Abs(initPath)
+		}
+
+		cmd := exec.Command(absolutePath)
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		cmd.Dir = filepath.ToSlash(servicePath + "/")
+
+		//Spawn a new go routine to run this subservice
+		go func(cmdObject *exec.Cmd) {
+			if err := cmd.Start(); err != nil {
+				panic(err)
+			}
+		}(cmd)
+
+		//Create the servie object
+		thisSubService = SubService{
+			Path:       binaryExecPath,
+			Info:       thisModuleInfo,
+			ServiceDir: serviceRoot,
+			Process:    cmd,
+		}
+		sr.logger.PrintAndLog("Subservice", "Starting service "+serviceRoot+" in compatibility mode", nil)
+	} else {
+		//Create a proxy for this service
+		//Get proxy endpoint from startDir dir
+		rProxyEndpoint := filepath.Dir(thisModuleInfo.StartDir)
+		//Check if this path is reversed
+		if stringInSlice(rProxyEndpoint, sr.ReservePaths) || rProxyEndpoint == "" {
+			if startupMode {
+				log.Fatal(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
+			} else {
+				return errors.New(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
+			}
+		}
+
+		//Assign a port for this subservice
+		thisServicePort := sr.GetNextUsablePort()
+
+		//Run the subservice with the given port
+		absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
+
+		if fileExists(servicePath + "/.startscript") {
+			initPath := servicePath + "/start.sh"
+			if runtime.GOOS == "windows" {
+				initPath = servicePath + "/start.bat"
+			}
+
+			if !fileExists(initPath) {
+				if startupMode {
+					log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
+				} else {
+					return errors.New(serviceRoot + "start.sh not found. Unable to startup service " + serviceRoot)
+				}
+
+			}
+			absolutePath, _ = filepath.Abs(initPath)
+		}
+
+		servicePort := ":" + intToString(thisServicePort)
+		if fileExists(filepath.Join(servicePath, "/.intport")) {
+			servicePort = intToString(thisServicePort)
+		}
+
+		cmd := exec.Command(absolutePath, "-port", servicePort, "-rpt", "http://localhost:"+intToString(sr.listenPort)+"/api/ajgi/interface")
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		cmd.Dir = filepath.ToSlash(servicePath + "/")
+		//log.Println(cmd.Dir,binaryExecPath)
+
+		//Spawn a new go routine to run this subservice
+		go func(cmd *exec.Cmd) {
+			if err := cmd.Start(); err != nil {
+				panic(err)
+			}
+		}(cmd)
+
+		//Create a subservice object for this subservice
+		thisSubService = SubService{
+			Port:       thisServicePort,
+			Path:       binaryExecPath,
+			ServiceDir: serviceRoot,
+			RpEndpoint: rProxyEndpoint,
+			Info:       thisModuleInfo,
+			Process:    cmd,
+		}
+
+		sr.logger.PrintAndLog("Subservice", "Subservice Registered: "+thisModuleInfo.Name, nil)
+
+		//Create a new proxy object
+		path, _ := url.Parse("http://localhost:" + intToString(thisServicePort))
+		proxy := reverseproxy.NewReverseProxy(path)
+		thisSubService.ProxyHandler = proxy
+	}
+
+	//Append this subservice into the list
+	sr.RunningSubService = append(sr.RunningSubService, thisSubService)
+
+	//Append this module into the loaded module list
+	sr.moduleHandler.LoadedModule = append(sr.moduleHandler.LoadedModule, &thisModuleInfo)
+
+	return nil
+}
+
+func (sr *SubServiceRouter) HandleListing(w http.ResponseWriter, r *http.Request) {
+	//List all subservice running in the background
+	type visableInfo struct {
+		Port       int
+		ServiceDir string
+		Path       string
+		RpEndpoint string
+		ProcessID  int
+		Info       modules.ModuleInfo
+	}
+
+	type disabledServiceInfo struct {
+		ServiceDir string
+		Path       string
+	}
+
+	enabled := []visableInfo{}
+	disabled := []disabledServiceInfo{}
+	for _, thisSubservice := range sr.RunningSubService {
+		enabled = append(enabled, visableInfo{
+			Port:       thisSubservice.Port,
+			Path:       thisSubservice.Path,
+			ServiceDir: thisSubservice.ServiceDir,
+			RpEndpoint: thisSubservice.RpEndpoint,
+			ProcessID:  thisSubservice.Process.Process.Pid,
+			Info:       thisSubservice.Info,
+		})
+	}
+
+	disabledModules, _ := filepath.Glob("subservice/*/.disabled")
+	for _, modFile := range disabledModules {
+		thisdsi := new(disabledServiceInfo)
+		thisdsi.ServiceDir = filepath.Base(filepath.Dir(modFile))
+		thisdsi.Path = filepath.Base(filepath.Dir(modFile))
+		if runtime.GOOS == "windows" {
+			thisdsi.Path = thisdsi.Path + ".exe"
+		}
+		disabled = append(disabled, *thisdsi)
+
+	}
+
+	jsonString, err := json.Marshal(struct {
+		Enabled  []visableInfo
+		Disabled []disabledServiceInfo
+	}{
+		Enabled:  enabled,
+		Disabled: disabled,
+	})
+	if err != nil {
+		sr.logger.PrintAndLog("Subservice", "Unable to list subservice folder", err)
+	}
+	sendJSONResponse(w, string(jsonString))
+}
+
+// Kill the subservice that is currently running
+func (sr *SubServiceRouter) HandleKillSubService(w http.ResponseWriter, r *http.Request) {
+	userinfo, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
+	//Require admin permission
+	if !userinfo.IsAdmin() {
+		sendErrorResponse(w, "Permission denied")
+		return
+	}
+
+	//OK. Get paramters
+	serviceDir, _ := mv(r, "serviceDir", true)
+	//moduleName, _ := mv(r, "moduleName", true)
+
+	err := sr.KillSubService(serviceDir)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+	} else {
+		sendOK(w)
+	}
+
+}
+
+func (sr *SubServiceRouter) HandleStartSubService(w http.ResponseWriter, r *http.Request) {
+	userinfo, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
+
+	//Require admin permission
+	if !userinfo.IsAdmin() {
+		sendErrorResponse(w, "Permission denied")
+		return
+	}
+
+	//OK. Get which dir to start
+	serviceDir, _ := mv(r, "serviceDir", true)
+	err := sr.StartSubService(serviceDir)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+	} else {
+		sendOK(w)
+	}
+
+}
+
+// Check if the user has permission to access such proxy module
+func (sr *SubServiceRouter) CheckUserPermissionOnSubservice(ss *SubService, u *user.User) bool {
+	moduleName := ss.Info.Name
+	return u.GetModuleAccessPermission(moduleName)
+}
+
+// Check if the target is reverse proxy. If yes, return the proxy handler and the rewritten url in string
+func (sr *SubServiceRouter) CheckIfReverseProxyPath(r *http.Request) (bool, *reverseproxy.ReverseProxy, string, *SubService) {
+	requestURL := r.URL.Path
+
+	for _, subservice := range sr.RunningSubService {
+		thisServiceProxyEP := subservice.RpEndpoint
+		if thisServiceProxyEP != "" {
+			if len(requestURL) > len(thisServiceProxyEP)+1 && requestURL[1:len(thisServiceProxyEP)+1] == thisServiceProxyEP {
+				//This is a proxy path. Generate the rewrite URL
+				//Get all GET paramters from URL
+				values := r.URL.Query()
+				counter := 0
+				parsedGetTail := ""
+				for k, v := range values {
+					if counter == 0 {
+						parsedGetTail = "?" + k + "=" + url.QueryEscape(v[0])
+					} else {
+						parsedGetTail = parsedGetTail + "&" + k + "=" + url.QueryEscape(v[0])
+					}
+					counter++
+				}
+
+				return true, subservice.ProxyHandler, requestURL[len(thisServiceProxyEP)+1:] + parsedGetTail, &subservice
+			}
+		}
+	}
+	return false, nil, "", &SubService{}
+}
+
+func (sr *SubServiceRouter) Close() {
+	//Handle shutdown of subprocesses. Kill all of them
+	for _, subservice := range sr.RunningSubService {
+		cmd := subservice.Process
+		if cmd != nil {
+			if runtime.GOOS == "windows" {
+				//Force kill with the power of CMD
+				kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
+				//kill.Stderr = os.Stderr
+				//kill.Stdout = os.Stdout
+				kill.Run()
+			} else {
+				//Send sigkill to process
+				cmd.Process.Kill()
+			}
+		}
+	}
+}
+
+func (sr *SubServiceRouter) KillSubService(serviceDir string) error {
+	//Remove them from the system
+	ssi := -1
+	moduleName := ""
+	for i, ss := range sr.RunningSubService {
+		if ss.ServiceDir == serviceDir {
+			ssi = i
+			moduleName = ss.Info.Name
+			//Kill the module cmd
+			cmd := ss.Process
+			if cmd != nil {
+				if runtime.GOOS == "windows" {
+					//Force kill with the power of CMD
+					kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
+					kill.Run()
+				} else {
+					err := cmd.Process.Kill()
+					if err != nil {
+						return err
+					}
+				}
+			}
+
+			//Write a suspended file into the module
+			ioutil.WriteFile("subservice/"+ss.ServiceDir+"/.disabled", []byte(""), 0755)
+		}
+	}
+
+	//Pop this service from running Subservice
+	if ssi != -1 {
+		i := ssi
+		copy(sr.RunningSubService[i:], sr.RunningSubService[i+1:])
+		sr.RunningSubService = sr.RunningSubService[:len(sr.RunningSubService)-1]
+	}
+
+	//Pop the related module from the loadedModule list
+	mi := -1
+	for i, m := range sr.moduleHandler.LoadedModule {
+		if m.Name == moduleName {
+			mi = i
+		}
+	}
+	if mi != -1 {
+		i := mi
+		copy(sr.moduleHandler.LoadedModule[i:], sr.moduleHandler.LoadedModule[i+1:])
+		sr.moduleHandler.LoadedModule = sr.moduleHandler.LoadedModule[:len(sr.moduleHandler.LoadedModule)-1]
+	}
+	return nil
+}
+
+func (sr *SubServiceRouter) StartSubService(serviceDir string) error {
+	if fileExists("subservice/" + serviceDir) {
+		err := sr.Launch("subservice/"+serviceDir, false)
+		if err != nil {
+			return err
+		}
+	} else {
+		return errors.New("Subservice directory not exists.")
+	}
+
+	//Sort the list
+	sort.Slice(sr.moduleHandler.LoadedModule, func(i, j int) bool {
+		return sr.moduleHandler.LoadedModule[i].Name < sr.moduleHandler.LoadedModule[j].Name
+	})
+
+	sort.Slice(sr.RunningSubService, func(i, j int) bool {
+		return sr.RunningSubService[i].Info.Name < sr.RunningSubService[j].Info.Name
+	})
+
+	return nil
+}
+
+// Get a list of subservice roots in realpath
+func (sr *SubServiceRouter) GetSubserviceRoot() []string {
+	subserviceRoots := []string{}
+	for _, subService := range sr.RunningSubService {
+		subserviceRoots = append(subserviceRoots, subService.Path)
+	}
+
+	return subserviceRoots
+}
+
+// Scan and get the next avaible port for subservice from its basePort
+func (sr *SubServiceRouter) GetNextUsablePort() int {
+	basePort := sr.BasePort
+	for sr.CheckIfPortInUse(basePort) {
+		basePort++
+	}
+	return basePort
+}
+
+func (sr *SubServiceRouter) CheckIfPortInUse(port int) bool {
+	for _, service := range sr.RunningSubService {
+		if service.Port == port {
+			return true
+		}
+	}
+	return false
+}
+
+func (sr *SubServiceRouter) HandleRoutingRequest(w http.ResponseWriter, r *http.Request, proxy *reverseproxy.ReverseProxy, subserviceObject *SubService, rewriteURL string) {
+	u, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
+	if !sr.CheckUserPermissionOnSubservice(subserviceObject, u) {
+		//Permission denied
+		http.NotFound(w, r)
+		return
+	}
+	//Perform reverse proxy serving
+	r.URL, _ = url.Parse(rewriteURL)
+	token, _ := sr.userHandler.GetAuthAgent().NewTokenFromRequest(w, r)
+	r.Header.Set("aouser", u.Username)
+	r.Header.Set("aotoken", token)
+	r.Header.Set("X-Forwarded-Host", r.Host)
+	if r.Header["Upgrade"] != nil && r.Header["Upgrade"][0] == "websocket" {
+		//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
+		r.Header.Set("A-Upgrade", "websocket")
+		u, _ := url.Parse("ws://localhost:" + strconv.Itoa(subserviceObject.Port) + r.URL.String())
+		wspHandler := websocketproxy.NewProxy(u)
+		wspHandler.ServeHTTP(w, r)
+		return
+	}
+
+	r.Host = r.URL.Host
+	err := proxy.ServeHTTP(w, r)
+	if err != nil {
+		//Check if it is cancelling events.
+		if !strings.Contains(err.Error(), "cancel") {
+			sr.logger.PrintAndLog("Subservice", subserviceObject.Info.Name+" IS NOT RESPONDING!", err)
+			sr.RestartSubService(subserviceObject)
+		}
+
+	}
+}
+
+// Handle fail start over when the remote target is not responding
+func (sr *SubServiceRouter) RestartSubService(ss *SubService) {
+	go func(ss *SubService) {
+		//Kill the original subservice
+		sr.KillSubService(ss.ServiceDir)
+		sr.logger.PrintAndLog("Subservice", "RESTARTING SUBSERVICE "+ss.Info.Name+" IN 10 SECOUNDS", nil)
+		time.Sleep(10000 * time.Millisecond)
+		sr.StartSubService(ss.ServiceDir)
+		sr.logger.PrintAndLog("Subservice", "SUBSERVICE "+ss.Info.Name+" RESTARTED", nil)
+	}(ss)
+}

+ 1 - 1
setting.go

@@ -100,7 +100,7 @@ func system_setting_getSettingGroups() []settingGroup {
 			Desc:     "System Security and Auth Credentials",
 		},
 		{
-			Name:     "Advance Options",
+			Name:     "Developer Options",
 			Group:    "Advance",
 			IconPath: "SystemAO/system_setting/img/code.svg",
 			Desc:     "Advance configs for developers",

+ 10 - 0
system.info.go

@@ -173,6 +173,16 @@ func SystemInfoInit() {
 
 	adminRouter.HandleFunc("/system/log/list", logViewer.HandleListLog)
 	adminRouter.HandleFunc("/system/log/read", logViewer.HandleReadLog)
+
+	registerSetting(settingModule{
+		Name:         "System Log",
+		Desc:         "View ArozOS System Log",
+		IconPath:     "SystemAO/updates/img/update.png",
+		Group:        "Advance",
+		StartDir:     "SystemAO/advance/logview.html",
+		RequireAdmin: true,
+	})
+
 }
 
 func InfoHandleGetRuntimeInfo(w http.ResponseWriter, r *http.Request) {

+ 143 - 0
web/SystemAO/advance/logview.html

@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<html ng-app="App">
+<head>
+    <title>System Logs</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
+    <link rel="stylesheet" href="../../script/semantic/semantic.min.css">
+    <script type="text/javascript" src="../../script/jquery.min.js"></script>
+    <script type="text/javascript" src="../../script/semantic/semantic.min.js"></script>
+    <style>
+        .clickable{
+            cursor: pointer;
+        }
+        .clickable:hover{
+            opacity: 0.7;
+        }
+        .logfile{
+            padding-left: 1em !important;
+            position: relative;
+            padding-right: 1em !important;
+        }
+
+        .loglist{
+            background-color: rgb(250, 250, 250);
+        }
+
+        .logfile .showing{
+            position: absolute;
+            top: 0.18em;
+            right: 0em;
+            margin-right: -0.4em;
+            opacity: 0;
+        }
+
+        .logfile.active .showing{
+            opacity: 1;
+        }
+
+        #logrender{
+            width: 100% !important;
+            height: calc(100% - 1.2em);
+            min-height: 50vh;
+            border: 1px solid rgb(231, 231, 231);
+            font-family: monospace;  
+        }
+    </style>
+</head>
+
+<body>
+    <div class="ui container">
+        <div class="ui stackable grid">
+            <div class="four wide column loglist">
+                <h3 class="ui header" style="padding-top: 1em;">
+                    <div class="content">
+                        Log View
+                        <div class="sub header">Check System Log in Real Time</div>
+                    </div>
+                </h3>
+                <div class="ui divider"></div>
+                <div id="logList" class="ui accordion">
+                    
+                </div>
+                <div class="ui divider"></div>
+                <small>Notes: Some log file might be huge. Make sure you have checked the log file size before opening</small>
+            </div>
+            <div class="twelve wide column">
+                <textarea id="logrender" spellcheck="false" readonly="true">
+← Pick a log file from the left menu to start debugging
+                </textarea>
+                <a href="#" onclick="openLogInNewTab();">Open In New Tab</a>
+                <br><br>
+            </div>
+        </div>
+    </div>
+    <div class="ui divider"></div>
+    <br>
+</body>
+<script>
+    var currentOpenedLogURL = "";
+
+    function openLogInNewTab(){
+        if (currentOpenedLogURL != ""){
+            window.open(currentOpenedLogURL);
+        }
+    }
+
+    function openLog(object, catergory, filename){
+        $(".logfile.active").removeClass('active');
+        $(object).addClass("active");
+        currentOpenedLogURL = "../../system/log/read?file=" + filename + "&catergory=" + catergory;
+        $.get(currentOpenedLogURL, function(data){
+            if (data.error !== undefined){
+                alert(data.error);
+                return;
+            }
+            $("#logrender").val(data);
+        }); 
+    }
+
+    function initLogList(){
+        $("#logList").html("");
+        $.get("../../system/log/list", function(data){
+            //console.log(data);
+            for (let [key, value] of Object.entries(data)) {
+                console.log(key, value);
+                value.reverse(); //Default value was from oldest to newest
+                var fileItemList = "";
+                value.forEach(file => {
+                    fileItemList += `<div class="item clickable logfile" onclick="openLog(this, '${key}','${file.Filename}');">
+                            <i class="file outline icon"></i>
+                            <div class="content">
+                                ${file.Title} (${formatBytes(file.Filesize)})
+                                <div class="showing"><i class="green chevron right icon"></i></div>
+                            </div>
+                        </div>`;
+                })
+                $("#logList").append(`<div class="title">
+                    <i class="dropdown icon"></i>
+                        ${key}
+                    </div>
+                    <div class="content">
+                        <div class="ui list">
+                            ${fileItemList}
+                        </div>
+                    </div>`);
+            }
+
+            $(".ui.accordion").accordion();
+        });
+    }
+    initLogList();
+
+    
+    function formatBytes(x){
+        var units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+        let l = 0, n = parseInt(x, 10) || 0;
+        while(n >= 1024 && ++l){
+            n = n/1024;
+        }
+        return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
+    }
+</script>
+</html>