0
0
mirror of https://github.com/rls-moe/nyx synced 2025-08-19 06:18:38 +00:00

Improved stability, improved spam algorithm, fixed some bugs

This commit is contained in:
Tim Schuster
2017-03-15 09:13:15 +01:00
parent afd9ae71cf
commit 19d0e9282d
37 changed files with 2099 additions and 255 deletions

View File

@@ -1,21 +1,139 @@
package admin
import (
"encoding/json"
"fmt"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"net/http"
"strings"
"time"
)
func handleCleanup(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
if sess.CAttr("mode") != "admin" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
fmt.Println("Beginning cleanup...")
db := middle.GetDB(r)
err := db.Update(func(tx *buntdb.Tx) error {
var delKeys = []string{}
err := db.View(func(tx *buntdb.Tx) error {
var err error
tx.AscendKeys("*", func(key, value string) bool {
keyType := detectType(key)
if keyType == "thread" {
var host string
host, err = resources.GetHostnameFromKey(key)
if err != nil {
fmt.Printf("Error: %s", err)
return false
}
var thread = &resources.Thread{}
err = json.Unmarshal([]byte(value), thread)
if err != nil {
fmt.Printf("Error: %s", err)
return false
}
threadTime := resources.DateFromId(thread.ID)
dur := threadTime.Sub(time.Now())
if dur > time.Hour*24*7 {
fmt.Printf("Sched %s for deletion: expired\n", key)
delKeys = append(delKeys, key)
return true
}
err = resources.FillReplies(tx, host, thread)
if err != nil {
fmt.Printf("Error: %s", err)
return false
}
if len(thread.GetReplies()) == 0 {
fmt.Printf("Sched %s for deletion: empty\n", key)
delKeys = append(delKeys, key)
return true
}
if _, err := resources.GetReply(tx, host, thread.Board, thread.ID, thread.StartReply); err == buntdb.ErrNotFound {
fmt.Printf("Sched %s for delection: main reply dead\n", key)
delKeys = append(delKeys, key)
return true
}
} else if keyType == "reply" {
var host string
host, err = resources.GetHostnameFromKey(key)
if err != nil {
fmt.Printf("Error: %s", err)
return false
}
var reply = &resources.Reply{}
err = json.Unmarshal([]byte(value), reply)
if err != nil {
fmt.Printf("Error: %s", err)
return false
}
replyTime := resources.DateFromId(reply.ID)
dur := replyTime.Sub(time.Now())
if dur > time.Hour*24*7 {
fmt.Printf("Sched %s for deletion: expired\n", key)
delKeys = append(delKeys, key)
return true
}
if val, ok := reply.Metadata["deleted"]; ok && val == "yes" {
fmt.Printf("Sched %s for deletion: deleted\n", key)
delKeys = append(delKeys, key)
return true
}
if err := resources.TestThread(tx, host, reply.Board, reply.Thread); err == buntdb.ErrNotFound {
fmt.Printf("Sched %s for deletion: missing parent %d: %s\n", key, reply.Thread, err)
delKeys = append(delKeys, key)
return true
}
}
return true
})
/* Insert cleanup codes here */
return err
})
fmt.Println("Removing sched' entries")
db.Update(func(tx *buntdb.Tx) error {
for _, v := range delKeys {
fmt.Printf("Deleting %s\n", v)
tx.Delete(v)
}
return nil
})
fmt.Println("Shrinking DB")
err = db.Shrink()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
fmt.Println("Finished Cleanup")
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}
func detectType(key string) string {
if strings.Contains(key, "/jack/") {
if strings.HasSuffix(key, "/board-data") {
return "board"
}
if strings.HasSuffix(key, "/thread") {
return "thread"
}
if strings.HasSuffix(key, "/reply-data") {
return "reply"
}
return "system"
}
return "none"
}

74
http/admin/delpost.go Normal file
View File

@@ -0,0 +1,74 @@
package admin
import (
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"strconv"
"go.rls.moe/nyx/resources"
"fmt"
"net/http"
"github.com/tidwall/buntdb"
)
func handleDelPost(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
if sess.CAttr("mode") != "admin" && sess.CAttr("mode") != "mod" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
rid, err := strconv.Atoi(r.FormValue("reply_id"))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
trid, err := strconv.Atoi(r.FormValue("thread_id"))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
board := r.FormValue("board")
if sess.CAttr("mode") == "mod" && sess.CAttr("board") != board {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Not on this board"))
return
}
db := middle.GetDB(r)
err = db.Update(func(tx *buntdb.Tx) error {
reply, err := resources.GetReply(tx, r.Host, board, trid, rid)
if err != nil {
return err
}
reply.Text = "[deleted]"
reply.Metadata["deleted"] = "yes"
reply.Image = nil
reply.Thumbnail = nil
err = resources.UpdateReply(tx, r.Host, board, reply)
if err != nil {
return err
}
return nil
})
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.Redirect(w, r, fmt.Sprintf("/%s/%d/thread.html", board, trid), http.StatusSeeOther)
}

View File

@@ -2,17 +2,12 @@ package admin
import (
"bytes"
"fmt"
"github.com/GeertJohan/go.rice"
"github.com/icza/session"
"github.com/pressly/chi"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"html/template"
"net/http"
"strconv"
"time"
)
@@ -27,8 +22,9 @@ var riceConf = rice.Config{
var box = riceConf.MustFindBox("http/admin/res/")
var (
panelTmpl = template.New("admin/panel")
loginTmpl = template.New("admin/login")
panelTmpl = template.New("admin/panel")
loginTmpl = template.New("admin/login")
statusTmpl = template.New("admin/status")
)
func init() {
@@ -41,6 +37,10 @@ func init() {
if err != nil {
panic(err)
}
statusTmpl, err = statusTmpl.Parse(box.MustString("status.html"))
if err != nil {
panic(err)
}
}
// Router sets up the Administration Panel
@@ -50,8 +50,12 @@ func AdminRouter(r chi.Router) {
r.Get("/index.html", serveLogin)
r.Get("/panel.html", servePanel)
r.Post("/new_board.sh", handleNewBoard)
r.Post("/cleanup.sh", handleCleanup)
r.Post("/login.sh", handleLogin)
r.Post("/logout.sh", handleLogout)
r.Post("/new_admin.sh", handleNewAdmin)
r.Post("/del_admin.sh", handleDelAdmin)
r.Get("/status.sh", serveStatus)
}
// Router sets up moderation functions
@@ -60,69 +64,6 @@ func ModRouter(r chi.Router) {
r.Post("/del_reply.sh", handleDelPost)
}
func handleDelPost(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
if sess.CAttr("mode") != "admin" && sess.CAttr("mode") != "mod" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
rid, err := strconv.Atoi(r.FormValue("reply_id"))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
trid, err := strconv.Atoi(r.FormValue("thread_id"))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
board := r.FormValue("board")
if sess.CAttr("mode") == "mod" && sess.CAttr("board") != board {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Not on this board"))
return
}
db := middle.GetDB(r)
err = db.Update(func(tx *buntdb.Tx) error {
reply, err := resources.GetReply(tx, r.Host, board, trid, rid)
if err != nil {
return err
}
reply.Text = "[deleted]"
reply.Metadata["deleted"] = "yes"
reply.Image = nil
reply.Thumbnail = nil
err = resources.UpdateReply(tx, r.Host, board, reply)
if err != nil {
return err
}
return nil
})
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.Redirect(w, r, fmt.Sprintf("/%s/%d/thread.html", board, trid), http.StatusSeeOther)
}
func serveLogin(w http.ResponseWriter, r *http.Request) {
dat := bytes.NewBuffer([]byte{})
err := loginTmpl.Execute(dat, middle.GetBaseCtx(r))
@@ -150,41 +91,3 @@ func servePanel(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "panel.html", time.Now(),
bytes.NewReader(dat.Bytes()))
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
}
session.Remove(sess, w)
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
}
db := middle.GetDB(r)
var admin = &resources.AdminPass{}
err = db.View(func(tx *buntdb.Tx) error {
var err error
admin, err = resources.GetAdmin(tx, r.FormValue("id"))
return err
})
if err != nil {
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
errw.ErrorWriter(err, w, r)
}
err = admin.VerifyLogin(r.FormValue("pass"))
if err != nil {
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
errw.ErrorWriter(err, w, r)
}
sess := session.NewSessionOptions(&session.SessOptions{
CAttrs: map[string]interface{}{"mode": "admin"},
})
session.Add(sess, w)
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}

50
http/admin/login.go Normal file
View File

@@ -0,0 +1,50 @@
package admin
import (
"github.com/icza/session"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"net/http"
)
func handleLogout(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
}
session.Remove(sess, w)
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
}
db := middle.GetDB(r)
var admin = &resources.AdminPass{}
err = db.View(func(tx *buntdb.Tx) error {
var err error
admin, err = resources.GetAdmin(tx, r.FormValue("id"))
return err
})
if err != nil {
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
errw.ErrorWriter(err, w, r)
return
}
err = admin.VerifyLogin(r.FormValue("pass"))
if err != nil {
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
errw.ErrorWriter(err, w, r)
return
}
sess := session.NewSessionOptions(&session.SessOptions{
CAttrs: map[string]interface{}{"mode": "admin"},
})
session.Add(sess, w)
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}

