├── Bootstrap ├── Autoload.php └── WebServer.php ├── Config ├── Redis.php └── Server.php ├── Core ├── Cache │ └── FileCache.php ├── Cookie.php ├── RandomKey.php ├── Response.php └── Session.php ├── Doc ├── cmd.png ├── dash.conf ├── dash1.png └── dash3.png ├── LICENSE ├── README.md ├── data └── web.log ├── index.html ├── linux-tools ├── README.md ├── free │ ├── LICENSE │ ├── README.md │ ├── doc │ │ ├── free.png │ │ └── swoole-free.conf │ ├── server │ │ └── server.php │ └── web │ │ ├── free.css │ │ ├── free.js │ │ ├── img │ │ └── forkme_right_orange_ff7600.png │ │ ├── index.html │ │ └── js │ │ ├── chroma.min.js │ │ ├── jquery-2.0.3.min.js │ │ ├── reconnecting-websocket.js │ │ ├── smoothie.js │ │ └── sugar-1.4.1.min.js └── uptime │ ├── LICENSE │ ├── README.md │ ├── doc │ ├── swoole-uptime.conf │ └── uptime.png │ ├── server │ └── server.php │ └── web │ ├── img │ └── forkme_right_orange_ff7600.png │ ├── index.html │ ├── js │ ├── chroma.min.js │ ├── jquery-2.0.3.min.js │ ├── reconnecting-websocket.js │ ├── smoothie.js │ └── sugar-1.4.1.min.js │ ├── uptime.css │ └── uptime.js ├── server ├── index.php ├── modules │ ├── config │ │ └── ping_hosts │ └── shell_files │ │ ├── arp_cache.sh │ │ ├── bandwidth.sh │ │ ├── common_applications.sh │ │ ├── cpu_info.sh │ │ ├── cpu_intensive_processes.sh │ │ ├── cron_history.sh │ │ ├── current_ram.sh │ │ ├── disk_partitions.sh │ │ ├── download_transfer_rate.sh │ │ ├── general_info.sh │ │ ├── internet_speed.sh │ │ ├── io_stats.sh │ │ ├── ip_addresses.sh │ │ ├── load_avg.sh │ │ ├── logged_in_users.sh │ │ ├── memcached.sh │ │ ├── memory_info.sh │ │ ├── network_connections.sh │ │ ├── number_of_cpu_cores.sh │ │ ├── ping.sh │ │ ├── ram_intensive_processes.sh │ │ ├── recent_account_logins.sh │ │ ├── redis.sh │ │ ├── scheduled_crons.sh │ │ ├── swap.sh │ │ ├── upload_transfer_rate.sh │ │ └── user_accounts.sh └── swooleindex.php ├── static ├── css │ ├── animate.css │ ├── main.css │ ├── theme-old.css │ └── themes.css ├── favicon.ico ├── img │ └── themes │ │ ├── congruent_pentagon.png │ │ ├── contemporary_china_2.png │ │ ├── crossword.png │ │ ├── food.png │ │ └── skulls.png └── js │ ├── angular-route.js │ ├── angular.min.js │ ├── linuxDash.js │ ├── modules.js │ └── smoothie.min.js ├── templates ├── app │ ├── base-plugin.html │ ├── key-value-list-plugin.html │ ├── line-chart-plugin.html │ ├── multi-line-chart-plugin.html │ ├── navbar.html │ ├── progress-bar-plugin.html │ ├── table-data-plugin.html │ ├── theme-switcher.html │ └── ui-elements │ │ ├── last-update.html │ │ └── top-bar.html ├── modules │ ├── cpu-load.html │ ├── disk-space.html │ ├── download-transfer-rate.html │ ├── ram-chart.html │ └── upload-transfer-rate.html ├── ping-speeds.html └── sections │ ├── accounts.html │ ├── applications.html │ ├── basic-info.html │ ├── network.html │ └── system-status.html └── web.php /Bootstrap/Autoload.php: -------------------------------------------------------------------------------- 1 | set (\Config\Server::getWebServerConfig()); 37 | $http->on('WorkerStart', array($this, 'onWorkerStart')); 38 | $http->on('request', array($this, 'onRequest')); 39 | $http->on('start', array($this, 'onStart')); 40 | $http->start(); 41 | } 42 | 43 | /** 44 | * server start的时候调用 45 | * @param unknown $serv 46 | */ 47 | public function onStart($serv) 48 | { 49 | echo "\033[1A\n\033[K-----------------------\033[47;30m SWOOLE \033[0m-----------------------------\n\033[0m"; 50 | echo 'swoole version:' . swoole_version() . " PHP version:".PHP_VERSION."\n"; 51 | echo "------------------------\033[47;30m WORKERS \033[0m---------------------------\n"; 52 | echo "\033[47;30mMasterPid\033[0m", str_pad('', self::$_maxMasterPidLength + 2 - strlen('MasterPid')), "\033[47;30mManagerPid\033[0m", str_pad('', self::$_maxManagerPidLength + 2 - strlen('ManagerPid')), "\033[47;30mWorkerId\033[0m", str_pad('', self::$_maxWorkerIdLength + 2 - strlen('WorkerId')), "\033[47;30mWorkerPid\033[0m\n"; 53 | } 54 | /** 55 | * worker start时调用 56 | * @param unknown $serv 57 | * @param int $worker_id 58 | */ 59 | public function onWorkerStart($serv, $worker_id) 60 | { 61 | global $argv; 62 | $worker_num = isset($serv->setting['worker_num']) ? $serv->setting['worker_num'] : 1; 63 | $task_worker_num = isset($serv->setting['task_worker_num']) ? $serv->setting['task_worker_num'] : 0; 64 | 65 | if($worker_id >= $worker_num) { 66 | swoole_set_process_name("php {$argv[0]}: task"); 67 | } else { 68 | swoole_set_process_name("php {$argv[0]}: worker"); 69 | } 70 | echo str_pad($serv->master_pid, self::$_maxMasterPidLength+2),str_pad($serv->manager_pid, self::$_maxManagerPidLength+2),str_pad($serv->worker_id, self::$_maxWorkerIdLength+2), str_pad($serv->worker_pid, self::$_maxWorkerIdLength), "\n";; 71 | define('APPLICATION_PATH', dirname(__DIR__)); 72 | } 73 | 74 | /** 75 | * 当request时调用 76 | * @param unknown $request 77 | * @param unknown $response 78 | */ 79 | public function onRequest($request, $response) 80 | { 81 | $_GET = $_POST = $_COOKIE = array(); 82 | $resp = \Core\Response::getInstance($response); 83 | $resp->setResponse($response); 84 | if (isset($request->get)) { 85 | $_GET = $request->get; 86 | } 87 | if (isset($request->post)) { 88 | $_POST = $request->post; 89 | } 90 | if (isset($request->cookie)) { 91 | $_COOKIE = $request->cookie; 92 | } 93 | try { 94 | ob_start(); 95 | include APPLICATION_PATH.'/server/swooleindex.php'; 96 | $result = ob_get_contents(); 97 | ob_end_clean(); 98 | $response->header("Content-Type", "text/html;charset=utf-8"); 99 | $result = empty($result) ? 'No message' : $result; 100 | $response->end($result); 101 | unset($result); 102 | } catch (Exception $e) { 103 | var_dump($e); 104 | } 105 | } 106 | 107 | /** 108 | * 致命错误处理 109 | */ 110 | public function handleFatal() 111 | { 112 | $error = error_get_last(); 113 | if (isset($error['type'])) { 114 | switch ($error['type']) { 115 | case E_ERROR : 116 | $severity = 'ERROR:Fatal run-time errors. Errors that can not be recovered from. Execution of the script is halted'; 117 | break; 118 | case E_PARSE : 119 | $severity = 'PARSE:Compile-time parse errors. Parse errors should only be generated by the parser'; 120 | break; 121 | case E_DEPRECATED: 122 | $severity = 'DEPRECATED:Run-time notices. Enable this to receive warnings about code that will not work in future versions'; 123 | break; 124 | case E_CORE_ERROR : 125 | $severity = 'CORE_ERROR :Fatal errors at PHP startup. This is like an E_ERROR in the PHP core'; 126 | break; 127 | case E_COMPILE_ERROR : 128 | $severity = 'COMPILE ERROR:Fatal compile-time errors. This is like an E_ERROR generated by the Zend Scripting Engine'; 129 | break; 130 | default: 131 | $severity = 'OTHER ERROR'; 132 | break; 133 | } 134 | $message = $error['message']; 135 | $file = $error['file']; 136 | $line = $error['line']; 137 | $log = "$message ($file:$line)\nStack trace:\n"; 138 | $trace = debug_backtrace(); 139 | foreach ($trace as $i => $t) { 140 | if (!isset($t['file'])) { 141 | $t['file'] = 'unknown'; 142 | } 143 | if (!isset($t['line'])) { 144 | $t['line'] = 0; 145 | } 146 | if (!isset($t['function'])) { 147 | $t['function'] = 'unknown'; 148 | } 149 | $log .= "#$i {$t['file']}({$t['line']}): "; 150 | if (isset($t['object']) && is_object($t['object'])) { 151 | $log .= get_class($t['object']) . '->'; 152 | } 153 | $log .= "{$t['function']}()\n"; 154 | } 155 | if (isset($_SERVER['REQUEST_URI'])) { 156 | $log .= '[QUERY] ' . $_SERVER['REQUEST_URI']; 157 | } 158 | file_put_contents('data/web_error.log', $log); 159 | } 160 | } 161 | 162 | public static function getInstance($ip="0.0.0.0", $port=6666) 163 | { 164 | if (!self::$instance) { 165 | self::$instance = new self($ip, $port); 166 | } 167 | return self::$instance; 168 | } 169 | } -------------------------------------------------------------------------------- /Config/Redis.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 13 | 'port' => '6379', 14 | ); 15 | return $config; 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /Config/Server.php: -------------------------------------------------------------------------------- 1 | 4, // worker进程数量 19 | 'max_request' => 1000, // 最大请求次数,当请求大于它时,将会自动重启该worker 20 | 'dispatch_mode' => 1, 21 | 'log_file' => 'data/web.log', 22 | 'daemonize' => false, // 守护进程设置成true 23 | ); 24 | return $config; 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Core/Cache/FileCache.php: -------------------------------------------------------------------------------- 1 | config = $config; 30 | } 31 | 32 | /** 33 | * 获取文件名 34 | * @param unknown $key 35 | * @return string 36 | */ 37 | protected function getFileName($key) 38 | { 39 | $file = $this->config['cache_dir'] . '/' . trim(str_replace('_', '/', $key), '/'); 40 | $dir = dirname($file); 41 | if (! is_dir($dir)) { 42 | mkdir($dir, 0755, true); 43 | } 44 | return $file; 45 | } 46 | 47 | /** 48 | * 设置缓存 49 | * @param unknown $key 50 | * @param unknown $value 51 | * @param number $timeout 52 | * @return number 53 | */ 54 | function set($key, $value, $timeout = 0) 55 | { 56 | $file = $this->getFileName($key); 57 | $data["value"] = $value; 58 | $data["timeout"] = $timeout; 59 | $data["mktime"] = time(); 60 | $res = file_put_contents($file, serialize($data)); 61 | return $res; 62 | } 63 | 64 | /** 65 | * 获取缓存数据 66 | * @param unknown $key 67 | * @return boolean|mixed 68 | */ 69 | function get($key) 70 | { 71 | $file = $this->getFileName($key); 72 | if (! is_file($file)) 73 | return false; 74 | $data = unserialize(file_get_contents($file)); 75 | if (empty($data) or ! isset($data['timeout']) or ! isset($data["value"])) { 76 | return false; 77 | } 78 | // 已过期 79 | if ($data["timeout"] != 0 and ($data["mktime"] + $data["timeout"]) < time()) { 80 | $this->delete($key); 81 | return false; 82 | } 83 | return $data['value']; 84 | } 85 | 86 | /** 87 | * 删除 88 | * @param unknown $key 89 | * @return boolean 90 | */ 91 | function delete($key) 92 | { 93 | $file = $this->getFileName($key); 94 | return unlink($file); 95 | } 96 | } -------------------------------------------------------------------------------- /Core/Cookie.php: -------------------------------------------------------------------------------- 1 | response = $response; 33 | } 34 | 35 | public function get($key, $default = null) 36 | { 37 | if (! isset($_COOKIE[$key])) { 38 | return $default; 39 | } else { 40 | return $_COOKIE[$key]; 41 | } 42 | } 43 | 44 | public function set($key, $value, $expire = 0) 45 | { 46 | if ($expire != 0) { 47 | $expire = time() + $expire; 48 | } 49 | $this->response->cookie($key, $value, $expire, self::$path, self::$domain, self::$secure, self::$httponly); 50 | } 51 | 52 | public function delete($key) 53 | { 54 | unset($_COOKIE[$key]); 55 | $this->set($key, null); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Core/RandomKey.php: -------------------------------------------------------------------------------- 1 | response; 29 | } 30 | 31 | public function setResponse($response) 32 | { 33 | if (!self::$instance) { 34 | self::$instance = new self(); 35 | } 36 | $this->response = $response; 37 | return true; 38 | } 39 | } -------------------------------------------------------------------------------- /Core/Session.php: -------------------------------------------------------------------------------- 1 | response 29 | * @return NULL 30 | */ 31 | public function __construct() 32 | { 33 | if (! $this->cache) { 34 | $this->cache = new \Core\Cache\FileCache(); 35 | } 36 | if (! $this->cookie) { 37 | $this->cookie = \Core\Cookie::getInstance(); 38 | } 39 | } 40 | 41 | public static function getInstance($response) 42 | { 43 | if (!self::$instance) { 44 | self::$instance = new self($response); 45 | } 46 | self::$instance->cookie->setResponse($response); 47 | return self::$instance; 48 | } 49 | 50 | 51 | public function start($readonly = false) 52 | { 53 | $this->readonly = $readonly; 54 | $this->open = true; 55 | $sessid = $this->cookie->get(self::$cookie_key); 56 | if (empty($sessid)) { 57 | $sessid = \Core\RandomKey::randmd5(40); 58 | $this->cookie->set(self::$cookie_key, $sessid, self::$cache_life); 59 | } 60 | $_SESSION = $this->load($sessid); 61 | } 62 | 63 | public function load($sessId) 64 | { 65 | $this->sessID = $sessId; 66 | $data = $this->get($sessId); 67 | if ($data) { 68 | return unserialize($data); 69 | } else { 70 | return array(); 71 | } 72 | } 73 | 74 | public function save() 75 | { 76 | return $this->set($this->sessID, serialize($_SESSION)); 77 | } 78 | 79 | /** 80 | * 打开Session 81 | * @param String $pSavePath 82 | * @param String $pSessName 83 | * @return Bool TRUE/FALSE 84 | */ 85 | public function open($save_path = '', $sess_name = '') 86 | { 87 | self::$cache_prefix = $save_path . '_' . $sess_name; 88 | return true; 89 | } 90 | 91 | /** 92 | * 关闭Session 93 | * @param NULL 94 | * @return Bool TRUE/FALSE 95 | */ 96 | public function close() 97 | { 98 | return true; 99 | } 100 | 101 | /** 102 | * 读取Session 103 | * @param String $sessId 104 | * @return Bool TRUE/FALSE 105 | */ 106 | public function get($sessId) 107 | { 108 | $session = $this->cache->get(self::$cache_prefix . $sessId); 109 | // 先读数据,如果没有,就初始化一个 110 | if (! empty($session)) { 111 | return $session; 112 | } else { 113 | return array(); 114 | } 115 | } 116 | 117 | /** 118 | * 设置Session的值 119 | * @param String $wSessId 120 | * @param String $wData 121 | * @return Bool true/FALSE 122 | */ 123 | public function set($sessId, $session = '') 124 | { 125 | $key = self::$cache_prefix . $sessId; 126 | $ret = $this->cache->set($key, $session, self::$cache_life); 127 | return $ret; 128 | } 129 | 130 | /** 131 | * 销毁Session 132 | * @param String $wSessId 133 | * @return Bool true/FALSE 134 | */ 135 | public function delete($sessId = '') 136 | { 137 | $sessId = empty($sessId) ? $this->sessID : $sessId; 138 | return $this->cache->delete(self::$cache_prefix . $sessId); 139 | } 140 | 141 | /** 142 | * 内存回收 143 | * @param NULL 144 | * @return Bool true/FALSE 145 | */ 146 | public function gc() 147 | { 148 | return true; 149 | } 150 | 151 | /** 152 | * 初始化Session,配置Session 153 | * @param NULL 154 | * @return Bool true/FALSE 155 | */ 156 | function initSess() 157 | { 158 | // 不使用 GET/POST 变量方式 159 | ini_set('session.use_trans_sid', 0); 160 | // 设置垃圾回收最大生存时间 161 | ini_set('session.gc_maxlifetime', self::$cache_life); 162 | // 使用 COOKIE 保存 SESSION ID 的方式 163 | ini_set('session.use_cookies', 1); 164 | ini_set('session.cookie_path', '/'); 165 | // 多主机共享保存 SESSION ID 的 COOKIE 166 | ini_set('session.cookie_domain', self::$sess_domain); 167 | // 将 session.save_handler 设置为 user,而不是默认的 files 168 | session_module_name('user'); 169 | // 定义 SESSION 各项操作所对应的方法名 170 | session_set_save_handler( 171 | array($this,'open'), 172 | array($this,'close'), 173 | array($this,'get'), 174 | array($this,'set'), 175 | array($this,'delete'), 176 | array($this,'gc') 177 | ); 178 | session_start(); 179 | return true; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Doc/cmd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/Doc/cmd.png -------------------------------------------------------------------------------- /Doc/dash.conf: -------------------------------------------------------------------------------- 1 | #设定虚拟主机配置 nginx + php-fpm 2 | server { 3 | #侦听80端口 4 | listen 80; 5 | server_name dash.test.com; 6 | 7 | #定义服务器的默认网站根目录位置 8 | root /var/www/code/swoole-linux-dash/; 9 | 10 | location / { 11 | #定义首页索引文件的名称 12 | index index.php index.html index.htm; 13 | 14 | } 15 | 16 | # 定义错误提示页面 17 | error_page 500 502 503 504 /50x.html; 18 | location = /50x.html { 19 | } 20 | 21 | #PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI默认配置. 22 | location ~ .php$ { 23 | fastcgi_pass 127.0.0.1:9000; 24 | fastcgi_index index.php; 25 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 26 | include fastcgi_params; 27 | } 28 | 29 | } 30 | 31 | #设定虚拟主机配置 nginx + swoole-http-server 32 | upstream swoole-dash { 33 | server 127.0.0.1:10000; #swoole_http_server 34 | } 35 | server { 36 | listen 80; 37 | server_name www.swoole-dash.com swoole-dash.com; 38 | 39 | root /var/www/code/swoole-linux-dash/; 40 | index index.html index.htm index.php; 41 | 42 | # send request back to swoole-server ## 43 | location ~ .php$ { 44 | proxy_pass http://swoole-dash; 45 | 46 | #Proxy Settings 47 | proxy_redirect off; 48 | proxy_set_header Host $host; 49 | proxy_set_header X-Real-IP $remote_addr; 50 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 51 | proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; 52 | proxy_max_temp_file_size 0; 53 | proxy_connect_timeout 90; 54 | proxy_send_timeout 90; 55 | proxy_read_timeout 90; 56 | proxy_buffer_size 4k; 57 | proxy_buffers 4 32k; 58 | proxy_busy_buffers_size 64k; 59 | proxy_temp_file_write_size 64k; 60 | } 61 | location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|js|css|ico)$ 62 | { 63 | access_log off; 64 | expires 0d; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Doc/dash1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/Doc/dash1.png -------------------------------------------------------------------------------- /Doc/dash3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/Doc/dash3.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # linux-tools 2 | linux-tools目录下的是linux性能工具集合,与swoole_vmstat类似 3 | 运用swoole友好的实现Linux性能监控工具集合(uptime等) 4 | 5 | ![Linux free screenshot](https://raw.githubusercontent.com/smalleyes/swoole-linux-dash/master/linux-tools/free/doc/free.png) 6 | ![Linux uptime screenshot](https://raw.githubusercontent.com/smalleyes/swoole-linux-dash/master/linux-tools/uptime/doc/uptime.png) 7 | 8 | # Swoole Linux Dash 9 | 10 | A simple, low-overhead web dashboard for Linux. 11 | 12 | ![Swoole Linux Dash screenshot](https://raw.githubusercontent.com/smalleyes/swoole-linux-dash/master/Doc/cmd.png) 13 | 14 | ![Swoole Linux Dash screenshot](https://raw.githubusercontent.com/smalleyes/swoole-linux-dash/master/Doc/dash1.png) 15 | 16 | ![Swoole Linux Dash screenshot](https://raw.githubusercontent.com/smalleyes/swoole-linux-dash/master/Doc/dash3.png) 17 | 18 | ## 说明 19 | * 一个简单的, 美丽的,基于web的linux监控面板 20 | * 可以运行在传统PHP-FPM环境也可以运行在基于swoole-http-server环境下 21 | * thanks to [https://github.com/afaqurk/linux-dash](https://github.com/afaqurk/linux-dash) 22 | 23 | ## 安装 24 | 25 | ### 1. 下载 Swoole Linux Dash 26 | 27 | linux shell Clone the git repo: 28 | ``` 29 | git clone https://github.com/smalleyes/swoole-linux-dash.git 30 | ``` 31 | linux wget the zip file: 32 | ``` 33 | wget https://github.com/smalleyes/swoole-linux-dash/archive/master.zip 34 | unzip master.zip 35 | ``` 36 | ### 2. 安全 37 | 请自行做好安全相关的限制. 38 | 39 | ## 依赖 40 | 41 | * PHP 5.3+ 42 | * Swoole 1.7.16 43 | * Linux, OS X and basic Windows support (Thanks to cygwin) 44 | 45 | ## 安装 Swoole扩展 46 | 47 | 1. Install from pecl 48 | 49 | ``` 50 | pecl install swoole 51 | ``` 52 | 53 | 2. Install from source 54 | 55 | ``` 56 | sudo apt-get install php5-dev 57 | git clone https://github.com/swoole/swoole-src.git 58 | cd swoole-src 59 | phpize 60 | ./configure 61 | make && make install 62 | ``` 63 | 64 | ## 运行 65 | 66 | * 配置NGINX虚拟主机 67 | * 配置文件位于Doc/dash.conf 68 | * 复制文件dash.conf到nginx,虚拟主机配置文件目录下(默认为nginx/conf.d目录下) 69 | * 重启nginx或重新加载nginx配置文件(nginx -s reload) 70 | * 配置hoshs文件,绑定ip域名对应关系 71 | * 使用php-cgi或php-fpm确保已正确安装环境再打开浏览器访问绑定的域名. 72 | * 使用swoole需要启动服务,php web.php再打开浏览器访问绑定的域名 73 | -------------------------------------------------------------------------------- /data/web.log: -------------------------------------------------------------------------------- 1 | [2015-07-24 11:48:15 *2776.0] WARN http_onReceive: connection[58] is closed. 2 | [2015-07-24 11:48:15 *2776.0] WARN http_onReceive: connection[59] is closed. 3 | [2015-07-24 11:48:15 *2776.0] WARN http_onReceive: connection[60] is closed. 4 | [2015-07-24 11:48:15 *2776.0] WARN http_onReceive: connection[61] is closed. 5 | [2015-07-24 11:48:15 *2776.0] WARN http_onReceive: connection[62] is closed. 6 | [2015-07-24 11:48:15 *2776.0] WARN http_onReceive: connection[63] is closed. 7 | [2015-07-24 11:48:15 *2776.0] WARN http_onReceive: connection[64] is closed. 8 | [2015-07-24 11:48:15 *2776.0] WARN http_onReceive: connection[65] is closed. 9 | [2015-07-24 11:48:15 *2776.0] WARN http_onReceive: connection[66] is closed. 10 | [2015-07-24 11:48:15 *2776.0] WARN http_onReceive: connection[67] is closed. 11 | [2015-07-24 11:48:15 *2776.0] WARN http_onReceive: connection[68] is closed. 12 | [2015-07-24 11:48:19 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#11 failed. Error: Socket operation on non-socket[88]. 13 | [2015-07-24 11:51:21 *2776.0] WARN http_onReceive: connection[324] is closed. 14 | [2015-07-24 11:51:21 *2776.0] WARN http_onReceive: connection[325] is closed. 15 | [2015-07-24 11:51:21 *2776.0] WARN http_onReceive: connection[326] is closed. 16 | [2015-07-24 11:51:21 *2776.0] WARN http_onReceive: connection[327] is closed. 17 | [2015-07-24 11:51:21 *2776.0] WARN http_onReceive: connection[328] is closed. 18 | [2015-07-24 11:51:21 *2776.0] WARN http_onReceive: connection[329] is closed. 19 | [2015-07-24 11:51:21 *2776.0] WARN http_onReceive: connection[330] is closed. 20 | [2015-07-24 11:51:43 *2776.0] WARN http_onReceive: connection[336] is closed. 21 | [2015-07-24 11:51:43 *2776.0] WARN http_onReceive: connection[337] is closed. 22 | [2015-07-24 11:51:43 *2776.0] WARN http_onReceive: connection[338] is closed. 23 | [2015-07-24 11:51:43 *2776.0] WARN http_onReceive: connection[339] is closed. 24 | [2015-07-24 12:10:57 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 25 | [2015-07-24 12:17:23 #2763.0] WARN swReactorThread_send: send [4] failed, session#2847 is closed. 26 | [2015-07-24 12:18:44 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 27 | [2015-07-24 12:24:39 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 28 | [2015-07-24 12:26:13 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 29 | [2015-07-24 12:28:16 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 30 | [2015-07-24 12:29:04 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#10 failed. Error: Socket operation on non-socket[88]. 31 | [2015-07-24 12:30:03 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 32 | [2015-07-24 12:31:00 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 33 | [2015-07-24 12:31:01 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 34 | [2015-07-24 12:37:21 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 35 | [2015-07-24 12:38:26 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 36 | [2015-07-24 12:40:11 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 37 | [2015-07-24 12:43:20 #2763.0] WARN swReactorThread_send: send [4] failed, session#5956 is closed. 38 | [2015-07-24 12:45:17 #2763.0] WARN swReactorThread_send: send [4] failed, session#6189 is closed. 39 | [2015-07-24 12:53:14 #2763.0] WARN swReactorThread_send: send [4] failed, session#7144 is closed. 40 | [2015-07-24 12:58:03 #2763.0] WARN swReactorThread_send: send [4] failed, session#7721 is closed. 41 | [2015-07-24 13:02:50 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 42 | [2015-07-24 13:03:26 #2763.0] WARN swReactorThread_send: send [4] failed, session#8367 is closed. 43 | [2015-07-24 13:10:17 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 44 | [2015-07-24 13:10:29 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 45 | [2015-07-24 13:12:02 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 46 | [2015-07-24 13:15:50 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 47 | [2015-07-24 13:17:42 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 48 | [2015-07-24 13:22:11 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 49 | [2015-07-24 13:23:19 #2763.0] WARN swReactorThread_send: send [4] failed, session#10754 is closed. 50 | [2015-07-24 13:25:28 #2763.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 51 | [2015-07-24 13:29:44 #2763.0] WARN swReactorThread_send: send [4] failed, session#11524 is closed. 52 | [2015-07-24 13:29:51 #2763.0] WARN swReactorThread_send: send [4] failed, session#11538 is closed. 53 | [2015-07-24 14:36:31 #30293.0] WARN swReactorThread_send: send [4] failed, session#94 is closed. 54 | [2015-07-24 14:39:36 #30293.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 55 | [2015-07-24 14:43:02 #30293.0] WARN swReactorThread_onReceive_http_request#1435: recv from connection#9 failed. Error: Socket operation on non-socket[88]. 56 | [2015-07-24 14:48:47 *13384.2] WARN http_onReceive: connection[27] is closed. 57 | [2015-07-24 14:48:47 *13384.2] WARN http_onReceive: connection[31] is closed. 58 | [2015-07-24 14:52:43 #13369.1] WARN swReactorThread_send: send [4] failed, session#188 is closed. 59 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | linux-dash : Server Monitoring Web Dashboard 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 |
26 |

