├── server ├── assets │ ├── btns │ │ ├── new.png │ │ ├── clean.png │ │ ├── logo.png │ │ ├── reboot.png │ │ ├── shutdown.png │ │ └── update.png │ ├── devices │ │ ├── linux.png │ │ └── raspberrypi.png │ ├── favicons │ │ ├── linux.ico │ │ ├── linux_light.ico │ │ └── raspberrypi.ico │ ├── css │ │ ├── login.css │ │ ├── index.css │ │ └── common.css │ ├── js │ │ ├── login.js │ │ ├── solid-gauge.js │ │ ├── common.js │ │ ├── exporting.js │ │ └── index.js │ └── views │ │ ├── login.tmpl │ │ └── index.tmpl ├── server_test.go └── server.go ├── screenshots ├── screenshot_index.png ├── screenshot_login.png ├── screenshot_index_dark.png └── screenshot_login_dark.png ├── .gitignore ├── go.mod ├── config └── config.go ├── device ├── device_test.go └── device.go ├── Makefile ├── README.md ├── main.go ├── CHANGELOG.md └── LICENSE /server/assets/btns/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/new.png -------------------------------------------------------------------------------- /server/assets/btns/clean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/clean.png -------------------------------------------------------------------------------- /server/assets/btns/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/logo.png -------------------------------------------------------------------------------- /server/assets/btns/reboot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/reboot.png -------------------------------------------------------------------------------- /server/assets/btns/shutdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/shutdown.png -------------------------------------------------------------------------------- /server/assets/btns/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/btns/update.png -------------------------------------------------------------------------------- /server/assets/devices/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/devices/linux.png -------------------------------------------------------------------------------- /screenshots/screenshot_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/screenshots/screenshot_index.png -------------------------------------------------------------------------------- /screenshots/screenshot_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/screenshots/screenshot_login.png -------------------------------------------------------------------------------- /server/assets/favicons/linux.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/favicons/linux.ico -------------------------------------------------------------------------------- /screenshots/screenshot_index_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/screenshots/screenshot_index_dark.png -------------------------------------------------------------------------------- /screenshots/screenshot_login_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/screenshots/screenshot_login_dark.png -------------------------------------------------------------------------------- /server/assets/devices/raspberrypi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/devices/raspberrypi.png -------------------------------------------------------------------------------- /server/assets/favicons/linux_light.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/favicons/linux_light.ico -------------------------------------------------------------------------------- /server/assets/favicons/raspberrypi.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plutobell/pi-dashboard-go/HEAD/server/assets/favicons/raspberrypi.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | command.md 18 | build/ 19 | -------------------------------------------------------------------------------- /server/assets/css/login.css: -------------------------------------------------------------------------------- 1 | /* 2 | @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go) 3 | @Description: Golang implementation of pi-dashboard 4 | @Author: github.com/plutobell 5 | @Creation: 2020-08-01 6 | @Last modification: 2021-09-02 7 | @Version: 1.6.0 8 | */ 9 | 10 | .box-radius { 11 | border-radius: var(--box-radius); 12 | } 13 | #login-box { 14 | width: 70%; 15 | margin: 0 auto; 16 | margin-top: 30%; 17 | } 18 | 19 | input:focus { 20 | outline: none !important; 21 | box-shadow: none !important; 22 | border: 1px solid var(--label-color) !important; 23 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plutobell/pi-dashboard-go 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gorilla/sessions v1.2.1 7 | github.com/labstack/echo-contrib v0.14.1 8 | github.com/labstack/echo/v4 v4.10.2 9 | github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect 10 | github.com/mattn/go-isatty v0.0.18 // indirect 11 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect 12 | github.com/shirou/gopsutil/v3 v3.23.3 13 | github.com/shoenig/go-m1cpu v0.1.5 // indirect 14 | golang.org/x/crypto v0.7.0 // indirect 15 | golang.org/x/sys v0.7.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/plutobell/pi-dashboard-go/config" 8 | ) 9 | 10 | func Test_getNowUsernameAndPassword(t *testing.T) { 11 | if username, password := getNowUsernameAndPassword(); username+":"+password == config.Auth { 12 | t.Log("Pass") 13 | } else { 14 | t.Error("Fail") 15 | } 16 | } 17 | 18 | func Test_getFileSystem(t *testing.T) { 19 | if _, ok := getFileSystem(false, "btns").(http.FileSystem); ok { 20 | t.Log("Pass") 21 | } else { 22 | t.Error("Fail") 23 | } 24 | } 25 | 26 | func Test_getRandomString(t *testing.T) { 27 | if res := getRandomString(16); len(res) == 16 { 28 | t.Log("Pass") 29 | } else { 30 | t.Error("Fail") 31 | } 32 | } 33 | 34 | func Test_getLatestVersionFromGitHub(t *testing.T) { 35 | if nowVersion, _, downloadURL := getLatestVersionFromGitHub(); nowVersion != "" && len(downloadURL) > 0 { 36 | t.Log("Pass") 37 | } else { 38 | t.Error("Fail") 39 | } 40 | } 41 | 42 | func Test_isRootUser(t *testing.T) { 43 | if res := isRootUser(); res == true || res == false { 44 | t.Log("Pass") 45 | } else { 46 | t.Error("Fail") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go) 2 | // @Description: Golang implementation of pi-dashboard 3 | // @Author: github.com/plutobell 4 | // @Creation: 2020-08-01 5 | // @Last modification: 2023-04-05 6 | // @Version: 1.7.0 7 | 8 | package config 9 | 10 | import "os/user" 11 | 12 | const ( 13 | //PROJECT 项目地址 14 | PROJECT string = "https://github.com/plutobell/pi-dashboard-go" 15 | //AUTHOR 作者信息 16 | AUTHOR string = "github:plutobell" 17 | //VERSION 版本信息 18 | VERSION string = "1.7.0" 19 | //USERNAME 默认用户 20 | USERNAME string = "pi" 21 | //PASSWORD 默认密码 22 | PASSWORD string = "123" 23 | ) 24 | 25 | var ( 26 | Help bool 27 | Version bool 28 | // Port 端口 29 | Port string 30 | // Title 网站标题 31 | Title string 32 | // Net 网卡名称 33 | Net string 34 | // Disk 硬盘路径 35 | Disk string 36 | // Auth 用户名和密码 37 | Auth string 38 | // Interval 页面更新间隔 39 | Interval string 40 | // SessionMaxAge 登录状态有效期 41 | SessionMaxAge string 42 | // 启用日志显示 43 | EnableLogger bool 44 | // SessionName Session名称 45 | SessionName string 46 | // FileName 当前文件名 47 | FileName string 48 | // LinuxUserInfo 当前Linux用户信息 49 | LinuxUserInfo *user.User 50 | // Theme 主题 51 | Theme string 52 | ) 53 | -------------------------------------------------------------------------------- /device/device_test.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_Popen(t *testing.T) { 9 | if res, err := Popen("uptime"); res != "False" && err == nil { 10 | t.Log("Pass") 11 | } else { 12 | t.Error("Fail") 13 | } 14 | } 15 | 16 | func Benchmark_TimeConsumingFunction(b *testing.B) { 17 | for i := 0; i < 10000; i++ { 18 | Info() 19 | } 20 | } 21 | 22 | func Test_resolveTime(t *testing.T) { 23 | if uptime := resolveTime("1000000"); uptime == "11 days 13:46" { 24 | t.Log("Pass") 25 | } else { 26 | t.Error("Fail") 27 | } 28 | } 29 | 30 | func Test_bytesRound(t *testing.T) { 31 | if last := bytesRound(1073741824, 2); last == "1.0GB" { 32 | t.Log("Pass") 33 | } else { 34 | t.Error("Fail") 35 | } 36 | } 37 | 38 | func Test_struct2Map(t *testing.T) { 39 | host := new(Host) 40 | host.Get() 41 | hostMap, _ := struct2Map(host, "json") 42 | if typeOf := reflect.TypeOf(hostMap); typeOf.Kind() == reflect.Map { 43 | t.Log("Pass") 44 | } else { 45 | t.Error("Fail") 46 | } 47 | } 48 | 49 | func Test_mergeMap(t *testing.T) { 50 | map1 := make(map[string]interface{}) 51 | map2 := map[string]interface{}{"a": "Apple"} 52 | if map1 := mergeMap(map1, map2); map1["a"] == "Apple" { 53 | t.Log("Pass") 54 | } else { 55 | t.Error("Fail") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /server/assets/js/login.js: -------------------------------------------------------------------------------- 1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go) 2 | // @Description: Golang implementation of pi-dashboard 3 | // @Author: github.com/plutobell 4 | // @Creation: 2020-08-01 5 | // @Last modification: 2021-09-02 6 | // @Version: 1.6.0 7 | 8 | $("form").keyup(function(event){ 9 | if(event.keyCode == 13){ 10 | $("#login-btn").trigger("click"); 11 | } 12 | }); 13 | 14 | $("#login-btn").click(function(){ 15 | $("#login-btn").attr("disabled", true); 16 | $("input").attr("disabled", true); 17 | 18 | var username = $("#username").val(); 19 | var password = $("#password").val(); 20 | var json = { 21 | "username": username, 22 | "password": password, 23 | }; 24 | if (username == "" || password == "") { 25 | $("#login-tips").text("Username or password is empty") 26 | $("#login-btn").attr("disabled", false); 27 | $("input").attr("disabled", false); 28 | } else { 29 | $.ajaxSetup(csrfAddToAjaxHeader()); 30 | $.post('/api/login', JSON.stringify(json), function(result){ 31 | if (result.status == true) { 32 | $("#login-tips").text("") 33 | $(window).attr('location','/'); 34 | } else if (result.status == false) { 35 | $("#login-tips").text("Wrong credentials") 36 | $("#login-btn").attr("disabled", false); 37 | $("input").attr("disabled", false); 38 | } 39 | }).fail(function() { 40 | $("#login-tips").text("Unknown error") 41 | $("#login-btn").attr("disabled", false); 42 | $("input").attr("disabled", false); 43 | }); 44 | } 45 | 46 | }); -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go) 2 | # @Description: Golang implementation of pi-dashboard 3 | # @Author: github.com/plutobell 4 | # @Creation: 2020-08-10 5 | # @Last modification: 2023-04-05 6 | # @Version: 1.7.0 7 | 8 | PROGRAM = pi-dashboard-go 9 | OUTPUT = build 10 | GOOS = linux 11 | OS_NAME = $(shell uname -o) 12 | 13 | build: clean vet main.go server device config go.mod go.sum 14 | @echo "-> Building" 15 | 16 | @echo "-> 1 Building the "${PROGRAM}_${GOOS}_armv5_32 17 | @GOOS=${GOOS} GOARCH=arm GOARM=5 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_armv5_32 18 | 19 | @echo "-> 2 Building the "${PROGRAM}_${GOOS}_armv6_32 20 | @GOOS=${GOOS} GOARCH=arm GOARM=6 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_armv6_32 21 | 22 | @echo "-> 3 Building the "${PROGRAM}_${GOOS}_armv7_32 23 | @GOOS=${GOOS} GOARCH=arm GOARM=7 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_armv7_32 24 | 25 | @echo "-> 4 Building the "${PROGRAM}_${GOOS}_armv8_64 26 | @GOOS=${GOOS} GOARCH=arm64 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_armv8_64 27 | 28 | @echo "-> 5 Building the "${PROGRAM}_${GOOS}_386 29 | @GOOS=${GOOS} GOARCH=386 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_386 30 | 31 | @echo "-> 6 Building the "${PROGRAM}_${GOOS}_amd64 32 | @GOOS=${GOOS} GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o ./${OUTPUT}/${PROGRAM}_${GOOS}_amd64 33 | 34 | @echo "-> Complete" 35 | 36 | run: clean vet 37 | @echo "-> Running" 38 | @go run ./ 39 | @echo "-> Complete" 40 | 41 | vet: 42 | @echo "-> Checking" 43 | @go vet 44 | @echo "-> Complete" 45 | 46 | test: 47 | @echo "-> Testing" 48 | @go test -v 49 | @go test -test.bench=".*" 50 | @echo "-> Complete" 51 | 52 | clean: 53 | @echo "-> Cleaning" 54 | @rm -rf ./build 55 | @echo "-> Complete" 56 | 57 | help: 58 | @echo "-> Commands: build | run | vet | test | clean | help" -------------------------------------------------------------------------------- /server/assets/css/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go) 3 | @Description: Golang implementation of pi-dashboard 4 | @Author: github.com/plutobell 5 | @Creation: 2020-08-01 6 | @Last modification: 2021-09-02 7 | @Version: 1.6.0 8 | */ 9 | 10 | #loading{ 11 | background: var(--backdrop-color); 12 | position: fixed; 13 | left: 0px; 14 | top: 0px; 15 | width: 100%; 16 | height: 100%; 17 | display: block; 18 | z-index: 2000; 19 | filter: alpha(opacity=70); 20 | opacity: 0.7 !important; 21 | } 22 | 23 | .spinner { 24 | width: 50px; 25 | height: 60px; 26 | text-align: center; 27 | font-size: 10px; 28 | position: absolute; 29 | left: 50%; 30 | top: 50%; 31 | transform: translate(-50%,-50%); 32 | } 33 | 34 | .spinner > div { 35 | background-color: #fefefd; 36 | height: 100%; 37 | width: 6px; 38 | border-radius: var(--box-radius); 39 | display: inline-block; 40 | 41 | -webkit-animation: stretchdelay 1.2s infinite ease-in-out; 42 | animation: stretchdelay 1.2s infinite ease-in-out; 43 | } 44 | 45 | .spinner .rect2 { 46 | -webkit-animation-delay: -1.1s; 47 | animation-delay: -1.1s; 48 | } 49 | 50 | .spinner .rect3 { 51 | -webkit-animation-delay: -1.0s; 52 | animation-delay: -1.0s; 53 | } 54 | 55 | .spinner .rect4 { 56 | -webkit-animation-delay: -0.9s; 57 | animation-delay: -0.9s; 58 | } 59 | 60 | .spinner .rect5 { 61 | -webkit-animation-delay: -0.8s; 62 | animation-delay: -0.8s; 63 | } 64 | 65 | @-webkit-keyframes stretchdelay { 66 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 67 | 20% { -webkit-transform: scaleY(1.0) } 68 | } 69 | 70 | @keyframes stretchdelay { 71 | 0%, 40%, 100% { 72 | transform: scaleY(0.4); 73 | -webkit-transform: scaleY(0.4); 74 | } 20% { 75 | transform: scaleY(1.0); 76 | -webkit-transform: scaleY(1.0); 77 | } 78 | } 79 | 80 | #command-btns{ 81 | list-style-type:none; 82 | display: block; 83 | margin-top: 0 auto; 84 | margin-top: 10px; 85 | padding: 0; 86 | } 87 | #command-btns li{ 88 | display: inline; 89 | white-space:nowrap; 90 | margin: auto 7px; 91 | cursor: pointer; 92 | } 93 | #command-btns li img:hover{ 94 | border: 1px solid #e5e6e4; 95 | border-radius: 90px; 96 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pi Dashboard Go 2 | **Pi Dashboard Go** is a Golang implementation of pi-dashboard 3 | 4 | * **[中文文档](https://ojoll.com/archives/86/)** 5 | 6 | 7 | 8 | ![](./screenshots/screenshot_index.png) 9 | 10 | ![](./screenshots/screenshot_login.png) 11 | 12 | ![](./screenshots/screenshot_index_dark.png) 13 | 14 | ![](./screenshots/screenshot_login_dark.png) 15 | 16 | 17 | 18 | 19 | 20 | ## Install 21 | 22 | Thanks to the characteristics of the **[Golang](https://golang.org/)** language, the deployment of **Pi Dashboard Go** is very simple: **single binary executable file**. 23 | 24 | #### Download 25 | 26 | Just download the executable file from the project **[Releases](https://github.com/plutobell/pi-dashboard-go/releases)** page, **no other dependencies**. 27 | 28 | #### Authority 29 | 30 | Grant executable permissions 31 | 32 | ``` 33 | chmod +x pi-dashboard-go 34 | ``` 35 | 36 | **Note:Pi Dashboard Go requires root privileges.** 37 | 38 | 39 | 40 | ## Use 41 | 42 | #### Usage 43 | 44 | **Pi Dashboard Go** can be configured via command line parameters: 45 | 46 | ```bash 47 | Pi Dashboard Go version: v1.7.0 48 | Project address: https://github.com/plutobell/pi-dashboard-go 49 | 50 | Usage: Pi Dashboard Go [-auth USR:PSW] [-disk Paths] [-help] 51 | [-interval Seconds] [-log] [-net NIC] [-port Port] 52 | [-session Days] [-theme Theme] [-title Title] [-version] 53 | 54 | Options: 55 | -auth string 56 | specify username and password (default "pi:123") 57 | -disk string 58 | specify the filesystem path (default "/") 59 | -help 60 | this help 61 | -interval string 62 | specify the update interval in seconds (default "1") 63 | -log 64 | enable log display 65 | -net string 66 | specify the network device (default "lo") 67 | -port string 68 | specify the running port (default "8080") 69 | -session string 70 | specify the login status validity in days (default "7") 71 | -theme string 72 | specify the theme between 'light' and 'dark' (default "light") 73 | -title string 74 | specify the website title (default "Pi Dashboard Go") 75 | -version 76 | show version and exit 77 | ``` 78 | 79 | 80 | 81 | ## Thanks 82 | 83 | * **[Pi Dashboard](https://github.com/spoonysonny/pi-dashboard)** 84 | * **[echo](https://github.com/labstack/echo)** 85 | * **[gopsutil](https://github.com/shirou/gopsutil)** 86 | 87 | * **[bootstrap](https://github.com/twbs/bootstrap)** 88 | * **[jquery](https://github.com/jquery/jquery)** 89 | * **[highcharts](https://github.com/highcharts/highcharts)** 90 | 91 | ## Changelog 92 | 93 | * **[Changelog](./CHANGELOG.md)** -------------------------------------------------------------------------------- /server/assets/js/solid-gauge.js: -------------------------------------------------------------------------------- 1 | /* 2 | Highcharts JS v9.1.2 (2021-06-16) 3 | 4 | Solid angular gauge module 5 | 6 | (c) 2010-2021 Torstein Honsi 7 | 8 | License: www.highcharts.com/license 9 | */ 10 | 'use strict';(function(a){"object"===typeof module&&module.exports?(a["default"]=a,module.exports=a):"function"===typeof define&&define.amd?define("highcharts/modules/solid-gauge",["highcharts","highcharts/highcharts-more"],function(f){a(f);a.Highcharts=f;return a}):a("undefined"!==typeof Highcharts?Highcharts:void 0)})(function(a){function f(a,k,l,c){a.hasOwnProperty(k)||(a[k]=c.apply(null,l))}a=a?a._modules:{};f(a,"Core/Axis/SolidGaugeAxis.js",[a["Core/Color/Color.js"],a["Core/Utilities.js"]],function(a, 11 | k){var l=a.parse,c=k.extend,e=k.merge,m;(function(a){var b={initDataClasses:function(a){var c=this.chart,n,p=0,g=this.options;this.dataClasses=n=[];a.dataClasses.forEach(function(b,d){b=e(b);n.push(b);b.color||("category"===g.dataClassColor?(d=c.options.colors,b.color=d[p++],p===d.length&&(p=0)):b.color=l(g.minColor).tweenTo(l(g.maxColor),d/(a.dataClasses.length-1)))})},initStops:function(a){this.stops=a.stops||[[0,this.options.minColor],[1,this.options.maxColor]];this.stops.forEach(function(a){a.color= 12 | l(a[1])})},toColor:function(a,c){var b=this.stops,l=this.dataClasses,g;if(l)for(g=l.length;g--;){var e=l[g];var d=e.from;b=e.to;if(("undefined"===typeof d||a>=d)&&("undefined"===typeof b||a<=b)){var k=e.color;c&&(c.dataClass=g);break}}else{this.logarithmic&&(a=this.val2lin(a));a=1-(this.max-a)/(this.max-this.min);for(g=b.length;g--&&!(a>b[g][0]););d=b[g]||b[g+1];b=b[g+1]||d;a=1-(b[0]-a)/(b[0]-d[0]||1);k=d.color.tweenTo(b.color,a)}return k}};a.init=function(a){c(a,b)}})(m||(m={}));return m});f(a,"Series/SolidGauge/SolidGaugeComposition.js", 13 | [a["Core/Renderer/SVG/SVGRenderer.js"]],function(a){a=a.prototype;var k=a.symbols.arc;a.symbols.arc=function(a,c,e,m,b){a=k(a,c,e,m,b);b&&b.rounded&&(e=((b.r||e)-(b.innerR||0))/2,c=a[0],b=a[2],"M"===c[0]&&"L"===b[0]&&(c=["A",e,e,0,1,1,c[1],c[2]],a[2]=["A",e,e,0,1,1,b[1],b[2]],a[4]=c));return a}});f(a,"Series/SolidGauge/SolidGaugeSeries.js",[a["Mixins/LegendSymbol.js"],a["Core/Series/SeriesRegistry.js"],a["Core/Axis/SolidGaugeAxis.js"],a["Core/Utilities.js"]],function(a,k,l,c){var e=this&&this.__extends|| 14 | function(){var a=function(b,h){a=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(a,b){a.__proto__=b}||function(a,b){for(var h in b)b.hasOwnProperty(h)&&(a[h]=b[h])};return a(b,h)};return function(b,h){function c(){this.constructor=b}a(b,h);b.prototype=null===h?Object.create(h):(c.prototype=h.prototype,new c)}}(),m=k.seriesTypes,b=m.gauge,f=m.pie.prototype,p=c.clamp,u=c.extend,n=c.isNumber,w=c.merge,g=c.pick,v=c.pInt,d={colorByPoint:!0,dataLabels:{y:0}};c=function(a){function c(){var b= 15 | null!==a&&a.apply(this,arguments)||this;b.data=void 0;b.points=void 0;b.options=void 0;b.axis=void 0;b.yAxis=void 0;b.startAngleRad=void 0;b.thresholdAngleRad=void 0;return b}e(c,a);c.prototype.translate=function(){var a=this.yAxis;l.init(a);!a.dataClasses&&a.options.dataClasses&&a.initDataClasses(a.options);a.initStops(a.options);b.prototype.translate.call(this)};c.prototype.drawPoints=function(){var a=this,b=a.yAxis,c=b.center,e=a.options,k=a.chart.renderer,d=e.overshoot,l=n(d)?d/180*Math.PI:0, 16 | f;n(e.threshold)&&(f=b.startAngleRad+b.translate(e.threshold,null,null,null,!0));this.thresholdAngleRad=g(f,b.startAngleRad);a.points.forEach(function(d){if(!d.isNull){var h=d.graphic,f=b.startAngleRad+b.translate(d.y,null,null,null,!0),m=v(g(d.options.radius,e.radius,100))*c[2]/200,q=v(g(d.options.innerRadius,e.innerRadius,60))*c[2]/200,r=b.toColor(d.y,d),t=Math.min(b.startAngleRad,b.endAngleRad),n=Math.max(b.startAngleRad,b.endAngleRad);"none"===r&&(r=d.color||a.color||"none");"none"!==r&&(d.color= 17 | r);f=p(f,t-l,n+l);!1===e.wrap&&(f=p(f,t,n));t=Math.min(f,a.thresholdAngleRad);f=Math.max(f,a.thresholdAngleRad);f-t>2*Math.PI&&(f=t+2*Math.PI);d.shapeArgs=q={x:c[0],y:c[1],r:m,innerR:q,start:t,end:f,rounded:e.rounded};d.startR=m;h?(m=q.d,h.animate(u({fill:r},q)),m&&(q.d=m)):d.graphic=h=k.arc(q).attr({fill:r,"sweep-flag":0}).add(a.group);a.chart.styledMode||("square"!==e.linecap&&h.attr({"stroke-linecap":"round","stroke-linejoin":"round"}),h.attr({stroke:e.borderColor||"none","stroke-width":e.borderWidth|| 18 | 0}));h&&h.addClass(d.getClassName(),!0)}})};c.prototype.animate=function(a){a||(this.startAngleRad=this.thresholdAngleRad,f.animate.call(this,a))};c.defaultOptions=w(b.defaultOptions,d);return c}(b);u(c.prototype,{drawLegendSymbol:a.drawRectangle});k.registerSeriesType("solidgauge",c);"";return c});f(a,"masters/modules/solid-gauge.src.js",[],function(){})}); 19 | //# sourceMappingURL=solid-gauge.js.map -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go) 2 | // @Description: Golang implementation of pi-dashboard 3 | // @Author: github.com/plutobell 4 | // @Creation: 2020-08-01 5 | // @Last modification: 2023-04-05 6 | // @Version: 1.7.0 7 | 8 | package main 9 | 10 | import ( 11 | "flag" 12 | "fmt" 13 | "log" 14 | "os" 15 | "os/user" 16 | "path/filepath" 17 | "strconv" 18 | "strings" 19 | "unicode" 20 | 21 | "github.com/plutobell/pi-dashboard-go/config" 22 | "github.com/plutobell/pi-dashboard-go/device" 23 | "github.com/plutobell/pi-dashboard-go/server" 24 | ) 25 | 26 | func init() { 27 | flag.BoolVar(&config.Help, "help", false, "this help") 28 | flag.BoolVar(&config.Version, "version", false, "show version and exit") 29 | flag.StringVar(&config.Port, "port", "8080", "specify the running port") 30 | flag.StringVar(&config.Title, "title", "Pi Dashboard Go", "specify the website title") 31 | flag.StringVar(&config.Net, "net", "lo", "specify the network device") 32 | flag.StringVar(&config.Disk, "disk", "/", "specify the filesystem path") 33 | flag.StringVar(&config.Auth, "auth", config.USERNAME+":"+config.PASSWORD, "specify username and password") 34 | flag.StringVar(&config.Interval, "interval", "1", "specify the update interval in seconds") 35 | flag.StringVar(&config.SessionMaxAge, "session", "7", "specify the login status validity in days") 36 | flag.StringVar(&config.Theme, "theme", "light", "specify the theme between 'light' and 'dark'") 37 | flag.BoolVar(&config.EnableLogger, "log", false, "enable log display") 38 | 39 | config.SessionName = "logged_in" 40 | config.FileName = filepath.Base(os.Args[0]) 41 | config.LinuxUserInfo, _ = user.Current() 42 | 43 | flag.Usage = usage 44 | } 45 | 46 | func main() { 47 | flag.Parse() 48 | 49 | if config.Help { 50 | flag.Usage() 51 | return 52 | } 53 | if config.Version { 54 | fmt.Println("Pi Dashboard Go v" + config.VERSION) 55 | fmt.Println("Project address: " + config.PROJECT) 56 | return 57 | } 58 | netDevs, err := device.Popen("cat /proc/net/dev") 59 | if err != nil { 60 | log.Fatal(err) 61 | return 62 | } 63 | if !strings.Contains(netDevs, config.Net+":") { 64 | fmt.Println("Network card does not exist") 65 | return 66 | } 67 | pathExists := false 68 | _, err = os.Stat(config.Disk) 69 | if err == nil { 70 | pathExists = true 71 | } 72 | if os.IsNotExist(err) { 73 | pathExists = false 74 | } 75 | 76 | if config.Disk != "/" { 77 | if !pathExists { 78 | fmt.Println("Disk does not exist") 79 | return 80 | } 81 | } 82 | authSlice := strings.Split(config.Auth, ":") 83 | if len(authSlice) != 2 { 84 | fmt.Println("Auth format error") 85 | return 86 | } 87 | if len([]rune(authSlice[0])) > 15 || len([]rune(authSlice[0])) == 0 { 88 | fmt.Println("Username is too long") 89 | return 90 | } 91 | if len([]rune(authSlice[1])) > 15 || len([]rune(authSlice[1])) == 0 { 92 | fmt.Println("Password is too long") 93 | return 94 | } 95 | if len([]rune(config.Title)) > 25 { 96 | fmt.Println("Title is too long") 97 | return 98 | } 99 | 100 | isDigit := true 101 | for _, r := range config.Interval { 102 | if !unicode.IsDigit(rune(r)) { 103 | isDigit = false 104 | break 105 | } 106 | } 107 | if !isDigit { 108 | fmt.Println("Interval parameter value is invalid") 109 | return 110 | } 111 | 112 | IntervalInt, err := strconv.Atoi(config.Interval) 113 | if err != nil { 114 | log.Fatal(err) 115 | return 116 | } 117 | if IntervalInt > 900 { 118 | fmt.Println("Interval is too long") 119 | return 120 | } else if IntervalInt < 0 { 121 | fmt.Println("Interval should be no less than 0") 122 | return 123 | } 124 | 125 | SessionMaxAgeInt, err := strconv.Atoi(config.SessionMaxAge) 126 | if err != nil { 127 | log.Fatal(err) 128 | return 129 | } 130 | if SessionMaxAgeInt > 365 { 131 | fmt.Println("Session days is too long") 132 | return 133 | } else if SessionMaxAgeInt < 0 { 134 | fmt.Println("Session days should be no less than 0") 135 | return 136 | } 137 | 138 | if config.Theme != "light" && config.Theme != "dark" { 139 | fmt.Println("Theme name not supported") 140 | return 141 | } 142 | 143 | server.Run() 144 | } 145 | 146 | func usage() { 147 | fmt.Fprintf(os.Stderr, `Pi Dashboard Go version: v%s 148 | Project address: %s 149 | 150 | Usage: %s [-auth USR:PSW] [-disk Paths] [-help] 151 | [-interval Seconds] [-log] [-net NIC] [-port Port] 152 | [-session Days] [-theme Theme] [-title Title] [-version] 153 | 154 | Options: 155 | `, config.VERSION, config.PROJECT, config.FileName) 156 | flag.PrintDefaults() 157 | } 158 | -------------------------------------------------------------------------------- /server/assets/views/login.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Login 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 47 |
48 | 49 |
50 |
51 |
52 |
53 |
54 |
55 |
Device
56 |
57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 | 65 |
66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 | 78 | 79 | 80 | 87 | 88 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog # 2 | 3 | **2023-04-05** 4 | 5 | * v1.7.0 : 6 | * Added a cli parameter called theme #4 7 | * Added proxy for version detection 8 | * Fixed the bug of disk path detection 9 | * Some other optimizations 10 | * Updated dependencies 11 | * Use Go v1.20.3 12 | 13 | **2021-09-02** 14 | 15 | * v1.6.0 : 16 | * Refactored device package to significantly improve performance and speed 17 | * Improved front-end display details 18 | * Updated dependencies 19 | 20 | **2021-08-28** 21 | 22 | * v1.5.1 : 23 | * Fixed the bug of load average display 24 | * Added the navbar to close automatically when clicked on mobile devices 25 | 26 | **2021-08-27** 27 | 28 | * v1.5.0 : 29 | * Added dark mode that follows the device switch 30 | * Added automatic jumping to the login page when not logged in 31 | * Some other optimizations 32 | * Updated dependencies 33 | 34 | **2021-08-24** 35 | 36 | * v1.4.1 : 37 | * Added operation functions permission detection 38 | * Added a prompt to the console for non-root users to run 39 | * v1.4.0 : 40 | * Adjusted the front-end page display style 41 | * Adjusted static file path structure 42 | * Added page loading time statistics 43 | * Added pop-up window for new version release notes display 44 | * Some other optimizations 45 | * Use Go v1.17 46 | * Updated dependencies 47 | 48 | **2021-08-14** 49 | 50 | * v1.3.3 : 51 | * Added automatic recognition and switching of favicon 52 | * Adjusted static file path structure 53 | * Adjusted project structure 54 | * Updated dependencies 55 | 56 | **2021-08-13** 57 | 58 | * v1.3.2 : 59 | * Added automatic version detection and prompting 60 | * v1.3.1 : 61 | * Added csrf protection 62 | * Adjusted some details 63 | * Updated dependencies 64 | 65 | **2021-08-12** 66 | 67 | * v1.3.0 : 68 | * Adjusted routing structure 69 | * Adjusted log formatting 70 | * Added gzip compression 71 | * Enhanced web security 72 | * Login request changed to asynchronous 73 | * Refactored part of the code 74 | * Updated dependencies 75 | 76 | **2021-08-10** 77 | 78 | * v1.2.1 : 79 | * Added login empty field checks 80 | * Added login failure prompt message 81 | * Added new command line parameter: log 82 | 83 | * v1.2.0 : 84 | * Rewrite login authentication 85 | * Added new login page 86 | * Added new command line parameter: session 87 | * Use Go v1.16.7 88 | * Updated dependencies 89 | 90 | **2021-06-17** 91 | 92 | * v1.1.2 : 93 | * Changed the way cpu usage is calculated 94 | * Updated dependencies 95 | 96 | **2021-06-16** 97 | 98 | * v1.1.1 : 99 | * Fix the bug of abnormal display of cpu information 100 | * Use Go v1.16.5 101 | * Updated dependencies 102 | 103 | **2021-04-05** 104 | 105 | * v1.1.0 : 106 | * Replace go.rice with go embed 107 | * Fix the bug of program error when unable to get cpu model 108 | * Added new command line parameter: interval 109 | * Added a different header image for non-Raspberry Pi devices 110 | 111 | **2021-03-31** 112 | 113 | * v1.0.10 : 114 | * Fix the bug of panic caused by empty device model information #1 115 | * Fix the bug of not finding dashboard.min.js #2 116 | * Fix the bug of invalid hostname command on arch 117 | * Added golang version display on view 118 | 119 | **2020-8-14** 120 | 121 | * v1.0.9 : 122 | * Fix swap display bug 123 | * Adapt to linux system under 386 and amd64 124 | * Optimize error handling for function Popen 125 | 126 | **2020-8-9** 127 | 128 | * v1.0.8 : 129 | * Optimize swap display details 130 | * Added shortcut buttons such as shutdown and reboot 131 | 132 | **2020-8-7** 133 | 134 | * v1.0.7 : 135 | * Optimize network card flow and curve display 136 | * Interface detail adjustment 137 | 138 | **2020-8-6** 139 | 140 | * v1.0.6 : 141 | * Fix the bug that the network card data display error 142 | * Fixed navigation bar at the top 143 | * Interface detail adjustment 144 | * v1.0.5 : 145 | * Interface color adjustment 146 | * Data update detection and prompt 147 | * Optimize code for server 148 | * Detail adjustment 149 | * v1.0.4 : 150 | * Adjust Cached calculation method 151 | * Added theme-color for mobile browser 152 | * Added display login user statistics 153 | * Bug fixes and details optimization 154 | 155 | **2020-8-5** 156 | 157 | * v1.0.3 : 158 | * Newly added time formatting function resolveTime 159 | * Detail optimization 160 | * v1.0.2 : 161 | * Improve command line parameter verification 162 | * Detail optimization 163 | * Added test case device_test.go 164 | * New page loading animation 165 | 166 | **2020-8-4** 167 | 168 | * v1.0.1 : Bug fixes, detail optimization 169 | * v1.0.0 -------------------------------------------------------------------------------- /server/assets/css/common.css: -------------------------------------------------------------------------------- 1 | /* 2 | @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go) 3 | @Description: Golang implementation of pi-dashboard 4 | @Author: github.com/plutobell 5 | @Creation: 2020-08-01 6 | @Last modification: 2023-04-05 7 | @Version: 1.7.0 8 | */ 9 | 10 | ::-webkit-scrollbar { 11 | width: 6.5px; 12 | height: 6.5px; 13 | } 14 | ::-webkit-scrollbar-track { 15 | border-radius: 3.5px; 16 | background: rgba(0,0,0,0.06); 17 | -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.08); 18 | } 19 | ::-webkit-scrollbar-thumb { 20 | border-radius: 3.5px; 21 | background: rgba(0,0,0,0.12); 22 | -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.2); 23 | } 24 | 25 | .label {color: var(--label-color); font-size: 75%; font-weight: bolder;} 26 | 27 | body{ 28 | -moz-user-select:none; 29 | -webkit-user-select:none; 30 | -ms-user-select:none; 31 | -khtml-user-select:none; 32 | user-select:none; 33 | margin-top: 70px; 34 | } 35 | 36 | 37 | .navbar, .dropdown-menu, .modal-content, 38 | .tooltip-arrow, .tooltip-inner, .tooltip, .arrow::before { 39 | filter: alpha(opacity=90) !important; 40 | opacity: 0.9 !important; 41 | border: 0 !important; 42 | box-shadow: 0 !important; 43 | } 44 | .navbar-toggler { 45 | color: #616161 !important; 46 | border-color: var(--navbar-color) !important; 47 | padding: 0 10px 0 10px !important; 48 | border-radius: var(--box-radius) !important; 49 | } 50 | .dropdown-menu { 51 | border-radius: var(--box-radius) !important; 52 | background-color: var(--navbar-color) !important; 53 | } 54 | .dropdown-item:active { 55 | border-radius: var(--box-radius) !important; 56 | background-color:#616161 !important; 57 | } 58 | .dropdown-item:hover { 59 | border-radius: var(--box-radius) !important; 60 | } 61 | 62 | .tooltip-inner { 63 | border-radius: var(--box-radius); 64 | padding: 5px 10px 5px 10px; 65 | background-color: var(--navbar-color) !important; 66 | } 67 | .tooltip.bs-tooltip-top .tooltip-arrow::before { 68 | border-top-color: var(--navbar-color); 69 | } 70 | 71 | .tooltip.bs-tooltip-bottom .tooltip-arrow::before { 72 | border-bottom-color: var(--navbar-color); 73 | } 74 | 75 | .tooltip.bs-tooltip-start .tooltip-arrow::before { 76 | border-left-color: var(--navbar-color); 77 | } 78 | 79 | .tooltip.bs-tooltip-end .tooltip-arrow::before { 80 | border-right-color: var(--navbar-color); 81 | } 82 | @media only screen 83 | and (max-device-width : 768px) { 84 | .tooltip { 85 | display: none !important; 86 | } 87 | } 88 | 89 | .modal-header, .modal-body, .modal-footer { 90 | border: 0 !important; 91 | } 92 | .modal-backdrop { 93 | background-color: var(--backdrop-color) !important; 94 | } 95 | .modal-open { 96 | overflow-y: hidden !important; 97 | } 98 | 99 | .btn-dark, 100 | .btn-dark:hover, 101 | .btn-dark:active, 102 | .btn-dark:visited, 103 | .btn-dark:focus { 104 | border-radius: var(--box-radius) !important; 105 | 106 | background-color: var(--navbar-color) !important; 107 | border-color: var(--navbar-color) !important; 108 | 109 | outline: none !important; 110 | box-shadow: none !important; 111 | border: 1px solid var(--navbar-color) !important; 112 | 113 | filter: alpha(opacity=90) !important; 114 | opacity: 0.9 !important; 115 | border: 0 !important; 116 | box-shadow: 0 !important; 117 | } 118 | .btn-close:focus { 119 | outline: none !important; 120 | box-shadow: none !important; 121 | } 122 | .inverted { 123 | filter: invert(100%); 124 | } 125 | 126 | 127 | @media (prefers-color-scheme: dark) { 128 | ::-webkit-scrollbar-track { 129 | background: #3f3f3f; 130 | -webkit-box-shadow: inset 0 0 5px #3f3f3f; 131 | } 132 | ::-webkit-scrollbar-thumb { 133 | background: #7c7c7c; 134 | -webkit-box-shadow: inset 0 0 10px #7c7c7c; 135 | } 136 | 137 | body{ 138 | background-color: #2d2d2d; 139 | color: #c9d1d9; 140 | } 141 | input { 142 | background-color: #3b3b3b !important; 143 | border-color: #3b3b3b !important; 144 | color: #c9d1d9 !important; 145 | } 146 | input:focus { 147 | border: 1px solid #6b6b6b !important; 148 | } 149 | 150 | :root { 151 | --label-color: #959c9c; 152 | --navbar-color: #474747; 153 | --cache-color: #414141; 154 | --cpu-memory-title-color: #5e5e5e; 155 | --temperature-color: #4b4b4b; 156 | --ip-color: #414141; 157 | --time-color: #5e5e5e; 158 | --uptime-color: #4b4b4b; 159 | --cpu-color: #414141; 160 | --memory-color: #414141; 161 | --real-memory-color: #414141; 162 | --swap-color: #414141; 163 | --disk-color: #5e5e5e; 164 | --box-bg-color: #373938; 165 | --backdrop-color: #363636; 166 | 167 | --guage-font-color: #b9bebe; 168 | --guage-stops-color-1: #626464; 169 | --guage-stops-color-5: #878b8b; 170 | --guage-stops-color-9: #b6b6b6; 171 | 172 | --net-in-color: #414141; 173 | --net-out-color: #5e5e5e; 174 | --net-grid-line-color: #3d3d3d; 175 | --net-line-color: #575757; 176 | 177 | --box-radius: 15px; 178 | } 179 | 180 | .modal-content { 181 | background-color: #252525; 182 | color: #c9d1d9; 183 | } 184 | .dark-bg { 185 | background-color: #505050 !important; 186 | } 187 | .spinner > div { 188 | background-color: #c9d1d9; 189 | } 190 | .navbar-toggler { 191 | color: #505050 !important; 192 | } 193 | 194 | #login-tips { 195 | color: #c9d1d9; 196 | } 197 | #pimodel { 198 | color: #c9d1d9; 199 | } 200 | #command-btns li img:hover{ 201 | border: 1px solid #606060 !important; 202 | } 203 | } -------------------------------------------------------------------------------- /server/assets/js/common.js: -------------------------------------------------------------------------------- 1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go) 2 | // @Description: Golang implementation of pi-dashboard 3 | // @Author: github.com/plutobell 4 | // @Creation: 2020-08-01 5 | // @Last modification: 2023-04-05 6 | // @Version: 1.7.0 7 | 8 | window.oncontextmenu=function(){return false;} 9 | window.onkeydown = window.onkeyup = window.onkeypress = function (event) { 10 | if (event.keyCode === 123) { 11 | event.preventDefault(); 12 | window.event.returnValue = false; 13 | } 14 | } 15 | window.addEventListener('keydown', function (event) { 16 | if (event.ctrlKey) { 17 | event.preventDefault(); 18 | } 19 | }) 20 | 21 | 22 | const themeVarLight = ` 23 | :root { 24 | --label-color: #979d9e; 25 | --navbar-color: #555555; 26 | --cache-color: #D9E4DD; 27 | --cpu-memory-title-color: #CDC9C3; 28 | --temperature-color: #FBF7F0; 29 | --ip-color: #D9E4DD; 30 | --time-color: #CDC9C3; 31 | --uptime-color: #FBF7F0; 32 | --cpu-color: #D9E4DD; 33 | --memory-color: #D9E4DD; 34 | --real-memory-color: #D9E4DD; 35 | --swap-color: #D9E4DD; 36 | --disk-color: #CDC9C3; 37 | --box-bg-color: #eaebe9; /* #E8EAE6 */ 38 | --backdrop-color: #363636; 39 | 40 | --guage-font-color: black; 41 | --guage-stops-color-1: #D9E4DD; 42 | --guage-stops-color-5: #CDC9C3; 43 | --guage-stops-color-9: #919191; 44 | 45 | --net-in-color: #D9E4DD; 46 | --net-out-color: #CDC9C3; 47 | --net-grid-line-color: #e6e6e6; 48 | --net-line-color: #ccd6eb; 49 | 50 | --box-radius: 15px; 51 | } 52 | `; 53 | const themeVarDark = ` 54 | ::-webkit-scrollbar-track { 55 | background: #3f3f3f; 56 | -webkit-box-shadow: inset 0 0 5px #3f3f3f; 57 | } 58 | ::-webkit-scrollbar-thumb { 59 | background: #7c7c7c; 60 | -webkit-box-shadow: inset 0 0 10px #7c7c7c; 61 | } 62 | 63 | body{ 64 | background-color: #2d2d2d; 65 | color: #c9d1d9; 66 | } 67 | input { 68 | background-color: #3b3b3b !important; 69 | border-color: #3b3b3b !important; 70 | color: #c9d1d9 !important; 71 | } 72 | input:focus { 73 | border: 1px solid #6b6b6b !important; 74 | } 75 | 76 | :root { 77 | --label-color: #959c9c; 78 | --navbar-color: #474747; 79 | --cache-color: #414141; 80 | --cpu-memory-title-color: #5e5e5e; 81 | --temperature-color: #4b4b4b; 82 | --ip-color: #414141; 83 | --time-color: #5e5e5e; 84 | --uptime-color: #4b4b4b; 85 | --cpu-color: #414141; 86 | --memory-color: #414141; 87 | --real-memory-color: #414141; 88 | --swap-color: #414141; 89 | --disk-color: #5e5e5e; 90 | --box-bg-color: #373938; 91 | --backdrop-color: #363636; 92 | 93 | --guage-font-color: #b9bebe; 94 | --guage-stops-color-1: #626464; 95 | --guage-stops-color-5: #878b8b; 96 | --guage-stops-color-9: #b6b6b6; 97 | 98 | --net-in-color: #414141; 99 | --net-out-color: #5e5e5e; 100 | --net-grid-line-color: #3d3d3d; 101 | --net-line-color: #575757; 102 | 103 | --box-radius: 15px; 104 | } 105 | 106 | .modal-content { 107 | background-color: #252525; 108 | color: #c9d1d9; 109 | } 110 | .dark-bg { 111 | background-color: #505050 !important; 112 | } 113 | .spinner > div { 114 | background-color: #c9d1d9; 115 | } 116 | .navbar-toggler { 117 | color: #505050 !important; 118 | } 119 | 120 | #login-tips { 121 | color: #c9d1d9; 122 | } 123 | #pimodel { 124 | color: #c9d1d9; 125 | } 126 | #command-btns li img:hover{ 127 | border: 1px solid #606060 !important; 128 | } 129 | `; 130 | var theme = $("meta[name='theme']").attr('content'); 131 | $(document).ready(function() { 132 | if (theme == "dark" || window.matchMedia('(prefers-color-scheme: dark)').matches) { 133 | $("#theme-var").text(themeVarDark); 134 | $("#modal-close-btn").addClass("btn-close-white"); 135 | $("footer").eq(0).addClass("border-secondary"); 136 | $("meta[name='theme-color']").attr('content', '#474747'); 137 | if ($("#favicon").text() == "linux.ico") { 138 | $("#device-photo").addClass("inverted"); 139 | $("#icon").attr("href", "favicons/linux_light.ico"); 140 | $("#shortcut-icon").attr("href", "favicons/linux_light.ico"); 141 | } 142 | $.getScript('js/index.js', function() {}); 143 | } else if (theme == "light" || window.matchMedia('(prefers-color-scheme: light)').matches) { 144 | $("#theme-var").text(themeVarLight); 145 | $("#modal-close-btn").removeClass("btn-close-white"); 146 | $("footer").eq(0).removeClass("border-secondary"); 147 | $("meta[name='theme-color']").attr('content', '#555555'); 148 | $("#device-photo").removeClass("inverted"); 149 | if ($("#favicon").text() == "linux.ico") { 150 | $("#icon").attr("href", "favicons/linux.ico"); 151 | $("#shortcut-icon").attr("href", "favicons/linux.ico"); 152 | } else { 153 | $("#icon").attr("href", "favicons/raspberrypi.ico"); 154 | $("#shortcut-icon").attr("href", "favicons/raspberrypi.ico"); 155 | } 156 | $.getScript('js/index.js', function() {}); 157 | } 158 | }); 159 | 160 | let media = window.matchMedia('(prefers-color-scheme: dark)'); 161 | let callback = (e) => { 162 | let prefersDarkMode = e.matches; 163 | if (prefersDarkMode) { 164 | $("#theme-var").text(themeVarDark); 165 | $("#modal-close-btn").addClass("btn-close-white"); 166 | $("footer").eq(0).addClass("border-secondary"); 167 | $("meta[name='theme-color']").attr('content', '#474747'); 168 | if ($("#favicon").text() == "linux.ico") { 169 | $("#device-photo").addClass("inverted"); 170 | $("#icon").attr("href", "favicons/linux_light.ico"); 171 | $("#shortcut-icon").attr("href", "favicons/linux_light.ico"); 172 | } 173 | $.getScript('js/index.js', function() {}); 174 | } else { 175 | $("#theme-var").text(themeVarLight); 176 | $("#modal-close-btn").removeClass("btn-close-white"); 177 | $("footer").eq(0).removeClass("border-secondary"); 178 | $("meta[name='theme-color']").attr('content', '#555555'); 179 | $("#device-photo").removeClass("inverted"); 180 | if ($("#favicon").text() == "linux.ico") { 181 | $("#icon").attr("href", "favicons/linux.ico"); 182 | $("#shortcut-icon").attr("href", "favicons/linux.ico"); 183 | } else { 184 | $("#icon").attr("href", "favicons/raspberrypi.ico"); 185 | $("#shortcut-icon").attr("href", "favicons/raspberrypi.ico"); 186 | } 187 | $.getScript('js/index.js', function() {}); 188 | } 189 | }; 190 | if (typeof media.addEventListener === 'function') { 191 | media.addEventListener('change', callback); 192 | } else if (typeof media.addEventListener === 'function') { 193 | media.addEventListener(callback); 194 | } 195 | 196 | $('.dropdown-item').on('click',function() { 197 | $('.navbar-collapse').collapse('hide'); 198 | }); 199 | $('#logout').on('click',function() { 200 | $('.navbar-collapse').collapse('hide'); 201 | }); 202 | 203 | function getCookie(name) { 204 | var cookieValue = null; 205 | if (document.cookie && document.cookie !== '') { 206 | var cookies = document.cookie.split(';'); 207 | for (var i = 0; i < cookies.length; i++) { 208 | var cookie = jQuery.trim(cookies[i]); 209 | // Does this cookie string begin with the name we want? 210 | if (cookie.substring(0, name.length + 1) === (name + '=')) { 211 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 212 | break; 213 | } 214 | } 215 | } 216 | return cookieValue; 217 | } 218 | 219 | function csrfSafeMethod(method) { 220 | // 这些HTTP方法不要求携带CSRF令牌。test()是js正则表达式方法,若模板匹配成功,则返回true 221 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); 222 | } 223 | 224 | function csrfAddToAjaxHeader() { 225 | var csrftoken = getCookie('cf_sid'); 226 | 227 | return { 228 | beforeSend: function(xhr, settings) { 229 | if (!csrfSafeMethod(settings.type) && !this.crossDomain) { 230 | xhr.setRequestHeader("X-XSRF-TOKEN", csrftoken); 231 | } 232 | } 233 | } 234 | } -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go) 2 | // @Description: Golang implementation of pi-dashboard 3 | // @Author: github.com/plutobell 4 | // @Creation: 2020-08-01 5 | // @Last modification: 2023-04-05 6 | // @Version: 1.7.0 7 | 8 | package server 9 | 10 | import ( 11 | "embed" 12 | "encoding/json" 13 | "fmt" 14 | "io" 15 | "io/fs" 16 | "io/ioutil" 17 | "math/rand" 18 | "net/http" 19 | "os" 20 | "runtime" 21 | "strconv" 22 | "strings" 23 | "text/template" 24 | "time" 25 | 26 | "github.com/plutobell/pi-dashboard-go/config" 27 | "github.com/plutobell/pi-dashboard-go/device" 28 | 29 | "github.com/gorilla/sessions" 30 | "github.com/labstack/echo-contrib/session" 31 | "github.com/labstack/echo/v4" 32 | "github.com/labstack/echo/v4/middleware" 33 | ) 34 | 35 | //go:embed assets 36 | var assets embed.FS 37 | 38 | // Template 模板 39 | type Template struct { 40 | templates *template.Template 41 | } 42 | 43 | // Render 渲染器 44 | func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 45 | return t.templates.ExecuteTemplate(w, name, data) 46 | } 47 | 48 | // Server 实例 49 | func Run() { 50 | //Echo 实例 51 | e := echo.New() 52 | port := ":" + config.Port 53 | 54 | //注册中间件 55 | e.Use(middleware.Recover()) 56 | e.Use(middleware.Secure()) 57 | e.Use(middleware.Decompress()) 58 | e.Use(middleware.Timeout()) 59 | e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ 60 | Level: 9, 61 | })) 62 | e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ 63 | TokenLookup: "header:X-XSRF-TOKEN", 64 | CookieName: "cf_sid", 65 | CookieMaxAge: 86400, 66 | })) 67 | // e.Use(session.Middleware(sessions.NewFilesystemStore("./", []byte(getRandomString(16))))) 68 | e.Use(session.Middleware(sessions.NewCookieStore([]byte(getRandomString(32))))) 69 | if config.EnableLogger { 70 | // e.Use(middleware.Logger()) 71 | e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ 72 | Format: "${remote_ip} - - [${time_rfc3339}] ${method} ${uri} ${status} ${latency_human} ${bytes_in} ${bytes_out} ${user_agent}\n", 73 | })) 74 | } 75 | 76 | //静态文件 77 | btnsHandler := http.FileServer(getFileSystem(false, "btns")) 78 | cssHandler := http.FileServer(getFileSystem(false, "css")) 79 | devicesHandler := http.FileServer(getFileSystem(false, "devices")) 80 | faviconsHandler := http.FileServer(getFileSystem(false, "favicons")) 81 | jsHandler := http.FileServer(getFileSystem(false, "js")) 82 | 83 | e.GET("/btns/*", echo.WrapHandler(http.StripPrefix("/btns/", btnsHandler))) 84 | e.GET("/css/*", echo.WrapHandler(http.StripPrefix("/css/", cssHandler))) 85 | e.GET("/devices/*", echo.WrapHandler(http.StripPrefix("/devices/", devicesHandler))) 86 | e.GET("/favicons/*", echo.WrapHandler(http.StripPrefix("/favicons/", faviconsHandler))) 87 | e.GET("/js/*", echo.WrapHandler(http.StripPrefix("/js/", jsHandler))) 88 | 89 | //初始化模版引擎 90 | t := &Template{ 91 | templates: template.Must(template.New("").ParseFS(assets, "assets/views/*.tmpl")), 92 | } 93 | 94 | //向echo实例注册模版引擎 95 | e.Renderer = t 96 | 97 | // 路由 98 | e.GET("/", Index) 99 | e.GET("/login", Login) 100 | e.POST("/api/*", API) 101 | 102 | // 启动服务 103 | e.HideBanner = true 104 | fmt.Println("⇨ Pi Dashboard Go v" + config.VERSION) 105 | if isRootUser() != true { 106 | fmt.Println("⇨ Some functions are unavailable to non-root users") 107 | } 108 | e.Logger.Fatal(e.Start(port)) 109 | } 110 | 111 | func Index(c echo.Context) error { 112 | username, _ := getNowUsernameAndPassword() 113 | 114 | sess, _ := session.Get(config.SessionName, c) 115 | //通过sess.Values读取会话数据 116 | userName, _ := sess.Values["id"] 117 | isLogin, _ := sess.Values["isLogin"] 118 | 119 | if userName != username || isLogin != true { 120 | return c.Redirect(http.StatusTemporaryRedirect, "/login") 121 | } 122 | 123 | device := device.Info() 124 | device["version"] = config.VERSION 125 | device["site_title"] = config.Title 126 | device["interval"] = config.Interval 127 | device["theme"] = config.Theme 128 | device["go_version"] = runtime.Version() 129 | 130 | return c.Render(http.StatusOK, "index.tmpl", device) 131 | } 132 | 133 | func Login(c echo.Context) error { 134 | username, _ := getNowUsernameAndPassword() 135 | 136 | sess, _ := session.Get(config.SessionName, c) 137 | //通过sess.Values读取会话数据 138 | userName := sess.Values["id"] 139 | isLogin := sess.Values["isLogin"] 140 | 141 | if userName == username && isLogin == true { 142 | return c.Redirect(http.StatusTemporaryRedirect, "/") 143 | } 144 | 145 | tempDevice := device.Info() 146 | device := make(map[string]string) 147 | device["version"] = config.VERSION 148 | device["site_title"] = config.Title 149 | device["theme"] = config.Theme 150 | device["go_version"] = runtime.Version() 151 | device["device_photo"] = tempDevice["device_photo"].(string) 152 | device["favicon"] = tempDevice["favicon"].(string) 153 | 154 | return c.Render(http.StatusOK, "login.tmpl", device) 155 | } 156 | 157 | func API(c echo.Context) error { 158 | switch method := strings.Split(c.Request().URL.Path, "api/")[1]; { 159 | 160 | case method == "login": 161 | username, password := getNowUsernameAndPassword() 162 | 163 | sess, _ := session.Get(config.SessionName, c) 164 | //通过sess.Values读取会话数据 165 | userName := sess.Values["id"] 166 | isLogin := sess.Values["isLogin"] 167 | 168 | if userName == username && isLogin == true { 169 | status := map[string]bool{ 170 | "status": true, 171 | } 172 | return c.JSON(http.StatusOK, status) 173 | } 174 | 175 | //获取登录信息 176 | json_map := make(map[string]interface{}) 177 | err := json.NewDecoder(c.Request().Body).Decode(&json_map) 178 | var loginUsername, loginPassword interface{} 179 | if err != nil { 180 | loginUsername = "" 181 | loginPassword = "" 182 | } else { 183 | loginUsername = json_map["username"] 184 | loginPassword = json_map["password"] 185 | } 186 | 187 | if loginUsername == username && loginPassword == password { 188 | maxAge, _ := strconv.Atoi(config.SessionMaxAge) 189 | 190 | sess, _ := session.Get(config.SessionName, c) 191 | sess.Options = &sessions.Options{ 192 | Path: "/", //所有页面都可以访问会话数据 193 | MaxAge: 86400 * maxAge, //会话有效期,单位秒 194 | HttpOnly: true, 195 | } 196 | //记录会话数据, sess.Values 是map类型,可以记录多个会话数据 197 | sess.Values["id"] = loginUsername 198 | sess.Values["isLogin"] = true 199 | //保存用户会话数据 200 | sess.Save(c.Request(), c.Response()) 201 | 202 | status := map[string]bool{ 203 | "status": true, 204 | } 205 | return c.JSON(http.StatusOK, status) 206 | } else { 207 | 208 | status := map[string]bool{ 209 | "status": false, 210 | } 211 | return c.JSON(http.StatusOK, status) 212 | } 213 | 214 | case method == "logout": 215 | sess, _ := session.Get(config.SessionName, c) 216 | sess.Options = &sessions.Options{ 217 | Path: "/", 218 | MaxAge: -1, 219 | HttpOnly: false, 220 | } 221 | sess.Values["id"] = "" 222 | sess.Values["isLogin"] = "" 223 | 224 | sess.Save(c.Request(), c.Response()) 225 | 226 | status := map[string]bool{ 227 | "status": true, 228 | } 229 | return c.JSON(http.StatusOK, status) 230 | 231 | case method == "device": 232 | username, _ := getNowUsernameAndPassword() 233 | 234 | sess, _ := session.Get(config.SessionName, c) 235 | //通过sess.Values读取会话数据 236 | userName := sess.Values["id"] 237 | isLogin := sess.Values["isLogin"] 238 | 239 | if userName != username || isLogin != true { 240 | status := map[string]string{ 241 | "status": "Unauthorized", 242 | } 243 | return c.JSON(http.StatusOK, status) 244 | } 245 | 246 | device := device.Info() 247 | device["version"] = config.VERSION 248 | device["site_title"] = config.Title 249 | device["interval"] = config.Interval 250 | device["go_version"] = runtime.Version() 251 | 252 | return c.JSON(http.StatusOK, device) 253 | 254 | case method == "operation": 255 | username, _ := getNowUsernameAndPassword() 256 | 257 | sess, _ := session.Get(config.SessionName, c) 258 | //通过sess.Values读取会话数据 259 | userName := sess.Values["id"] 260 | isLogin := sess.Values["isLogin"] 261 | 262 | if userName != username || isLogin != true { 263 | status := map[string]string{ 264 | "status": "Unauthorized", 265 | } 266 | return c.JSON(http.StatusOK, status) 267 | } 268 | 269 | operation := c.QueryParam("action") 270 | 271 | if operation != "checknewversion" && !isRootUser() { 272 | status := map[string]string{ 273 | "status": "NotRootUser", 274 | } 275 | 276 | return c.JSON(http.StatusOK, status) 277 | } 278 | 279 | status := map[string]bool{ 280 | "status": true, 281 | } 282 | 283 | switch operation { 284 | case "reboot": 285 | go device.Popen("reboot") 286 | return c.JSON(http.StatusOK, status) 287 | case "shutdown": 288 | go device.Popen("shutdown -h now") 289 | return c.JSON(http.StatusOK, status) 290 | case "dropcaches": 291 | go device.Popen("echo 3 > /proc/sys/vm/drop_caches") 292 | return c.JSON(http.StatusOK, status) 293 | case "checknewversion": 294 | nowVersion, releaseNotes, _ := getLatestVersionFromGitHub() 295 | result := make(map[string]string) 296 | if nowVersion > config.VERSION { 297 | result["new_version"] = nowVersion 298 | result["new_version_notes"] = releaseNotes 299 | result["new_version_url"] = config.PROJECT + "/releases/tag/v" + nowVersion 300 | } else { 301 | result["new_version"] = "" 302 | result["new_version_notes"] = "" 303 | result["new_version_url"] = "" 304 | } 305 | 306 | return c.JSON(http.StatusOK, result) 307 | 308 | } 309 | 310 | } 311 | 312 | status := map[string]string{ 313 | "status": "UnknownMethod", 314 | } 315 | return c.JSON(http.StatusOK, status) 316 | } 317 | 318 | func getNowUsernameAndPassword() (username, password string) { 319 | username = config.USERNAME 320 | password = config.PASSWORD 321 | auth := strings.Split(config.Auth, ":") 322 | if len(auth) == 2 { 323 | username = auth[0] 324 | password = auth[1] 325 | } 326 | 327 | return username, password 328 | } 329 | 330 | func getFileSystem(useOS bool, dir string) http.FileSystem { 331 | assetsDir := "assets/" 332 | 333 | if useOS { 334 | // using live mode. 335 | return http.FS(os.DirFS(assetsDir + dir)) 336 | } 337 | 338 | // using embed mode. 339 | fsys, err := fs.Sub(assets, assetsDir+dir) 340 | if err != nil { 341 | panic(err) 342 | } 343 | 344 | return http.FS(fsys) 345 | } 346 | 347 | func getRandomString(len int) string { 348 | str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 349 | bytes := []byte(str) 350 | result := []byte{} 351 | 352 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 353 | for i := 0; i < len; i++ { 354 | result = append(result, bytes[r.Intn(62)]) 355 | } 356 | 357 | return string(result) 358 | } 359 | 360 | func getLatestVersionFromGitHub() ( 361 | nowVersion string, releaseNotes string, downloadURL []string) { 362 | 363 | url := "https://api.github.com/repos/plutobell/pi-dashboard-go/releases/latest" 364 | 365 | client := http.Client{ 366 | Transport: &http.Transport{ 367 | Proxy: http.ProxyFromEnvironment, 368 | }, 369 | } 370 | 371 | resp, err := client.Get(url) 372 | if err != nil { 373 | panic(err) 374 | } 375 | defer resp.Body.Close() 376 | 377 | body, err := ioutil.ReadAll(resp.Body) 378 | if err != nil { 379 | panic(err) 380 | } 381 | 382 | result := make(map[string]interface{}) 383 | err = json.Unmarshal(body, &result) 384 | if err != nil { 385 | panic(err) 386 | } 387 | 388 | for key, value := range result { 389 | if key == "tag_name" { 390 | nowVersion = value.(string)[1:] 391 | } 392 | if key == "assets" { 393 | assets := value.([]interface{}) 394 | for _, architecture := range assets { 395 | for key, value := range architecture.(map[string]interface{}) { 396 | if key == "browser_download_url" { 397 | downloadURL = append(downloadURL, value.(string)) 398 | } 399 | } 400 | } 401 | } 402 | if key == "body" { 403 | releaseNotes = value.(string) 404 | } 405 | } 406 | 407 | return nowVersion, releaseNotes, downloadURL 408 | } 409 | 410 | func isRootUser() bool { 411 | if config.LinuxUserInfo.Gid == "0" && 412 | config.LinuxUserInfo.Uid == "0" && 413 | config.LinuxUserInfo.Username == "root" { 414 | return true 415 | } 416 | 417 | return false 418 | } 419 | -------------------------------------------------------------------------------- /server/assets/js/exporting.js: -------------------------------------------------------------------------------- 1 | /* 2 | Highcharts JS v9.1.2 (2021-06-16) 3 | 4 | Exporting module 5 | 6 | (c) 2010-2021 Torstein Honsi 7 | 8 | License: www.highcharts.com/license 9 | */ 10 | 'use strict';(function(c){"object"===typeof module&&module.exports?(c["default"]=c,module.exports=c):"function"===typeof define&&define.amd?define("highcharts/modules/exporting",["highcharts"],function(q){c(q);c.Highcharts=q;return c}):c("undefined"!==typeof Highcharts?Highcharts:void 0)})(function(c){function q(c,m,h,k){c.hasOwnProperty(m)||(c[m]=k.apply(null,h))}c=c?c._modules:{};q(c,"Extensions/FullScreen.js",[c["Core/Chart/Chart.js"],c["Core/Globals.js"],c["Core/Renderer/HTML/AST.js"],c["Core/Utilities.js"]], 11 | function(c,m,h,k){var n=k.addEvent;k=function(){function c(e){this.chart=e;this.isOpen=!1;e=e.renderTo;this.browserProps||("function"===typeof e.requestFullscreen?this.browserProps={fullscreenChange:"fullscreenchange",requestFullscreen:"requestFullscreen",exitFullscreen:"exitFullscreen"}:e.mozRequestFullScreen?this.browserProps={fullscreenChange:"mozfullscreenchange",requestFullscreen:"mozRequestFullScreen",exitFullscreen:"mozCancelFullScreen"}:e.webkitRequestFullScreen?this.browserProps={fullscreenChange:"webkitfullscreenchange", 12 | requestFullscreen:"webkitRequestFullScreen",exitFullscreen:"webkitExitFullscreen"}:e.msRequestFullscreen&&(this.browserProps={fullscreenChange:"MSFullscreenChange",requestFullscreen:"msRequestFullscreen",exitFullscreen:"msExitFullscreen"}))}c.prototype.close=function(){var e=this.chart,c=e.options.chart;if(this.isOpen&&this.browserProps&&e.container.ownerDocument instanceof Document)e.container.ownerDocument[this.browserProps.exitFullscreen]();this.unbindFullscreenEvent&&(this.unbindFullscreenEvent= 13 | this.unbindFullscreenEvent());e.setSize(this.origWidth,this.origHeight,!1);this.origHeight=this.origWidth=void 0;c.width=this.origWidthOption;c.height=this.origHeightOption;this.origHeightOption=this.origWidthOption=void 0;this.isOpen=!1;this.setButtonText()};c.prototype.open=function(){var e=this,c=e.chart,h=c.options.chart;h&&(e.origWidthOption=h.width,e.origHeightOption=h.height);e.origWidth=c.chartWidth;e.origHeight=c.chartHeight;if(e.browserProps){var k=n(c.container.ownerDocument,e.browserProps.fullscreenChange, 14 | function(){e.isOpen?(e.isOpen=!1,e.close()):(c.setSize(null,null,!1),e.isOpen=!0,e.setButtonText())}),m=n(c,"destroy",k);e.unbindFullscreenEvent=function(){k();m()};if(h=c.renderTo[e.browserProps.requestFullscreen]())h["catch"](function(){alert("Full screen is not supported inside a frame.")})}};c.prototype.setButtonText=function(){var e=this.chart,c=e.exportDivElements,k=e.options.exporting,m=k&&k.buttons&&k.buttons.contextButton.menuItems;e=e.options.lang;k&&k.menuItemDefinitions&&e&&e.exitFullscreen&& 15 | e.viewFullscreen&&m&&c&&c.length&&h.setElementHTML(c[m.indexOf("viewFullscreen")],this.isOpen?e.exitFullscreen:k.menuItemDefinitions.viewFullscreen.text||e.viewFullscreen)};c.prototype.toggle=function(){this.isOpen?this.close():this.open()};return c}();m.Fullscreen=k;n(c,"beforeRender",function(){this.fullscreen=new m.Fullscreen(this)});return m.Fullscreen});q(c,"Mixins/Navigation.js",[],function(){return{initUpdate:function(c){c.navigation||(c.navigation={updates:[],update:function(c,h){this.updates.forEach(function(k){k.update.call(k.context, 16 | c,h)})}})},addUpdate:function(c,m){m.navigation||this.initUpdate(m);m.navigation.updates.push({update:c,context:m})}}});q(c,"Extensions/Exporting.js",[c["Core/Chart/Chart.js"],c["Mixins/Navigation.js"],c["Core/Globals.js"],c["Core/DefaultOptions.js"],c["Core/Color/Palette.js"],c["Core/Renderer/SVG/SVGRenderer.js"],c["Core/Utilities.js"]],function(c,m,h,k,n,q,e){var z=h.doc,H=h.isTouchDevice,B=h.win;k=k.defaultOptions;var D=q.prototype.symbols,x=e.addEvent,r=e.css,y=e.createElement,F=e.discardElement, 17 | A=e.extend,I=e.find,E=e.fireEvent,J=e.isObject,p=e.merge,G=e.objectEach,t=e.pick,K=e.removeEvent,L=e.uniqueKey;A(k.lang,{viewFullscreen:"View in full screen",exitFullscreen:"Exit from full screen",printChart:"Print chart",downloadPNG:"Download PNG image",downloadJPEG:"Download JPEG image",downloadPDF:"Download PDF document",downloadSVG:"Download SVG vector image",contextButtonTitle:"Chart context menu"});k.navigation||(k.navigation={});p(!0,k.navigation,{buttonOptions:{theme:{},symbolSize:14,symbolX:12.5, 18 | symbolY:10.5,align:"right",buttonSpacing:3,height:22,verticalAlign:"top",width:24}});p(!0,k.navigation,{menuStyle:{border:"1px solid "+n.neutralColor40,background:n.backgroundColor,padding:"5px 0"},menuItemStyle:{padding:"0.5em 1em",color:n.neutralColor80,background:"none",fontSize:H?"14px":"11px",transition:"background 250ms, color 250ms"},menuItemHoverStyle:{background:n.highlightColor80,color:n.backgroundColor},buttonOptions:{symbolFill:n.neutralColor60,symbolStroke:n.neutralColor60,symbolStrokeWidth:3, 19 | theme:{padding:5}}});k.exporting={type:"image/png",url:"https://export.highcharts.com/",printMaxWidth:780,scale:2,buttons:{contextButton:{className:"highcharts-contextbutton",menuClassName:"highcharts-contextmenu",symbol:"menu",titleKey:"contextButtonTitle",menuItems:"viewFullscreen printChart separator downloadPNG downloadJPEG downloadPDF downloadSVG".split(" ")}},menuItemDefinitions:{viewFullscreen:{textKey:"viewFullscreen",onclick:function(){this.fullscreen.toggle()}},printChart:{textKey:"printChart", 20 | onclick:function(){this.print()}},separator:{separator:!0},downloadPNG:{textKey:"downloadPNG",onclick:function(){this.exportChart()}},downloadJPEG:{textKey:"downloadJPEG",onclick:function(){this.exportChart({type:"image/jpeg"})}},downloadPDF:{textKey:"downloadPDF",onclick:function(){this.exportChart({type:"application/pdf"})}},downloadSVG:{textKey:"downloadSVG",onclick:function(){this.exportChart({type:"image/svg+xml"})}}}};h.post=function(a,b,f){var d=y("form",p({method:"post",action:a,enctype:"multipart/form-data"}, 21 | f),{display:"none"},z.body);G(b,function(a,b){y("input",{type:"hidden",name:b,value:a},null,d)});d.submit();F(d)};h.isSafari&&h.win.matchMedia("print").addListener(function(a){h.printingChart&&(a.matches?h.printingChart.beforePrint():h.printingChart.afterPrint())});A(c.prototype,{sanitizeSVG:function(a,b){var f=a.indexOf("")+6,d=a.substr(f);a=a.substr(0,f);b&&b.exporting&&b.exporting.allowHTML&&d&&(d=''+ 22 | d.replace(/(<(?:img|br).*?(?=>))>/g,"$1 />")+"",a=a.replace("",d+""));a=a.replace(/zIndex="[^"]+"/g,"").replace(/symbolName="[^"]+"/g,"").replace(/jQuery[0-9]+="[^"]+"/g,"").replace(/url\(("|")(.*?)("|");?\)/g,"url($2)").replace(/url\([^#]+#/g,"url(#").replace(/]+(>|$)/g,"").replace(/[\s_]+/g,"-").replace(/[^a-z0-9\-]/g,"").replace(/^[\-]+/g,"").replace(/[\-]+/g,"-").substr(0,24).replace(/[\-]+$/g,""));if(!b||5>b.length)b="chart";return b},exportChart:function(a,b){b=this.getSVGForExport(a,b);a=p(this.options.exporting,a);h.post(a.url,{filename:a.filename?a.filename.replace(/\//g,"-"):this.getFilename(),type:a.type, 28 | width:a.width||0,scale:a.scale,svg:b},a.formAttributes)},moveContainers:function(a){(this.fixedDiv?[this.fixedDiv,this.scrollingContainer]:[this.container]).forEach(function(b){a.appendChild(b)})},beforePrint:function(){var a=z.body,b=this.options.exporting.printMaxWidth,f={childNodes:a.childNodes,origDisplay:[],resetParams:void 0};this.isPrinting=!0;this.pointer.reset(null,0);E(this,"beforePrint");b&&this.chartWidth>b&&(f.resetParams=[this.options.chart.width,void 0,!1],this.setSize(b,void 0,!1)); 29 | [].forEach.call(f.childNodes,function(a,b){1===a.nodeType&&(f.origDisplay[b]=a.style.display,a.style.display="none")});this.moveContainers(a);this.printReverseInfo=f},afterPrint:function(){if(this.printReverseInfo){var a=this.printReverseInfo.childNodes,b=this.printReverseInfo.origDisplay,f=this.printReverseInfo.resetParams;this.moveContainers(this.renderTo);[].forEach.call(a,function(a,f){1===a.nodeType&&(a.style.display=b[f]||"")});this.isPrinting=!1;f&&this.setSize.apply(this,f);delete this.printReverseInfo; 30 | delete h.printingChart;E(this,"afterPrint")}},print:function(){var a=this;a.isPrinting||(h.printingChart=a,h.isSafari||a.beforePrint(),setTimeout(function(){B.focus();B.print();h.isSafari||setTimeout(function(){a.afterPrint()},1E3)},1))},contextMenu:function(a,b,f,d,c,h,k){var g=this,u=g.options.navigation,m=g.chartWidth,C=g.chartHeight,v="cache-"+a,l=g[v],w=Math.max(c,h);if(!l){g.exportContextMenu=g[v]=l=y("div",{className:a},{position:"absolute",zIndex:1E3,padding:w+"px",pointerEvents:"auto"},g.fixedDiv|| 31 | g.container);var p=y("ul",{className:"highcharts-menu"},{listStyle:"none",margin:0,padding:0},l);g.styledMode||r(p,A({MozBoxShadow:"3px 3px 10px #888",WebkitBoxShadow:"3px 3px 10px #888",boxShadow:"3px 3px 10px #888"},u.menuStyle));l.hideMenu=function(){r(l,{display:"none"});k&&k.setState(0);g.openMenu=!1;r(g.renderTo,{overflow:"hidden"});r(g.container,{overflow:"hidden"});e.clearTimeout(l.hideTimer);E(g,"exportMenuHidden")};g.exportEvents.push(x(l,"mouseleave",function(){l.hideTimer=B.setTimeout(l.hideMenu, 32 | 500)}),x(l,"mouseenter",function(){e.clearTimeout(l.hideTimer)}),x(z,"mouseup",function(b){g.pointer.inClass(b.target,a)||l.hideMenu()}),x(l,"click",function(){g.openMenu&&l.hideMenu()}));b.forEach(function(a){"string"===typeof a&&(a=g.options.exporting.menuItemDefinitions[a]);if(J(a,!0)){var b=void 0;a.separator?b=y("hr",null,null,p):("viewData"===a.textKey&&g.isDataTableVisible&&(a.textKey="hideData"),b=y("li",{className:"highcharts-menu-item",onclick:function(b){b&&b.stopPropagation();l.hideMenu(); 33 | a.onclick&&a.onclick.apply(g,arguments)}},null,p),b.appendChild(z.createTextNode(a.text||g.options.lang[a.textKey])),g.styledMode||(b.onmouseover=function(){r(this,u.menuItemHoverStyle)},b.onmouseout=function(){r(this,u.menuItemStyle)},r(b,A({cursor:"pointer"},u.menuItemStyle))));g.exportDivElements.push(b)}});g.exportDivElements.push(p,l);g.exportMenuWidth=l.offsetWidth;g.exportMenuHeight=l.offsetHeight}b={display:"block"};f+g.exportMenuWidth>m?b.right=m-f-c-w+"px":b.left=f-w+"px";d+h+g.exportMenuHeight> 34 | C&&"top"!==k.alignOptions.verticalAlign?b.bottom=C-d-w+"px":b.top=d+h-w+"px";r(l,b);r(g.renderTo,{overflow:""});r(g.container,{overflow:""});g.openMenu=!0;E(g,"exportMenuShown")},addButton:function(a){var b=this,f=b.renderer,d=p(b.options.navigation.buttonOptions,a),c=d.onclick,e=d.menuItems,h=d.symbolSize||12;b.btnCount||(b.btnCount=0);b.exportDivElements||(b.exportDivElements=[],b.exportSVGElements=[]);if(!1!==d.enabled&&d.theme){var g=d.theme,k=g.states,m=k&&k.hover;k=k&&k.select;var C;b.styledMode|| 35 | (g.fill=t(g.fill,n.backgroundColor),g.stroke=t(g.stroke,"none"));delete g.states;c?C=function(a){a&&a.stopPropagation();c.call(b,a)}:e&&(C=function(a){a&&a.stopPropagation();b.contextMenu(v.menuClassName,e,v.translateX,v.translateY,v.width,v.height,v);v.setState(2)});d.text&&d.symbol?g.paddingLeft=t(g.paddingLeft,30):d.text||A(g,{width:d.width,height:d.height,padding:0});b.styledMode||(g["stroke-linecap"]="round",g.fill=t(g.fill,n.backgroundColor),g.stroke=t(g.stroke,"none"));var v=f.button(d.text, 36 | 0,0,C,g,m,k).addClass(a.className).attr({title:t(b.options.lang[d._titleKey||d.titleKey],"")});v.menuClassName=a.menuClassName||"highcharts-menu-"+b.btnCount++;if(d.symbol){var l=f.symbol(d.symbol,d.symbolX-h/2,d.symbolY-h/2,h,h,{width:h,height:h}).addClass("highcharts-button-symbol").attr({zIndex:1}).add(v);b.styledMode||l.attr({stroke:d.symbolStroke,fill:d.symbolFill,"stroke-width":d.symbolStrokeWidth||1})}v.add(b.exportingGroup).align(A(d,{width:v.width,x:t(d.x,b.buttonOffset)}),!0,"spacingBox"); 37 | b.buttonOffset+=(v.width+d.buttonSpacing)*("right"===d.align?-1:1);b.exportSVGElements.push(v,l)}},destroyExport:function(a){var b=a?a.target:this;a=b.exportSVGElements;var f=b.exportDivElements,d=b.exportEvents,c;a&&(a.forEach(function(a,d){a&&(a.onclick=a.ontouchstart=null,c="cache-"+a.menuClassName,b[c]&&delete b[c],b.exportSVGElements[d]=a.destroy())}),a.length=0);b.exportingGroup&&(b.exportingGroup.destroy(),delete b.exportingGroup);f&&(f.forEach(function(a,d){e.clearTimeout(a.hideTimer);K(a, 38 | "mouseleave");b.exportDivElements[d]=a.onmouseout=a.onmouseover=a.ontouchstart=a.onclick=null;F(a)}),f.length=0);d&&(d.forEach(function(a){a()}),d.length=0)}});q.prototype.inlineToAttributes="fill stroke strokeLinecap strokeLinejoin strokeWidth textAnchor x y".split(" ");q.prototype.inlineBlacklist=[/-/,/^(clipPath|cssText|d|height|width)$/,/^font$/,/[lL]ogical(Width|Height)$/,/perspective/,/TapHighlightColor/,/^transition/,/^length$/];q.prototype.unstyledElements=["clipPath","defs","desc"];c.prototype.inlineStyles= 39 | function(){function a(a){return a.replace(/([A-Z])/g,function(a,b){return"-"+b.toLowerCase()})}function b(c){function f(b,f){w=r=!1;if(k){for(t=k.length;t--&&!r;)r=k[t].test(f);w=!r}"transform"===f&&"none"===b&&(w=!0);for(t=e.length;t--&&!w;)w=e[t].test(f)||"function"===typeof b;w||z[f]===b&&"svg"!==c.nodeName||g[c.nodeName][f]===b||(d&&-1===d.indexOf(f)?l+=a(f)+":"+b+";":b&&c.setAttribute(a(f),b))}var l="",w,r,t;if(1===c.nodeType&&-1===m.indexOf(c.nodeName)){var u=B.getComputedStyle(c,null);var z= 40 | "svg"===c.nodeName?{}:B.getComputedStyle(c.parentNode,null);if(!g[c.nodeName]){n=q.getElementsByTagName("svg")[0];var x=q.createElementNS(c.namespaceURI,c.nodeName);n.appendChild(x);g[c.nodeName]=p(B.getComputedStyle(x,null));"text"===c.nodeName&&delete g.text.fill;n.removeChild(x)}if(h.isFirefox||h.isMS)for(var y in u)f(u[y],y);else G(u,f);l&&(u=c.getAttribute("style"),c.setAttribute("style",(u?u+";":"")+l));"svg"===c.nodeName&&c.setAttribute("stroke-width","1px");"text"!==c.nodeName&&[].forEach.call(c.children|| 41 | c.childNodes,b)}}var c=this.renderer,d=c.inlineToAttributes,e=c.inlineBlacklist,k=c.inlineWhitelist,m=c.unstyledElements,g={},n;c=z.createElement("iframe");r(c,{width:"1px",height:"1px",visibility:"hidden"});z.body.appendChild(c);var q=c.contentWindow.document;q.open();q.write('');q.close();b(this.container.querySelector("svg"));n.parentNode.removeChild(n);c.parentNode.removeChild(c)};D.menu=function(a,b,c,d){return[["M",a,b+2.5],["L",a+c,b+2.5],["M", 42 | a,b+d/2+.5],["L",a+c,b+d/2+.5],["M",a,b+d-1.5],["L",a+c,b+d-1.5]]};D.menuball=function(a,b,c,d){a=[];d=d/3-2;return a=a.concat(D.circle(c-d,b,d,d),D.circle(c-d,b+d+4,d,d),D.circle(c-d,b+2*(d+4),d,d))};c.prototype.renderExporting=function(){var a=this,b=a.options.exporting,c=b.buttons,d=a.isDirtyExporting||!a.exportSVGElements;a.buttonOffset=0;a.isDirtyExporting&&a.destroyExport();d&&!1!==b.enabled&&(a.exportEvents=[],a.exportingGroup=a.exportingGroup||a.renderer.g("exporting-group").attr({zIndex:3}).add(), 43 | G(c,function(b){a.addButton(b)}),a.isDirtyExporting=!1)};x(c,"init",function(){var a=this;a.exporting={update:function(b,c){a.isDirtyExporting=!0;p(!0,a.options.exporting,b);t(c,!0)&&a.redraw()}};m.addUpdate(function(b,c){a.isDirtyExporting=!0;p(!0,a.options.navigation,b);t(c,!0)&&a.redraw()},a)});c.prototype.callbacks.push(function(a){a.renderExporting();x(a,"redraw",a.renderExporting);x(a,"destroy",a.destroyExport)});""});q(c,"masters/modules/exporting.src.js",[],function(){})}); 44 | //# sourceMappingURL=exporting.js.map -------------------------------------------------------------------------------- /device/device.go: -------------------------------------------------------------------------------- 1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go) 2 | // @Description: Golang implementation of pi-dashboard 3 | // @Author: github.com/plutobell 4 | // @Creation: 2020-08-01 5 | // @Last modification: 2023-04-05 6 | // @Version: 1.7.0 7 | 8 | package device 9 | 10 | import ( 11 | "encoding/json" 12 | "errors" 13 | "fmt" 14 | "io/ioutil" 15 | "math" 16 | "os/exec" 17 | "reflect" 18 | "runtime" 19 | "strconv" 20 | "strings" 21 | "time" 22 | 23 | "github.com/plutobell/pi-dashboard-go/config" 24 | "github.com/shirou/gopsutil/v3/cpu" 25 | "github.com/shirou/gopsutil/v3/disk" 26 | "github.com/shirou/gopsutil/v3/host" 27 | "github.com/shirou/gopsutil/v3/load" 28 | "github.com/shirou/gopsutil/v3/mem" 29 | "github.com/shirou/gopsutil/v3/net" 30 | ) 31 | 32 | // piCpuModelInfo Raspberry Pi CPU型号信息 33 | var piCpuModelInfo = map[string]string{ 34 | "Raspberry Pi 4 Model B": "BCM2711", 35 | "Raspberry Pi 3 Model B+": "BCM2837B0", 36 | "Raspberry Pi 3 Model B": "BCM2837/A0/B0", 37 | "Raspberry Pi 2 Model B": "BCM2836/7", 38 | "Raspberry Pi Model B+": "BCM2835", 39 | "Raspberry Pi Model B": "BCM2835", 40 | "Raspberry Pi 3 Model A+": "BCM2837B0", 41 | "Raspberry Pi Model A+": "BCM2835", 42 | "Raspberry Pi Zero WH": "BCM2835", 43 | "Raspberry Pi Zero W": "BCM2835", 44 | "Raspberry Pi Zero": "BCM2835", 45 | } 46 | 47 | // Command 命令列表 48 | var command map[string]string = map[string]string{ 49 | "ip": "ip a | grep -w inet | grep -v inet6 | grep -v 127 | awk '{ print $2 }'", 50 | "login_user_count": "who -q | awk 'NR==2{print $2}'", 51 | "system": "cat /etc/os-release | grep PRETTY_NAME=", 52 | "uname": "uname -a", 53 | "model": "cat /proc/cpuinfo | grep -i Model |sort -u |head -1", 54 | "cpu_status": "top -bn1 | grep -w '%Cpu(s):' | awk '{ print $2,$4,$6,$8,$10,$12,$14,$16}'", 55 | "cpu_model_name": "lscpu | grep 'Model name' | awk '{ print $3}'", 56 | "cpu_freq": "cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq", 57 | } 58 | 59 | var ( 60 | arch string 61 | os string 62 | model string 63 | ) 64 | 65 | type Host struct { 66 | Model string `json:"model"` 67 | HostName string `json:"hostname"` 68 | Uptime string `json:"uptime_raw"` 69 | UptimeFormat string `json:"uptime"` 70 | IP string `json:"ip"` 71 | System string `json:"system"` 72 | Uname string `json:"uname"` 73 | CurrentUser string `json:"current_user"` 74 | OS string `json:"os"` 75 | LoginUserCount string `json:"login_user_count"` 76 | NowTimeHMS string `json:"now_time_hms"` 77 | NowTimeYMD string `json:"now_time_ymd"` 78 | } 79 | 80 | type CPU struct { 81 | Arch string `json:"arch"` 82 | Cores string `json:"cpu_cores"` 83 | Mhz string `json:"cpu_freq"` 84 | ModelName string `json:"cpu_model_name"` 85 | Revision string `json:"cpu_revision"` 86 | Idle string `json:"cpu_status_idle"` 87 | Iowait string `json:"cpu_status_iowait"` 88 | Irq string `json:"cpu_status_irq"` 89 | Nice string `json:"cpu_status_nice"` 90 | Softirg string `json:"cpu_status_softirq"` 91 | System string `json:"cpu_status_system"` 92 | User string `json:"cpu_status_user"` 93 | Temperature string `json:"cpu_temperature"` 94 | UsedPercent string `json:"cpu_used"` 95 | } 96 | 97 | type Disk struct { 98 | Path string `json:"disk_name"` 99 | Free string `json:"disk_free"` 100 | Total string `json:"disk_total"` 101 | Used string `json:"disk_used"` 102 | UsedPercent string `json:"disk_used_percent"` 103 | } 104 | 105 | type Memory struct { 106 | Total string `json:"memory_total"` 107 | Used string `json:"memory_used"` 108 | Free string `json:"memory_free"` 109 | Percent string `json:"memory_percent"` 110 | Available string `json:"memory_available"` 111 | Buffers string `json:"memory_buffers"` 112 | Cached string `json:"memory_cached"` 113 | CachedPercent string `json:"memory_cached_percent"` 114 | RealPercent string `json:"memory_real_percent"` 115 | RealUsed string `json:"memory_real_used"` 116 | 117 | SwapTotal string `json:"swap_total"` 118 | SwapFree string `json:"swap_free"` 119 | SwapUsed string `json:"swap_used"` 120 | SwapUsedPercent string `json:"swap_used_percent"` 121 | } 122 | 123 | type Net struct { 124 | Name string `json:"net_dev_name"` 125 | DataIn string `json:"net_status_in_data"` 126 | DataOut string `json:"net_status_out_data"` 127 | DataInFormat string `json:"net_status_in_data_format"` 128 | DatatOutFormat string `json:"net_status_out_data_format"` 129 | PackageIn string `json:"net_status_in_package"` 130 | PackageOut string `json:"net_status_out_package"` 131 | LoadAvg string `json:"net_status_load_average"` 132 | } 133 | 134 | type NetLo struct { 135 | Name string `json:"net_lo_dev_name"` 136 | DataIn string `json:"net_status_lo_in_data"` 137 | DataOut string `json:"net_status_lo_out_data"` 138 | DataInFormat string `json:"net_status_lo_in_data_format"` 139 | DatatOutFormat string `json:"net_status_lo_out_data_format"` 140 | PackageIn string `json:"net_status_lo_in_package"` 141 | PackageOut string `json:"net_status_lo_out_package"` 142 | LoadAvg string `json:"net_status_lo_load_average"` 143 | } 144 | 145 | type Load struct { 146 | Load1 string `json:"load_average_1m"` 147 | Load5 string `json:"load_average_5m"` 148 | Load15 string `json:"load_average_15m"` 149 | } 150 | 151 | type Process struct { 152 | Running string `json:"load_average_process_running"` 153 | Total string `json:"load_average_process_total"` 154 | } 155 | 156 | func init() { 157 | arch = runtime.GOARCH 158 | os = runtime.GOOS 159 | 160 | } 161 | 162 | func (hh *Host) Get() { 163 | h, err := host.Info() 164 | if err != nil { 165 | panic(err) 166 | } 167 | 168 | nowTime := strings.Split(time.Now().Format("2006-01-02 15:04:05"), " ") 169 | hh.NowTimeYMD = nowTime[0] 170 | hh.NowTimeHMS = nowTime[1] 171 | 172 | hh.HostName = h.Hostname 173 | hh.Uptime = strconv.Itoa(int(h.Uptime)) 174 | hh.UptimeFormat = resolveTime(hh.Uptime) 175 | hh.OS = h.OS 176 | 177 | if hh.OS == "linux" { 178 | res, err := Popen(command["model"]) 179 | if err != nil { 180 | panic(err) 181 | } 182 | res = strings.Replace(res, "\n", "", -1) 183 | if strings.Contains(arch, "arm") { 184 | if strings.Contains(res, ":") { 185 | hh.Model = strings.TrimSpace(strings.Split(res, ":")[1]) 186 | } else { 187 | hh.Model = "NaN" 188 | } 189 | } else { 190 | hh.Model = "Linux Computer" 191 | } 192 | model = hh.Model 193 | } 194 | 195 | if hh.OS == "windows" { 196 | hh.System = h.Platform + " " + h.PlatformVersion 197 | } else { 198 | res, err := Popen(command["system"]) 199 | if err != nil { 200 | panic(err) 201 | } 202 | res = strings.Replace(res, "\n", "", -1) 203 | if strings.Contains(res, "\"") { 204 | hh.System = strings.Replace(strings.Replace(strings.Split(res, "\"")[1], " GNU/Linux ", " ", -1), "\"", "", -1) 205 | } else { 206 | hh.System = "NaN" 207 | } 208 | } 209 | 210 | if hh.OS == "linux" { 211 | res, err := Popen(command["ip"]) 212 | if err != nil { 213 | panic(err) 214 | } 215 | res = strings.Replace(res, "\n", "", -1) 216 | hh.IP = strings.Split(res, "/")[0] 217 | 218 | res, err = Popen(command["uname"]) 219 | if err != nil { 220 | panic(err) 221 | } 222 | res = strings.Replace(res, "\n", "", -1) 223 | hh.Uname = res 224 | 225 | res, err = Popen(command["login_user_count"]) 226 | if err != nil { 227 | panic(err) 228 | } 229 | res = strings.Replace(res, "\n", "", -1) 230 | hh.LoginUserCount = strings.Split(res, "=")[1] 231 | 232 | hh.CurrentUser = config.LinuxUserInfo.Username 233 | } 234 | } 235 | 236 | func (cc *CPU) Get() { 237 | cpuInfo, err := cpu.Info() 238 | if err != nil { 239 | panic(err) 240 | } 241 | 242 | cpuCounts, err := cpu.Counts(false) 243 | if err != nil { 244 | panic(err) 245 | } 246 | cc.Cores = strconv.Itoa(cpuCounts) 247 | cc.Arch = arch 248 | cc.Revision = "" 249 | 250 | if strings.Contains(arch, "arm") { 251 | res, err := Popen(command["cpu_model_name"]) 252 | if err != nil { 253 | panic(err) 254 | } 255 | res = strings.Replace(res, "\n", "", -1) 256 | cc.ModelName = res 257 | for key, value := range piCpuModelInfo { 258 | if strings.Contains(model, key) { 259 | cc.Revision = cc.ModelName 260 | cc.ModelName = value 261 | break 262 | } 263 | } 264 | } else if cpuInfo[0].ModelName != "" { 265 | cc.ModelName = cpuInfo[0].ModelName 266 | } else { 267 | cc.ModelName = "NaN" 268 | } 269 | 270 | cpuPercent, err := cpu.Percent(0, false) 271 | cc.UsedPercent = strconv.FormatFloat(cpuPercent[0], 'f', 1, 64) 272 | 273 | if strings.Contains(arch, "arm") { 274 | res, err := Popen(command["cpu_freq"]) 275 | if err != nil { 276 | panic(err) 277 | } 278 | res = strings.Replace(res, "\n", "", -1) 279 | cpuFreq, _ := strconv.ParseInt(res, 10, 64) 280 | cc.Mhz = strconv.FormatInt(cpuFreq/1000, 10) 281 | } else { 282 | cc.Mhz = strconv.FormatInt(int64(cpuInfo[0].Mhz), 10) 283 | } 284 | 285 | if os == "linux" { 286 | res, err := Popen(command["cpu_status"]) 287 | if err != nil { 288 | panic(err) 289 | } 290 | res = strings.Replace(res, "\n", "", -1) 291 | cpuStatusRaw := res 292 | exceptionSituation := []string{"id,", "wa,", "hi,", "si,"} 293 | for _, exception := range exceptionSituation { 294 | if exception == "id" { 295 | cpuStatusRaw = strings.Replace(cpuStatusRaw, exception, "100.0", -1) 296 | } else { 297 | cpuStatusRaw = strings.Replace(cpuStatusRaw, exception, "0.0", -1) 298 | } 299 | } 300 | cpuStatus := strings.Split(cpuStatusRaw, " ") 301 | cc.User = cpuStatus[0] 302 | cc.Nice = cpuStatus[1] 303 | cc.System = cpuStatus[2] 304 | cc.Idle = cpuStatus[3] 305 | cc.Iowait = cpuStatus[4] 306 | cc.Irq = cpuStatus[5] 307 | cc.Softirg = cpuStatus[6] 308 | } else { 309 | cpuTimes, _ := cpu.Times(false) 310 | if err != nil { 311 | panic(err) 312 | } 313 | timesTotal := cpuTimes[0].Total() 314 | cc.Idle = strconv.FormatFloat(cpuTimes[0].Idle/timesTotal*100, 'f', 1, 64) 315 | cc.Iowait = strconv.FormatFloat(cpuTimes[0].Iowait/timesTotal*100, 'f', 1, 64) 316 | cc.Irq = strconv.FormatFloat(cpuTimes[0].Irq/timesTotal*100, 'f', 1, 64) 317 | cc.Nice = strconv.FormatFloat(cpuTimes[0].Nice/timesTotal*100, 'f', 1, 64) 318 | cc.Softirg = strconv.FormatFloat(cpuTimes[0].Softirq/timesTotal*100, 'f', 1, 64) 319 | cc.System = strconv.FormatFloat((cpuTimes[0].System / timesTotal * 100), 'f', 1, 64) 320 | cc.User = strconv.FormatFloat(cpuTimes[0].User/timesTotal*100, 'f', 1, 64) 321 | } 322 | 323 | temperature, err := host.SensorsTemperatures() 324 | if err != nil { 325 | panic(err) 326 | } 327 | if len(temperature) != 0 { 328 | cc.Temperature = strconv.FormatFloat(temperature[0].Temperature, 'f', 1, 64) 329 | } else { 330 | cc.Temperature = "NaN" 331 | } 332 | } 333 | 334 | func (dd *Disk) Get(path string) { 335 | d, err := disk.Usage(path) 336 | if err != nil { 337 | panic(err) 338 | } 339 | 340 | dd.Path = strings.ToUpper(d.Path) 341 | dd.Total = strconv.FormatFloat(float64(d.Total)/1024/1024/1024, 'f', 1, 64) 342 | dd.Free = strconv.FormatFloat(float64(d.Free)/1024/1024/1024, 'f', 1, 64) 343 | dd.Used = strconv.FormatFloat(float64(d.Used)/1024/1024/1024, 'f', 1, 64) 344 | dd.UsedPercent = strconv.FormatFloat(d.UsedPercent, 'f', 1, 64) 345 | } 346 | 347 | func (mm *Memory) Get() { 348 | m, err := mem.VirtualMemory() 349 | if err != nil { 350 | panic(err) 351 | } 352 | 353 | mm.Total = strconv.FormatFloat(float64(m.Total)/1024/1024, 'f', 1, 64) 354 | mm.Free = strconv.FormatFloat(float64(m.Free)/1024/1024, 'f', 1, 64) 355 | mm.Buffers = strconv.FormatFloat(float64(m.Buffers)/1024/1024, 'f', 1, 64) 356 | mm.Cached = strconv.FormatFloat(float64(m.Cached)/1024/1024, 'f', 1, 64) 357 | mm.Available = strconv.FormatFloat(float64(m.Available)/1024/1024, 'f', 1, 64) 358 | mm.Used = strconv.FormatFloat(float64((m.Used+m.Buffers+m.Cached))/1024/1024, 'f', 1, 64) 359 | mm.Percent = strconv.FormatFloat(float64((m.Used+m.Buffers+m.Cached))/float64(m.Total)*100, 'f', 1, 64) 360 | mm.CachedPercent = strconv.FormatFloat(float64(m.Cached)/float64(m.Total)*100, 'f', 1, 64) 361 | mm.RealUsed = strconv.FormatFloat(float64((m.Total-m.Available)/1024/1024), 'f', 1, 64) 362 | mm.RealPercent = strconv.FormatFloat((float64(m.Total)-float64(m.Available))/float64(m.Total)*100, 'f', 1, 64) 363 | 364 | mm.SwapTotal = strconv.FormatFloat(float64(m.SwapTotal)/1024/1024, 'f', 1, 64) 365 | mm.SwapFree = strconv.FormatFloat(float64(m.SwapFree)/1024/1024, 'f', 1, 64) 366 | mm.SwapUsed = strconv.FormatFloat((float64(m.SwapTotal)-float64(m.SwapFree))/1024/1024, 'f', 1, 64) 367 | if m.SwapTotal != 0 && m.SwapTotal-m.SwapFree != 0 { 368 | swapUsedFloat, _ := strconv.ParseFloat(mm.SwapUsed, 64) 369 | swapTotalFloat, _ := strconv.ParseFloat(mm.SwapTotal, 64) 370 | mm.SwapUsedPercent = strconv.FormatFloat(swapUsedFloat/swapTotalFloat*100, 'f', 1, 64) 371 | } else { 372 | mm.SwapUsedPercent = "0.0" 373 | } 374 | if m.SwapFree == 0 && m.SwapTotal == 0 { 375 | mm.SwapUsedPercent = "0.0" 376 | mm.SwapFree = "0.0" 377 | mm.SwapTotal = "0.0" 378 | mm.SwapUsed = "0.0" 379 | } 380 | 381 | } 382 | 383 | func (nn *Net) Get(name string) { 384 | n, err := net.IOCounters(true) 385 | if err != nil { 386 | panic(err) 387 | } 388 | 389 | var existed bool 390 | for _, item := range n { 391 | if item.Name == name { 392 | nn.Name = item.Name 393 | nn.DataIn = strconv.Itoa(int(item.BytesRecv)) 394 | nn.DataOut = strconv.Itoa(int(item.BytesSent)) 395 | nn.PackageIn = strconv.Itoa(int(item.PacketsRecv)) 396 | nn.PackageOut = strconv.Itoa(int(item.PacketsSent)) 397 | 398 | existed = true 399 | } 400 | } 401 | if !existed { 402 | fmt.Println("Not found " + name) 403 | } else { 404 | packageInFloat, _ := strconv.ParseFloat(nn.PackageIn, 64) 405 | packageOutFloat, _ := strconv.ParseFloat(nn.PackageOut, 64) 406 | nn.LoadAvg = strconv.FormatFloat((packageInFloat+packageOutFloat)/2, 'f', 1, 64) 407 | 408 | dataInFloat, _ := strconv.ParseFloat(nn.DataIn, 64) 409 | dataOutFloat, _ := strconv.ParseFloat(nn.DataOut, 64) 410 | nn.DataInFormat = bytesRound(float64(dataInFloat), 2) 411 | nn.DatatOutFormat = bytesRound(float64(dataOutFloat), 2) 412 | } 413 | } 414 | 415 | func (nn *NetLo) Get(name string) { 416 | n, err := net.IOCounters(true) 417 | if err != nil { 418 | panic(err) 419 | } 420 | 421 | var existed bool 422 | for _, item := range n { 423 | if item.Name == name { 424 | nn.Name = item.Name 425 | nn.DataIn = strconv.Itoa(int(item.BytesRecv)) 426 | nn.DataOut = strconv.Itoa(int(item.BytesSent)) 427 | nn.PackageIn = strconv.Itoa(int(item.PacketsRecv)) 428 | nn.PackageOut = strconv.Itoa(int(item.PacketsSent)) 429 | 430 | existed = true 431 | } 432 | } 433 | if !existed { 434 | fmt.Println("Not found " + name) 435 | } else { 436 | packageInFloat, _ := strconv.ParseFloat(nn.PackageIn, 64) 437 | packageOutFloat, _ := strconv.ParseFloat(nn.PackageOut, 64) 438 | nn.LoadAvg = strconv.FormatFloat((packageInFloat+packageOutFloat)/2, 'f', 1, 64) 439 | 440 | dataInFloat, _ := strconv.ParseFloat(nn.DataIn, 64) 441 | dataOutFloat, _ := strconv.ParseFloat(nn.DataOut, 64) 442 | nn.DataInFormat = bytesRound(float64(dataInFloat), 2) 443 | nn.DatatOutFormat = bytesRound(float64(dataOutFloat), 2) 444 | } 445 | } 446 | 447 | func (ll *Load) Get() { 448 | l, err := load.Avg() 449 | if err != nil { 450 | panic(err) 451 | } 452 | 453 | ll.Load1 = strconv.FormatFloat(l.Load1, 'f', 2, 64) 454 | ll.Load5 = strconv.FormatFloat(l.Load5, 'f', 2, 64) 455 | ll.Load15 = strconv.FormatFloat(l.Load15, 'f', 2, 64) 456 | } 457 | 458 | func (pp *Process) Get() { 459 | p, err := load.Misc() 460 | if err != nil { 461 | panic(err) 462 | } 463 | 464 | pp.Running = strconv.Itoa(p.ProcsRunning) 465 | pp.Total = strconv.Itoa(p.ProcsTotal) 466 | } 467 | 468 | func (hh Host) String() string { 469 | s, _ := json.Marshal(hh) 470 | return string(s) 471 | } 472 | 473 | func (cc CPU) String() string { 474 | s, _ := json.Marshal(cc) 475 | return string(s) 476 | } 477 | 478 | func (dd Disk) String() string { 479 | s, _ := json.Marshal(dd) 480 | return string(s) 481 | } 482 | 483 | func (mm Memory) String() string { 484 | s, _ := json.Marshal(mm) 485 | return string(s) 486 | } 487 | 488 | func (nn Net) String() string { 489 | s, _ := json.Marshal(nn) 490 | return string(s) 491 | } 492 | 493 | func (ll Load) String() string { 494 | s, _ := json.Marshal(ll) 495 | return string(s) 496 | } 497 | 498 | func (pp Process) String() string { 499 | s, _ := json.Marshal(pp) 500 | return string(s) 501 | } 502 | 503 | func Info() map[string]interface{} { 504 | device := make(map[string]interface{}) 505 | 506 | host := new(Host) 507 | host.Get() 508 | hostMap, _ := struct2Map(host, "json") 509 | device = mergeMap(device, hostMap) 510 | // fmt.Println("host:", host) 511 | 512 | cpu := new(CPU) 513 | cpu.Get() 514 | cpuMap, _ := struct2Map(cpu, "json") 515 | device = mergeMap(device, cpuMap) 516 | // fmt.Println("cpu:", cpu) 517 | 518 | disk := new(Disk) 519 | disk.Get(config.Disk) 520 | diskMap, _ := struct2Map(disk, "json") 521 | device = mergeMap(device, diskMap) 522 | // fmt.Println("disk:", disk) 523 | 524 | mem := new(Memory) 525 | mem.Get() 526 | memMap, _ := struct2Map(mem, "json") 527 | device = mergeMap(device, memMap) 528 | // fmt.Println("memory:", mem) 529 | 530 | netLo := new(NetLo) 531 | netLo.Get("lo") 532 | netLoMap, _ := struct2Map(netLo, "json") 533 | device = mergeMap(device, netLoMap) 534 | // fmt.Println("net:", netLo) 535 | 536 | net := new(Net) 537 | net.Get(config.Net) 538 | netMap, _ := struct2Map(net, "json") 539 | device = mergeMap(device, netMap) 540 | // fmt.Println("net:", net) 541 | 542 | load := new(Load) 543 | load.Get() 544 | loadMap, _ := struct2Map(load, "json") 545 | device = mergeMap(device, loadMap) 546 | // fmt.Println("load:", load) 547 | 548 | process := new(Process) 549 | process.Get() 550 | processMap, _ := struct2Map(process, "json") 551 | device = mergeMap(device, processMap) 552 | // fmt.Println("process:", process) 553 | 554 | if strings.Contains(strings.ToLower(device["model"].(string)), "raspberry") { 555 | device["device_photo"] = "raspberrypi.png" 556 | device["favicon"] = "raspberrypi.ico" 557 | } else { 558 | device["device_photo"] = "linux.png" 559 | device["favicon"] = "linux.ico" 560 | } 561 | 562 | return device 563 | } 564 | 565 | // Popen 函数用于执行系统命令 566 | func Popen(command string) (string, error) { 567 | cmd := exec.Command("/bin/bash", "-c", command) 568 | 569 | //创建获取命令输出管道 570 | stdout, err := cmd.StdoutPipe() 571 | if err != nil { 572 | //fmt.Printf("Error:can not obtain stdout pipe for command:%s\n", err) 573 | return "", errors.New("Error:can not obtain stdout pipe for command") 574 | } 575 | 576 | //执行命令 577 | if err := cmd.Start(); err != nil { 578 | //fmt.Println("Error:The command is err,", err) 579 | return "", errors.New("Error:The command is err") 580 | } 581 | 582 | //读取所有输出 583 | bytes, err := ioutil.ReadAll(stdout) 584 | if err != nil { 585 | //fmt.Println("ReadAll Stdout:", err.Error()) 586 | return "", errors.New("ReadAll Stdout:" + err.Error()) 587 | } 588 | 589 | if err := cmd.Wait(); err != nil { 590 | //fmt.Println("wait:", err.Error()) 591 | return "", errors.New("wait:" + err.Error()) 592 | } 593 | 594 | return string(bytes), nil 595 | } 596 | 597 | func resolveTime(str string) string { 598 | var uptime string 599 | strF, _ := strconv.ParseFloat(str, 10) 600 | second := int64(strF) 601 | var min = second / 60 602 | var hours = min / 60 603 | var days = int64(math.Floor(float64(hours / 24))) 604 | hours = int64(math.Floor(float64(hours - (days * 24)))) 605 | min = int64(math.Floor(float64(min - (days * 60 * 24) - (hours * 60)))) 606 | 607 | if days != 0 { 608 | if days == 1 { 609 | uptime = strconv.FormatInt(days, 10) + " day " 610 | } else { 611 | uptime = strconv.FormatInt(days, 10) + " days " 612 | } 613 | } 614 | if hours != 0 { 615 | uptime = uptime + strconv.FormatInt(hours, 10) + ":" 616 | } 617 | 618 | return uptime + strconv.FormatInt(min, 10) 619 | } 620 | 621 | func bytesRound(number, round float64) string { 622 | var last string 623 | if number < 0 { 624 | last = "0" + "B" 625 | } else if number < 1024 { 626 | numberStr := strconv.FormatFloat(number, 'f', 1, 64) 627 | last = numberStr + "B" 628 | } else if number < 1048576 { 629 | number = number / 1024 630 | last = strconv.FormatFloat(math.Round(number*math.Pow(10, round))/math.Pow(10, round), 'f', 1, 64) + "KB" 631 | } else if number < 1048576000 { 632 | number = number / 1048576 633 | last = strconv.FormatFloat(math.Round(number*math.Pow(10, round))/math.Pow(10, round), 'f', 1, 64) + "MB" 634 | } else { 635 | number = number / 1048576000 636 | last = strconv.FormatFloat(math.Round(number*math.Pow(10, round))/math.Pow(10, round), 'f', 1, 64) + "GB" 637 | } 638 | return last 639 | } 640 | 641 | func struct2Map(in interface{}, tagName string) (map[string]interface{}, error) { 642 | out := make(map[string]interface{}) 643 | 644 | v := reflect.ValueOf(in) 645 | if v.Kind() == reflect.Ptr { 646 | v = v.Elem() 647 | } 648 | 649 | if v.Kind() != reflect.Struct { 650 | return nil, fmt.Errorf("ToMap only accepts struct or struct pointer; got %T", v) 651 | } 652 | 653 | t := v.Type() 654 | for i := 0; i < v.NumField(); i++ { 655 | fi := t.Field(i) 656 | if tagValue := fi.Tag.Get(tagName); tagValue != "" { 657 | out[tagValue] = v.Field(i).Interface() 658 | } 659 | } 660 | return out, nil 661 | } 662 | 663 | func mergeMap(map1, map2 map[string]interface{}) map[string]interface{} { 664 | for k, v := range map2 { 665 | map1[k] = v 666 | } 667 | 668 | return map1 669 | } 670 | -------------------------------------------------------------------------------- /server/assets/js/index.js: -------------------------------------------------------------------------------- 1 | // @Program : Pi Dashboard Go (https://github.com/plutobell/pi-dashboard-go) 2 | // @Description: Golang implementation of pi-dashboard 3 | // @Author: github.com/plutobell 4 | // @Creation: 2020-08-01 5 | // @Last modification: 2021-09-02 6 | // @Version: 1.6.0 7 | 8 | var new_version = "" 9 | var new_version_notes = "" 10 | var new_version_url = "" 11 | 12 | initTooltips(); 13 | unScroll(); 14 | 15 | $(document).ready(function() { 16 | var net_in_color = $(":root").css("--net-in-color"); 17 | var net_out_color = $(":root").css("--net-out-color"); 18 | var net_grid_line_color = $(":root").css("--net-grid-line-color"); 19 | var net_line_color = $(":root").css("--net-line-color"); 20 | var guage_font_color = $(":root").css("--guage-font-color"); 21 | var guage_stops_color_1 = $(":root").css("--guage-stops-color-1"); 22 | var guage_stops_color_5 = $(":root").css("--guage-stops-color-5"); 23 | var guage_stops_color_9 = $(":root").css("--guage-stops-color-9"); 24 | 25 | $("#year").text(new Date().getFullYear()); 26 | $("#loading").hide(); 27 | $('#ModalBox').modal('hide'); 28 | removeUnScroll(); 29 | 30 | Highcharts.setOptions({ 31 | global: { 32 | useUTC: false 33 | }, 34 | credits: { 35 | enabled: false 36 | }, 37 | navigation: { 38 | buttonOptions: { 39 | enabled: false 40 | } 41 | } 42 | }); 43 | 44 | var gaugeOptions = { 45 | 46 | chart: { 47 | type: 'solidgauge', 48 | backgroundColor: 'transparent' 49 | }, 50 | 51 | title: null, 52 | 53 | pane: { 54 | center: ['50%', '85%'], 55 | size: '140%', 56 | startAngle: -90, 57 | endAngle: 90, 58 | background: { 59 | // backgroundColor: (Highcharts.theme && Highcharts.theme.background2) || '#FFFFFF', 60 | backgroundColor: 'transparent', 61 | innerRadius: '60%', 62 | outerRadius: '100%', 63 | shape: 'arc' 64 | } 65 | }, 66 | 67 | tooltip: { 68 | enabled: false 69 | }, 70 | 71 | // the value axis 72 | yAxis: { 73 | stops: [ 74 | [0.1, guage_stops_color_1], 75 | [0.5, guage_stops_color_5], 76 | [0.9, guage_stops_color_9] 77 | ], 78 | lineWidth: 0, 79 | minorTickInterval: null, 80 | tickAmount: 2, 81 | title: { 82 | y: -70 83 | }, 84 | labels: { 85 | y: 16 86 | } 87 | }, 88 | 89 | plotOptions: { 90 | solidgauge: { 91 | dataLabels: { 92 | y: 5, 93 | borderWidth: 0, 94 | useHTML: true 95 | } 96 | } 97 | } 98 | }; 99 | 100 | 101 | var chartCPU = Highcharts.chart('container-cpu', Highcharts.merge(gaugeOptions, { 102 | yAxis: { 103 | min: 0, 104 | max: 100, 105 | title: { 106 | text: '' 107 | } 108 | }, 109 | 110 | series: [{ 111 | name: 'CPU', 112 | data: [0], 113 | dataLabels: { 114 | format: '
{y:.1f}' + 116 | '%
' 117 | }, 118 | tooltip: { 119 | valueSuffix: ' %' 120 | } 121 | }] 122 | 123 | })); 124 | 125 | var chartRAM = Highcharts.chart('container-mem', Highcharts.merge(gaugeOptions, { 126 | yAxis: { 127 | min: 0, 128 | max: parseFloat(init_vals.mem.total), 129 | title: { 130 | text: '' 131 | } 132 | }, 133 | 134 | series: [{ 135 | name: 'MEMORY', 136 | data: [0], 137 | dataLabels: { 138 | format: '
{y:.1f}
' + 140 | 'MB
' 141 | }, 142 | tooltip: { 143 | valueSuffix: ' MB' 144 | } 145 | }] 146 | 147 | })); 148 | 149 | var chartCache = Highcharts.chart('container-cache', Highcharts.merge(gaugeOptions, { 150 | yAxis: { 151 | min: 0, 152 | max: parseFloat(init_vals.mem.total), 153 | title: { 154 | text: '' 155 | } 156 | }, 157 | 158 | series: [{ 159 | name: 'CACHE', 160 | data: [0], 161 | dataLabels: { 162 | format: '
{y:.1f}
' + 164 | 'MB
' 165 | }, 166 | tooltip: { 167 | valueSuffix: ' MB' 168 | } 169 | }] 170 | 171 | })); 172 | 173 | var chartRAM_real = Highcharts.chart('container-mem-real', Highcharts.merge(gaugeOptions, { 174 | yAxis: { 175 | min: 0, 176 | max: parseFloat(init_vals.mem.total), 177 | title: { 178 | text: '' 179 | } 180 | }, 181 | 182 | series: [{ 183 | name: 'REAL MEMORY', 184 | data: [0], 185 | dataLabels: { 186 | format: '
{y:.1f}
' + 188 | 'MB
' 189 | }, 190 | tooltip: { 191 | valueSuffix: ' MB' 192 | } 193 | }] 194 | 195 | })); 196 | 197 | var chartSWAP = Highcharts.chart('container-swap', Highcharts.merge(gaugeOptions, { 198 | yAxis: { 199 | min: 0, 200 | max: parseFloat(init_vals.mem.swap.total), 201 | title: { 202 | text: '' 203 | } 204 | }, 205 | 206 | series: [{ 207 | name: 'SWAP', 208 | data: [0], 209 | dataLabels: { 210 | format: '
{y:.1f}
' + 212 | 'MB
' 213 | }, 214 | tooltip: { 215 | valueSuffix: ' MB' 216 | } 217 | }] 218 | 219 | })); 220 | 221 | var chartDisk = Highcharts.chart('container-disk', Highcharts.merge(gaugeOptions, { 222 | yAxis: { 223 | min: 0, 224 | max: parseFloat(init_vals.disk.total), 225 | title: { 226 | text: '' 227 | } 228 | }, 229 | 230 | series: [{ 231 | name: 'DISK', 232 | data: [0], 233 | dataLabels: { 234 | format: '
{y:.1f}
' + 236 | 'GB
' 237 | }, 238 | tooltip: { 239 | valueSuffix: ' GB' 240 | } 241 | }] 242 | 243 | })); 244 | 245 | 246 | var chartNetInterface1 = Highcharts.chart('container-net-interface-1', { 247 | chart: { 248 | backgroundColor: 'transparent', 249 | type: 'line' 250 | }, 251 | title: { 252 | text: '' 253 | }, 254 | legend: { 255 | enabled: false 256 | }, 257 | xAxis: { 258 | categories: [], 259 | title: { 260 | text: '' 261 | }, 262 | lineColor: net_line_color 263 | }, 264 | yAxis: { 265 | title: { 266 | text: '', 267 | style: { 268 | fontWeight: 'normal' 269 | } 270 | }, 271 | gridLineColor: net_grid_line_color 272 | // labels: { 273 | // style: { 274 | // color: '#ffffff', 275 | // } 276 | // } 277 | }, 278 | series: [ 279 | { 280 | name: 'IN', 281 | data: [0], 282 | color: net_in_color, 283 | marker: { 284 | enabled: false 285 | } 286 | }, 287 | { 288 | name: 'OUT', 289 | data: [0], 290 | color: net_out_color, 291 | marker: { 292 | enabled: false 293 | } 294 | } 295 | ] 296 | }); 297 | net_In1 = [0,0,0,0,0,0,0,0,0,0]; 298 | net_Out1 = [0,0,0,0,0,0,0,0,0,0]; 299 | 300 | var chartNetInterface2 = Highcharts.chart('container-net-interface-2', { 301 | chart: { 302 | backgroundColor: 'transparent', 303 | type: 'line' 304 | }, 305 | title: { 306 | text: '' 307 | }, 308 | legend: { 309 | enabled: false 310 | }, 311 | xAxis: { 312 | categories: [], 313 | title: { 314 | text: '' 315 | }, 316 | lineColor: net_line_color 317 | }, 318 | yAxis: { 319 | title: { 320 | text: '', 321 | style: { 322 | fontWeight: 'normal' 323 | } 324 | }, 325 | gridLineColor: net_grid_line_color 326 | }, 327 | series: [ 328 | { 329 | name: 'IN', 330 | data: [0], 331 | color: net_in_color, 332 | marker: { 333 | enabled: false 334 | } 335 | }, 336 | { 337 | name: 'OUT', 338 | data: [0], 339 | color: net_out_color, 340 | marker: { 341 | enabled: false 342 | } 343 | } 344 | ] 345 | }); 346 | net_In2 = [0,0,0,0,0,0,0,0,0,0]; 347 | net_Out2 = [0,0,0,0,0,0,0,0,0,0]; 348 | 349 | var interval = setInterval(function() { 350 | // var date = new Date(); 351 | // var year = date.getFullYear(); 352 | 353 | $.ajaxSetup(csrfAddToAjaxHeader()); 354 | $.post('api/device', function(data){ 355 | if (data.status == "Unauthorized") { 356 | $("#loading").show(); 357 | unScroll(); 358 | clearInterval(interval); 359 | $(window).attr('location','/login'); 360 | } else { 361 | $("#loading").hide(); 362 | removeUnScroll(); 363 | 364 | $("#login-users").text(data.login_user_count); 365 | $("#hostip").text(data.ip); 366 | $("#hostname").text(data.hostname); 367 | $("#uname").text(data.uname); 368 | $("#system").text(data.system); 369 | // $("#time").text(data.now_time_hms); 370 | // $("#date").text(data.now_time_ymd); 371 | $("#uptime").text(data.uptime); 372 | $("#cpu-temp").text(data.cpu_temperature); 373 | $("#cpu-freq").text(data.cpu_freq); 374 | $("#cpu-stat-idl").text(data.cpu_status_idle); 375 | $("#cpu-stat-use").text(data.cpu_status_user); 376 | $("#cpu-stat-sys").text(data.cpu_status_system); 377 | $("#cpu-stat-nic").text(data.cpu_status_nice); 378 | $("#cpu-stat-iow").text(data.cpu_status_iowait); 379 | $("#cpu-stat-irq").text(data.cpu_status_irq); 380 | $("#cpu-stat-sirq").text(data.cpu_status_softirq); 381 | $("#mem-percent").text(data.memory_percent); 382 | $("#mem-free").text(data.memory_free); 383 | $("#mem-cached").text(data.memory_cached); 384 | $("#mem-swap-total").text(data.swap_total); 385 | $("#mem-cache-percent").text(data.memory_cached_percent); 386 | $("#mem-buffers").text(data.memory_buffers); 387 | $("#mem-real-percent").text(data.memory_real_percent); 388 | $("#mem-real-free").text(data.memory_available); 389 | $("#mem-swap-percent").text(data.swap_used_percent); 390 | $("#mem-swap-free").text(data.swap_free); 391 | $("#disk-percent").text(data.disk_used_percent); 392 | $("#disk-free").text(data.disk_free); 393 | $("#loadavg-1m").text(data.load_average_1m); 394 | $("#loadavg-5m").text(data.load_average_5m); 395 | $("#loadavg-15m").text(data.load_average_15m); 396 | $("#loadavg-running").text(data.load_average_process_running); 397 | $("#loadavg-threads").text(data.load_average_process_total); 398 | 399 | $("#net-interface-1-total-in").text(data.net_status_lo_in_data_format); 400 | $("#net-interface-1-total-out").text(data.net_status_lo_out_data_format); 401 | $("#net-interface-2-total-in").text(data.net_status_in_data_format); 402 | $("#net-interface-2-total-out").text(data.net_status_out_data_format); 403 | 404 | $("#version").text("v" + data.version); 405 | $("#version").attr('data-bs-original-title', "Compiled with " + data.go_version); 406 | 407 | // $("#year").text(new Date().getFullYear()); 408 | 409 | if(window.dashboard != null) 410 | { 411 | window.dashboard_old = window.dashboard; 412 | } 413 | window.dashboard = data; 414 | } 415 | }).fail(function() { 416 | $("#loading").show(); 417 | $('#ModalBox').modal('hide'); 418 | unScroll(); 419 | }); 420 | 421 | if(window.dashboard != null){ 422 | var point; 423 | if (chartCPU) { 424 | point = chartCPU.series[0].points[0]; 425 | point.update(parseFloat(window.dashboard.cpu_used)); 426 | } 427 | if (chartRAM) { 428 | point = chartRAM.series[0].points[0]; 429 | point.update(parseFloat(window.dashboard.memory_used)); 430 | } 431 | if (chartCache) { 432 | point = chartCache.series[0].points[0]; 433 | point.update(parseFloat(window.dashboard.memory_cached)); 434 | } 435 | if (chartRAM_real) { 436 | point = chartRAM_real.series[0].points[0]; 437 | point.update(parseFloat(window.dashboard.memory_real_used)); 438 | } 439 | if (chartSWAP) { 440 | point = chartSWAP.series[0].points[0]; 441 | point.update(parseFloat(window.dashboard.swap_used)); 442 | } 443 | if (chartDisk) { 444 | point = chartDisk.series[0].points[0]; 445 | point.update(parseFloat(window.dashboard.disk_used)); 446 | } 447 | 448 | if(window.dashboard_old != null) { 449 | if(chartNetInterface1.series[0].data.length >=30){ 450 | chartNetInterface1.series[0].addPoint(parseInt(window.dashboard.net_status_lo_in_data) - parseInt(window.dashboard_old.net_status_lo_in_data), true, true); 451 | } 452 | else{ 453 | chartNetInterface1.series[0].addPoint(parseInt(window.dashboard.net_status_lo_in_data) - parseInt(window.dashboard_old.net_status_lo_in_data)); 454 | } 455 | 456 | if(chartNetInterface1.series[1].data.length >=30){ 457 | chartNetInterface1.series[1].addPoint(parseInt(window.dashboard.net_status_lo_out_data) - parseInt(window.dashboard_old.net_status_lo_out_data), true, true); 458 | } 459 | else{ 460 | chartNetInterface1.series[1].addPoint(parseInt(window.dashboard.net_status_lo_out_data) - parseInt(window.dashboard_old.net_status_lo_out_data)); 461 | } 462 | } 463 | 464 | if(window.dashboard_old != null) { 465 | if(chartNetInterface2.series[0].data.length >=30){ 466 | chartNetInterface2.series[0].addPoint(parseInt(window.dashboard.net_status_in_data) - parseInt(window.dashboard_old.net_status_in_data), true, true); 467 | } 468 | else{ 469 | chartNetInterface2.series[0].addPoint(parseInt(window.dashboard.net_status_in_data) - parseInt(window.dashboard_old.net_status_in_data)); 470 | } 471 | 472 | if(chartNetInterface2.series[1].data.length >=30){ 473 | chartNetInterface2.series[1].addPoint(parseInt(window.dashboard.net_status_out_data) - parseInt(window.dashboard_old.net_status_out_data), true, true); 474 | } 475 | else{ 476 | chartNetInterface2.series[1].addPoint(parseInt(window.dashboard.net_status_out_data) - parseInt(window.dashboard_old.net_status_out_data)); 477 | } 478 | } 479 | 480 | } 481 | 482 | }, (parseInt($("#interval").text()) * 1000) ); 483 | }); 484 | 485 | // Loading Consumption Time 486 | $(window).on('load', function() { 487 | var endTime = new Date().getTime(); 488 | loading_time = endTime - startTime 489 | if (loading_time < 1000) { 490 | $("#loading-time").attr('data-bs-original-title', "Loading time: " + loading_time + "ms"); 491 | } else { 492 | $("#loading-time").attr('data-bs-original-title', "Loading time: " + (loading_time / 1000).toFixed(3) + "s"); 493 | }; 494 | }); 495 | 496 | $(document).ready(function() { 497 | setInterval(function() { 498 | var nowDate = new Date(); 499 | var year = nowDate.getFullYear(); 500 | var month = nowDate.getMonth() + 1 < 10 ? "0" + (nowDate.getMonth() + 1) : nowDate.getMonth() + 1; 501 | var day = nowDate.getDate() < 10 ? "0" + nowDate.getDate() : nowDate.getDate(); 502 | var hour = nowDate.getHours()< 10 ? "0" + nowDate.getHours() : nowDate.getHours(); 503 | var minute = nowDate.getMinutes()< 10 ? "0" + nowDate.getMinutes() : nowDate.getMinutes(); 504 | var second = nowDate.getSeconds()< 10 ? "0" + nowDate.getSeconds() : nowDate.getSeconds(); 505 | 506 | var hms = hour + ":" + minute + ":" + second 507 | var ymd = year + "-" + month + "-" + day 508 | 509 | $("#time").text(hms); 510 | $("#date").text(ymd); 511 | $("#year").text(year); 512 | }, (500) ); 513 | }); 514 | 515 | function unScroll() { 516 | var top = $(document).scrollTop(); 517 | $(document).on('scroll.unable',function (e) { 518 | $(document).scrollTop(top); 519 | }) 520 | $(document.body).css({ 521 | "overflow-y": "hidden" 522 | }); 523 | } 524 | 525 | function removeUnScroll() { 526 | $(document).unbind("scroll.unable"); 527 | $(document.body).css({ 528 | "overflow-y": "auto" 529 | }); 530 | } 531 | 532 | 533 | $("#logout").click(function(){ 534 | $("#logout").css("pointer-events", "none"); 535 | $.ajaxSetup(csrfAddToAjaxHeader()); 536 | $.post('/api/logout', function(result){ 537 | if (result.status == true) { 538 | $("#logout").css("pointer-events", "auto"); 539 | $(window).attr('location','/login'); 540 | } else { 541 | $("#logout").css("pointer-events", "auto"); 542 | showModalBox("Sign Out", "Sign out failed"); 543 | } 544 | }).fail(function() { 545 | $("#logout").css("pointer-events", "auto"); 546 | showModalBox("Sign Out", "Sign out failed"); 547 | }); 548 | }); 549 | 550 | $("#reboot").click(function(){ 551 | $("#reboot").css("pointer-events", "none"); 552 | $.ajaxSetup(csrfAddToAjaxHeader()); 553 | $.post('/api/operation?action=reboot', function(data){ 554 | if (data.status == true) { 555 | showModalBox("Reboot Device", "OK") 556 | $("#loading").show(); 557 | unScroll(); 558 | $("#reboot").css("pointer-events", "auto"); 559 | } else if (data.status == "NotRootUser") { 560 | showModalBox("Reboot Device", '"Reboot Device" requires running Pi Dashboard Go as root user') 561 | unScroll(); 562 | $("#reboot").css("pointer-events", "auto"); 563 | } else if (data.status == "Unauthorized") { 564 | $(window).attr('location','/login'); 565 | } else { 566 | showModalBox("Reboot Device", "Unknown Error"); 567 | $("#loading").show(); 568 | unScroll(); 569 | $("#reboot").css("pointer-events", "auto"); 570 | } 571 | }).fail(function() { 572 | showModalBox("Reboot Device", "Request Error"); 573 | $("#loading").show(); 574 | unScroll(); 575 | $("#reboot").css("pointer-events", "auto"); 576 | }); 577 | }); 578 | 579 | $("#shutdown").click(function(){ 580 | $("#shutdown").css("pointer-events", "none"); 581 | $.ajaxSetup(csrfAddToAjaxHeader()); 582 | $.post('/api/operation?action=shutdown', function(data){ 583 | if (data.status == true) { 584 | showModalBox("Shutdown Device", "OK"); 585 | $("#loading").show(); 586 | unScroll(); 587 | $("#shutdown").css("pointer-events", "auto"); 588 | } else if (data.status == "NotRootUser") { 589 | showModalBox("Shutdown Device", '"Shutdown Device" requires running Pi Dashboard Go as root user') 590 | unScroll(); 591 | $("#shutdown").css("pointer-events", "auto"); 592 | } else if (data.status == "Unauthorized") { 593 | $(window).attr('location','/login'); 594 | } else { 595 | showModalBox("Shutdown Device", "Unknown Error"); 596 | $("#loading").show(); 597 | unScroll(); 598 | $("#shutdown").css("pointer-events", "auto"); 599 | } 600 | }).fail(function() { 601 | showModalBox("Shutdown Device", "Request Error"); 602 | $("#loading").show(); 603 | unScroll(); 604 | $("#shutdown").css("pointer-events", "auto"); 605 | }); 606 | }); 607 | 608 | $("#dropcaches").click(function(){ 609 | $("#dropcaches").css("pointer-events", "none"); 610 | $.ajaxSetup(csrfAddToAjaxHeader()); 611 | $.post('/api/operation?action=dropcaches', function(data){ //$.getJSON() 612 | if (data.status == true) { 613 | showModalBox("Drop Caches", "OK"); 614 | // $("#loading").show(); 615 | // unScroll(); 616 | $("#dropcaches").css("pointer-events", "auto"); 617 | } else if (data.status == "NotRootUser") { 618 | showModalBox("Drop Caches", '"Drop Caches" requires running Pi Dashboard Go as root user') 619 | // $("#loading").show(); 620 | // unScroll(); 621 | $("#dropcaches").css("pointer-events", "auto"); 622 | } else if (data.status == "Unauthorized") { 623 | $(window).attr('location','/login'); 624 | } else { 625 | showModalBox("Drop Caches", "Unknown Error"); 626 | // $("#loading").show(); 627 | // unScroll(); 628 | $("#dropcaches").css("pointer-events", "auto"); 629 | } 630 | }).fail(function() { 631 | showModalBox("Drop Caches", "Request Error"); 632 | // $("#loading").show(); 633 | // unScroll(); 634 | $("#dropcaches").css("pointer-events", "auto"); 635 | }); 636 | }); 637 | 638 | // Check New Version 639 | $(document).ready(function(){ 640 | $.ajaxSetup(csrfAddToAjaxHeader()); 641 | $.post('/api/operation?action=checknewversion', function(data){ 642 | if (data.new_version != "" && data.new_version_url != "") { 643 | // $("#new-url").attr("href", data.new_version_url); 644 | new_version = data.new_version 645 | new_version_notes = data.new_version_notes 646 | new_version_url = data.new_version_url 647 | 648 | $('#new-box').attr('data-bs-original-title', "v" + data.new_version + " Available"); 649 | $("#new-box").show(1000); 650 | } else { 651 | new_version = "" 652 | new_version_notes = "" 653 | new_version_url = "" 654 | 655 | $("#new-box").hide(1000); 656 | // $("#new-url").attr("href", "javascript:void(0);"); 657 | $('#new-box').attr('data-bs-original-title', 'New Version'); 658 | } 659 | }).fail(function() { 660 | new_version = "" 661 | new_version_notes = "" 662 | new_version_url = "" 663 | 664 | $("#new-box").hide(1000); 665 | // $("#new-url").attr("href", "javascript:void(0);"); 666 | $('#new-box').attr('data-bs-original-title', 'New Version'); 667 | }); 668 | }); 669 | $("#new-box").click(function(){ 670 | $("#new-box").css("pointer-events", "none"); 671 | $("#new-download").show(); 672 | 673 | $('#ModalBox-Title').text("v" + new_version + " Available"); 674 | $("#ModalBox-Body").empty(); 675 | $("#ModalBox-Body").attr('style', 'text-align: left !important;'); 676 | var notes_array = new_version_notes.split("*"); 677 | $("#ModalBox-Body").append("Release Notes"); 678 | $("#ModalBox-Body").append(""); 679 | for (var i = 0; i < notes_array.length; i++) { 680 | if (notes_array[i] != "") { 681 | insertHtml = "
  • " + $.trim(notes_array[i]) + "
  • " 682 | $('#ModalBox-Body').find('ul').eq(0).append(insertHtml); 683 | } 684 | } 685 | 686 | $('#ModalBox').modal('show'); 687 | $("#new-box").css("pointer-events", "auto"); 688 | }); 689 | $("#new-download").click(function(){ 690 | window.open(new_version_url, "_blank"); 691 | }); 692 | 693 | function showModalBox(title, body) { 694 | $("#new-download").hide(); 695 | $('#ModalBox-Title').text(title); 696 | $("#ModalBox-Body").empty(); 697 | $("#ModalBox-Body").attr('style', 'text-align: center !important;'); 698 | $('#ModalBox-Body').text(body); 699 | $('#ModalBox').modal('show'); 700 | } 701 | 702 | function initTooltips() { 703 | var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) 704 | var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { 705 | return new bootstrap.Tooltip(tooltipTriggerEl) 706 | }); 707 | } 708 | 709 | -------------------------------------------------------------------------------- /server/assets/views/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.site_title}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 34 | 35 | 36 |
    37 | 66 |
    67 | 68 |
    69 |
    70 |
    71 |
    Device
    72 |
    73 |
    {{.model}}
    74 |
    IP
    {{.ip}}
    75 |
    TIME
    {{.now_time_hms}}
    {{.now_time_ymd}}
    76 |
    UPTIME
    {{.uptime}}
    77 |
    LOGIN USER(S)
    {{.login_user_count}}
    78 | 79 | 80 |
    SYSTEM
    {{.system}}
    81 |
    HOSTNAME
    {{.hostname}}
    82 |
    83 |
    OPERATION
    84 |
    85 |
      86 |
    • 87 | Reboot Device 88 |
    • 89 |
    • 90 | Shutdown Device 91 |
    • 92 |
    • 93 | Drop Caches 94 |
    • 95 | 98 |
    99 |
    100 |
    101 |
    {{.uname}}
    102 |
    103 |
    104 |
    105 |
    106 |
    107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 |
    114 |
    CPU
    115 |
    116 |
    117 |
    {{.cpu_freq}}
    MHz
    118 |
    119 |
    120 |
    {{.cpu_cores}}
    CORE
    121 |
    122 |
    123 |
    {{.cpu_temperature}}
    124 |
    125 |
    126 |
    {{.cpu_status_idle}}%
    IDLE
    127 |
    128 |
    129 |
    {{.cpu_status_user}}%
    USER
    130 |
    131 |
    132 |
    {{.cpu_status_system}}%
    SYS
    133 |
    134 |
    135 |
    {{.cpu_status_nice}}%
    NICE
    136 |
    137 |
    138 |
    {{.cpu_status_iowait}}%
    IOW
    139 |
    140 |
    141 |
    {{.cpu_status_irq}}%
    IRQ
    142 |
    143 |
    144 |
    {{.cpu_status_softirq}}%
    SIRQ
    145 |
    146 |
    147 |
    {{.cpu_model_name}} {{.cpu_revision}}
    148 |
    149 |
    150 |
    151 |
    152 |
    153 |
    154 |
    155 |
    156 |
    157 |
    158 |
    159 |
    MEMORY
    160 |
    161 |
    162 |
    {{.memory_percent}}%
    USED
    163 |
    {{.memory_free}}MB
    FREE
    164 |
    165 |
    166 |
    {{.memory_cached}}MB
    CACHED
    167 |
    {{.swap_total}}MB
    SWAP
    168 |
    169 |
    170 |
    {{.load_average_1m}}
    AVG.1M
    171 |
    172 |
    173 |
    {{.load_average_5m}}
    AVG.5M
    174 |
    175 |
    176 |
    {{.load_average_15m}}
    AVG.15M
    177 |
    178 |
    179 |
    {{.load_average_process_running}}/{{.load_average_process_total}}
    RUNNING
    180 |
    181 |
    182 |
    183 |
    184 | 185 |
    186 |
    187 |
    188 |
    189 |
    190 |
    191 |
    192 |
    CACHE
    193 |
    194 |
    195 |
    {{.memory_cached_percent}}%
    USED
    196 |
    197 |
    198 |
    {{.memory_buffers}}MB
    BUFFERS
    199 |
    200 |
    201 |
    202 |
    203 |
    204 |
    205 |
    206 |
    207 |
    208 |
    REAL MEMORY
    209 |
    210 |
    211 |
    {{.memory_real_percent}}%
    USED
    212 |
    213 |
    214 |
    {{.memory_available}}MB
    FREE
    215 |
    216 |
    217 |
    218 |
    219 |
    220 |
    221 |
    222 |
    223 |
    224 |
    SWAP
    225 |
    226 |
    227 |
    {{.swap_used_percent}}%
    USED
    228 |
    229 |
    230 |
    {{.swap_free}}MB
    FREE
    231 |
    232 |
    233 |
    234 |
    235 |
    236 |
    237 |
    238 |
    239 |
    240 |
    DISK ( {{.disk_name}} )
    241 |
    242 |
    243 |
    {{.disk_used_percent}}%
    USED
    244 |
    245 |
    246 |
    {{.disk_free}}GB
    FREE
    247 |
    248 |
    249 |
    250 |
    251 | 252 | 253 |
    254 |
    255 |
    256 |
    257 |
    258 |
    259 |
    260 |
    261 |
    lo
    262 |
    {{.net_status_lo_in_data_format}}
    IN
    263 |
    {{.net_status_lo_out_data_format}}
    OUT
    264 |
    265 |
    266 |
    267 |
    268 |
    269 |
    270 |
    271 |
    272 |
    273 |
    {{.net_dev_name}}
    274 |
    {{.net_status_in_data_format}}
    IN
    275 |
    {{.net_status_out_data_format}}
    OUT
    276 |
    277 |
    278 |
    279 |
    280 |
    281 |
    282 |
    283 |
    284 | 285 |
    286 | 301 |
    302 | 303 |
    304 |
    305 |
    306 |
    307 |
    308 |
    309 |
    310 |
    311 |
    312 | 313 | 314 | 331 | 332 | 333 | 334 | 335 | 336 | 343 | 344 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------