92
http/admin/newadmin.go Normal file
View File

@@ -0,0 +1,92 @@
package admin
import (
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"net/http"
)
func handleDelAdmin(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if !middle.IsAdminSession(sess) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
db := middle.GetDB(r)
adminID := r.FormValue("adminid")
if len(adminID) > 255 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too long", "The ID of the administrator is too long"), w, r)
return
}
if len(adminID) < 4 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too short", "The ID of the administrator is too short"), w, r)
return
}
if err = db.Update(func(tx *buntdb.Tx) error {
return resources.DelAdmin(tx, adminID)
}); err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}
func handleNewAdmin(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if !middle.IsAdminSession(sess) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
db := middle.GetDB(r)
var admin = &resources.AdminPass{}
admin.ID = r.FormValue("adminid")
if len(admin.ID) > 255 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too long", "The ID of the administrator is too long"), w, r)
return
}
if len(admin.ID) < 4 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too short", "The ID of the administrator is too short"), w, r)
return
}
if len(r.FormValue("adminpass")) > 255 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too long", "The Password of the administrator is too long"), w, r)
return
}
if len(r.FormValue("adminpass")) < 12 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too short", "The Password of the administrator is too short"), w, r)
return
}
admin.HashLogin(r.FormValue("adminpass"))
if err = db.Update(func(tx *buntdb.Tx) error {
return resources.NewAdmin(tx, admin)
}); err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}