Linux Dash

27 | A simple linux dashboard for swoole 28 | 29 | 30 |
31 | 32 |
33 |

Setting server...

34 | 35 |
36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /linux-tools/README.md: -------------------------------------------------------------------------------- 1 | # swoole-linux-tools 2 | 运用swoole友好的实现Linux性能监控工具集合(free,uptime等) 3 | 4 | ![Linux free screenshot](https://raw.githubusercontent.com/smalleyes/swoole-linux-dash/master/linux-tools/free/doc/free.png) 5 | ![Linux uptime screenshot](https://raw.githubusercontent.com/smalleyes/swoole-linux-dash/master/linux-tools/uptime/doc/uptime.png) 6 | 7 | ## 依赖 8 | 9 | * PHP 5.3+ 10 | * Swoole 1.7.16 11 | * Linux, OS X and basic Windows support (Thanks to cygwin) 12 | 13 | ## 安装 Swoole扩展 14 | 15 | 1. Install from pecl 16 | 17 | ``` 18 | pecl install swoole 19 | ``` 20 | 21 | 2. Install from source 22 | 23 | ``` 24 | sudo apt-get install php5-dev 25 | git clone https://github.com/swoole/swoole-src.git 26 | cd swoole-src 27 | phpize 28 | ./configure 29 | make && make install 30 | ``` 31 | -------------------------------------------------------------------------------- /linux-tools/free/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /linux-tools/free/README.md: -------------------------------------------------------------------------------- 1 | # swoole-free 2 | 3 | ## 依赖 4 | 5 | * PHP 5.3+ 6 | * Swoole 1.7.16 7 | * Linux, OS X and basic Windows support (Thanks to cygwin) 8 | 9 | ## 安装 Swoole扩展 10 | 11 | 1. Install from pecl 12 | 13 | ``` 14 | pecl install swoole 15 | ``` 16 | 17 | 2. Install from source 18 | 19 | ``` 20 | sudo apt-get install php5-dev 21 | git clone https://github.com/swoole/swoole-src.git 22 | cd swoole-src 23 | phpize 24 | ./configure 25 | make && make install 26 | ``` 27 | 28 | ## 运行 29 | 30 | 1. cd free/server 31 | 2. php server.php 32 | 3. 修改web目录下stats.js代码 var ws = new ReconnectingWebSocket("ws://192.168.1.10:8881"); 改成服务器的IP 33 | 4. 用浏览器打开web目录下的index.html 34 | 35 | ## 运行结果 36 | 37 | 1. 打开页面如下所示 38 | ![one](https://raw.githubusercontent.com/smalleyes/swoole-linux-dash/master/linux-tools/free/doc/free.png) 39 | 40 | ## nginx配置文件 41 | 1. 在doc/swoole-free.conf 42 | 43 | -------------------------------------------------------------------------------- /linux-tools/free/doc/free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/linux-tools/free/doc/free.png -------------------------------------------------------------------------------- /linux-tools/free/doc/swoole-free.conf: -------------------------------------------------------------------------------- 1 | #设定虚拟主机配置 2 | server { 3 | #侦听80端口 4 | listen 80; 5 | server_name free.iizhu.com; 6 | 7 | #定义服务器的默认网站根目录位置 8 | root /var/www/code/swoole-free/web/; 9 | 10 | location / { 11 | #定义首页索引文件的名称 12 | index index.php index.html index.htm; 13 | 14 | } 15 | 16 | # 定义错误提示页面 17 | error_page 500 502 503 504 /50x.html; 18 | location = /50x.html { 19 | } 20 | 21 | #PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI默认配置. 22 | location ~ .php$ { 23 | fastcgi_pass 127.0.0.1:9000; 24 | fastcgi_index index.php; 25 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 26 | include fastcgi_params; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /linux-tools/free/server/server.php: -------------------------------------------------------------------------------- 1 | column('cmd', swoole_table::TYPE_STRING, 32); 32 | $table->column('param', swoole_table::TYPE_STRING, 4); 33 | $table->create(); 34 | $table->set('free', array('cmd' => $free_path,'param'=>$param)); 35 | 36 | $server = new swoole_websocket_server($host, $port); 37 | $server->table = $table; //将table保存在serv对象上 38 | 39 | /** 40 | * websocket server配置 41 | */ 42 | $server->set (array( 43 | 'worker_num' => 1, //worker进程数量 44 | 'daemonize' => false, //守护进程设置成true 45 | 'max_request' => 1000, //最大请求次数,当请求大于它时,将会自动重启该worker 46 | 'dispatch_mode' => 1 47 | )); 48 | 49 | /** 50 | * websocket server start 51 | * 成功后回调 52 | */ 53 | $server->on('start', function ($serv) use($_maxMasterPidLength, $_maxManagerPidLength, $_maxWorkerIdLength, $_maxWorkerPidLength) { 54 | echo "\033[1A\n\033[K-----------------------\033[47;30m SWOOLE \033[0m-----------------------------\n\033[0m"; 55 | echo 'swoole version:' . swoole_version() . " PHP version:".PHP_VERSION."\n"; 56 | echo "------------------------\033[47;30m WORKERS \033[0m---------------------------\n"; 57 | echo "\033[47;30mMasterPid\033[0m", str_pad('', $_maxMasterPidLength + 2 - strlen('MasterPid')), 58 | "\033[47;30mManagerPid\033[0m", str_pad('', $_maxManagerPidLength + 2 - strlen('ManagerPid')), 59 | "\033[47;30mWorkerId\033[0m", str_pad('', $_maxWorkerIdLength + 2 - strlen('WorkerId')), 60 | "\033[47;30mWorkerPid\033[0m", str_pad('', $_maxWorkerPidLength + 2 - strlen('WorkerPid')),"\n"; 61 | }); 62 | 63 | 64 | /** 65 | * 当WebSocket客户端与服务器建立连接并完成握手后会回调此函数。 66 | */ 67 | $server->on('open', function (swoole_websocket_server $server, $request) { 68 | $fd = $request->fd; 69 | echo "server: handshake success with fd{$fd}\n"; 70 | $server->push($fd, "Mem -----------Swap---------\n"); 71 | $server->push($fd, "total used free shared buff/cache available Total Used Free\n"); 72 | }); 73 | 74 | /** 75 | * 当服务器收到来自客户端的数据帧时会回调此函数。 76 | */ 77 | $server->on('message', function (swoole_websocket_server $server, $frame) { 78 | echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n"; 79 | $server->push($frame->fd, "this is server"); 80 | }); 81 | 82 | /** 83 | * 当客户端关闭的时候调用 84 | */ 85 | $server->on('close', function ($ser, $fd) { 86 | echo "client {$fd} closed\n"; 87 | }); 88 | 89 | /** 90 | * 当worker 启动的时候调用 91 | */ 92 | $server->on('workerStart',function ($serv, $worker_id) use($_maxMasterPidLength, $_maxManagerPidLength, $_maxWorkerIdLength, $_maxWorkerPidLength) { 93 | echo str_pad($serv->master_pid, $_maxMasterPidLength+2), 94 | str_pad($serv->manager_pid, $_maxManagerPidLength+2), 95 | str_pad($serv->worker_id, $_maxWorkerIdLength+2), 96 | str_pad($serv->worker_pid, $_maxWorkerIdLength), "\n"; 97 | if ($worker_id==0) { 98 | $data = $serv->table->get('free'); 99 | $serv->tick(1000, function($id) use($serv, $data) { 100 | $conn_list = $serv->connection_list(); 101 | if (!empty($conn_list)) { 102 | exec($data['cmd'].$data['param'],$string); 103 | foreach($conn_list as $fd) { 104 | $conn = $serv->connection_info($fd); 105 | if (!empty($conn)) { 106 | $str = str_replace('Mem:', '', $string['1']); 107 | $str .= str_replace('Swap:', '', $string['2']); 108 | $serv->push($fd, $str); 109 | } 110 | } 111 | } 112 | }); 113 | } 114 | }); 115 | 116 | $server->start(); -------------------------------------------------------------------------------- /linux-tools/free/web/free.css: -------------------------------------------------------------------------------- 1 | .template { 2 | display: none !important; 3 | } 4 | * { 5 | cursor: default; 6 | } 7 | body { 8 | background-color: #111; 9 | color: #eee; 10 | font-family: "helvetica neue", helvetica, arial, sans-serif; 11 | } 12 | h2 { 13 | margin: 40px 0 0 0; 14 | font-weight: 300; 15 | } 16 | main { 17 | width: 600px; 18 | margin: auto; 19 | } 20 | section { 21 | clear: left; 22 | } 23 | .stats { 24 | margin: 0; 25 | } 26 | .stat { 27 | list-style-type: none; 28 | float: left; 29 | margin: 0; 30 | width: 130px; 31 | font-size: 12px; 32 | } 33 | .stat-name { 34 | display: inline-block; 35 | text-align: right; 36 | width: 50px; 37 | margin-right: 5px; 38 | } 39 | .stat-value { 40 | font-weight: bold; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /linux-tools/free/web/free.js: -------------------------------------------------------------------------------- 1 | var allTimeSeries = {}; 2 | var allValueLabels = {}; 3 | var descriptions = { 4 | 'Memory': { 5 | 'total': 'total memory', 6 | 'used': 'used memory', 7 | 'free': 'free memory', 8 | 'shared': 'shared memory', 9 | 'buff/cache': 'buff/cache memory', 10 | 'available': 'available memory' 11 | }, 12 | 'Swap': { 13 | 'Total': 'Swap total memory', 14 | 'Used': 'Swap used memory', 15 | 'Free': 'Swap free memory' 16 | } 17 | } 18 | 19 | function streamStats() { 20 | 21 | var ws = new ReconnectingWebSocket("ws://192.168.1.10:8881"); 22 | var lineCount; 23 | var colHeadings; 24 | 25 | ws.onopen = function() { 26 | console.log('connect'); 27 | lineCount = 0; 28 | }; 29 | 30 | ws.onclose = function() { 31 | console.log('disconnect'); 32 | }; 33 | 34 | ws.onmessage = function(e) { 35 | switch (lineCount++) { 36 | case 0: // ignore first line 37 | break; 38 | 39 | case 1: // column headings 40 | colHeadings = e.data.trim().split(/ +/); 41 | break; 42 | 43 | default: // subsequent lines 44 | var colValues = e.data.trim().split(/ +/); 45 | var stats = {}; 46 | for (var i = 0; i < colHeadings.length; i++) { 47 | stats[colHeadings[i]] = parseInt(colValues[i]); 48 | } 49 | receiveStats(stats); 50 | } 51 | }; 52 | } 53 | 54 | function initCharts() { 55 | Object.each(descriptions, function(sectionName, values) { 56 | var section = $('.chart.template').clone().removeClass('template').appendTo('#charts'); 57 | 58 | section.find('.title').text(sectionName); 59 | 60 | var smoothie = new SmoothieChart({ 61 | grid: { 62 | sharpLines: true, 63 | verticalSections: 5, 64 | strokeStyle: 'rgba(119,119,119,0.45)', 65 | millisPerLine: 1000 66 | }, 67 | minValue: 0, 68 | labels: { 69 | disabled: true 70 | } 71 | }); 72 | smoothie.streamTo(section.find('canvas').get(0), 1000); 73 | 74 | var colors = chroma.brewer['Pastel2']; 75 | var index = 0; 76 | Object.each(values, function(name, valueDescription) { 77 | var color = colors[index++]; 78 | 79 | var timeSeries = new TimeSeries(); 80 | smoothie.addTimeSeries(timeSeries, { 81 | strokeStyle: color, 82 | fillStyle: chroma(color).darken().alpha(0.5).css(), 83 | lineWidth: 3 84 | }); 85 | allTimeSeries[name] = timeSeries; 86 | 87 | var statLine = section.find('.stat.template').clone().removeClass('template').appendTo(section.find('.stats')); 88 | statLine.attr('title', valueDescription).css('color', color); 89 | statLine.find('.stat-name').text(name); 90 | allValueLabels[name] = statLine.find('.stat-value'); 91 | }); 92 | }); 93 | } 94 | 95 | function receiveStats(stats) { 96 | Object.each(stats, function(name, value) { 97 | var timeSeries = allTimeSeries[name]; 98 | if (timeSeries) { 99 | timeSeries.append(Date.now(), value); 100 | allValueLabels[name].text(value); 101 | } 102 | }); 103 | } 104 | 105 | $(function() { 106 | initCharts(); 107 | streamStats(); 108 | }); 109 | -------------------------------------------------------------------------------- /linux-tools/free/web/img/forkme_right_orange_ff7600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/linux-tools/free/web/img/forkme_right_orange_ff7600.png -------------------------------------------------------------------------------- /linux-tools/free/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | free 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Fork me on GitHub 15 | 16 | 17 |
18 |

Linux command: free

19 |
20 |

21 | 22 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /linux-tools/free/web/js/reconnecting-websocket.js: -------------------------------------------------------------------------------- 1 | // MIT License: 2 | // 3 | // Copyright (c) 2010-2012, Joe Walnes 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | /** 24 | * This behaves like a WebSocket in every way, except if it fails to connect, 25 | * or it gets disconnected, it will repeatedly poll until it succesfully connects 26 | * again. 27 | * 28 | * It is API compatible, so when you have: 29 | * ws = new WebSocket('ws://....'); 30 | * you can replace with: 31 | * ws = new ReconnectingWebSocket('ws://....'); 32 | * 33 | * The event stream will typically look like: 34 | * onconnecting 35 | * onopen 36 | * onmessage 37 | * onmessage 38 | * onclose // lost connection 39 | * onconnecting 40 | * onopen // sometime later... 41 | * onmessage 42 | * onmessage 43 | * etc... 44 | * 45 | * It is API compatible with the standard WebSocket API. 46 | * 47 | * Latest version: https://github.com/joewalnes/reconnecting-websocket/ 48 | * - Joe Walnes 49 | */ 50 | function ReconnectingWebSocket(url, protocols) { 51 | protocols = protocols || []; 52 | 53 | // These can be altered by calling code. 54 | this.debug = false; 55 | this.reconnectInterval = 1000; 56 | this.timeoutInterval = 2000; 57 | 58 | var self = this; 59 | var ws; 60 | var forcedClose = false; 61 | var timedOut = false; 62 | 63 | this.url = url; 64 | this.protocols = protocols; 65 | this.readyState = WebSocket.CONNECTING; 66 | this.URL = url; // Public API 67 | 68 | this.onopen = function(event) { 69 | }; 70 | 71 | this.onclose = function(event) { 72 | }; 73 | 74 | this.onconnecting = function(event) { 75 | }; 76 | 77 | this.onmessage = function(event) { 78 | }; 79 | 80 | this.onerror = function(event) { 81 | }; 82 | 83 | function connect(reconnectAttempt) { 84 | ws = new WebSocket(url, protocols); 85 | 86 | self.onconnecting(); 87 | if (self.debug || ReconnectingWebSocket.debugAll) { 88 | console.debug('ReconnectingWebSocket', 'attempt-connect', url); 89 | } 90 | 91 | var localWs = ws; 92 | var timeout = setTimeout(function() { 93 | if (self.debug || ReconnectingWebSocket.debugAll) { 94 | console.debug('ReconnectingWebSocket', 'connection-timeout', url); 95 | } 96 | timedOut = true; 97 | localWs.close(); 98 | timedOut = false; 99 | }, self.timeoutInterval); 100 | 101 | ws.onopen = function(event) { 102 | clearTimeout(timeout); 103 | if (self.debug || ReconnectingWebSocket.debugAll) { 104 | console.debug('ReconnectingWebSocket', 'onopen', url); 105 | } 106 | self.readyState = WebSocket.OPEN; 107 | reconnectAttempt = false; 108 | self.onopen(event); 109 | }; 110 | 111 | ws.onclose = function(event) { 112 | clearTimeout(timeout); 113 | ws = null; 114 | if (forcedClose) { 115 | self.readyState = WebSocket.CLOSED; 116 | self.onclose(event); 117 | } else { 118 | self.readyState = WebSocket.CONNECTING; 119 | self.onconnecting(); 120 | if (!reconnectAttempt && !timedOut) { 121 | if (self.debug || ReconnectingWebSocket.debugAll) { 122 | console.debug('ReconnectingWebSocket', 'onclose', url); 123 | } 124 | self.onclose(event); 125 | } 126 | setTimeout(function() { 127 | connect(true); 128 | }, self.reconnectInterval); 129 | } 130 | }; 131 | ws.onmessage = function(event) { 132 | if (self.debug || ReconnectingWebSocket.debugAll) { 133 | console.debug('ReconnectingWebSocket', 'onmessage', url, event.data); 134 | } 135 | self.onmessage(event); 136 | }; 137 | ws.onerror = function(event) { 138 | if (self.debug || ReconnectingWebSocket.debugAll) { 139 | console.debug('ReconnectingWebSocket', 'onerror', url, event); 140 | } 141 | self.onerror(event); 142 | }; 143 | } 144 | connect(url); 145 | 146 | this.send = function(data) { 147 | if (ws) { 148 | if (self.debug || ReconnectingWebSocket.debugAll) { 149 | console.debug('ReconnectingWebSocket', 'send', url, data); 150 | } 151 | return ws.send(data); 152 | } else { 153 | throw 'INVALID_STATE_ERR : Pausing to reconnect websocket'; 154 | } 155 | }; 156 | 157 | this.close = function() { 158 | if (ws) { 159 | forcedClose = true; 160 | ws.close(); 161 | } 162 | }; 163 | 164 | /** 165 | * Additional public API method to refresh the connection if still open (close, re-open). 166 | * For example, if the app suspects bad data / missed heart beats, it can try to refresh. 167 | */ 168 | this.refresh = function() { 169 | if (ws) { 170 | ws.close(); 171 | } 172 | }; 173 | } 174 | 175 | /** 176 | * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true. 177 | */ 178 | ReconnectingWebSocket.debugAll = false; 179 | 180 | -------------------------------------------------------------------------------- /linux-tools/free/web/js/smoothie.js: -------------------------------------------------------------------------------- 1 | // MIT License: 2 | // 3 | // Copyright (c) 2010-2013, Joe Walnes 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | /** 24 | * Smoothie Charts - http://smoothiecharts.org/ 25 | * (c) 2010-2013, Joe Walnes 26 | * 2013, Drew Noakes 27 | * 28 | * v1.0: Main charting library, by Joe Walnes 29 | * v1.1: Auto scaling of axis, by Neil Dunn 30 | * v1.2: fps (frames per second) option, by Mathias Petterson 31 | * v1.3: Fix for divide by zero, by Paul Nikitochkin 32 | * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds 33 | * v1.5: Set default frames per second to 50... smoother. 34 | * .start(), .stop() methods for conserving CPU, by Dmitry Vyal 35 | * options.interpolation = 'bezier' or 'line', by Dmitry Vyal 36 | * options.maxValue to fix scale, by Dmitry Vyal 37 | * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla 38 | * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin 39 | * Smooth rescaling, by Kostas Michalopoulos 40 | * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni 41 | * v1.9: Display timestamps along the bottom, by Nick and Stev-io 42 | * (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D) 43 | * Refactored by Krishna Narni, to support timestamp formatting function 44 | * v1.10: Switch to requestAnimationFrame, removed the now obsoleted options.fps, by Gergely Imreh 45 | * v1.11: options.grid.sharpLines option added, by @drewnoakes 46 | * Addressed warning seen in Firefox when seriesOption.fillStyle undefined, by @drewnoakes 47 | * v1.12: Support for horizontalLines added, by @drewnoakes 48 | * Support for yRangeFunction callback added, by @drewnoakes 49 | * v1.13: Fixed typo (#32), by @alnikitich 50 | * v1.14: Timer cleared when last TimeSeries removed (#23), by @davidgaleano 51 | * Fixed diagonal line on chart at start/end of data stream, by @drewnoakes 52 | * v1.15: Support for npm package (#18), by @dominictarr 53 | * Fixed broken removeTimeSeries function (#24) by @davidgaleano 54 | * Minor performance and tidying, by @drewnoakes 55 | * v1.16: Bug fix introduced in v1.14 relating to timer creation/clearance (#23), by @drewnoakes 56 | * TimeSeries.append now deals with out-of-order timestamps, and can merge duplicates, by @zacwitte (#12) 57 | * Documentation and some local variable renaming for clarity, by @drewnoakes 58 | * v1.17: Allow control over font size (#10), by @drewnoakes 59 | * Timestamp text won't overlap, by @drewnoakes 60 | * v1.18: Allow control of max/min label precision, by @drewnoakes 61 | * Added 'borderVisible' chart option, by @drewnoakes 62 | * Allow drawing series with fill but no stroke (line), by @drewnoakes 63 | * v1.19: Avoid unnecessary repaints, and fixed flicker in old browsers having multiple charts in document (#40), by @asbai 64 | */ 65 | 66 | ;(function(exports) { 67 | 68 | var Util = { 69 | extend: function() { 70 | arguments[0] = arguments[0] || {}; 71 | for (var i = 1; i < arguments.length; i++) 72 | { 73 | for (var key in arguments[i]) 74 | { 75 | if (arguments[i].hasOwnProperty(key)) 76 | { 77 | if (typeof(arguments[i][key]) === 'object') { 78 | if (arguments[i][key] instanceof Array) { 79 | arguments[0][key] = arguments[i][key]; 80 | } else { 81 | arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]); 82 | } 83 | } else { 84 | arguments[0][key] = arguments[i][key]; 85 | } 86 | } 87 | } 88 | } 89 | return arguments[0]; 90 | } 91 | }; 92 | 93 | /** 94 | * Initialises a new TimeSeries with optional data options. 95 | * 96 | * Options are of the form (defaults shown): 97 | * 98 | *
 99 |    * {
100 |    *   resetBounds: true,        // enables/disables automatic scaling of the y-axis
101 |    *   resetBoundsInterval: 3000 // the period between scaling calculations, in millis
102 |    * }
103 |    * 
104 | * 105 | * Presentation options for TimeSeries are specified as an argument to SmoothieChart.addTimeSeries. 106 | * 107 | * @constructor 108 | */ 109 | function TimeSeries(options) { 110 | this.options = Util.extend({}, TimeSeries.defaultOptions, options); 111 | this.data = []; 112 | this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries. 113 | this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries. 114 | } 115 | 116 | TimeSeries.defaultOptions = { 117 | resetBoundsInterval: 3000, 118 | resetBounds: true 119 | }; 120 | 121 | /** 122 | * Recalculate the min/max values for this TimeSeries object. 123 | * 124 | * This causes the graph to scale itself in the y-axis. 125 | */ 126 | TimeSeries.prototype.resetBounds = function() { 127 | if (this.data.length) { 128 | // Walk through all data points, finding the min/max value 129 | this.maxValue = this.data[0][1]; 130 | this.minValue = this.data[0][1]; 131 | for (var i = 1; i < this.data.length; i++) { 132 | var value = this.data[i][1]; 133 | if (value > this.maxValue) { 134 | this.maxValue = value; 135 | } 136 | if (value < this.minValue) { 137 | this.minValue = value; 138 | } 139 | } 140 | } else { 141 | // No data exists, so set min/max to NaN 142 | this.maxValue = Number.NaN; 143 | this.minValue = Number.NaN; 144 | } 145 | }; 146 | 147 | /** 148 | * Adds a new data point to the TimeSeries, preserving chronological order. 149 | * 150 | * @param timestamp the position, in time, of this data point 151 | * @param value the value of this data point 152 | * @param sumRepeatedTimeStampValues if timestamp has an exact match in the series, this flag controls 153 | * whether it is replaced, or the values summed (defaults to false.) 154 | */ 155 | TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) { 156 | // Rewind until we hit an older timestamp 157 | var i = this.data.length - 1; 158 | while (i > 0 && this.data[i][0] > timestamp) { 159 | i--; 160 | } 161 | 162 | if (this.data.length > 0 && this.data[i][0] === timestamp) { 163 | // Update existing values in the array 164 | if (sumRepeatedTimeStampValues) { 165 | // Sum this value into the existing 'bucket' 166 | this.data[i][1] += value; 167 | value = this.data[i][1]; 168 | } else { 169 | // Replace the previous value 170 | this.data[i][1] = value; 171 | } 172 | } else if (i < this.data.length - 1) { 173 | // Splice into the correct position to keep timestamps in order 174 | this.data.splice(i + 1, 0, [timestamp, value]); 175 | } else { 176 | // Add to the end of the array 177 | this.data.push([timestamp, value]); 178 | } 179 | 180 | this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value); 181 | this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value); 182 | }; 183 | 184 | TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) { 185 | // We must always keep one expired data point as we need this to draw the 186 | // line that comes into the chart from the left, but any points prior to that can be removed. 187 | var removeCount = 0; 188 | while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) { 189 | removeCount++; 190 | } 191 | if (removeCount !== 0) { 192 | this.data.splice(0, removeCount); 193 | } 194 | }; 195 | 196 | /** 197 | * Initialises a new SmoothieChart. 198 | * 199 | * Options are optional, and should be of the form below. Just specify the values you 200 | * need and the rest will be given sensible defaults as shown: 201 | * 202 | *
203 |    * {
204 |    *   minValue: undefined,        // specify to clamp the lower y-axis to a given value
205 |    *   maxValue: undefined,        // specify to clamp the upper y-axis to a given value
206 |    *   maxValueScale: 1,           // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
207 |    *   yRangeFunction: undefined,  // function({min: , max: }) { return {min: , max: }; }
208 |    *   scaleSmoothing: 0.125,      // controls the rate at which y-value zoom animation occurs
209 |    *   millisPerPixel: 20,         // sets the speed at which the chart pans by
210 |    *   maxDataSetLength: 2,
211 |    *   interpolation: 'bezier'     // or 'linear'
212 |    *   timestampFormatter: null,   // Optional function to format time stamps for bottom of chart. You may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
213 |    *   horizontalLines: [],        // [ { value: 0, color: '#ffffff', lineWidth: 1 } ],
214 |    *   grid:
215 |    *   {
216 |    *     fillStyle: '#000000',     // the background colour of the chart
217 |    *     lineWidth: 1,             // the pixel width of grid lines
218 |    *     strokeStyle: '#777777',   // colour of grid lines
219 |    *     millisPerLine: 1000,      // distance between vertical grid lines
220 |    *     sharpLines: false,        // controls whether grid lines are 1px sharp, or softened
221 |    *     verticalSections: 2,      // number of vertical sections marked out by horizontal grid lines
222 |    *     borderVisible: true       // whether the grid lines trace the border of the chart or not
223 |    *   },
224 |    *   labels
225 |    *   {
226 |    *     disabled: false,          // enables/disables labels showing the min/max values
227 |    *     fillStyle: '#ffffff',     // colour for text of labels,
228 |    *     fontSize: 15,
229 |    *     fontFamily: 'sans-serif',
230 |    *     precision: 2
231 |    *   },
232 |    * }
233 |    * 
234 | * 235 | * @constructor 236 | */ 237 | function SmoothieChart(options) { 238 | this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options); 239 | this.seriesSet = []; 240 | this.currentValueRange = 1; 241 | this.currentVisMinValue = 0; 242 | this.lastRenderTimeMillis = 0; 243 | } 244 | 245 | SmoothieChart.defaultChartOptions = { 246 | millisPerPixel: 20, 247 | maxValueScale: 1, 248 | interpolation: 'bezier', 249 | scaleSmoothing: 0.125, 250 | maxDataSetLength: 2, 251 | grid: { 252 | fillStyle: '#000000', 253 | strokeStyle: '#777777', 254 | lineWidth: 1, 255 | sharpLines: false, 256 | millisPerLine: 1000, 257 | verticalSections: 2, 258 | borderVisible: true 259 | }, 260 | labels: { 261 | fillStyle: '#ffffff', 262 | disabled: false, 263 | fontSize: 10, 264 | fontFamily: 'monospace', 265 | precision: 2 266 | }, 267 | horizontalLines: [] 268 | }; 269 | 270 | // Based on http://inspirit.github.com/jsfeat/js/compatibility.js 271 | SmoothieChart.AnimateCompatibility = (function() { 272 | var requestAnimationFrame = function(callback, element) { 273 | var requestAnimationFrame = 274 | window.requestAnimationFrame || 275 | window.webkitRequestAnimationFrame || 276 | window.mozRequestAnimationFrame || 277 | window.oRequestAnimationFrame || 278 | window.msRequestAnimationFrame || 279 | function(callback) { 280 | return window.setTimeout(function() { 281 | callback(new Date().getTime()); 282 | }, 16); 283 | }; 284 | return requestAnimationFrame.call(window, callback, element); 285 | }, 286 | cancelAnimationFrame = function(id) { 287 | var cancelAnimationFrame = 288 | window.cancelAnimationFrame || 289 | function(id) { 290 | clearTimeout(id); 291 | }; 292 | return cancelAnimationFrame.call(window, id); 293 | }; 294 | 295 | return { 296 | requestAnimationFrame: requestAnimationFrame, 297 | cancelAnimationFrame: cancelAnimationFrame 298 | }; 299 | })(); 300 | 301 | SmoothieChart.defaultSeriesPresentationOptions = { 302 | lineWidth: 1, 303 | strokeStyle: '#ffffff' 304 | }; 305 | 306 | /** 307 | * Adds a TimeSeries to this chart, with optional presentation options. 308 | * 309 | * Presentation options should be of the form (defaults shown): 310 | * 311 | *
312 |    * {
313 |    *   lineWidth: 1,
314 |    *   strokeStyle: '#ffffff',
315 |    *   fillStyle: undefined
316 |    * }
317 |    * 
318 | */ 319 | SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) { 320 | this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)}); 321 | if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) { 322 | timeSeries.resetBoundsTimerId = setInterval( 323 | function() { 324 | timeSeries.resetBounds(); 325 | }, 326 | timeSeries.options.resetBoundsInterval 327 | ); 328 | } 329 | }; 330 | 331 | /** 332 | * Removes the specified TimeSeries from the chart. 333 | */ 334 | SmoothieChart.prototype.removeTimeSeries = function(timeSeries) { 335 | // Find the correct timeseries to remove, and remove it 336 | var numSeries = this.seriesSet.length; 337 | for (var i = 0; i < numSeries; i++) { 338 | if (this.seriesSet[i].timeSeries === timeSeries) { 339 | this.seriesSet.splice(i, 1); 340 | break; 341 | } 342 | } 343 | // If a timer was operating for that timeseries, remove it 344 | if (timeSeries.resetBoundsTimerId) { 345 | // Stop resetting the bounds, if we were 346 | clearInterval(timeSeries.resetBoundsTimerId); 347 | } 348 | }; 349 | 350 | /** 351 | * Instructs the SmoothieChart to start rendering to the provided canvas, with specified delay. 352 | * 353 | * @param canvas the target canvas element 354 | * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series 355 | * from appearing on screen, with new values flashing into view, at the expense of some latency. 356 | */ 357 | SmoothieChart.prototype.streamTo = function(canvas, delayMillis) { 358 | this.canvas = canvas; 359 | this.delay = delayMillis; 360 | this.start(); 361 | }; 362 | 363 | /** 364 | * Starts the animation of this chart. 365 | */ 366 | SmoothieChart.prototype.start = function() { 367 | if (this.frame) { 368 | // We're already running, so just return 369 | return; 370 | } 371 | 372 | // Renders a frame, and queues the next frame for later rendering 373 | var animate = function() { 374 | this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() { 375 | this.render(); 376 | animate(); 377 | }.bind(this)); 378 | }.bind(this); 379 | 380 | animate(); 381 | }; 382 | 383 | /** 384 | * Stops the animation of this chart. 385 | */ 386 | SmoothieChart.prototype.stop = function() { 387 | if (this.frame) { 388 | SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame); 389 | delete this.frame; 390 | } 391 | }; 392 | 393 | SmoothieChart.prototype.updateValueRange = function() { 394 | // Calculate the current scale of the chart, from all time series. 395 | var chartOptions = this.options, 396 | chartMaxValue = Number.NaN, 397 | chartMinValue = Number.NaN; 398 | 399 | for (var d = 0; d < this.seriesSet.length; d++) { 400 | // TODO(ndunn): We could calculate / track these values as they stream in. 401 | var timeSeries = this.seriesSet[d].timeSeries; 402 | if (!isNaN(timeSeries.maxValue)) { 403 | chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue; 404 | } 405 | 406 | if (!isNaN(timeSeries.minValue)) { 407 | chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue; 408 | } 409 | } 410 | 411 | // Scale the chartMaxValue to add padding at the top if required 412 | if (chartOptions.maxValue != null) { 413 | chartMaxValue = chartOptions.maxValue; 414 | } else { 415 | chartMaxValue *= chartOptions.maxValueScale; 416 | } 417 | 418 | // Set the minimum if we've specified one 419 | if (chartOptions.minValue != null) { 420 | chartMinValue = chartOptions.minValue; 421 | } 422 | 423 | // If a custom range function is set, call it 424 | if (this.options.yRangeFunction) { 425 | var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue}); 426 | chartMinValue = range.min; 427 | chartMaxValue = range.max; 428 | } 429 | 430 | if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) { 431 | var targetValueRange = chartMaxValue - chartMinValue; 432 | var valueRangeDiff = (targetValueRange - this.currentValueRange); 433 | var minValueDiff = (chartMinValue - this.currentVisMinValue); 434 | this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1; 435 | this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff; 436 | this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff; 437 | } 438 | 439 | this.valueRange = { min: chartMinValue, max: chartMaxValue }; 440 | }; 441 | 442 | SmoothieChart.prototype.render = function(canvas, time) { 443 | var nowMillis = new Date().getTime(); 444 | 445 | if (!this.isAnimatingScale) { 446 | // We're not animating. We can use the last render time and the scroll speed to work out whether 447 | // we actually need to paint anything yet. If not, we can return immediately. 448 | 449 | // Render at least every 1/6th of a second. The canvas may be resized, which there is 450 | // no reliable way to detect. 451 | var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel); 452 | 453 | if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) { 454 | return; 455 | } 456 | } 457 | this.lastRenderTimeMillis = nowMillis; 458 | 459 | canvas = canvas || this.canvas; 460 | time = time || nowMillis - (this.delay || 0); 461 | 462 | // Round time down to pixel granularity, so motion appears smoother. 463 | time -= time % this.options.millisPerPixel; 464 | 465 | var context = canvas.getContext('2d'), 466 | chartOptions = this.options, 467 | dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight }, 468 | // Calculate the threshold time for the oldest data points. 469 | oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel), 470 | valueToYPixel = function(value) { 471 | var offset = value - this.currentVisMinValue; 472 | return this.currentValueRange === 0 473 | ? dimensions.height 474 | : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height)); 475 | }.bind(this), 476 | timeToXPixel = function(t) { 477 | return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel)); 478 | }; 479 | 480 | this.updateValueRange(); 481 | 482 | context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily; 483 | 484 | // Save the state of the canvas context, any transformations applied in this method 485 | // will get removed from the stack at the end of this method when .restore() is called. 486 | context.save(); 487 | 488 | // Move the origin. 489 | context.translate(dimensions.left, dimensions.top); 490 | 491 | // Create a clipped rectangle - anything we draw will be constrained to this rectangle. 492 | // This prevents the occasional pixels from curves near the edges overrunning and creating 493 | // screen cheese (that phrase should need no explanation). 494 | context.beginPath(); 495 | context.rect(0, 0, dimensions.width, dimensions.height); 496 | context.clip(); 497 | 498 | // Clear the working area. 499 | context.save(); 500 | context.fillStyle = chartOptions.grid.fillStyle; 501 | context.clearRect(0, 0, dimensions.width, dimensions.height); 502 | context.fillRect(0, 0, dimensions.width, dimensions.height); 503 | context.restore(); 504 | 505 | // Grid lines... 506 | context.save(); 507 | context.lineWidth = chartOptions.grid.lineWidth; 508 | context.strokeStyle = chartOptions.grid.strokeStyle; 509 | // Vertical (time) dividers. 510 | if (chartOptions.grid.millisPerLine > 0) { 511 | var textUntilX = dimensions.width - context.measureText(minValueString).width + 4; 512 | for (var t = time - (time % chartOptions.grid.millisPerLine); 513 | t >= oldestValidTime; 514 | t -= chartOptions.grid.millisPerLine) { 515 | var gx = timeToXPixel(t); 516 | if (chartOptions.grid.sharpLines) { 517 | gx -= 0.5; 518 | } 519 | context.beginPath(); 520 | context.moveTo(gx, 0); 521 | context.lineTo(gx, dimensions.height); 522 | context.stroke(); 523 | context.closePath(); 524 | 525 | // Display timestamp at bottom of this line if requested, and it won't overlap 526 | if (chartOptions.timestampFormatter && gx < textUntilX) { 527 | // Formats the timestamp based on user specified formatting function 528 | // SmoothieChart.timeFormatter function above is one such formatting option 529 | var tx = new Date(t), 530 | ts = chartOptions.timestampFormatter(tx), 531 | tsWidth = context.measureText(ts).width; 532 | textUntilX = gx - tsWidth - 2; 533 | context.fillStyle = chartOptions.labels.fillStyle; 534 | context.fillText(ts, gx - tsWidth, dimensions.height - 2); 535 | } 536 | } 537 | } 538 | 539 | // Horizontal (value) dividers. 540 | for (var v = 1; v < chartOptions.grid.verticalSections; v++) { 541 | var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections); 542 | if (chartOptions.grid.sharpLines) { 543 | gy -= 0.5; 544 | } 545 | context.beginPath(); 546 | context.moveTo(0, gy); 547 | context.lineTo(dimensions.width, gy); 548 | context.stroke(); 549 | context.closePath(); 550 | } 551 | // Bounding rectangle. 552 | if (chartOptions.grid.borderVisible) { 553 | context.beginPath(); 554 | context.strokeRect(0, 0, dimensions.width, dimensions.height); 555 | context.closePath(); 556 | } 557 | context.restore(); 558 | 559 | // Draw any horizontal lines... 560 | if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) { 561 | for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) { 562 | var line = chartOptions.horizontalLines[hl], 563 | hly = Math.round(valueToYPixel(line.value)) - 0.5; 564 | context.strokeStyle = line.color || '#ffffff'; 565 | context.lineWidth = line.lineWidth || 1; 566 | context.beginPath(); 567 | context.moveTo(0, hly); 568 | context.lineTo(dimensions.width, hly); 569 | context.stroke(); 570 | context.closePath(); 571 | } 572 | } 573 | 574 | // For each data set... 575 | for (var d = 0; d < this.seriesSet.length; d++) { 576 | context.save(); 577 | var timeSeries = this.seriesSet[d].timeSeries, 578 | dataSet = timeSeries.data, 579 | seriesOptions = this.seriesSet[d].options; 580 | 581 | // Delete old data that's moved off the left of the chart. 582 | timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength); 583 | 584 | // Set style for this dataSet. 585 | context.lineWidth = seriesOptions.lineWidth; 586 | context.strokeStyle = seriesOptions.strokeStyle; 587 | // Draw the line... 588 | context.beginPath(); 589 | // Retain lastX, lastY for calculating the control points of bezier curves. 590 | var firstX = 0, lastX = 0, lastY = 0; 591 | for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) { 592 | var x = timeToXPixel(dataSet[i][0]), 593 | y = valueToYPixel(dataSet[i][1]); 594 | 595 | if (i === 0) { 596 | firstX = x; 597 | context.moveTo(x, y); 598 | } else { 599 | switch (chartOptions.interpolation) { 600 | case "linear": 601 | case "line": { 602 | context.lineTo(x,y); 603 | break; 604 | } 605 | case "bezier": 606 | default: { 607 | // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves 608 | // 609 | // Assuming A was the last point in the line plotted and B is the new point, 610 | // we draw a curve with control points P and Q as below. 611 | // 612 | // A---P 613 | // | 614 | // | 615 | // | 616 | // Q---B 617 | // 618 | // Importantly, A and P are at the same y coordinate, as are B and Q. This is 619 | // so adjacent curves appear to flow as one. 620 | // 621 | context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop 622 | Math.round((lastX + x) / 2), lastY, // controlPoint1 (P) 623 | Math.round((lastX + x)) / 2, y, // controlPoint2 (Q) 624 | x, y); // endPoint (B) 625 | break; 626 | } 627 | } 628 | } 629 | 630 | lastX = x; lastY = y; 631 | } 632 | 633 | if (dataSet.length > 1) { 634 | if (seriesOptions.fillStyle) { 635 | // Close up the fill region. 636 | context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY); 637 | context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1); 638 | context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); 639 | context.fillStyle = seriesOptions.fillStyle; 640 | context.fill(); 641 | } 642 | 643 | if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') { 644 | context.stroke(); 645 | } 646 | context.closePath(); 647 | } 648 | context.restore(); 649 | } 650 | 651 | // Draw the axis values on the chart. 652 | if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) { 653 | var maxValueString = parseFloat(this.valueRange.max).toFixed(chartOptions.labels.precision), 654 | minValueString = parseFloat(this.valueRange.min).toFixed(chartOptions.labels.precision); 655 | context.fillStyle = chartOptions.labels.fillStyle; 656 | context.fillText(maxValueString, dimensions.width - context.measureText(maxValueString).width - 2, chartOptions.labels.fontSize); 657 | context.fillText(minValueString, dimensions.width - context.measureText(minValueString).width - 2, dimensions.height - 2); 658 | } 659 | 660 | context.restore(); // See .save() above. 661 | }; 662 | 663 | // Sample timestamp formatting function 664 | SmoothieChart.timeFormatter = function(date) { 665 | function pad2(number) { return (number < 10 ? '0' : '') + number } 666 | return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds()); 667 | }; 668 | 669 | exports.TimeSeries = TimeSeries; 670 | exports.SmoothieChart = SmoothieChart; 671 | 672 | })(typeof exports === 'undefined' ? this : exports); 673 | 674 | -------------------------------------------------------------------------------- /linux-tools/uptime/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /linux-tools/uptime/README.md: -------------------------------------------------------------------------------- 1 | # swoole-uptime 2 | 3 | ## 依赖 4 | 5 | * PHP 5.3+ 6 | * Swoole 1.7.16 7 | * Linux, OS X and basic Windows support (Thanks to cygwin) 8 | 9 | ## 安装 Swoole扩展 10 | 11 | 1. Install from pecl 12 | 13 | ``` 14 | pecl install swoole 15 | ``` 16 | 17 | 2. Install from source 18 | 19 | ``` 20 | sudo apt-get install php5-dev 21 | git clone https://github.com/swoole/swoole-src.git 22 | cd swoole-src 23 | phpize 24 | ./configure 25 | make && make install 26 | ``` 27 | 28 | ## 运行 29 | 30 | 1. cd uptime/server 31 | 2. php server.php 32 | 3. 修改web目录下stats.js代码 var ws = new ReconnectingWebSocket("ws://192.168.1.10:8880"); 改成服务器的IP 33 | 4. 用浏览器打开web目录下的index.html 34 | 35 | ## 运行结果 36 | 37 | 1. 打开页面如下所示 38 | ![one](https://raw.githubusercontent.com/smalleyes/swoole-linux-dash/master/linux-tools/uptime/doc/uptime.png) 39 | 40 | ## nginx配置文件 41 | 1. 在doc/swoole-uptime.conf 42 | 43 | -------------------------------------------------------------------------------- /linux-tools/uptime/doc/swoole-uptime.conf: -------------------------------------------------------------------------------- 1 | #设定虚拟主机配置 2 | server { 3 | #侦听80端口 4 | listen 80; 5 | server_name uptime.iizhu.com; 6 | 7 | #定义服务器的默认网站根目录位置 8 | root /var/www/code/swoole-uptime/web/; 9 | 10 | location / { 11 | #定义首页索引文件的名称 12 | index index.php index.html index.htm; 13 | 14 | } 15 | 16 | # 定义错误提示页面 17 | error_page 500 502 503 504 /50x.html; 18 | location = /50x.html { 19 | } 20 | 21 | #PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI默认配置. 22 | location ~ .php$ { 23 | fastcgi_pass 127.0.0.1:9000; 24 | fastcgi_index index.php; 25 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 26 | include fastcgi_params; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /linux-tools/uptime/doc/uptime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/linux-tools/uptime/doc/uptime.png -------------------------------------------------------------------------------- /linux-tools/uptime/server/server.php: -------------------------------------------------------------------------------- 1 | column('cmd', swoole_table::TYPE_STRING, 32); 31 | $table->create(); 32 | $table->set('uptime', array('cmd' => $uptime_path)); 33 | 34 | $server = new swoole_websocket_server($host, $port); 35 | $server->table = $table; //将table保存在serv对象上 36 | 37 | /** 38 | * websocket server配置 39 | */ 40 | $server->set (array( 41 | 'worker_num' => 1, //worker进程数量 42 | 'daemonize' => false, //守护进程设置成true 43 | 'max_request' => 1000, //最大请求次数,当请求大于它时,将会自动重启该worker 44 | 'dispatch_mode' => 1 45 | )); 46 | 47 | /** 48 | * websocket server start 49 | * 成功后回调 50 | */ 51 | $server->on('start', function ($serv) use($_maxMasterPidLength, $_maxManagerPidLength, $_maxWorkerIdLength, $_maxWorkerPidLength) { 52 | echo "\033[1A\n\033[K-----------------------\033[47;30m SWOOLE \033[0m-----------------------------\n\033[0m"; 53 | echo 'swoole version:' . swoole_version() . " PHP version:".PHP_VERSION."\n"; 54 | echo "------------------------\033[47;30m WORKERS \033[0m---------------------------\n"; 55 | echo "\033[47;30mMasterPid\033[0m", str_pad('', $_maxMasterPidLength + 2 - strlen('MasterPid')), 56 | "\033[47;30mManagerPid\033[0m", str_pad('', $_maxManagerPidLength + 2 - strlen('ManagerPid')), 57 | "\033[47;30mWorkerId\033[0m", str_pad('', $_maxWorkerIdLength + 2 - strlen('WorkerId')), 58 | "\033[47;30mWorkerPid\033[0m", str_pad('', $_maxWorkerPidLength + 2 - strlen('WorkerPid')),"\n"; 59 | }); 60 | 61 | /** 62 | * 当worker 启动的时候调用 63 | */ 64 | $server->on('workerStart',function ($serv, $worker_id) use($_maxMasterPidLength, $_maxManagerPidLength, $_maxWorkerIdLength, $_maxWorkerPidLength) { 65 | echo str_pad($serv->master_pid, $_maxMasterPidLength+2), 66 | str_pad($serv->manager_pid, $_maxManagerPidLength+2), 67 | str_pad($serv->worker_id, $_maxWorkerIdLength+2), 68 | str_pad($serv->worker_pid, $_maxWorkerIdLength), "\n"; 69 | 70 | if ($worker_id==0) { 71 | $data = $serv->table->get('uptime'); 72 | $serv->tick(1000, function($id) use($serv, $data) { 73 | $conn_list = $serv->connection_list(); 74 | if (!empty($conn_list)) { 75 | $str = exec($data['cmd'],$string); 76 | foreach($conn_list as $fd) { 77 | $conn = $serv->connection_info($fd); 78 | if (!empty($conn)) { 79 | $nowtime = substr($str, 0, strpos($str,'up')); 80 | $str = str_replace($nowtime, '', $str); 81 | $str = str_replace('up', '', $str); 82 | $str = str_replace('days', '', $str); 83 | $str = str_replace('users', '', $str); 84 | $str = str_replace('load average:', '', $str); 85 | $str = $nowtime.','.$str; 86 | $serv->push($fd, $str); 87 | } 88 | } 89 | } 90 | }); 91 | } 92 | }); 93 | 94 | /** 95 | * 当WebSocket客户端与服务器建立连接并完成握手后会回调此函数。 96 | */ 97 | $server->on('open', function (swoole_websocket_server $server, $request) { 98 | $fd = $request->fd; 99 | echo "server: handshake success with fd{$fd}\n"; 100 | $server->push($fd, "nowtime -----------Linetime---------- ---users-- -----load average----\n"); 101 | $server->push($fd, "nowtime,days,time,users,one,five,fifteen\n"); 102 | }); 103 | 104 | /** 105 | * 当服务器收到来自客户端的数据帧时会回调此函数。 106 | */ 107 | $server->on('message', function (swoole_websocket_server $server, $frame) { 108 | echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n"; 109 | $server->push($frame->fd, "this is server"); 110 | }); 111 | 112 | /** 113 | * 当客户端关闭的时候调用 114 | */ 115 | $server->on('close', function ($ser, $fd) { 116 | echo "client {$fd} closed\n"; 117 | }); 118 | 119 | 120 | $server->start(); -------------------------------------------------------------------------------- /linux-tools/uptime/web/img/forkme_right_orange_ff7600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/linux-tools/uptime/web/img/forkme_right_orange_ff7600.png -------------------------------------------------------------------------------- /linux-tools/uptime/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | swoole tools uptime 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Fork me on GitHub 15 | 16 | 17 |
18 |
19 |

SERVER TIME:00:00:00

20 |
21 |
22 |

Up 0DAYS, 0:00

23 |
24 |
25 |

26 | 27 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /linux-tools/uptime/web/js/reconnecting-websocket.js: -------------------------------------------------------------------------------- 1 | // MIT License: 2 | // 3 | // Copyright (c) 2010-2012, Joe Walnes 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | /** 24 | * This behaves like a WebSocket in every way, except if it fails to connect, 25 | * or it gets disconnected, it will repeatedly poll until it succesfully connects 26 | * again. 27 | * 28 | * It is API compatible, so when you have: 29 | * ws = new WebSocket('ws://....'); 30 | * you can replace with: 31 | * ws = new ReconnectingWebSocket('ws://....'); 32 | * 33 | * The event stream will typically look like: 34 | * onconnecting 35 | * onopen 36 | * onmessage 37 | * onmessage 38 | * onclose // lost connection 39 | * onconnecting 40 | * onopen // sometime later... 41 | * onmessage 42 | * onmessage 43 | * etc... 44 | * 45 | * It is API compatible with the standard WebSocket API. 46 | * 47 | * Latest version: https://github.com/joewalnes/reconnecting-websocket/ 48 | * - Joe Walnes 49 | */ 50 | function ReconnectingWebSocket(url, protocols) { 51 | protocols = protocols || []; 52 | 53 | // These can be altered by calling code. 54 | this.debug = false; 55 | this.reconnectInterval = 1000; 56 | this.timeoutInterval = 2000; 57 | 58 | var self = this; 59 | var ws; 60 | var forcedClose = false; 61 | var timedOut = false; 62 | 63 | this.url = url; 64 | this.protocols = protocols; 65 | this.readyState = WebSocket.CONNECTING; 66 | this.URL = url; // Public API 67 | 68 | this.onopen = function(event) { 69 | }; 70 | 71 | this.onclose = function(event) { 72 | }; 73 | 74 | this.onconnecting = function(event) { 75 | }; 76 | 77 | this.onmessage = function(event) { 78 | }; 79 | 80 | this.onerror = function(event) { 81 | }; 82 | 83 | function connect(reconnectAttempt) { 84 | ws = new WebSocket(url, protocols); 85 | 86 | self.onconnecting(); 87 | if (self.debug || ReconnectingWebSocket.debugAll) { 88 | console.debug('ReconnectingWebSocket', 'attempt-connect', url); 89 | } 90 | 91 | var localWs = ws; 92 | var timeout = setTimeout(function() { 93 | if (self.debug || ReconnectingWebSocket.debugAll) { 94 | console.debug('ReconnectingWebSocket', 'connection-timeout', url); 95 | } 96 | timedOut = true; 97 | localWs.close(); 98 | timedOut = false; 99 | }, self.timeoutInterval); 100 | 101 | ws.onopen = function(event) { 102 | clearTimeout(timeout); 103 | if (self.debug || ReconnectingWebSocket.debugAll) { 104 | console.debug('ReconnectingWebSocket', 'onopen', url); 105 | } 106 | self.readyState = WebSocket.OPEN; 107 | reconnectAttempt = false; 108 | self.onopen(event); 109 | }; 110 | 111 | ws.onclose = function(event) { 112 | clearTimeout(timeout); 113 | ws = null; 114 | if (forcedClose) { 115 | self.readyState = WebSocket.CLOSED; 116 | self.onclose(event); 117 | } else { 118 | self.readyState = WebSocket.CONNECTING; 119 | self.onconnecting(); 120 | if (!reconnectAttempt && !timedOut) { 121 | if (self.debug || ReconnectingWebSocket.debugAll) { 122 | console.debug('ReconnectingWebSocket', 'onclose', url); 123 | } 124 | self.onclose(event); 125 | } 126 | setTimeout(function() { 127 | connect(true); 128 | }, self.reconnectInterval); 129 | } 130 | }; 131 | ws.onmessage = function(event) { 132 | if (self.debug || ReconnectingWebSocket.debugAll) { 133 | console.debug('ReconnectingWebSocket', 'onmessage', url, event.data); 134 | } 135 | self.onmessage(event); 136 | }; 137 | ws.onerror = function(event) { 138 | if (self.debug || ReconnectingWebSocket.debugAll) { 139 | console.debug('ReconnectingWebSocket', 'onerror', url, event); 140 | } 141 | self.onerror(event); 142 | }; 143 | } 144 | connect(url); 145 | 146 | this.send = function(data) { 147 | if (ws) { 148 | if (self.debug || ReconnectingWebSocket.debugAll) { 149 | console.debug('ReconnectingWebSocket', 'send', url, data); 150 | } 151 | return ws.send(data); 152 | } else { 153 | throw 'INVALID_STATE_ERR : Pausing to reconnect websocket'; 154 | } 155 | }; 156 | 157 | this.close = function() { 158 | if (ws) { 159 | forcedClose = true; 160 | ws.close(); 161 | } 162 | }; 163 | 164 | /** 165 | * Additional public API method to refresh the connection if still open (close, re-open). 166 | * For example, if the app suspects bad data / missed heart beats, it can try to refresh. 167 | */ 168 | this.refresh = function() { 169 | if (ws) { 170 | ws.close(); 171 | } 172 | }; 173 | } 174 | 175 | /** 176 | * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true. 177 | */ 178 | ReconnectingWebSocket.debugAll = false; 179 | 180 | -------------------------------------------------------------------------------- /linux-tools/uptime/web/uptime.css: -------------------------------------------------------------------------------- 1 | .template { 2 | display: none !important; 3 | } 4 | * { 5 | cursor: default; 6 | } 7 | body { 8 | background-color: #111; 9 | color: #eee; 10 | font-family: "helvetica neue", helvetica, arial, sans-serif; 11 | } 12 | h2 { 13 | margin: 40px 0 0 0; 14 | font-weight: 300; 15 | } 16 | main { 17 | width: 600px; 18 | margin: auto; 19 | } 20 | section { 21 | clear: left; 22 | } 23 | .stats { 24 | margin: 0; 25 | } 26 | .stat { 27 | list-style-type: none; 28 | float: left; 29 | margin: 0; 30 | width: 130px; 31 | font-size: 12px; 32 | } 33 | .stat-name { 34 | display: inline-block; 35 | text-align: right; 36 | width: 50px; 37 | margin-right: 5px; 38 | } 39 | .stat-value { 40 | font-weight: bold; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /linux-tools/uptime/web/uptime.js: -------------------------------------------------------------------------------- 1 | var allTimeSeries = {}; 2 | var allValueLabels = {}; 3 | var descriptions = { 4 | 'now time': { 5 | 'nowtime': 'server now time', 6 | }, 7 | 'Line time': { 8 | 'days': 'How long the server starts days', 9 | 'time': 'How long', 10 | }, 11 | 'users': { 12 | 'users': 'Now the number of users logged into the server', 13 | }, 14 | 'load average': { 15 | 'one': '1 minutes server load average', 16 | 'five': '5 minutes server load average', 17 | 'fifteen': '15 minutes server load average', 18 | } 19 | 20 | } 21 | 22 | function streamStats() { 23 | 24 | var ws = new ReconnectingWebSocket("ws://192.168.1.10:8880"); 25 | var lineCount; 26 | var colHeadings; 27 | 28 | ws.onopen = function() { 29 | console.log('connect'); 30 | lineCount = 0; 31 | }; 32 | 33 | ws.onclose = function() { 34 | console.log('disconnect'); 35 | }; 36 | 37 | ws.onmessage = function(e) { 38 | 39 | switch (lineCount++) { 40 | case 0: // ignore first line 41 | break; 42 | 43 | case 1: // column headings 44 | colHeadings = e.data.trim().split(/,/); 45 | break; 46 | 47 | default: // subsequent lines 48 | var colValues = e.data.trim().split(/,/); 49 | var stats = {}; 50 | var len = colHeadings.length; 51 | for (var i = 0; i < len; i++) { 52 | stats[colHeadings[i]] = colValues[i]; 53 | } 54 | receiveStats(stats); 55 | } 56 | }; 57 | } 58 | 59 | function initCharts() { 60 | Object.each(descriptions, function(sectionName, values) { 61 | if(sectionName=='load average') { 62 | var section = $('.chart.template').clone().removeClass('template').appendTo('#charts'); 63 | section.find('.title').text(sectionName); 64 | var smoothie = new SmoothieChart({ 65 | grid: { 66 | sharpLines: true, 67 | verticalSections: 5, 68 | strokeStyle: 'rgba(119,119,119,0.45)', 69 | millisPerLine: 1000 70 | }, 71 | minValue: 0, 72 | labels: { 73 | disabled: true 74 | } 75 | }); 76 | 77 | smoothie.streamTo(section.find('canvas').get(0), 1000); 78 | 79 | var colors = chroma.brewer['Pastel2']; 80 | var index = 0; 81 | Object.each(values, function(name, valueDescription) { 82 | var color = colors[index++]; 83 | 84 | var timeSeries = new TimeSeries(); 85 | smoothie.addTimeSeries(timeSeries, { 86 | strokeStyle: color, 87 | fillStyle: chroma(color).darken().alpha(0.5).css(), 88 | lineWidth: 3 89 | }); 90 | allTimeSeries[name] = timeSeries; 91 | 92 | var statLine = section.find('.stat.template').clone().removeClass('template').appendTo(section.find('.stats')); 93 | statLine.attr('title', valueDescription).css('color', color); 94 | statLine.find('.stat-name').text(name); 95 | allValueLabels[name] = statLine.find('.stat-value'); 96 | }); 97 | } 98 | 99 | }); 100 | } 101 | 102 | function receiveStats(stats) { 103 | Object.each(stats, function(name, value) { 104 | if(name=='nowtime') { 105 | $(".nowtime").find(".time").html(value); 106 | }else if(name=='days') { 107 | $(".online-time").find(".days").html(value); 108 | }else if(name=='time') { 109 | $(".online-time").find(".time").html(value); 110 | }else{ 111 | var timeSeries = allTimeSeries[name]; 112 | if (timeSeries) { 113 | timeSeries.append(Date.now(), value); 114 | allValueLabels[name].text(value); 115 | } 116 | } 117 | }); 118 | } 119 | 120 | $(function() { 121 | initCharts(); 122 | streamStats(); 123 | }); 124 | -------------------------------------------------------------------------------- /server/index.php: -------------------------------------------------------------------------------- 1 | 1 \ 6 | {print "{ \"address\": \"" $1 "\", " \ 7 | "\"hw_type\": \"" $2 "\", " \ 8 | "\"hw_address\": \"" $3 "\", " \ 9 | "\"flags\": \"" $4 "\", " \ 10 | "\"mask\": \"" $5 "\" }, " \ 11 | } \ 12 | END {print "]"}' \ 13 | | /bin/sed 'N;$s/},/}/;P;D') 14 | 15 | if [ -z "$result" ]; then echo {} 16 | else echo $result 17 | fi -------------------------------------------------------------------------------- /server/modules/shell_files/bandwidth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | /bin/cat /proc/net/dev \ 3 | | awk 'BEGIN {print "["} NR>2 {print "{ \"interface\": \"" $1 "\"," \ 4 | " \"tx\": " $2 "," \ 5 | " \"rx\": " $10 " }," } END {print "]"}' \ 6 | | /bin/sed 'N;$s/,\n/\n/;P;D' 7 | -------------------------------------------------------------------------------- /server/modules/shell_files/common_applications.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(whereis php node mysql mongo vim python ruby java apache2 nginx openssl vsftpd make \ 3 | | awk -F: '{if(length($2)==0) { installed="false"; } else { installed="true"; } \ 4 | print \ 5 | "{ \ 6 | \"binary\": \""$1"\", \ 7 | \"location\": \""$2"\", \ 8 | \"installed\": "installed" \ 9 | },"}') 10 | 11 | echo "[" ${result%?} "]" -------------------------------------------------------------------------------- /server/modules/shell_files/cpu_info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | result=$(/usr/bin/lscpu \ 4 | | /usr/bin/awk -F: '{print "\""$1"\": \""$2"\"," } '\ 5 | ) 6 | 7 | echo "{" ${result%?} "}" -------------------------------------------------------------------------------- /server/modules/shell_files/cpu_intensive_processes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | result=$(/bin/ps axo pid,user,pcpu,rss,vsz,comm --sort -pcpu,-rss,-vsz \ 4 | | head -n 15 \ 5 | | /usr/bin/awk 'BEGIN{OFS=":"} NR>1 {print "{ \"pid\": " $1 \ 6 | ", \"user\": \"" $2 "\"" \ 7 | ", \"cpu_percent\": " $3 \ 8 | ", \"rss\": " $4 \ 9 | ", \"vsz\": " $5 \ 10 | ", \"command\": \"" $6 "\"" "},"\ 11 | }') 12 | 13 | echo "[" ${result%?} "]" -------------------------------------------------------------------------------- /server/modules/shell_files/cron_history.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grepCmd=$(which grep) 4 | cronLog='/var/log/syslog' 5 | numberOfLines='50' 6 | 7 | # Month, Day, Time, Hostname, tag, user, 8 | 9 | result=$($grepCmd -m$numberOfLines CRON $cronLog \ 10 | | awk '{ s = ""; for (i = 6; i <= NF; i++) s = s $i " "; \ 11 | print "{\"time\" : \"" $1" "$2" "$3 "\"," \ 12 | "\"user\" : \"" $6 "\"," \ 13 | "\"message\" : \"" $5" "s "\"" \ 14 | "}," 15 | }' 16 | ) 17 | 18 | echo [${result%?}] -------------------------------------------------------------------------------- /server/modules/shell_files/current_ram.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | freeCmd=`which free` 4 | awkCmd=`which awk` 5 | 6 | procps=$($freeCmd -V | /bin/grep procps-ng) 7 | 8 | if [ -z "$procps" ]; then 9 | $freeCmd -tmo | $awkCmd 'NR==2 {print "{ \"total\": " $2 ", \"used\": " $3-$6-$7 ", \"free\": " $4+$6+$7 " }"}' 10 | else 11 | $freeCmd -tm | $awkCmd 'NR==2 {print "{ \"total\": " $2 ", \"used\": " $3-$6-$7 ", \"free\": " $4+$6+$7 " }"}' 12 | fi 13 | -------------------------------------------------------------------------------- /server/modules/shell_files/disk_partitions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(/bin/df -Ph | awk 'NR>1 {print "{\"file_system\": \"" $1 "\", \"size\": \"" $2 "\", \"used\": \"" $3 "\", \"avail\": \"" $4 "\", \"used%\": \"" $5 "\", \"mounted\": \"" $6 "\"},"}') 3 | 4 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /server/modules/shell_files/download_transfer_rate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | files=(/sys/class/net/*) 4 | pos=$(( ${#files[*]} - 1 )) 5 | last=${files[$pos]} 6 | 7 | json_output="{" 8 | 9 | for interface in "${files[@]}" 10 | do 11 | basename=$(basename "$interface") 12 | 13 | # find the number of bytes transfered for this interface 14 | in1=$(cat /sys/class/net/"$basename"/statistics/rx_bytes) 15 | 16 | # wait a second 17 | sleep 1 18 | 19 | # check same interface again 20 | in2=$(cat /sys/class/net/"$basename"/statistics/rx_bytes) 21 | 22 | # get the difference (transfer rate) 23 | in_bytes=$((in2 - in1)) 24 | 25 | # convert transfer rate to KB 26 | in_kbytes=$((in_bytes / 1024)) 27 | 28 | # convert transfer rate to KB 29 | json_output="$json_output \"$basename\": $in_kbytes" 30 | 31 | # if it is not the last line 32 | if [[ ! $interface == $last ]] 33 | then 34 | # add a comma to the line (JSON formatting) 35 | json_output="$json_output," 36 | fi 37 | done 38 | 39 | # close the JSON object & print to screen 40 | echo "$json_output}" 41 | -------------------------------------------------------------------------------- /server/modules/shell_files/general_info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function displaytime { 4 | local T=$1 5 | local D=$((T/60/60/24)) 6 | local H=$((T/60/60%24)) 7 | local M=$((T/60%60)) 8 | local S=$((T%60)) 9 | [[ $D > 0 ]] && printf '%d days ' $D 10 | [[ $H > 0 ]] && printf '%d hours ' $H 11 | [[ $M > 0 ]] && printf '%d minutes ' $M 12 | [[ $D > 0 || $H > 0 || $M > 0 ]] && printf 'and ' 13 | printf '%d seconds\n' $S 14 | } 15 | 16 | os=$(/usr/bin/lsb_release -ds;/bin/uname -r) 17 | hostname=$(/bin/hostname) 18 | uptime_seconds=$(/bin/cat /proc/uptime | awk '{print $1}') 19 | server_time=$(date) 20 | 21 | echo { \ 22 | \"OS\": \"$os\", \ 23 | \"Hostname\": \"$hostname\", \ 24 | \"Uptime\": \" $(displaytime ${uptime_seconds%.*}) \", \ 25 | \"Server Time\": \"$server_time\" \ 26 | } -------------------------------------------------------------------------------- /server/modules/shell_files/internet_speed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPTPATH=`dirname $(readlink -f $0)` 4 | SPEED_TEST_SCRIPT=$SCRIPTPATH"/../python_files/speedtest_cli.py" 5 | 6 | $SPEED_TEST_SCRIPT \ 7 | | grep 'Upload\|Download' \ 8 | | awk 'BEGIN {print "{"} {print "\"" $1 "\": \"" $2 " " $3 "\"," } END {print "}"}' \ 9 | | /bin/sed 'N;$s/",/"/;P;D' -------------------------------------------------------------------------------- /server/modules/shell_files/io_stats.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(/bin/cat /proc/diskstats | /usr/bin/awk \ 3 | '{ if($4==0 && $8==0 && $12==0 && $13==0) next } \ 4 | {print "{ \"device\": \"" $3 "\", \"reads\": \""$4"\", \"writes\": \"" $8 "\", \"in_progress\": \"" $12 "\", \"time_in_io\": \"" $13 "\"},"}' 5 | ) 6 | 7 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /server/modules/shell_files/ip_addresses.sh: -------------------------------------------------------------------------------- 1 | awkCmd=`which awk` 2 | grepCmd=`which grep` 3 | sedCmd=`which sed` 4 | ifconfigCmd=`which ifconfig` 5 | trCmd=`which tr` 6 | digCmd=`which dig` 7 | 8 | externalIp=`$digCmd +short myip.opendns.com @resolver1.opendns.com` 9 | 10 | $ifconfigCmd \ 11 | | $grepCmd -B1 "inet addr" \ 12 | | $awkCmd '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' \ 13 | | $awkCmd -v exIp="$externalIp" -F: 'BEGIN {print "["} { print "{ \"interface\": \"" $1 "\", \"ip\": \"" $3 "\" },"} END {print "{ \"interface\": \"external\", \"ip\": \""exIp"\" } ]"}' \ 14 | | $trCmd -d '\r\n' 15 | -------------------------------------------------------------------------------- /server/modules/shell_files/load_avg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | numberOfCores=$(/bin/grep -c 'model name' /proc/cpuinfo) 3 | 4 | if [ $numberOfCores -eq 0 ]; then 5 | numberOfCores=1 6 | fi 7 | 8 | result=$(/bin/cat /proc/loadavg | /usr/bin/awk '{print "{ \"1_min_avg\": " ($1*100)/'$numberOfCores' ", \"5_min_avg\": " ($2*100)/'$numberOfCores' ", \"15_min_avg\": " ($3*100)/'$numberOfCores' "}," }') 9 | 10 | echo ${result%?} -------------------------------------------------------------------------------- /server/modules/shell_files/logged_in_users.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(COLUMNS=300 /usr/bin/w -h | /usr/bin/awk '{print "{\"user\": \"" $1 "\", \"from\": \"" $3 "\", \"when\": \"" $4 "\"},"}') 3 | 4 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /server/modules/shell_files/memcached.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "stats" \ 3 | | /bin/nc -w 1 127.0.0.1 11211 \ 4 | | /bin/grep 'bytes' \ 5 | | /usr/bin/awk 'BEGIN {print "{"} {print "\"" $2 "\": " $3 } END {print "}"}' \ 6 | | /usr/bin/tr '\r' ',' \ 7 | | /bin/sed 'N;$s/,\n/\n/;P;D' -------------------------------------------------------------------------------- /server/modules/shell_files/memory_info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | /bin/cat /proc/meminfo \ 3 | | /usr/bin/awk -F: 'BEGIN {print "{"} {print "\"" $1 "\": \"" $2 "\"," } END {print "}"}' \ 4 | | /bin/sed 'N;$s/,\n/\n/;P;D' 5 | -------------------------------------------------------------------------------- /server/modules/shell_files/network_connections.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | netstatCmd=`which netstat` 3 | awkCmd=`which awk` 4 | sortCmd=`which sort` 5 | uniqCmd=`which uniq` 6 | awkCmd=`which awk` 7 | sedCmd=`which sed` 8 | 9 | $netstatCmd -ntu \ 10 | | $awkCmd 'NR>2 {print $5}' \ 11 | | $sortCmd \ 12 | | $uniqCmd -c \ 13 | | $awkCmd 'BEGIN {print "["} {print "{ \"connections\": " $1 ", \"address\": \"" $2 "\" }," } END {print "]"}' \ 14 | | $sedCmd 'N;$s/},/}/;P;D' 15 | -------------------------------------------------------------------------------- /server/modules/shell_files/number_of_cpu_cores.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | numberOfCores=$(/bin/grep -c 'model name' /proc/cpuinfo) 4 | 5 | if [length($numberOfCores)]; then 6 | echo "cannnot be found"; 7 | fi -------------------------------------------------------------------------------- /server/modules/shell_files/ping.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # http://askubuntu.com/questions/413367/ping-multiple-ips-using-bash 3 | 4 | # get absolute path to config file 5 | SCRIPTPATH=`dirname $(readlink -f $0)` 6 | CONFIG_PATH=$SCRIPTPATH"/../config/ping_hosts" 7 | 8 | catCmd=`which cat` 9 | pingCmd=`which ping` 10 | awkCmd=`which awk` 11 | sedCmd=`which sed` 12 | numOfLinesInConfig=`$sedCmd -n '$=' $CONFIG_PATH` 13 | result='[' 14 | 15 | { $catCmd $CONFIG_PATH; echo; } \ 16 | | while read output 17 | do 18 | singlePing=$($pingCmd -qc 2 $output \ 19 | | $awkCmd -F/ 'BEGIN { endLine="}," } /^rtt/ { if ('$numOfLinesInConfig'==1){endLine="}"} print "{" "\"host\": \"'$output'\", \"ping\": " $5 " " endLine }' \ 20 | ) 21 | numOfLinesInConfig=$(($numOfLinesInConfig-1)) 22 | result=$result$singlePing 23 | if [ $numOfLinesInConfig -eq 1 ] 24 | then 25 | echo $result"]" 26 | fi 27 | done \ 28 | | $sedCmd 's/\},]/}]/g' 29 | -------------------------------------------------------------------------------- /server/modules/shell_files/ram_intensive_processes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(/bin/ps axo pid,user,pmem,rss,vsz,comm --sort -pmem,-rss,-vsz \ 3 | | head -n 15 \ 4 | | /usr/bin/awk 'NR>1 {print "{ \"pid\": " $1 \ 5 | ", \"user\": \"" $2 \ 6 | "\", \"mem_per\": " $3 \ 7 | ", \"rss\": " $4 \ 8 | ", \"vsz\": " $5 \ 9 | ", \"command\": \"" $6 \ 10 | "\"},"}') 11 | 12 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /server/modules/shell_files/recent_account_logins.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(/usr/bin/lastlog -t 365 \ 3 | | /usr/bin/awk 'NR>1 {\ 4 | print "{ \ 5 | \"user\": \"" $1 "\", \ 6 | \"ip\": \"" $3 "\","" \ 7 | \"date\": \"" $5" "$6" "$7" "$8" "$9 "\"}," 8 | }' 9 | ) 10 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /server/modules/shell_files/redis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ########### Enter Your Redis Password HERE ######### 4 | redisPassword='' 5 | ########### Enter Your Redis Password HERE ######### 6 | 7 | redisCommand=$(which redis-cli); 8 | 9 | if [ -n "$redisPassword" ]; then 10 | redisCommand="$redisCommand -a $redisPassword" 11 | fi 12 | 13 | result=$($redisCommand INFO \ 14 | | grep 'redis_version\|connected_clients\|connected_slaves\|used_memory_human\|total_connections_received\|total_commands_processed' \ 15 | | awk -F: '{print "\"" $1 "\":" "\"" $2 }' \ 16 | | tr '\r' '"' | tr '\n' ',' 17 | ) 18 | echo { ${result%?} } -------------------------------------------------------------------------------- /server/modules/shell_files/scheduled_crons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ###### 3 | # Credit: http://stackoverflow.com/questions/134906/how-do-i-list-all-cron-jobs-for-all-users#answer-137173 4 | ###### 5 | 6 | catCmd=`which cat` 7 | awkCmd=`which awk` 8 | sedCmd=`which sed` 9 | egrepCmd=`which egrep` 10 | echoCmd=`which echo` 11 | crontabCmd=`which crontab` 12 | 13 | # System-wide crontab file and cron job directory. Change these for your system. 14 | CRONTAB='/etc/crontab' 15 | CRONDIR='/etc/cron.d' 16 | 17 | # Single tab character. Annoyingly necessary. 18 | tab=$(echo -en "\t") 19 | 20 | # Given a stream of crontab lines, exclude non-cron job lines, replace 21 | # whitespace characters with a single space, and remove any spaces from the 22 | # beginning of each line. 23 | function clean_cron_lines() { 24 | while read line ; do 25 | $echoCmd "${line}" | 26 | $egrepCmd --invert-match '^($|\s*#|\s*[[:alnum:]_]+=)' | 27 | $sedCmd --regexp-extended "s/\s+/ /g" | 28 | $sedCmd --regexp-extended "s/^ //" 29 | done; 30 | } 31 | 32 | # Given a stream of cleaned crontab lines, $echoCmd any that don't include the 33 | # run-parts command, and for those that do, show each job file in the run-parts 34 | # directory as if it were scheduled explicitly. 35 | function lookup_run_parts() { 36 | while read line ; do 37 | match=$($echoCmd "${line}" | $egrepCmd -o 'run-parts (-{1,2}\S+ )*\S+') 38 | 39 | if [[ -z "${match}" ]] ; then 40 | $echoCmd "${line}" 41 | else 42 | cron_fields=$($echoCmd "${line}" | cut -f1-6 -d' ') 43 | cron_job_dir=$($echoCmd "${match}" | awk '{print $NF}') 44 | 45 | if [[ -d "${cron_job_dir}" ]] ; then 46 | for cron_job_file in "${cron_job_dir}"/* ; do # */ 47 | [[ -f "${cron_job_file}" ]] && $echoCmd "${cron_fields} ${cron_job_file}" 48 | done 49 | fi 50 | fi 51 | done; 52 | } 53 | 54 | # Temporary file for crontab lines. 55 | temp=$(mktemp) || exit 1 56 | 57 | # Add all of the jobs from the system-wide crontab file. 58 | $catCmd "${CRONTAB}" | clean_cron_lines | lookup_run_parts >"${temp}" 59 | 60 | # Add all of the jobs from the system-wide cron directory. 61 | $catCmd "${CRONDIR}"/* | clean_cron_lines >>"${temp}" # */ 62 | 63 | # Add each user's crontab (if it exists). Insert the user's name between the 64 | # five time fields and the command. 65 | while read user ; do 66 | $crontabCmd -l -u "${user}" 2>/dev/null | 67 | clean_cron_lines | 68 | $sedCmd --regexp-extended "s/^((\S+ +){5})(.+)$/\1${user} \3/" >>"${temp}" 69 | done < <(cut --fields=1 --delimiter=: /etc/passwd) 70 | 71 | # Output the collected crontab lines. 72 | 73 | ## Changes: Parses output into JSON 74 | 75 | $catCmd "${temp}" \ 76 | | awk 'BEGIN {print "["} \ 77 | {print "{ \"min(s)\": \"" $1 \ 78 | "\", \"hours(s)\": \"" $2 "\", " \ 79 | " \"day(s)\": \"" $3 "\", " \ 80 | " \"month\": \"" $4 "\", " \ 81 | " \"weekday\": \"" $5 "\", " \ 82 | " \"user\": \"" $6 "\", " \ 83 | " \"command\": \"" $7$8$9$10 "\" " \ 84 | "}," } \ 85 | END {print "]"}' \ 86 | | $sedCmd 'N;$s/,\n/\n/;P;D' 87 | 88 | rm --force "${temp}" -------------------------------------------------------------------------------- /server/modules/shell_files/swap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(/bin/cat /proc/swaps \ 3 | | /usr/bin/awk 'NR>1 {print "{ \"filename\": \"" $1"\", \"type\": \""$2"\", \"size\": \""$3"\", \"used\": \""$4"\", \"priority\": \""$5"\"}," }' 4 | ) 5 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /server/modules/shell_files/upload_transfer_rate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | files=(/sys/class/net/*) 4 | pos=$(( ${#files[*]} - 1 )) 5 | last=${files[$pos]} 6 | 7 | json_output="{" 8 | 9 | for interface in "${files[@]}" 10 | do 11 | basename=$(basename "$interface") 12 | 13 | # find the number of bytes transfered for this interface 14 | out1=$(cat /sys/class/net/"$basename"/statistics/tx_bytes) 15 | 16 | # wait a second 17 | sleep 1 18 | 19 | # check same interface again 20 | out2=$(cat /sys/class/net/"$basename"/statistics/tx_bytes) 21 | 22 | # get the difference (transfer rate) 23 | out_bytes=$((out2 - out1)) 24 | 25 | # convert transfer rate to KB 26 | out_kbytes=$((out_bytes / 1024)) 27 | 28 | # convert transfer rate to KB 29 | json_output="$json_output \"$basename\": $out_kbytes" 30 | 31 | # if it is not the last line 32 | if [[ ! $interface == $last ]] 33 | then 34 | # add a comma to the line (JSON formatting) 35 | json_output="$json_output," 36 | fi 37 | done 38 | 39 | # close the JSON object & print to screen 40 | echo "$json_output}" 41 | -------------------------------------------------------------------------------- /server/modules/shell_files/user_accounts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | result=$(/usr/bin/awk -F: '{ \ 3 | if ($3<=499){userType="system";} \ 4 | else {userType="user";} \ 5 | print "{ \"type\": \"" userType "\"" ", \"user\": \"" $1 "\", \"home\": \"" $6 "\" }," }' < /etc/passwd 6 | ) 7 | 8 | length=$(echo ${#result}) 9 | 10 | if [ $length -eq 0 ]; then 11 | result=$(getent passwd | /usr/bin/awk -F: '{ if ($3<=499){userType="system";} else {userType="user";} print "{ \"type\": \"" userType "\"" ", \"user\": \"" $1 "\", \"home\": \"" $6 "\" }," }') 12 | fi 13 | 14 | echo [ ${result%?} ] -------------------------------------------------------------------------------- /server/swooleindex.php: -------------------------------------------------------------------------------- 1 | div { 261 | background-color: #009587; 262 | height: 100%; 263 | width: 6px; 264 | display: inline-block; 265 | 266 | -webkit-animation: stretchdelay 1.2s infinite ease-in-out; 267 | animation: stretchdelay 1.2s infinite ease-in-out; 268 | } 269 | 270 | .spinner .rect2 { 271 | -webkit-animation-delay: -1.1s; 272 | animation-delay: -1.1s; 273 | } 274 | 275 | .spinner .rect3 { 276 | -webkit-animation-delay: -1.0s; 277 | animation-delay: -1.0s; 278 | } 279 | 280 | .spinner .rect4 { 281 | -webkit-animation-delay: -0.9s; 282 | animation-delay: -0.9s; 283 | } 284 | 285 | .spinner .rect5 { 286 | -webkit-animation-delay: -0.8s; 287 | animation-delay: -0.8s; 288 | } 289 | 290 | @-webkit-keyframes stretchdelay { 291 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 292 | 20% { -webkit-transform: scaleY(1.0) } 293 | } 294 | 295 | @keyframes stretchdelay { 296 | 0%, 40%, 100% { 297 | transform: scaleY(0.4); 298 | -webkit-transform: scaleY(0.4); 299 | } 20% { 300 | transform: scaleY(1.0); 301 | -webkit-transform: scaleY(1.0); 302 | } 303 | } 304 | 305 | /**** General Elements ****/ 306 | table 307 | { 308 | font-size: 10px; 309 | margin: 0; 310 | min-width: 400px; 311 | border-collapse: collapse; 312 | text-align: left; 313 | table-layout:fixed; 314 | } 315 | table th, 316 | table td { 317 | padding: 5px; 318 | max-width: 250px; 319 | word-wrap: break-word; 320 | } 321 | table th 322 | { 323 | font-weight: 600; 324 | text-transform: uppercase; 325 | border-bottom: 1px solid #f1f1f1; 326 | } 327 | table td 328 | { 329 | border-bottom: 1px solid #f1f1f1; 330 | padding: 9px 8px; 331 | } 332 | table tbody tr:hover td 333 | { 334 | background-color: #fafafa; 335 | } 336 | table.metrics-table { 337 | text-align: center; 338 | } 339 | canvas { 340 | float: none; 341 | margin: 0 auto; 342 | height: 30%; 343 | width: 70%; 344 | } 345 | /********************************************* 346 | Widget Elements 347 | *********************************************/ 348 | .progress-bar { 349 | background-color: #eec; 350 | border-radius: 10px; /* (height of inner div) / 2 + padding */ 351 | padding: 0px; 352 | clear: both; 353 | display: inline-block; 354 | overflow: hidden; 355 | white-space: nowrap; 356 | } 357 | .progress-bar > div { 358 | background-color: #1EAEDB; 359 | width: 0%; 360 | height: 5px; 361 | border-radius: 5px; 362 | } 363 | .table-data-plugin .filter-container { 364 | padding-bottom: 0; 365 | margin: 0; 366 | } 367 | .table-data-plugin .filter, 368 | .table-data-plugin .filter:focus, 369 | .table-data-plugin .filter:active { 370 | height: 20px; 371 | padding: 5px; 372 | margin: 5px; 373 | border: none; 374 | outline-color: transparent; 375 | background: transparent; 376 | width: 100%; 377 | margin: 0; 378 | text-align: center; 379 | font-size: 15px; 380 | } 381 | .table-data-plugin .filter:focus { 382 | border-bottom: 1px solid #ff5722; 383 | } 384 | .table-data-plugin thead tr th a, 385 | .table-data-plugin thead tr th a:visited { 386 | color: black; 387 | text-decoration: none; 388 | } 389 | .table-data-plugin .column-sort-caret { 390 | font-size: 10px; 391 | color: #1EAEDB; 392 | } 393 | -------------------------------------------------------------------------------- /static/css/theme-old.css: -------------------------------------------------------------------------------- 1 | /************************************** 2 | Theme: linux-dash beta 3 | **************************************/ 4 | html.old { 5 | background: #F9F6F1; 6 | } 7 | html.old body { 8 | margin: 0; 9 | } 10 | html.old body * { 11 | font-family: "Open Sans"; 12 | letter-spacing: 0; 13 | } 14 | html.old body .hero { 15 | background: #00BA8B; 16 | color: #ffffff; 17 | padding: 0; 18 | } 19 | html.old body .hero h4 { 20 | color: #ffffff; 21 | display: inline-block; 22 | font-size: 20px; 23 | font-weight: 600; 24 | height: 40px; 25 | line-height: 35px; 26 | margin: 0; 27 | vertical-align: middle; 28 | } 29 | html.old body .hero small { 30 | letter-spacing: 0.1rem; 31 | line-height: 40px; 32 | margin-left: 20px; 33 | opacity: 0.9; 34 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 35 | } 36 | html.old body .hero #theme-switcher { 37 | right: -315px; 38 | } 39 | html.old body .hero #theme-switcher.open { 40 | right: 0; 41 | } 42 | html.old body .hero nav-bar { 43 | background: #ffffff; 44 | border-bottom: 1px solid #d6d6d6; 45 | color: #333333; 46 | } 47 | html.old body .hero nav-bar br { 48 | display: none; 49 | } 50 | html.old body .hero nav-bar ul { 51 | display: inline-block; 52 | } 53 | html.old body .hero nav-bar ul li { 54 | display: inline-block; 55 | margin: 0; 56 | } 57 | html.old body .hero nav-bar ul li:not(:first-child) { 58 | border-left: 1px solid #e6e6e6; 59 | } 60 | html.old body .hero nav-bar ul li a { 61 | color: #B2AFAA; 62 | display: block; 63 | font-size: 12px; 64 | font-weight: bold; 65 | line-height: 30px; 66 | margin: 0; 67 | padding: 0 15px; 68 | text-transform: capitalize; 69 | } 70 | html.old body .hero nav-bar ul li:hover a { 71 | color: #888888; 72 | } 73 | html.old body #plugins { 74 | display: flex; 75 | flex-flow: row wrap; 76 | justify-content: flex-start; 77 | } 78 | html.old body .plugin { 79 | border: 1px solid #d5d5d5; 80 | border-radius: 0 0 5px 5px; 81 | box-shadow: none; 82 | margin: 10px; 83 | } 84 | html.old body .plugin .top-bar { 85 | background: transparent linear-gradient(to bottom, #f9f6f1 0%, #f2efea 100%) repeat scroll 0px 0px; 86 | border-bottom: 1px solid #d6d6d6; 87 | color: #525252; 88 | font-size: 14px; 89 | font-weight: bold; 90 | height: 40px; 91 | line-height: 40px; 92 | padding: 0 0 0 15px; 93 | position: relative; 94 | text-align: left; 95 | text-transform: none; 96 | } 97 | html.old body .plugin .top-bar last-update { 98 | float: right; 99 | margin: 0 10px; 100 | opacity: 0.8; 101 | } 102 | html.old body .plugin .top-bar refresh-btn { 103 | float: right; 104 | } 105 | html.old body .plugin .top-bar refresh-btn button { 106 | background: #ffffff; 107 | border: 1px solid rgba(0, 0, 0, 0.15); 108 | border-radius: 4px; 109 | box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1) inset, 0px 1px 2px rgba(0, 0, 0, 0.1); 110 | color: #555555; 111 | cursor: pointer; 112 | box-shadow: none; 113 | display: inline-block; 114 | float: none; 115 | font-size: 14px; 116 | height: auto; 117 | margin: 0 -5px 0 10px; 118 | padding: 0 4px; 119 | width: auto; 120 | } 121 | html.old body .plugin .top-bar refresh-btn button:hover { 122 | background: #e6e6e6; 123 | } 124 | html.old body .plugin .plugin-body { 125 | border: none; 126 | height: auto; 127 | line-height: normal; 128 | max-height: 300px; 129 | padding: 0; 130 | } 131 | html.old body .plugin .plugin-body table { 132 | border-collapse: separate; 133 | border-spacing: 0; 134 | font-size: 12px; 135 | min-width: 300px; 136 | } 137 | html.old body .plugin .plugin-body table tr:not(:first-child) th { 138 | border-top: 1px solid #dddddd; 139 | } 140 | html.old body .plugin .plugin-body table td, 141 | html.old body .plugin .plugin-body table th { 142 | border-bottom: none; 143 | padding: 4px 5px; 144 | } 145 | html.old body .plugin .plugin-body table td:not(:first-child), 146 | html.old body .plugin .plugin-body table th:not(:first-child) { 147 | border-left: 1px solid #dddddd; 148 | } 149 | html.old body .plugin .plugin-body table th:not(.filter-container) { 150 | background: transparent -moz-linear-gradient(center top, #fafafa 0%, #e9e9e9 100%) repeat scroll 0% 0%; 151 | text-transform: uppercase; 152 | } 153 | html.old body .plugin .plugin-body table th.filter-container .filter { 154 | border-bottom: none; 155 | font-size: 12px; 156 | height: auto; 157 | padding: 2px; 158 | } 159 | html.old body .plugin .plugin-body table th.filter-container .filter::before { 160 | content: 'Search >'; 161 | opacity: 0.5; 162 | position: absolute; 163 | left: 0; 164 | } 165 | html.old body .plugin .plugin-body table td { 166 | border-top: 1px solid #dddddd; 167 | } 168 | -------------------------------------------------------------------------------- /static/css/themes.css: -------------------------------------------------------------------------------- 1 | @import "theme-old.css"; 2 | 3 | /************************************** 4 | Theme: Winter 5 | **************************************/ 6 | 7 | html.winter { 8 | background: url(../img/themes/contemporary_china_2.png) ; 9 | } 10 | html.winter .hero nav-bar ul li.active a, 11 | html.winter .hero nav-bar ul li a:hover { 12 | color: #23568f; 13 | } 14 | html.winter .plugin { 15 | background-color: rgba(255, 255, 255, 0.60); 16 | } 17 | html.winter .plugin .top-bar { 18 | color: #012e40; 19 | } 20 | html.winter .plugin refresh-btn button { 21 | background-color: #4c6c73; 22 | } 23 | table th { 24 | color: #012e40; 25 | } 26 | 27 | /************************************** 28 | Theme: Summer 29 | **************************************/ 30 | 31 | html.summer { 32 | clear: both; 33 | background: url(../img/themes/congruent_pentagon.png); 34 | } 35 | html.summer .hero nav-bar ul li.active a, 36 | html.summer .hero nav-bar ul li a:hover { 37 | color: #D84315; 38 | } 39 | html.summer .plugin{ 40 | background-color: rgba(255, 255, 255, 0.8); 41 | } 42 | html.summer .plugin .top-bar { 43 | color: #BF360C; 44 | } 45 | html.summer .plugin refresh-btn button { 46 | background-color: #BF360C; 47 | } 48 | 49 | /************************************** 50 | Theme: Spring 51 | **************************************/ 52 | 53 | html.spring { 54 | clear: both; 55 | background: url(../img/themes/food.png); 56 | } 57 | html.spring .hero nav-bar ul li.active a, 58 | html.spring .hero nav-bar ul li a:hover { 59 | color: #E65100; 60 | } 61 | html.spring .plugin { 62 | background-color: rgba(255,255,255,0.95); 63 | } 64 | html.spring .plugin .top-bar { 65 | color: #FF6D00; 66 | } 67 | html.spring .plugin refresh-btn button { 68 | background-color: #E65100; 69 | } 70 | 71 | /************************************** 72 | Theme: Fall 73 | **************************************/ 74 | 75 | html.fall { 76 | background: url(../img/themes/skulls.png); 77 | } 78 | html.fall .plugin { 79 | background: url(../img/themes/crossword.png); 80 | } 81 | html.fall .hero nav-bar ul li.active a, 82 | html.fall .hero nav-bar ul li a:hover, 83 | html.fall .plugin .top-bar { 84 | color: #F09819; 85 | } 86 | html.fall .plugin refresh-btn button { 87 | background-color: #FF512F; 88 | } 89 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/static/favicon.ico -------------------------------------------------------------------------------- /static/img/themes/congruent_pentagon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/static/img/themes/congruent_pentagon.png -------------------------------------------------------------------------------- /static/img/themes/contemporary_china_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/static/img/themes/contemporary_china_2.png -------------------------------------------------------------------------------- /static/img/themes/crossword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/static/img/themes/crossword.png -------------------------------------------------------------------------------- /static/img/themes/food.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/static/img/themes/food.png -------------------------------------------------------------------------------- /static/img/themes/skulls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-linux-dash/1eac7495f038fd4d756f6eccfc1e8e3aeaf8f06d/static/img/themes/skulls.png -------------------------------------------------------------------------------- /static/js/linuxDash.js: -------------------------------------------------------------------------------- 1 | var linuxDash = angular.module('linuxDash', ['ngRoute']); 2 | 3 | ////////////////// Routing ///////////////////////////// 4 | linuxDash.config(['$routeProvider', 5 | function($routeProvider) { 6 | 7 | $routeProvider. 8 | when('/system-status', { 9 | templateUrl: 'templates/sections/system-status.html', 10 | }). 11 | when('/basic-info', { 12 | templateUrl: 'templates/sections/basic-info.html', 13 | }). 14 | when('/network', { 15 | templateUrl: 'templates/sections/network.html', 16 | }). 17 | when('/accounts', { 18 | templateUrl: 'templates/sections/accounts.html', 19 | }). 20 | when('/apps', { 21 | templateUrl: 'templates/sections/applications.html', 22 | }). 23 | otherwise({ 24 | redirectTo: '/system-status' 25 | }); 26 | 27 | }]); 28 | 29 | ////////////////// Global Application ////////////////// 30 | linuxDash.controller('body', function ($scope, serverAddress) { 31 | $scope.serverSet = false; 32 | 33 | serverAddress.configure().then(function (res) { 34 | $scope.serverSet = true; 35 | }); 36 | 37 | }); 38 | 39 | /** 40 | * Figures out which server-side file works and sets it to localStorage 41 | */ 42 | linuxDash.service('serverAddress', ['$q', function ($q) { 43 | 44 | this.configure = function () { 45 | return $q.when(localStorage.setItem('serverFile','server/')); 46 | } 47 | 48 | }]); 49 | 50 | /** 51 | * Gets data from server and runs callbacks on response.data.data 52 | */ 53 | linuxDash.service('server', ['$http', function ($http) { 54 | 55 | this.get = function (moduleName, callback) { 56 | var serverAddress = localStorage.getItem('serverFile'); 57 | var moduleAddress = serverAddress + '?module=' + moduleName; 58 | 59 | return $http.get(moduleAddress).then(function (response) { 60 | return callback(response.data); 61 | }); 62 | }; 63 | 64 | }]); 65 | 66 | /** 67 | * Sidebar for SPA 68 | */ 69 | linuxDash.directive('navBar',function ($location) { 70 | return { 71 | restrict: 'E', 72 | templateUrl: 'templates/app/navbar.html', 73 | link: function (scope) { 74 | scope.items = [ 75 | 'system-status', 76 | 'basic-info', 77 | 'network', 78 | 'accounts', 79 | 'apps' 80 | ]; 81 | 82 | scope.getNavItemName = function (url) { 83 | return url.replace('-', ' '); 84 | }; 85 | 86 | scope.isActive = function(route) { 87 | return '/' + route === $location.path(); 88 | }; 89 | } 90 | }; 91 | 92 | }); 93 | 94 | 95 | ////////////////// UI Element Directives ////////////////// 96 | 97 | /** 98 | * Shows loader 99 | */ 100 | linuxDash.directive('loader', function() { 101 | return { 102 | restrict: 'E', 103 | scope: { 104 | width: '@' 105 | }, 106 | template: '
' + 107 | '
' + 108 | '
' + 109 | '
' + 110 | '
' + 111 | '
' + 112 | '
' 113 | }; 114 | }); 115 | 116 | /** 117 | * Top Bar for widget 118 | */ 119 | linuxDash.directive('topBar', function() { 120 | return { 121 | restrict: 'E', 122 | scope: { 123 | heading: '=', 124 | refresh: '&', 125 | lastUpdated: '=' 126 | }, 127 | templateUrl: 'templates/app/ui-elements/top-bar.html', 128 | link: function(scope, element, attrs){ 129 | var $refreshBtn = element.find('refresh-btn').eq(0); 130 | 131 | if (typeof attrs.noRefreshBtn !== 'undefined') { 132 | $refreshBtn.remove(); 133 | } 134 | } 135 | }; 136 | }); 137 | 138 | /** 139 | * Shows refresh button and calls 140 | * provided expression on-click 141 | */ 142 | linuxDash.directive('refreshBtn', function() { 143 | return { 144 | restrict: 'E', 145 | scope: { 146 | refresh: '&' 147 | }, 148 | template: '' 149 | }; 150 | }); 151 | 152 | /** 153 | * Message shown when no data is found from server 154 | */ 155 | linuxDash.directive('noData', function() { 156 | return { 157 | restrict: 'E', 158 | template: 'No Data' 159 | }; 160 | }); 161 | 162 | /** 163 | * Displays last updated timestamp for widget 164 | */ 165 | linuxDash.directive('lastUpdate', function() { 166 | return { 167 | restrict: 'E', 168 | scope: { 169 | timestamp: '=' 170 | }, 171 | templateUrl: 'templates/app/ui-elements/last-update.html' 172 | }; 173 | }); 174 | 175 | ////////////////// Plugin Directives ////////////////// 176 | 177 | /** 178 | * Fetches and displays table data 179 | */ 180 | linuxDash.directive('tableData', [ 'server', function(server) { 181 | return { 182 | restrict: 'E', 183 | scope: { 184 | heading: '@', 185 | moduleName: '@' 186 | }, 187 | templateUrl: 'templates/app/table-data-plugin.html', 188 | link: function (scope, element) { 189 | 190 | scope.sortByColumn = null; 191 | 192 | // set the column to sort by 193 | scope.setSortColumn = function (column) { 194 | 195 | // if the column is already being sorted 196 | // reverse the order 197 | if (column === scope.sortByColumn) { 198 | scope.sortByColumn = '-' + column; 199 | } 200 | else { 201 | scope.sortByColumn = column; 202 | } 203 | }; 204 | 205 | scope.getData = function () { 206 | delete scope.tableRows; 207 | 208 | server.get(scope.moduleName, function (serverResponseData) { 209 | 210 | if (serverResponseData.length > 0) { 211 | scope.tableHeaders = Object.keys(serverResponseData[0]); 212 | } 213 | 214 | scope.tableRows = serverResponseData; 215 | scope.lastGet = new Date().getTime(); 216 | 217 | if(serverResponseData.length < 1) { 218 | scope.emptyResult = true; 219 | } 220 | }); 221 | }; 222 | 223 | scope.getData(); 224 | } 225 | }; 226 | }]); 227 | 228 | /** 229 | * Fetches and displays table data 230 | */ 231 | linuxDash.directive('keyValueList', ['server', function(server) { 232 | return { 233 | restrict: 'E', 234 | scope: { 235 | heading: '@', 236 | moduleName: '@', 237 | }, 238 | templateUrl: 'templates/app/key-value-list-plugin.html', 239 | link: function (scope, element) { 240 | 241 | scope.getData = function () { 242 | delete scope.tableRows; 243 | 244 | server.get(scope.moduleName, function (serverResponseData) { 245 | scope.tableRows = serverResponseData; 246 | scope.lastGet = new Date().getTime(); 247 | 248 | if(Object.keys(serverResponseData).length === 0) { 249 | scope.emptyResult = true; 250 | } 251 | }); 252 | }; 253 | 254 | scope.getData(); 255 | } 256 | }; 257 | }]); 258 | 259 | /** 260 | * Fetches and displays data as line chart at a certain refresh rate 261 | */ 262 | linuxDash.directive('lineChartPlugin', ['$interval', '$compile', 'server', function($interval, $compile, server) { 263 | return { 264 | restrict: 'E', 265 | scope: { 266 | heading: '@', 267 | moduleName: '@', 268 | refreshRate: '=', 269 | maxValue: '=', 270 | minValue: '=', 271 | getDisplayValue: '=', 272 | metrics: '=', 273 | color: '@' 274 | }, 275 | templateUrl: 'templates/app/line-chart-plugin.html', 276 | link: function (scope, element) { 277 | 278 | if (!scope.color) { 279 | scope.color = '0, 255, 0'; 280 | } 281 | var series; 282 | 283 | 284 | // smoothieJS - Create new chart 285 | var chart = new SmoothieChart({ 286 | borderVisible:false, 287 | sharpLines:true, 288 | grid: { 289 | fillStyle: '#ffffff', 290 | strokeStyle: 'rgba(232,230,230,0.93)', 291 | sharpLines: true, 292 | millisPerLine: 3000, 293 | borderVisible: false 294 | }, 295 | labels:{ 296 | fontSize: 11, 297 | precision: 0, 298 | fillStyle: '#0f0e0e' 299 | }, 300 | maxValue: parseInt(scope.maxValue), 301 | minValue: parseInt(scope.minValue), 302 | horizontalLines: [{ value: 5, color: '#eff', lineWidth: 1 }] 303 | }); 304 | 305 | // smoothieJS - set up canvas element for chart 306 | canvas = element.find('canvas')[0], 307 | series = new TimeSeries(); 308 | chart.addTimeSeries(series, { strokeStyle: 'rgba(' + scope.color + ', 1)', fillStyle: 'rgba(' + scope.color + ', 0.2)', lineWidth: 2 }); 309 | chart.streamTo(canvas, 1000); 310 | 311 | // update data on chart 312 | scope.getData = function () { 313 | server.get(scope.moduleName, function (serverResponseData) { 314 | 315 | scope.lastGet = new Date().getTime(); 316 | 317 | // change graph colour depending on usage 318 | if (scope.maxValue / 4 * 3 < scope.getDisplayValue(serverResponseData)) { 319 | chart.seriesSet[0].options.strokeStyle = 'rgba(255, 89, 0, 1)'; 320 | chart.seriesSet[0].options.fillStyle = 'rgba(255, 89, 0, 0.2)'; 321 | } 322 | else if (scope.maxValue / 3 < scope.getDisplayValue(serverResponseData)) { 323 | chart.seriesSet[0].options.strokeStyle = 'rgba(255, 238, 0, 1)'; 324 | chart.seriesSet[0].options.fillStyle = 'rgba(255, 238, 0, 0.2)'; 325 | } 326 | else { 327 | chart.seriesSet[0].options.strokeStyle = 'rgba(' + scope.color + ', 1)'; 328 | chart.seriesSet[0].options.fillStyle = 'rgba(' + scope.color + ', 0.2)'; 329 | } 330 | 331 | // update chart with this response 332 | series.append(scope.lastGet, scope.getDisplayValue(serverResponseData)); 333 | 334 | // update the metrics for this chart 335 | scope.metrics.forEach(function (metricObj) { 336 | metricObj.data = metricObj.generate(serverResponseData) ; 337 | }); 338 | 339 | }); 340 | }; 341 | 342 | // set the directive-provided interval 343 | // at which to run the chart update 344 | $interval(scope.getData, scope.refreshRate); 345 | } 346 | }; 347 | }]); 348 | 349 | /** 350 | * Fetches and displays data as line chart at a certain refresh rate 351 | * 352 | */ 353 | linuxDash.directive('multiLineChartPlugin', ['$interval', '$compile', 'server', function($interval, $compile, server) { 354 | return { 355 | restrict: 'E', 356 | scope: { 357 | heading: '@', 358 | moduleName: '@', 359 | refreshRate: '=', 360 | getDisplayValue: '=', 361 | units: '=', 362 | delay: '=' 363 | }, 364 | templateUrl: 'templates/app/multi-line-chart-plugin.html', 365 | link: function (scope, element) { 366 | 367 | // smoothieJS - Create new chart 368 | var chart = new SmoothieChart({ 369 | borderVisible:false, 370 | sharpLines:true, 371 | grid: { 372 | fillStyle:'#ffffff', 373 | strokeStyle:'rgba(232,230,230,0.93)', 374 | sharpLines:true, 375 | borderVisible:false 376 | }, 377 | labels:{ 378 | fontSize:12, 379 | precision:0, 380 | fillStyle:'#0f0e0e' 381 | }, 382 | maxValue: 100, 383 | minValue: 0, 384 | horizontalLines: [{ value: 1, color: '#ecc', lineWidth: 1 }] 385 | }); 386 | 387 | var seriesOptions = [ 388 | { strokeStyle: 'rgba(255, 0, 0, 1)', lineWidth: 2 }, 389 | { strokeStyle: 'rgba(0, 255, 0, 1)', lineWidth: 2 }, 390 | { strokeStyle: 'rgba(0, 0, 255, 1)', lineWidth: 2 }, 391 | { strokeStyle: 'rgba(255, 255, 0, 1)', lineWidth: 1 } 392 | ]; 393 | 394 | // smoothieJS - set up canvas element for chart 395 | var canvas = element.find('canvas')[0]; 396 | var seriesArray = []; 397 | scope.metricsArray = []; 398 | 399 | // get the data once to set up # of lines on chart 400 | server.get(scope.moduleName, function (serverResponseData) { 401 | 402 | var numberOfLines = Object.keys(serverResponseData).length; 403 | 404 | for (var x=0; x < numberOfLines; x++) { 405 | var keyForThisLine = Object.keys(serverResponseData)[x]; 406 | 407 | seriesArray[x] = new TimeSeries(); 408 | chart.addTimeSeries(seriesArray[x], seriesOptions[x]); 409 | scope.metricsArray[x] = { 410 | name: keyForThisLine, 411 | color: seriesOptions[x].strokeStyle, 412 | }; 413 | } 414 | 415 | }); 416 | 417 | var delay = 1000; 418 | 419 | if(angular.isDefined(scope.delay)) 420 | delay = scope.delay; 421 | 422 | chart.streamTo(canvas, delay); 423 | 424 | // update data on chart 425 | scope.getData = function () { 426 | server.get(scope.moduleName, function (serverResponseData) { 427 | scope.lastGet = new Date().getTime(); 428 | 429 | var keyCount = 0; 430 | var maxAvg = 100; 431 | 432 | // update chart with current response 433 | for(var key in serverResponseData) { 434 | seriesArray[keyCount].append(scope.lastGet, serverResponseData[key]); 435 | keyCount++; 436 | maxAvg = Math.max(maxAvg, serverResponseData[key]); 437 | } 438 | 439 | // update the metrics for this chart 440 | scope.metricsArray.forEach(function (metricObj) { 441 | // metricObj.data = metricObj.generate(serverResponseData) ; 442 | metricObj.data = serverResponseData[metricObj.name].toString() + ' ' + scope.units; 443 | }); 444 | 445 | // round up the average and set the maximum scale 446 | var len = parseInt(Math.log10(maxAvg)); 447 | var div = Math.pow(10, len); 448 | chart.options.maxValue = Math.ceil(maxAvg / div) * div; 449 | }); 450 | }; 451 | 452 | var refreshRate = (angular.isDefined(scope.refreshRate))? scope.refreshRate: 1000; 453 | $interval(scope.getData, refreshRate); 454 | } 455 | }; 456 | }]); 457 | 458 | /** 459 | * Base plugin structure 460 | */ 461 | linuxDash.directive('plugin', function() { 462 | return { 463 | restrict: 'E', 464 | transclude: true, 465 | templateUrl: 'templates/app/base-plugin.html' 466 | } 467 | }); 468 | 469 | /** 470 | * Progress bar element 471 | */ 472 | linuxDash.directive('progressBarPlugin',function() { 473 | return { 474 | restrict: 'E', 475 | scope: { 476 | width: '@', 477 | moduleName: '@', 478 | name: '@', 479 | value: '@', 480 | max: '@' 481 | }, 482 | templateUrl: 'templates/app/progress-bar-plugin.html' 483 | }; 484 | }); 485 | 486 | 487 | /** 488 | * Theme switcher 489 | */ 490 | linuxDash.directive('themeSwitcher',['$location', function($location) { 491 | return { 492 | restrict: 'E', 493 | templateUrl: 'templates/app/theme-switcher.html', 494 | link: function (scope) { 495 | 496 | // alternate themes available 497 | scope.themes = [ 498 | { 499 | name: 'winter', 500 | }, 501 | { 502 | name: 'summer', 503 | }, 504 | { 505 | name: 'spring', 506 | }, 507 | { 508 | name: 'fall', 509 | }, 510 | { 511 | name: 'old', 512 | }, 513 | ]; 514 | 515 | scope.themeSwitcherOpen = false; 516 | 517 | scope.switchTheme = function (theme) { 518 | 519 | if(theme.selected) { 520 | scope.setDefaultTheme(); 521 | return; 522 | } 523 | 524 | scope.removeExistingThemes(); 525 | theme.selected = true; 526 | document.getElementsByTagName('html')[0].className = theme.name; 527 | localStorage.setItem('theme', theme.name); 528 | }; 529 | 530 | scope.toggleThemeSwitcher = function () { 531 | scope.themeSwitcherOpen = !scope.themeSwitcherOpen; 532 | }; 533 | 534 | scope.removeExistingThemes = function () { 535 | scope.themes.forEach(function (item) { 536 | item.selected = false; 537 | }); 538 | }; 539 | 540 | scope.setDefaultTheme = function () { 541 | scope.removeExistingThemes(); 542 | document.getElementsByTagName('html')[0].className = ''; 543 | localStorage.setItem('theme', null); 544 | }; 545 | 546 | // on load, check if theme was set in localStorage 547 | if(localStorage.getItem('theme')) { 548 | 549 | scope.themes.forEach(function (theme) { 550 | 551 | if(theme.name === localStorage.getItem('theme')) 552 | { 553 | scope.switchTheme(theme); 554 | } 555 | 556 | }); 557 | } 558 | } 559 | }; 560 | }]); 561 | -------------------------------------------------------------------------------- /static/js/modules.js: -------------------------------------------------------------------------------- 1 | ////////////////// Widget Directives /////////////////// 2 | 3 | linuxDash.directive('diskSpace', ['server', function(server) { 4 | return { 5 | restrict: 'E', 6 | scope: {}, 7 | templateUrl: 'templates/modules/disk-space.html', 8 | link: function (scope) { 9 | 10 | scope.heading = "Disk Partitions"; 11 | 12 | scope.getData = function () { 13 | server.get('disk_partitions', function (serverResponseData) { 14 | scope.diskSpaceData = serverResponseData; 15 | }); 16 | 17 | scope.lastGet = new Date().getTime(); 18 | }; 19 | 20 | scope.getData(); 21 | 22 | scope.getKB = function (stringSize) { 23 | var lastChar = stringSize.slice(-1), 24 | size = parseInt(stringSize); 25 | 26 | switch (lastChar){ 27 | case 'M': return size * Math.pow(1024, 1); 28 | case 'G': return size * Math.pow(1024, 2); 29 | case 'T': return size * Math.pow(1024, 3); 30 | case 'P': return size * Math.pow(1024, 4); 31 | case 'E': return size * Math.pow(1024, 5); 32 | case 'Z': return size * Math.pow(1024, 6); 33 | case 'Y': return size * Math.pow(1024, 7); 34 | default: return size; 35 | } 36 | }; 37 | } 38 | }; 39 | }]); 40 | 41 | linuxDash.directive('ramChart', ['server', function(server) { 42 | return { 43 | restrict: 'E', 44 | scope: {}, 45 | templateUrl: 'templates/modules/ram-chart.html', 46 | link: function (scope) { 47 | 48 | // get max ram available on machine before we 49 | // can start charting 50 | server.get('current_ram', function (resp) { 51 | scope.maxRam = resp['total']; 52 | scope.minRam = 0; 53 | }); 54 | 55 | scope.ramToDisplay = function (serverResponseData) { 56 | return serverResponseData['used']; 57 | }; 58 | 59 | scope.ramMetrics = [ 60 | { 61 | name: 'Used', 62 | generate: function (serverResponseData) { 63 | var ratio = serverResponseData['used'] / serverResponseData['total']; 64 | var percentage = parseInt(ratio * 100); 65 | 66 | return serverResponseData['used'] + ' MB (' 67 | + percentage.toString() + '%)'; 68 | } 69 | }, 70 | { 71 | name: 'Free', 72 | generate: function (serverResponseData) { 73 | return serverResponseData['free'].toString() 74 | + ' MB of ' 75 | + serverResponseData['total'] 76 | + 'MB'; 77 | } 78 | } 79 | ]; 80 | } 81 | }; 82 | }]); 83 | 84 | linuxDash.directive('cpuLoadChart', ['server', function(server) { 85 | return { 86 | restrict: 'E', 87 | scope: {}, 88 | templateUrl: 'templates/modules/cpu-load.html', 89 | link: function (scope) { 90 | scope.units = '%'; 91 | } 92 | }; 93 | }]); 94 | 95 | linuxDash.directive('uploadTransferRateChart', ['server', function(server) { 96 | return { 97 | restrict: 'E', 98 | scope: {}, 99 | templateUrl: 'templates/modules/upload-transfer-rate.html', 100 | link: function (scope) { 101 | scope.delay = 2000; 102 | scope.units = 'KB/s'; 103 | } 104 | }; 105 | }]); 106 | 107 | linuxDash.directive('downloadTransferRateChart', ['server', function(server) { 108 | return { 109 | restrict: 'E', 110 | scope: {}, 111 | templateUrl: 'templates/modules/download-transfer-rate.html', 112 | link: function (scope) { 113 | scope.delay = 2000; 114 | scope.units = 'KB/s'; 115 | } 116 | }; 117 | }]); 118 | 119 | /////////////// Table Data Modules //////////////////// 120 | var simpleTableModules = [ 121 | { 122 | name: 'machineInfo', 123 | template: '' 124 | }, 125 | { 126 | name: 'ipAddresses', 127 | template: '' 128 | }, 129 | { 130 | name: 'ramIntensiveProcesses', 131 | template: '' 132 | }, 133 | { 134 | name: 'cpuIntensiveProcesses', 135 | template: '' 136 | }, 137 | { 138 | name: 'networkConnections', 139 | template: '' 140 | }, 141 | { 142 | name: 'serverAccounts', 143 | template: '' 144 | }, 145 | { 146 | name: 'loggedInAccounts', 147 | template: '' 148 | }, 149 | { 150 | name: 'recentLogins', 151 | template: '' 152 | }, 153 | { 154 | name: 'arpCacheTable', 155 | template: '' 156 | }, 157 | { 158 | name: 'commonApplications', 159 | template: '' 160 | }, 161 | { 162 | name: 'pingSpeeds', 163 | template: '' 164 | }, 165 | { 166 | name: 'bandwidth', 167 | template: '' 168 | }, 169 | { 170 | name: 'swapUsage', 171 | template: '' 172 | }, 173 | { 174 | name: 'internetSpeed', 175 | template: '' 176 | }, 177 | { 178 | name: 'memcached', 179 | template: '' 180 | }, 181 | { 182 | name: 'redis', 183 | template: '' 184 | }, 185 | { 186 | name: 'memoryInfo', 187 | template: '' 188 | }, 189 | { 190 | name: 'cpuInfo', 191 | template: '' 192 | }, 193 | { 194 | name: 'ioStats', 195 | template: '' 196 | }, 197 | { 198 | name: 'scheduledCrons', 199 | template: '' 200 | }, 201 | { 202 | name: 'cronHistory', 203 | template: '' 204 | }, 205 | ]; 206 | 207 | simpleTableModules.forEach(function (module, key) { 208 | 209 | linuxDash.directive(module.name, ['server', function(server) { 210 | var moduleDirective = { 211 | restrict: 'E', 212 | scope: {}, 213 | }; 214 | 215 | if (module.templateUrl) { 216 | moduleDirective['templateUrl'] = 'templates/modules/' + module.templateUrl 217 | } 218 | 219 | if (module.template) { 220 | moduleDirective['template'] = module.template; 221 | } 222 | 223 | return moduleDirective; 224 | }]); 225 | 226 | }); 227 | -------------------------------------------------------------------------------- /static/js/smoothie.min.js: -------------------------------------------------------------------------------- 1 | (function(e){function n(e){this.options=t.extend({},n.defaultOptions,e);this.clear()}function r(e){this.options=t.extend({},r.defaultChartOptions,e);this.seriesSet=[];this.currentValueRange=1;this.currentVisMinValue=0;this.lastRenderTimeMillis=0}var t={extend:function(){arguments[0]=arguments[0]||{};for(var e=1;ethis.maxValue){this.maxValue=t}if(t=0&&this.data[r][0]>e){r--}if(r===-1){this.data.splice(0,0,[e,t])}else if(this.data.length>0&&this.data[r][0]===e){if(n){this.data[r][1]+=t;t=this.data[r][1]}else{this.data[r][1]=t}}else if(r=t&&this.data[n+1][0]0){e.resetBoundsTimerId=setInterval(function(){e.resetBounds()},e.options.resetBoundsInterval)}};r.prototype.removeTimeSeries=function(e){var t=this.seriesSet.length;for(var n=0;n.1||Math.abs(a)>.1;this.currentValueRange+=e.scaleSmoothing*u;this.currentVisMinValue+=e.scaleSmoothing*a}this.valueRange={min:n,max:t}};r.prototype.render=function(e,t){var n=(new Date).getTime();if(!this.isAnimatingScale){var r=Math.min(1e3/6,this.options.millisPerPixel);if(n-this.lastRenderTimeMillis0){i.beginPath();for(var l=t-t%s.grid.millisPerLine;l>=u;l-=s.grid.millisPerLine){var c=f(l);if(s.grid.sharpLines){c-=.5}i.moveTo(c,0);i.lineTo(c,o.height)}i.stroke();i.closePath()}for(var h=1;h1){if(w.fillStyle){i.lineTo(o.width+w.lineWidth+1,x);i.lineTo(o.width+w.lineWidth+1,o.height+w.lineWidth+1);i.lineTo(E,o.height+w.lineWidth);i.fillStyle=w.fillStyle;i.fill()}if(w.strokeStyle&&w.strokeStyle!=="none"){i.stroke()}i.closePath()}i.restore()}if(!s.labels.disabled&&!isNaN(this.valueRange.min)&&!isNaN(this.valueRange.max)){var k=s.yMaxFormatter(this.valueRange.max,s.labels.precision),L=s.yMinFormatter(this.valueRange.min,s.labels.precision);i.fillStyle=s.labels.fillStyle;i.fillText(k,o.width-i.measureText(k).width-2,s.labels.fontSize);i.fillText(L,o.width-i.measureText(L).width-2,o.height-2)}if(s.timestampFormatter&&s.grid.millisPerLine>0){var A=o.width-i.measureText(L).width+4;for(var l=t-t%s.grid.millisPerLine;l>=u;l-=s.grid.millisPerLine){var c=f(l);if(c 2 | 3 | 7 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /templates/app/key-value-list-plugin.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
{{ name }}{{ value }}
17 | 18 |
19 | 20 | 21 |
22 | -------------------------------------------------------------------------------- /templates/app/line-chart-plugin.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
{{ metric.name }}{{ metric.data }}
17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /templates/app/multi-line-chart-plugin.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 |
12 |
15 |
16 |
{{ metric.name }}{{ metric.data }}
22 | 23 | 24 |
25 | 26 | -------------------------------------------------------------------------------- /templates/app/navbar.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | -------------------------------------------------------------------------------- /templates/app/progress-bar-plugin.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
-------------------------------------------------------------------------------- /templates/app/table-data-plugin.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 |
14 | 15 |
19 | {{ header }} 20 | 21 | {{ (header === sortByColumn) ? '▲': ''; }} 22 | {{ ('-' + header === sortByColumn) ? '▼': ''; }} 23 | 24 |
30 | {{ row[header] }} 31 |
35 | 36 |
37 | 38 | 39 |
40 | -------------------------------------------------------------------------------- /templates/app/theme-switcher.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
9 | {{ theme.name }} 10 |
11 | 12 |
-------------------------------------------------------------------------------- /templates/app/ui-elements/last-update.html: -------------------------------------------------------------------------------- 1 | Loading... 2 | 3 | {{ timestamp | date:'hh:mm:ss a' }} 4 | -------------------------------------------------------------------------------- /templates/app/ui-elements/top-bar.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{heading}} 4 | 5 |
-------------------------------------------------------------------------------- /templates/modules/cpu-load.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /templates/modules/disk-space.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 31 | 34 | 35 | 36 | 37 |
NameStats% FullMount Path
{{partition['file_system']}} 22 | 26 | 27 | 29 | {{ partition['used'] }} / {{ partition['size'] }} 30 | 32 | {{ partition['used%'] }} 33 | {{ partition['mounted'] }}
38 | 39 |
40 | -------------------------------------------------------------------------------- /templates/modules/download-transfer-rate.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /templates/modules/ram-chart.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /templates/modules/upload-transfer-rate.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /templates/ping-speeds.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /templates/sections/accounts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /templates/sections/applications.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /templates/sections/basic-info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /templates/sections/network.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /templates/sections/system-status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /web.php: -------------------------------------------------------------------------------- 1 |