View File

@@ -11,12 +11,7 @@ import (
func handleNewBoard(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
if sess.CAttr("mode") != "admin" {
if !middle.IsAdminSession(sess) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return

View File

@@ -19,20 +19,53 @@
<input type="submit" value="Logout" />
</form>
</div>
<div class="panel boardmgr">
<form method="POST" action="/admin/new_board.sh">
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
<input type="text" placeholder="shortname" name="shortname"/>
<input type="text" placeholder="longname" name="longname"/>
<input type="submit" value="Create Board" />
<input type="reset" value="Reset" />
<br clear="left" /><hr />
<div class="postarea">
<form id="postform1" action="/admin/new_board.sh" method="POST">
<table>
<tbody>
<tr>
<td class="postblock">
Action
</td>
<td>
New Board
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
</td>
</tr>
<tr>
<td class="postblock">
Short Name
</td>
<td>
<input type="text" placeholder="shortname" name="shortname"/>
</td>
</tr>
<tr>
<td class="postblock">
Long Name
</td>
<td>
<input type="text" placeholder="longname" name="longname"/>
</td>
</tr>
<tr>
<td class="postblock"></td>
<td>
<input type="submit" value="Create Board" />
<input type="reset" value="Reset" />
</td>
</tr>
</tbody>
</table>
</form>
</div>
<br clear="left" /><hr />
<div class="postarea">
<form id="postform1" action="/admin/new_admin.sh" method="POST">
<form id="postform2" action="/admin/new_admin.sh" method="POST">
<table>
<tbody>
<tr>
@@ -53,8 +86,10 @@
</td>
<td>
<input type="text"
minlength="8"
name="id" />
minlength="4"
maxlength="255"
name="adminid"
required />
</td>
</tr>
<tr>
@@ -65,7 +100,83 @@
<input type="password"
minlength="12"
maxlength="255"
name="id" />
name="adminpass"
required />
</td>
</tr>
<tr>
<td class="postblock"></td>
<td>
<input type="submit" value="Create Admin ID" />
<input type="reset" value="Reset" />
</td>
</tr>
</tbody>
</table>
</form>
</div>
<br clear="left" /><hr />
<div class="postarea">
<form id="postform3" action="/admin/del_admin.sh" method="POST">
<table>
<tbody>
<tr>
<td class="postblock">
Action
</td>
<td>
Delete Administrator
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
</td>
</tr>
<tr>
<td class="postblock">
ID
</td>
<td>
<input type="text"
minlength="4"
maxlength="255"
name="adminid"
required />
</td>
</tr>
<tr>
<td class="postblock"></td>
<td>
<input type="submit" value="Delete Admin ID" />
<input type="reset" value="Reset" />
</td>
</tr>
</tbody>
</table>
</form>
</div>
<br clear="left" /><hr />
<div class="postarea">
<form id="postform4" action="/admin/cleanup.sh" method="POST">
<table>
<tbody>
<tr>
<td class="postblock">
Action
</td>
<td>
Cleanup Database
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
</td>
</tr>
<tr>
<td class="postblock"></td>
<td>
<input type="submit" value="Start Cleanup" />
<input type="reset" value="Reset" />
</td>
</tr>
</tbody>

125
http/admin/res/status.html Normal file
View File

@@ -0,0 +1,125 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Config.Site.Title}} Status</title>
<link rel="stylesheet" href="/@/style.css">
<link rel="stylesheet" href="/@/custom.css">
<link rel="stylesheet" href="/@/admin.css">
</head>
<body>
<div class="banner logo">
<h2>Runtime Statistics</h2>
</div>
<br/>
<br/>
<div class="postarea">
<form id="postform1" action="/admin/new_board.sh" method="POST">
<table>
<tbody>
<tr><td class="postblock">
Start Time
</td><td>
{{.Uptime.human}}
</td></tr>
<tr><td class="postblock">
Uptime (Precise)
</td><td>
{{ .Uptime.hours | printf "%.4f"}} hours<br/>
{{ .Uptime.seconds | printf "%.4f"}} seconds
</td></tr>
<tr><td class="postblock">
Num. CPU
</td><td>
{{.GC.numcpu}}
</td></tr>
<tr><td class="postblock">
Num. GoRoutines
</td><td>
{{.GC.numgor}}
</td></tr>
<tr><td class="postblock">
Go Version<br/>
Arch/OS/Compiler
</td><td>
{{.GC.version}}<br/>
{{.GC.arch}} / {{.GC.os}} / {{.GC.compiler}}
</td></tr>
<tr><td class="postblock">
Current Allocs
</td><td>
{{.GC.memory.alloc}}
</td></tr>
<tr><td class="postblock">
Cumulative Allocs
</td><td>
{{.GC.memory.calloc}}
</td></tr>
<tr><td class="postblock">
Used Sys Memory
</td><td>
{{.GC.memory.sysmem}}
</td></tr>
<tr><td class="postblock">
Pointer Lookups
</td><td>
{{.GC.memory.lookups}}
</td></tr>
<tr><td class="postblock">
MAllocs
</td><td>
{{.GC.memory.mallocs}}
</td></tr>
<tr><td class="postblock">
MFrees
</td><td>
{{.GC.memory.frees}}
</td></tr>
<tr><td class="postblock">
Live Objects
</td><td>
{{.GC.memory.liveobj}}
</td></tr>
<tr><td class="postblock">
Heap Allocated
</td><td>
{{.GC.memory.heapalloc}}
</td></tr>
<tr><td class="postblock">
Heap Released
</td><td>
{{.GC.memory.heaprelease}}
</td></tr>
<tr><td class="postblock">
GC Metadata
</td><td>
{{.GC.memory.gcmeta}}
</td></tr>
<tr><td class="postblock">
GC Pause
</td><td>
{{.GC.memory.pause}}
</td></tr>
<tr><td class="postblock">
GC Invokations
</td><td>
{{.GC.memory.gctimes}}
</td></tr>
<tr><td class="postblock">
GC Forced
</td><td>
{{.GC.memory.fgctimes}}
</td></tr>
<tr><td class="postblock">
GC CPU Usage
</td><td>
{{.GC.memory.cpufrac}}
</td></tr>
</tbody>
</table>
</form>
</div>
<br clear="left" /><hr />
</body>
</html>

87
http/admin/status.go Normal file
View File

@@ -0,0 +1,87 @@
package admin
import (
"bytes"
"fmt"
"github.com/dustin/go-humanize"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"net/http"
"runtime"
"sync"
"time"
)
var memStat = map[string]interface{}{}
var memStatLock = new(sync.RWMutex)
var startTime = time.Now().UTC()
func init() {
go func() {
update()
ticker := time.Tick(time.Second * 10)
for _ = range ticker {
update()
}
}()
}
func update() {
memStatLock.Lock()
defer memStatLock.Unlock()
memStat["Uptime"] = uptime()
memStat["GC"] = gcStat()
}
func uptime() map[string]interface{} {
return map[string]interface{}{
"human": humanize.Time(startTime),
"hours": time.Now().Sub(startTime).Hours(),
"seconds": time.Now().Sub(startTime).Seconds(),
}
}
func gcStat() map[string]interface{} {
m := &runtime.MemStats{}
runtime.ReadMemStats(m)
mem := map[string]interface{}{}
mem["alloc"] = fmt.Sprintf("%.5f GiB", float64(m.Alloc)/1024/1024/1024)
mem["calloc"] = fmt.Sprintf("%.5f GiB", float64(m.TotalAlloc)/1024/1024/1024)
mem["sysmem"] = fmt.Sprintf("%.5f MiB", float64(m.Sys)/1024/1024)
mem["lookups"] = fmt.Sprintf("× %d", m.Lookups)
mem["mallocs"] = fmt.Sprintf("× %d", m.Mallocs)
mem["frees"] = fmt.Sprintf("× %d", m.Frees)
mem["liveobj"] = fmt.Sprintf("× %d", m.Mallocs-m.Frees)
mem["heapalloc"] = fmt.Sprintf("%.5f MiB", float64(m.HeapSys)/1024/1024)
mem["heaprelease"] = fmt.Sprintf("%.5f MiB", float64(m.HeapReleased)/1024/1024)
mem["gcmeta"] = fmt.Sprintf("%.5f MiB", float64(m.GCSys)/1024/1024)
mem["pause"] = fmt.Sprintf("%.5f min", float64(m.PauseTotalNs)/1000/1000/1000/60)
mem["gctimes"] = fmt.Sprintf("× %d", m.NumGC)
mem["fgctimes"] = fmt.Sprintf("× %d", m.NumForcedGC)
mem["cpufrac"] = fmt.Sprintf("%.5f %%", m.GCCPUFraction*100)
return map[string]interface{}{
"numcpu": runtime.NumCPU(),
"numgor": runtime.NumGoroutine(),
"version": runtime.Version(),
"arch": runtime.GOARCH,
"os": runtime.GOOS,
"compiler": runtime.Compiler,
"memory": mem,
}
}
func serveStatus(w http.ResponseWriter, r *http.Request) {
memStatLock.RLock()
defer memStatLock.RUnlock()
ctx := middle.GetBaseCtx(r)
ctx["Uptime"] = memStat["Uptime"]
ctx["GC"] = memStat["GC"]
dat := bytes.NewBuffer([]byte{})
err := statusTmpl.Execute(dat, ctx)
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.ServeContent(w, r, "panel.html", time.Now(),
bytes.NewReader(dat.Bytes()))
}