diff --git a/.gitignore b/.gitignore index 83ff2c2..98259ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ .* -apps/taptank/frameworks/ +bin/getopt_long +bin/python_env/ +bin/beanstalkd/ +bin/openresty/ +bin/redis/ + +tmp/ +logs/ +db/ diff --git a/CHANGELOG b/CHANGELOG index b709f77..eb631e6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,113 +1,136 @@ # Change Log +### 0.8.0 + +- CHANGES & IMPROVE: + - refactor all global functions, wrap they as separated modules + - use supervisor control all processes + - packages/redis: unified redis driver for NginxLua and LuaSocket + - packages/beanstalkd: unified beanstalkd driver for NginxLua and LuaSocket + - packages/mysql: unified mysql driver for NginxLua and LuaSocket + - packages/event: improved event binding component + - packages/gbc: refactor "GameBox Cloud Core" as a package + - packages/tests: unittests support + - NginxRedisLoop: subscribe any channel without more redis connections + - add Vagrant support + - upgrade all libs + - improved cc.class(), cc.bind(), cc.unbind() + - improved cc.import() + - remove mysql driver + +- BUGFIX: + - fix Job Worker + + ### 0.7.0 -- CHNAGES: - - When user writes a new action, an property of the action named `ACCEPTED_REQUEST_TYPE` can be set. It is used for determining which type of the request can access the action. -- IMPROVE: - - `Job Worker` only prints job finished info in debug mode. - - The codes of the actions of `Job Worker` can be updated dynamically. +- CHNAGES: + - When user writes a new action, an property of the action named `ACCEPTED_REQUEST_TYPE` can be set. It is used for determining which type of the request can access the action. + +- IMPROVE: + - `Job Worker` only prints job finished info in debug mode. + - The codes of the actions of `Job Worker` can be updated dynamically. -- BUGFIX: - - `luasec-0.5` should be installed correctly in 32-bit linux. - - The actions of `Job Worker` should be settled only in `worker/actinos`. +- BUGFIX: + - `luasec-0.5` should be installed correctly in 32-bit linux. + - The actions of `Job Worker` should be settled only in `worker/actinos`. ### 0.6.0 -- CHNAGES: - - `Quick Server` rename to `GameBox Cloud Core`, short name is `gbc-core`. - - `start_quick_server.sh` rename to `start_server`. - - `stop_quick_server.sh` rename to `stop_server`. - - `status_quick_server.sh` rename to `check_server`. - - the `conf/config.lua` option `quickserverRootPath` rename to `serverRootPath`. +- CHNAGES: + - `Quick Server` rename to `GameBox Cloud Core`, short name is `gbc-core`. + - `start_quick_server.sh` rename to `start_server`. + - `stop_quick_server.sh` rename to `stop_server`. + - `status_quick_server.sh` rename to `check_server`. + - the `conf/config.lua` option `quickserverRootPath` rename to `serverRootPath`. ### 0.5.1 -- FEATURE: - - Job Worker is finished as a module of Quick Server. - - Job Service package is offered. User can use it to add a job easily. +- FEATURE: + - Job Worker is finished as a module of Quick Server. + - Job Service package is offered. User can use it to add a job easily. -- BUGFIX: - - start/stop/status shells can exit normally, if some errors happend in progress. - - start/stop/status shells can get the config of Quick Server in any path. - - Monitor can show correct number of jobs in Beanstalkd. +- BUGFIX: + - start/stop/status shells can exit normally, if some errors happend in progress. + - start/stop/status shells can get the config of Quick Server in any path. + - Monitor can show correct number of jobs in Beanstalkd. -- IMPROVE: - - In error messages, redundant paths can be stripped. - - Quick Server supports Mac OS now. - - start/stop/status use more colors for displaying. - - start/stop/status can show the version and mode of Quick Server. - - tools.sh can display the result better, even if the result is a lua table. - - Support job worker module in shells. - - Add lib luasec-0.5.0. +- IMPROVE: + - In error messages, redundant paths can be stripped. + - Quick Server supports Mac OS now. + - start/stop/status use more colors for displaying. + - start/stop/status can show the version and mode of Quick Server. + - tools.sh can display the result better, even if the result is a lua table. + - Support job worker module in shells. + - Add lib luasec-0.5.0. ### 0.5.0 - UPGRADE: - - upgrade luasocket to 3.0-rc1. - - add luainspect lib. + - upgrade luasocket to 3.0-rc1. + - add luainspect lib. - IMPROVE: make installation better, improve installation shell. - - offer an unique install.sh instead of install_ubunutu.sh and install_centos.sh. - - except fundamental packages from linux distro, such as "git", "zip" and some building tools, all other packages can be installed without online. This can save much time. - - the install.sh supports parameters, ex. user can set the path of installation. + - offer an unique install.sh instead of install_ubunutu.sh and install_centos.sh. + - except fundamental packages from linux distro, such as "git", "zip" and some building tools, all other packages can be installed without online. This can save much time. + - the install.sh supports parameters, ex. user can set the path of installation. - IMPROVE: offer all new wiki docs by Sphinx project in ".rst" format. - - the new wiki can be outputed in many file formats, such as html, pdf, latex, etc. - - it is easy to generated with make tools. - - the wiki docs in html format is released with Quick Server, in "docs/" dir. + - the new wiki can be outputed in many file formats, such as html, pdf, latex, etc. + - it is easy to generated with make tools. + - the wiki docs in html format is released with Quick Server, in "docs/" dir. - CHNAGE: almost all Quick Server codes are refactored. - - new architecture, all modules with "Server" in name are refactored to "Connect". - - package mechanism, all functions is offerd as package. - - Http and WebSocket modules are refactored, inheriting from "ConnectBase". - - new broadcast mechanism. - - adjust options in config.lua, more simple, more better. - - new maintain and manager shells. - - monitor the processes of Quick Server at any time, and the status can be shown in web. + - new architecture, all modules with "Server" in name are refactored to "Connect". + - package mechanism, all functions is offerd as package. + - Http and WebSocket modules are refactored, inheriting from "ConnectBase". + - new broadcast mechanism. + - adjust options in config.lua, more simple, more better. + - new maintain and manager shells. + - monitor the processes of Quick Server at any time, and the status can be shown in web. ### 0.4.0 - UPGRADE: From Openresty 1.7.2.x to 1.7.7.x. - IMPROVE: make install_ubunutu.sh better in order that user can install Quick Server convenienty, and fix some bugs in it. - - create a symbol link for nginx, after Openresty installation. - - Add option "--no-check-certificate" for each "wget" command. - - "status\_quick\_server.sh" shell file should be copied to installation directory while installation is finished. - - Change those shell tools director, move them from "/opt" to installation path. - - Add a new shell tool named "restart\_nginx\_only.sh" in order to restart nginx processes. - - Add a parameter for install_ubuntu.sh for specifying installation path insted of absolute path. - - Quick Server configure files, including "redis.conf" and "nginx.conf", are modified automatically via "sed" tool. + - create a symbol link for nginx, after Openresty installation. + - Add option "--no-check-certificate" for each "wget" command. + - "status\_quick\_server.sh" shell file should be copied to installation directory while installation is finished. + - Change those shell tools director, move them from "/opt" to installation path. + - Add a new shell tool named "restart\_nginx\_only.sh" in order to restart nginx processes. + - Add a parameter for install_ubuntu.sh for specifying installation path insted of absolute path. + - Quick Server configure files, including "redis.conf" and "nginx.conf", are modified automatically via "sed" tool. - FEATURE: Support plugin mechanism. - - Add some methods into "cc" framework, "load" and "bind" etc, for package file(lua file). - - Give two plugin examples: "Ranklist" and "ChatRoom", converted from "RanklistAction" and "ChatAction". - - Add simple functionality tests for above plugins. + - Add some methods into "cc" framework, "load" and "bind" etc, for package file(lua file). + - Give two plugin examples: "Ranklist" and "ChatRoom", converted from "RanklistAction" and "ChatAction". + - Add simple functionality tests for above plugins. - IMPROVE: Add a demo action named "HelloworldAction" - - include a simple method "/sayhello" to show "hello world". - - there are two other methods "addandcount" and "sayletters" to show how to write a function with plugins. - - Add "helloworld" client which supports both html and websocket. + - include a simple method "/sayhello" to show "hello world". + - there are two other methods "addandcount" and "sayletters" to show how to write a function with plugins. + - Add "helloworld" client which supports both html and websocket. - CHANGE: delete install_mac.sh. The installation of mac env will be supported in next version. - OTHER MINOR CHANGES: - - IMPROVE: upgrade Quick Server wiki. - - BUGFIX: when deploying lua codes defined by user, the target directory in Quick Server shoule be created. - - IMPROVE: The target directory in Quick Server can be configured via "luaRepoPrefix" in config.lua for deploying lua codes defined by user. - - CHANGE: obsolete the old interface of uploading user codes. - - IMPROVE: Add two sql files: "base.sql" and "pre_condition.sql" in conf/sql for configuring MySql. - - CHANGE: don't need to set "root" privilege in nginx conf file. - - IMPROVE: Add README file for each sub-dir. - - CHANGE: change some shells which are used to encapsulate "nginx" command in "openresty/nginx". + - IMPROVE: upgrade Quick Server wiki. + - BUGFIX: when deploying lua codes defined by user, the target directory in Quick Server shoule be created. + - IMPROVE: The target directory in Quick Server can be configured via "luaRepoPrefix" in config.lua for deploying lua codes defined by user. + - CHANGE: obsolete the old interface of uploading user codes. + - IMPROVE: Add two sql files: "base.sql" and "pre_condition.sql" in conf/sql for configuring MySql. + - CHANGE: don't need to set "root" privilege in nginx conf file. + - IMPROVE: Add README file for each sub-dir. + - CHANGE: change some shells which are used to encapsulate "nginx" command in "openresty/nginx". ### 0.4.0-rc0 - FEATURE: Add an new action "SessionAction" to generate **session_id** for user. - FEATURE: adjust many interfaces in RanklistAction. - - Each interface calling needs checking **session_id**. - - "Add" interface can generate a uid according to the **nickname** when user calls it first time. - - the format of uid is "nickname+numbers" in order to keep each uid unique. - - score, remove, getrank and getrevrank should get key from param "uid". - - "GetRevRank" also replies score. - - "GetRankAction" also return socre. - - "AddAction" can return a percent to indicate "rank/total", in other words, it's the user's positiono in a ranklist. - - Add some test cases for RanklistAction. + - Each interface calling needs checking **session_id**. + - "Add" interface can generate a uid according to the **nickname** when user calls it first time. + - the format of uid is "nickname+numbers" in order to keep each uid unique. + - score, remove, getrank and getrevrank should get key from param "uid". + - "GetRevRank" also replies score. + - "GetRankAction" also return socre. + - "AddAction" can return a percent to indicate "rank/total", in other words, it's the user's positiono in a ranklist. + - Add some test cases for RanklistAction. - BUGFIX: if a redis command fails, it replies an int 0 not a string "0". ### 0.3.9 @@ -120,9 +143,9 @@ ### 0.3.8 - FEATURE: implement chatting room, ChatAction module. - - Add a new sub-table as the configuration for ChatAction in config.lua. - - assign an user to a channel automatically. - - Add some tests cases for ChatAction. + - Add a new sub-table as the configuration for ChatAction in config.lua. + - assign an user to a channel automatically. + - Add some tests cases for ChatAction. - IMPROVE: remove BeginSession interface. - IMPROVE: optimize some lua codes in the initialization of mysql and redis. diff --git a/README.md b/README.md index 32ab755..c85716f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ GameBox Cloud Core 为开发者提供一个稳定可靠,可伸缩的服务端 - [OpenResty](http://openresty.org) - [LuaJIT](http://luajit.org) -
- 使用 Lua 脚本语言开发服务端功能 @@ -21,7 +20,6 @@ GameBox Cloud Core 为开发者提供一个稳定可靠,可伸缩的服务端 用 Lua 脚本语言开发服务端功能还有一个巨大的好处,那就是可以和使用 Cocos2d-Lua(quick-cocos2d-x)的客户端共享大量代码。比如数据 Schema 定义、数据对象、游戏逻辑等等,都可以在客户端和服务端之间共享同一份代码。做过网络游戏的同学一定对如何保持客户端和服务端代码在数据接口上的一致头疼过。现在使用 GameBox Cloud Core,这些问题统统消失不见。 -
- 支持短连接和长连接,满足从异步网络到实时网络的各种需求 @@ -34,19 +32,12 @@ GameBox Cloud Core 为开发者提供一个稳定可靠,可伸缩的服务端 - [WebSocket RFC 文档](https://tools.ietf.org/html/rfc6455) - [WebSocket](http://zh.wikipedia.org/wiki/WebSocket) -
-- 支持插件机制,使用第三方插件加快功能开发 +## Get Started - GameBox Cloud Core 支持插件机制,开发者可以使用成熟的第三方插件来加快服务端功能开发。未来 GameBox Cloud 团队也将提供插件仓库,让开发者可以分享各种有用的插件。 - -
- -### Get Started - -- [安装 GameBox Cloud Core](http://gameboxcloud.com/docs/core/install/) -- [创建 Hello,World 应用程序](http://gameboxcloud.com/docs/core/helloworld/) -- [更多文档](http://gameboxcloud.com/docs/) -- [版本日志](http://gameboxcloud.com/docs/core/changelog/) -- [源代码仓库](https://bitbucket.org/gameboxcloud/gbc-core) +- [快速开始](https://github.com/dualface/gbc-docs/blob/master/src/guide/get-started.md) +- [更多文档](https://github.com/dualface/gbc-docs/) +- [源代码仓库](https://github.com/dualface/gbc-core) +- [项目管理](https://www.pivotaltracker.com/n/projects/1474648) +- [Bug 报告](https://github.com/dualface/gbc-core/issues) - 技术支持: QQ群 <424776815> diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..a3df0a6 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.8.0 diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..14474b9 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,73 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. +Vagrant.configure(2) do |config| + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. + + # Every Vagrant development environment requires a box. You can search for + # boxes at https://atlas.hashicorp.com/search. + config.vm.box = "ubuntu/vivid64" + + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine. In the example below, + # accessing "localhost:8080" will access port 80 on the guest machine. + config.vm.network "forwarded_port", guest: 8088, host: 8088 + + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Create a public network, which generally matched to bridged network. + # Bridged networks make the machine appear as another physical device on + # your network. + # config.vm.network "public_network" + + # Share an additional folder to the guest VM. The first argument is + # the path on the host to the actual folder. The second argument is + # the path on the guest to mount the folder. And the optional third + # argument is a set of non-required options. + # config.vm.synced_folder "../data", "/vagrant_data" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + # config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine + # vb.gui = true + # + # # Customize the amount of memory on the VM: + # vb.memory = "1024" + # end + # + # View the documentation for the provider you are using for more + # information on available options. + + # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies + # such as FTP and Heroku are also available. See the documentation at + # https://docs.vagrantup.com/v2/push/atlas.html for more information. + # config.push.define "atlas" do |push| + # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" + # end + + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + config.vm.provision :shell, run: "always", path: "vagrant-support/bootstrap.sh" + + # config.vm.provision "shell", inline: <<-SHELL + # sudo apt-get update + # sudo apt-get install -y apache2 + # SHELL +end diff --git a/apps/admin/actions/MonitorAction.lua b/apps/admin/actions/MonitorAction.lua deleted file mode 100644 index 4b6dee8..0000000 --- a/apps/admin/actions/MonitorAction.lua +++ /dev/null @@ -1,207 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local tonumber = tonumber -local string_format = string.format -local string_match = string.match -local string_sub = string.sub -local string_lower = string.lower -local string_split = string.split -local string_find = string.find -local table_insert = table.insert -local math_trunc = math.trunc -local io_popen = io.popen - -local _MONITOR_PROC_DICT_KEY = "_MONITOR_PROC_DICT" -local _MONITOR_LIST_PATTERN = "_MONITOR_%s_%s_LIST" -local _MONITOR_MEM_INFO_KEY = "_MONITOR_MEM_INFO" -local _MONITOR_CPU_INFO_KEY = "_MONITOR_CPU_INFO" -local _MONITOR_DISK_INFO_KEY = "_MONITOR_DISK_INFO" - -local MonitorAction = class("MonitorAction") - -function MonitorAction:ctor(connect) - self.connect = connect - self._interval = connect.config.monitor.interval -end - -function MonitorAction:getalldataAction(arg) - local result = {} - local process = self:_getProcess() - for _, procName in ipairs(process) do - result[procName] = self:_fillData(procName, {"SEC", "MINUTE", "HOUR"}, 0) - end - - result.interval = self._interval - result.cpu_cores = self:_getSystemInfo(_MONITOR_CPU_INFO_KEY) - result.mem_total, result.mem_free = self:_getSystemInfo(_MONITOR_MEM_INFO_KEY) - result.disk_total, result.disk_free = self:_getSystemInfo(_MONITOR_DISK_INFO_KEY) - - return result -end - -function MonitorAction:getdataAction(arg) - local timeSpan = self:_convertToSec(arg.time_span) - - if not timeSpan or timeSpan <= 0 then - return self:getalldataAction(arg) - end - - local listType = {} - local start = 0 - if timeSpan <= 60 then - table_insert(listType, "SEC") - start = -math_trunc(timeSpan / self._interval) - -- indicate that client has a query interval less than monitoring interval - -- so at least return the latest data. - if start == 0 then - start = -1 - end - elseif timeSpan <= 3600 then - table_insert(listType, "MINUTE") - start = -math_trunc(timeSpan / 60) - else - table_insert(listType, "HOUR") - start = -math_trunc(timeSpan / 3600) - end - - local result = {} - local process = self:_getProcess() - for _, procName in ipairs(process) do - result[procName] = self:_fillData(procName, listType, start) - if procName == "REDIS-SERVER" then - result["REDIS_SERVER"] = result[procName] - result[procName] = nil - end - end - - result.interval = self._interval - result.cpu_cores = self:_getSystemInfo(_MONITOR_CPU_INFO_KEY) - result.mem_total, result.mem_free = self:_getSystemInfo(_MONITOR_MEM_INFO_KEY) - result.disk_total, result.disk_free = self:_getSystemInfo(_MONITOR_DISK_INFO_KEY) - - return result -end - -function MonitorAction:_getProcess() - local redis = self.connect:getRedis() - local process = redis:command("HKEYS", _MONITOR_PROC_DICT_KEY) - - return process -end - -function MonitorAction:_getSystemInfo(key) - local redis = self.connect:getRedis() - local res = string_split(redis:command("GET", key), "|") - - return res[1], res[2] -end - -function MonitorAction:_convertToSec(timeSpan) - if not timeSpan then return nil end - - local time = string_match(string_lower(timeSpan), "^(%d+[s|h|m])") - if time == nil then - throw("time format error.") - end - local unit = string_sub(time, -1) - local number = tonumber(string_sub(time, 1, -2)) - if not number then - return nil - end - - if unit == "h" then - return number * 3600 - end - - if unit == "m" then - return number * 60 - end - - if unit == "s" then - return number - end - - return nil -end - -function MonitorAction:_fillData(procName, listType, start) - local redis = self.connect:getRedis() - local t = {} - t.cpu = {} - if not string_find(procName, "BEANSTALKD") then - t.mem = {} - if not string_find(procName, "NGINX_WORKER") then - t.conn_num = {} - end - else - t.total_jobs = {} - end - - for _, typ in ipairs(listType) do - local list = string_format(_MONITOR_LIST_PATTERN, procName, typ) - local data = redis:command("LRANGE", list, start, -1) - local field = self:_getFiled(typ) - t.cpu[field] = {} - if not string_find(procName, "BEANSTALKD") then - t.mem[field] = {} - if not string_find(procName, "NGINX_WORKER") then - t.conn_num[field] = {} - end - else - t.total_jobs[field] = {} - end - - for _, v in ipairs(data) do - local tmp = string_split(v, "|") - table_insert(t.cpu[field], tonumber(tmp[1])) - if not string_find(procName, "BEANSTALKD") then - table_insert(t.mem[field], tonumber(tmp[2])) - if not string_find(procName, "NGINX_WORKER") then - table_insert(t.conn_num[field], tonumber(tmp[3])) - end - else - table_insert(t.total_jobs[field], tonumber(tmp[3])) - end - end - end - - return t -end - -function MonitorAction:_getFiled(typ) - if typ == "SEC" then - return "last_60s" - end - - if typ == "MINUTE" then - return "last_hour" - end - - if typ == "HOUR" then - return "last_day" - end -end - -return MonitorAction diff --git a/apps/tests/actions/BeanstalkdAction.lua b/apps/tests/actions/BeanstalkdAction.lua new file mode 100644 index 0000000..c6d8f61 --- /dev/null +++ b/apps/tests/actions/BeanstalkdAction.lua @@ -0,0 +1,207 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local helper = cc.import(".helper") +local tests = cc.import("#tests") +local check = tests.Check +local Beanstalkd = cc.import("#beanstalkd") + +local BeanstalkdTestCase = cc.class("BeanstalkdTestCase", tests.TestCase) + +local _newbean, _flush + +local _DEFAULT_TUBE = "default" +local _TEST_TUBE = "_test_" +local _JOB_PRIORITY = 0 +local _JOB_DELAY = 1 +local _JOB_TTR = 2 +local _JOB_WORD = "hello, number is " .. math.random(1, 100) + +function BeanstalkdTestCase:setup() + local config = self:getInstanceConfig() + self._beanstalkd = _newbean(config.server.beanstalkd) + _flush(self._beanstalkd) +end + +function BeanstalkdTestCase:teardown() + _flush(self._beanstalkd) +end + +function BeanstalkdTestCase:basicsAction() + local bean = self._beanstalkd + local errors = bean.ERRORS + + -- add job, reserve it + local id = bean:put(_JOB_WORD, _JOB_PRIORITY, _JOB_DELAY, _JOB_TTR) + check.isInt(id) + local job = bean:reserve() + check.equals(job, {id = id, data = _JOB_WORD}) + + -- sleep, reserve again with deadline_soon + helper.sleep(_JOB_TTR - 1) + + check.equals({bean:reserve(0)}, {nil, errors.DEADLINE_SOON}) + check.equals({bean:touch(job.id)}, {true}) + + -- delete it + check.equals({bean:delete(job.id)}, {true}) + -- delete non exists job + check.equals({bean:delete(job.id)}, {nil, errors.NOT_FOUND}) + + -- reserve with timeout + check.equals({bean:reserve(1)}, {nil, errors.TIMED_OUT}) + + return true +end + +function BeanstalkdTestCase:releaseAction() + local bean = self._beanstalkd + local errors = bean.ERRORS + + -- add job, reserve it, release it + local id = bean:put(_JOB_WORD, 0, _JOB_DELAY, _JOB_TTR) + check.isInt(id) + local job = bean:reserve() + check.equals(job, {id = id, data = _JOB_WORD}) + check.equals({bean:release(job.id, _JOB_PRIORITY, _JOB_DELAY)}, {true}) + + -- release non exists job + check.equals({bean:release(job.id, _JOB_PRIORITY, _JOB_DELAY)}, {nil, errors.NOT_FOUND}) + -- delete it + check.equals({bean:delete(job.id)}, {true}) + + return true +end + +function BeanstalkdTestCase:changestateAction() + local bean = self._beanstalkd + local errors = bean.ERRORS + + -- add job, peek it + local id = bean:put(_JOB_WORD, 0, _JOB_DELAY, _JOB_TTR) + check.isInt(id) + + local expected = {id = id, data = _JOB_WORD} + local job = bean:peek(id) + check.equals(job, expected) + + -- peek delayed job + local job = bean:peek("delayed") + check.equals(job, expected) + + -- reserve it, bury reserved job, peek buried job + check.equals({bean:reserve()}, {expected}) + check.equals({bean:bury(job.id, _JOB_PRIORITY)}, {true}) + local job = bean:peek("buried") + check.equals(job, expected) + + -- kick it + check.equals({bean:kick(100)}, {1}) + -- wait it ready + helper.sleep(_JOB_DELAY) + local job = bean:peek("ready") + check.equals(job, expected) + + return true +end + +function BeanstalkdTestCase:statsAction() + local bean = self._beanstalkd + local errors = bean.ERRORS + + -- add job + local id = bean:put(_JOB_WORD, 0, _JOB_DELAY, _JOB_TTR) + check.isInt(id) + + -- get job info + local res = bean:statsJob(id) + check.equals(res["id"], tostring(id)) + check.equals(res["state"], "delayed") + + -- get tube info + local res = bean:statsTube(_TEST_TUBE) + check.equals(res["name"], _TEST_TUBE) + check.equals(res["current-jobs-delayed"], "1") + + -- get system info + local res = bean:stats() + check.equals(res["current-jobs-ready"], "0") + check.equals(res["current-jobs-delayed"], "1") + + -- list tubes + local tubes = bean:listTubes() + check.isTable(tubes) + check.contains(tubes, _TEST_TUBE) + + -- list used tube + local tube = bean:listTubeUsed() + check.equals(tube, _TEST_TUBE) + + -- list watched tubes + local tubes = bean:listTubesWatched() + check.isTable(tubes) + check.contains(tubes, _TEST_TUBE) + + return true +end + +function BeanstalkdTestCase:tubeAction() + local bean = self._beanstalkd + local errors = bean.ERRORS + + -- use, watch, ignore + check.equals({bean:use(_TEST_TUBE)}, {_TEST_TUBE}) + + local count = bean:watch(_TEST_TUBE) + check.isInt(count) + check.greaterThan(count, 0) + + local count2 = bean:ignore(_DEFAULT_TUBE) + check.isInt(count2) + + return true +end + +-- private + +_newbean = function(config) + local beanstalkd = Beanstalkd:new() + beanstalkd:connect(config.host, config.port) + beanstalkd:use(_TEST_TUBE) + beanstalkd:watch(_TEST_TUBE) + beanstalkd:ignore(_DEFAULT_TUBE) + return beanstalkd +end + +_flush = function(bean) + bean:kick(10000) + + while true do + local data = bean:reserve(0) + if not data then break end + bean:delete(data.id) + end +end + +return BeanstalkdTestCase diff --git a/apps/tests/actions/ComponentsAction.lua b/apps/tests/actions/ComponentsAction.lua new file mode 100644 index 0000000..d6e72d3 --- /dev/null +++ b/apps/tests/actions/ComponentsAction.lua @@ -0,0 +1,109 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + + +local Event = cc.import("#event") + +local helper = cc.import(".helper") +local tests = cc.import("#tests") +local check = tests.Check + +local ComponentsTestCase = cc.class("ComponentsTestCase", tests.TestCase) + +function ComponentsTestCase:setup() +end + +function ComponentsTestCase:teardown() +end + +function ComponentsTestCase:bindingAction() + local Sheep = cc.class("Sheep") + local sheep = Sheep:new() + + -- add component + local eventComponent = cc.addComponent(sheep, Event) + check.isTable(eventComponent) + check.isFunction(eventComponent.bind) + + -- get component by class + local eventComponent_ = cc.getComponent(sheep, Event) + check.equals(tostring(eventComponent), tostring(eventComponent_)) + + -- get component by class name + local eventComponent_ = cc.getComponent(sheep, Event.__cname) + check.equals(tostring(eventComponent), tostring(eventComponent_)) + + -- bind listeners + local results = {} + + local tag1 = eventComponent:bind("RUN", function(event) + results[#results + 1] = {event.name, event.step} + end) + + local tag2 = eventComponent:bind("RUN", function(event) + results[#results + 1] = {event.name, event.step} + end) -- add second listener for event "RUN" + + local tag3 = eventComponent:bind("WALK", function(event) + results[#results + 1] = {event.name, event.step} + end) + + -- trigger events + local step1 = math.random(10000, 20000) + local step2 = math.random(30000, 40000) + local step3 = math.random(50000, 60000) + + eventComponent:trigger({name = "RUN", step = step1}) + eventComponent:trigger({name = "WALK", step = step2}) + + -- unbind listener + eventComponent:unbind(tag2) + eventComponent:trigger({name = "RUN", step = step3}) + + -- check + check.equals(results, { + {"RUN", step1}, + {"RUN", step1}, + {"WALK", step2}, + {"RUN", step3}, + }) + + -- remove component by class + cc.removeComponent(sheep, Event) + check.isNil(cc.getComponent(sheep, Event)) + + -- remove component by class name + cc.addComponent(sheep, Event) + cc.removeComponent(sheep, Event.__cname) + check.isNil(cc.getComponent(sheep, Event)) + + -- remove component by component object + local eventComponent = cc.addComponent(sheep, Event) + cc.removeComponent(sheep, eventComponent) + check.isNil(cc.getComponent(sheep, Event)) + + return true +end + +return ComponentsTestCase diff --git a/apps/tests/actions/JobsAction.lua b/apps/tests/actions/JobsAction.lua new file mode 100644 index 0000000..cbc8315 --- /dev/null +++ b/apps/tests/actions/JobsAction.lua @@ -0,0 +1,132 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local helper = cc.import(".helper") +local tests = cc.import("#tests") +local check = tests.Check + +local JobsTestCase = cc.class("JobsTestCase", tests.TestCase) + +local _TEST_REDIS_KEY = 'jobs.test.number' + +local _flush + +function JobsTestCase:setup() + local instance = self:getInstance() + self._jobs = instance:getJobs() + self._redis = instance:getRedis() + _flush(self._jobs, self._redis) +end + +function JobsTestCase:teardown() + _flush(self._jobs, self._redis) +end + +function JobsTestCase:addAction() + local number = math.random(1, 10000) + local data = {number = number, key = _TEST_REDIS_KEY} + + local delay = 1 + local jobid = self._jobs:add({ + action = 'jobs.trigging', + data = data, + delay = delay, + }) + check.isInt(jobid) + helper.sleep(delay + 1) -- waiting for job done + + -- query job result from redis + local res = tonumber(self._redis:get(_TEST_REDIS_KEY)) + check.equals(res, number * 2) + + return true +end + +function JobsTestCase:atAction() + local number = math.random(20000, 30000) + local data = {number = number, key = _TEST_REDIS_KEY} + + local time = os.time() + 1 + local jobid = self._jobs:at({ + action = 'jobs.trigging', + data = data, + time = time, + }) + check.isInt(jobid) + helper.sleep(2) -- waiting for job done + + -- query job result from redis + local now = os.time() + local res = tonumber(self._redis:get(_TEST_REDIS_KEY)) + check.equals(res, number * 2) + check.isTrue(math.abs(now - time) <= 1) + + return true +end + +function JobsTestCase:getAction() + local number = math.random(40000, 50000) + local data = {number = number, key = _TEST_REDIS_KEY} + + local delay = 2 + local jobid = self._jobs:add({ + action = 'jobs.trigging', + data = data, + delay = delay, + }) + check.isInt(jobid) + + -- query job + local job = self._jobs:get(jobid) + check.isTable(job) + check.equals(job.id, jobid) + check.equals(job.data, data) + + -- delete job + local res = self._jobs:delete(jobid) + check.isTrue(res) + + return true +end + +-- remove all jobs +function JobsTestCase:_flush() + local states = {"ready", "delayed", "buried"} + for _, state in ipairs(states) do + while true do + local job = self._jobs:queryNext(state) + if not job then break end + self._jobs:remove(job.id) + end + end +end + +-- private + +_flush = function(jobs, redis) + redis:del(_TEST_REDIS_KEY) +end + +return JobsTestCase + diff --git a/apps/tests/actions/RedisAction.lua b/apps/tests/actions/RedisAction.lua new file mode 100644 index 0000000..de09a93 --- /dev/null +++ b/apps/tests/actions/RedisAction.lua @@ -0,0 +1,287 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local Redis = cc.import("#redis") +local helper = cc.import(".helper") +local tests = cc.import("#tests") +local check = tests.Check + +local RedisTestCase = cc.class("RedisTestCase", tests.TestCase) + +local _TEST_DB_INDEX = 15 +local _CLEANUP_CODE = [[ +local keys = redis.call('KEYS', '*') +for _, key in ipairs(keys) do + redis.call('DEL', key) +end +]] + +local _newredis, _runcmds + +function RedisTestCase:setup() + local config = self:getInstanceConfig() + self._redis = _newredis(config.server.redis) +end + +function RedisTestCase:teardown() + local redis = self._redis + redis:select(_TEST_DB_INDEX) + redis:eval(_CLEANUP_CODE, 0) + redis:close() + self._redis = nil +end + +function RedisTestCase:typesAction() + local redis = self._redis + + -- check return types: + -- Simple Strings + local word = "hello world" + check.equals(redis:echo(word), word) + check.equals(redis:ping(), "PONG") + + -- Errors + local ok, err = redis:auth("INVALID_PASSWORD") + check.contains(string.lower(err), "no password is set") + + -- Integers + check.equals(redis:del("NON_EXISTS_KEY"), 0) + redis:del("TEST_KEY") + check.equals(redis:set("TEST_KEY", word), "OK") + check.equals(redis:del("TEST_KEY"), 1) + + -- Bulk Strings + check.equals(redis:set("TEST_KEY", word), "OK") + check.equals(redis:get("TEST_KEY"), word) + + -- Arrays + check.equals(redis:set("TEST_KEY_1", word), "OK") + check.equals(redis:set("TEST_KEY_2", word), "OK") + local keys = redis:keys("TEST_KEY_*") + check.isTable(keys) + table.sort(keys) + check.equals(keys, {"TEST_KEY_1", "TEST_KEY_2"}) + + check.equals(redis:mget("TEST_KEY", "NON_EXISTS_KEY"), {word, redis.null}) + + return true +end + +function RedisTestCase:pipelineAction() + local redis = self._redis + + local word = "hello world" + local commands = { + {"echo", word}, + {"ping"}, + {"del", "NON_EXISTS_KEY"}, + {"set", "TEST_KEY", word}, + {"del", "TEST_KEY"}, + {"set", "TEST_KEY_1", word}, + {"set", "TEST_KEY_2", word}, + {"keys", "TEST_KEY_*"}, + {"mget", "TEST_KEY", "TEST_KEY_1", "NON_EXISTS_KEY"}, + {"eval", "return {redis.call('get', 'NON_EXISTS_KEY'), KEYS[1], ARGV[1]}", 1, "KEY_1", "ARG_1"}, + {"auth", "NO_PASSWORD"}, -- get error in pipeline + } + + local expected = { + word, + "PONG", + 0, + "OK", + 1, + "OK", + "OK", + {"TEST_KEY_1", "TEST_KEY_2"}, + {redis.null, word, redis.null}, + { + redis.null, "KEY_1", "ARG_1", + }, + } + + local function _checkResult(vals) + table.sort(vals[8]) + local last = table.remove(vals) + + check.equals(vals, expected) + check.isTable(last) + check.isFalse(last[1]) + check.contains(last[2], "no password is set") + end + + -- test commit pipeline + redis:initPipeline() + _runcmds(redis, commands) + local vals = redis:commitPipeline() + check.isTable(vals) + _checkResult(vals) + + -- test cancel pipeline + redis:initPipeline() + _runcmds(redis, commands) + redis:cancelPipeline() -- cleanup pipeline + + redis:initPipeline() -- start again + _runcmds(redis, commands) + local vals = redis:commitPipeline() + check.isTable(vals) + _checkResult(vals) + + return true +end + +function RedisTestCase:pubsubAction() + local redis = self._redis + + local channel1 = "MSG_CHANNEL_1" + local channel2 = "MSG_CHANNEL_2" + local channel3 = "MSG_CHANNEL_3" + local channel4 = "MSG_CHANNEL_4" + + -- use current instance to subscribe to channels + check.equals(redis:subscribe(channel1, channel2), { + "subscribe", channel1, 1 + }) + check.equals(redis:readReply(), { + "subscribe", channel2, 2 + }) + + -- use an other instance publish message to channels + local config = self:getInstanceConfig() + local redis2 = _newredis(config.server.redis) + redis2:publish(channel1, "hello") + + check.equals(redis:readReply(), { + "message", channel1, "hello" + }) + + redis:subscribe(channel3, channel4) + check.equals(redis:readReply(), { + "subscribe", channel4, 4 + }) + + redis2:publish(channel2, "world") + check.equals(redis:readReply(), { + "message", channel2, "world" + }) + + for i = 1, 10 do + redis2:publish(channel2, "world_" .. i) + end + + for i = 1, 10 do + check.equals(redis:readReply(), { + "message", channel2, "world_" .. i + }) + end + + redis2:close() + + -- unsubscribe from channels + check.equals(redis:unsubscribe(channel1), { + "unsubscribe", channel1, 3 + }) + check.equals(redis:unsubscribe(channel2), { + "unsubscribe", channel2, 2 + }) + check.equals(redis:unsubscribe(channel3), { + "unsubscribe", channel3, 1 + }) + check.equals(redis:unsubscribe(channel4), { + "unsubscribe", channel4, 0 + }) + check.equals(redis:echo("hello"), "hello") + + return true +end + +function RedisTestCase:loopAction() + local redis = self._redis + + local channel1 = "MSG_CHANNEL_1" + local channel2 = "MSG_CHANNEL_2" + local channel3 = "MSG_CHANNEL_3" + local channel4 = "MSG_CHANNEL_4" + + -- Loop will use an other redis instance + local msgs = {} + local loop, err = redis:makeSubscribeLoop() + if not loop then + check.equals(self:getInstance():getRequestType(), "cli") + return true + end + + local cmdchannel = "_TEST_CMD_CHANNEL" + loop:start(function(channel, msg) + msgs[#msgs + 1] = {channel, msg} + end, cmdchannel) + + loop:subscribe(channel1, channel2) + + -- publish message to channels + redis:publish(channel1, "hello1") + redis:publish(channel2, "hello2") + + loop:unsubscribe(channel2) + redis:publish(channel2, "hello2") -- skip + + loop:psubscribe("MSG_CHANNEL_*") + redis:publish(channel2, "hello2") + redis:publish(channel3, "hello3") + redis:publish(channel4, "hello4") + + loop:stop() -- read all messages and stop loop + + check.equals(msgs, { + {channel1, "hello1"}, + {channel2, "hello2"}, + -- + {channel2, "hello2"}, + {channel3, "hello3"}, + {channel4, "hello4"}, + }) + + return true +end + +-- private + +_newredis = function(config) + local redis, err = helper.newredis(config) + check.isNil(err, err) + redis:select(_TEST_DB_INDEX) + redis:eval(_CLEANUP_CODE, 0) + return redis +end + +_runcmds = function(redis, commands) + local res = {} + for i, args in ipairs(commands) do + res[i] = redis:doCommand(unpack(args)) + end + return res +end + +return RedisTestCase diff --git a/apps/tests/actions/SessionAction.lua b/apps/tests/actions/SessionAction.lua new file mode 100644 index 0000000..22ed81b --- /dev/null +++ b/apps/tests/actions/SessionAction.lua @@ -0,0 +1,123 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + + +local Session = cc.import("#session") + +local helper = cc.import(".helper") +local tests = cc.import("#tests") +local check = tests.Check + +local SessionTestCase = cc.class("SessionTestCase", tests.TestCase) + +local _KEYS = table.readonly({ + NON_EXISTS_KEY = "NON_EXISTS_KEY", + STRING_KEY = "STRING_KEY", + NUMBER_KEY = "NUMBER_KEY", +}) + +local _newredis + +function SessionTestCase:setup() + local config = self:getInstanceConfig() + self._redis = _newredis(config.server.redis) +end + +function SessionTestCase:teardown() +end + +function SessionTestCase:createAction() + local redis = self._redis + local session = Session:new(redis) + session:start() + + local sid = session:getSid() + check.isString(sid) + + check.isNil(session:get(_KEYS.NON_EXISTS_KEY)) + + local number = math.random(1, 100000) + session:set(_KEYS.NUMBER_KEY, number) + check.equals(session:get(_KEYS.NUMBER_KEY), number) + + local word = "hello " .. tostring(number) + session:set(_KEYS.STRING_KEY, word) + check.equals(session:get(_KEYS.STRING_KEY), word) + + -- save session + session:save() + + -- create an other session use same sid + local session2 = Session:new(redis) + session2:start(sid) + + check.equals(session:get(_KEYS.NUMBER_KEY), number) + check.equals(session:get(_KEYS.STRING_KEY), word) + + -- destroy first session + session:destroy() + + -- second session should is destroyed also + check.isFalse(session2:isAlive()) + + return true +end + +function SessionTestCase:expiredAction() + local redis = self._redis + local session = Session:new(redis, {expired = 1}) + session:start() + + local number = math.random(1, 100000) + session:set(_KEYS.NUMBER_KEY, number) + check.isFalse(session:isAlive()) + check.equals(session:get(_KEYS.NUMBER_KEY), number) + + session:save() + check.isTrue(session:isAlive()) + + local expired = 2 + session:setKeepAlive(expired) + check.equals(session:getExpired(), expired) + helper.sleep(1) + check.isTrue(session:isAlive()) + check.equals(session:get(_KEYS.NUMBER_KEY), number) + + helper.sleep(2) + check.isFalse(session:isAlive()) + + return true +end + +-- private + +_newredis = function(config) + local redis, err = helper.newredis(config) + check.isNil(err, err) + redis:select(_TEST_DB_INDEX) + redis:eval(_CLEANUP_CODE, 0) + return redis +end + +return SessionTestCase diff --git a/apps/tests/actions/helper.lua b/apps/tests/actions/helper.lua new file mode 100644 index 0000000..6ce86ba --- /dev/null +++ b/apps/tests/actions/helper.lua @@ -0,0 +1,34 @@ + +local Redis = cc.import("#redis") +local Beanstalkd = cc.import("#beanstalkd") + +local _M = {} + +_M.newredis = function(config) + local redis = Redis:new() + local ok, err + if config.socket then + ok, err = redis:connect(config.socket) + else + ok, err = redis:connect(config.host, config.port) + end + if not ok then + return nil, err + end + return redis +end + +_M.newbeanstalkd = function(config) + local bean = Beanstalkd:new() + local ok, err = bean:connect(config.host, config.port) + if not ok then + return nil, err + end + return bean +end + +_M.sleep = function(n) + os.execute("sleep " .. tonumber(n)) +end + +return _M diff --git a/apps/tests/conf/app_config.lua b/apps/tests/conf/app_config.lua new file mode 100644 index 0000000..8257d73 --- /dev/null +++ b/apps/tests/conf/app_config.lua @@ -0,0 +1,6 @@ + +local config = { + numOfJobWorkers = 1, +} + +return config diff --git a/apps/tests/conf/app_entry.conf b/apps/tests/conf/app_entry.conf new file mode 100644 index 0000000..dfa5c7b --- /dev/null +++ b/apps/tests/conf/app_entry.conf @@ -0,0 +1,4 @@ + +location /tests/ { + content_by_lua 'nginxBootstrap:runapp("_APP_ROOT_")'; +} diff --git a/apps/tests/jobs/JobsAction.lua b/apps/tests/jobs/JobsAction.lua new file mode 100644 index 0000000..6598963 --- /dev/null +++ b/apps/tests/jobs/JobsAction.lua @@ -0,0 +1,15 @@ + +local gbc = cc.import("#gbc") +local JobsAction = cc.class("JobsAction", gbc.ActionBase) + +JobsAction.ACCEPTED_REQUEST_TYPE = "worker" + +function JobsAction:triggingAction(job) + local key = job.data.key + local number = job.data.number + + local redis = self:getInstance():getRedis() + redis:set(key, number * 2) +end + +return JobsAction diff --git a/apps/tests/shells/run_tests b/apps/tests/shells/run_tests new file mode 100755 index 0000000..00ef59b --- /dev/null +++ b/apps/tests/shells/run_tests @@ -0,0 +1,13 @@ +#!/bin/bash + +CUR_DIR=$(cd "$(dirname $0)" && pwd) +ROOT_DIR=$(dirname "$CUR_DIR") +ROOT_DIR=$(dirname "$ROOT_DIR") +ROOT_DIR=$(dirname "$ROOT_DIR") +source "$ROOT_DIR/bin/shell_func.sh" + +if [ $? -ne 0 ]; then echo "Terminating..." >&2; exit 1; fi + +DEBUG=1 + +$LUA_BIN -e "ROOT_DIR='$ROOT_DIR'; DEBUG=$DEBUG; dofile('$CUR_DIR/run_tests_func.lua'); runTests('$*')" diff --git a/apps/tests/shells/run_tests_func.lua b/apps/tests/shells/run_tests_func.lua new file mode 100644 index 0000000..0ec4754 --- /dev/null +++ b/apps/tests/shells/run_tests_func.lua @@ -0,0 +1,249 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local os_execute = os.execute +local os_remove = os.remove +local string_format = string.format +local string_lower = string.lower +local string_sub = string.sub + +package.path = ROOT_DIR .. "/src/?.lua;" .. ROOT_DIR .. "/apps/tests/?.lua;" .. package.path +require("framework.init") +cc.DEBUG = cc.DEBUG_VERBOSE + +local json = cc.import("#json") +local Factory = cc.import("#gbc").Factory + +-- declare Tests class + +local Tests = cc.class("Tests") + +local _CURL_PATTERN = "curl -s --no-keepalive -o - '%s'" + +local _parseargs, _findtests +local _testsrv, _testcli +local _help + +function Tests:ctor(appConfig, appRootPath) + self._url = string_format("http://localhost:%s/tests/?action=%%s", tostring(appConfig.server.nginx.port)) + self._config = appConfig + self._root = appRootPath +end + +function Tests:run(args) + local opts, err = _parseargs(args) + if not opts then + _help() + return + end + + if #opts.tests == 0 then + local casesDir = self._root .. "/actions" + opts.tests = _findtests(casesDir) + end + + local pass + for _, casename in ipairs(opts.tests) do + if string_sub(casename, -6) ~= "Action" then + -- casename passed from command line arguments + casename = string.ucfirst(string.lower(casename)) .. "Action" + end + + local ok, testCaseClass = pcall(require, "actions." .. casename) + if not ok then + -- testCaseClass is error message + cc.printf("ERR: not found test '%s'\n\n%s", casename, testCaseClass) + break + end + if type(testCaseClass) ~= "table" then + cc.printf("ERR: '%s' isn't module", casename) + break + end + + local actionPackageName = string_lower(string_sub(casename, 1, -7)) + local tests = {} + for methodName, _2 in pairs(testCaseClass) do + if string_sub(methodName, -6) == "Action" then + tests[#tests + 1] = actionPackageName .. "." .. string_lower(string_sub(methodName, 1, -7)) + end + end + + table.sort(tests) + + print(string_format("## Test Case : %s", actionPackageName)) + + for _3, action in ipairs(tests) do + if opts.testsrv then + pass = self:_runtest(_testsrv, {action}, "SERVER " .. action) + if (not pass) and (not opts.continue) then + break + end + end + + if opts.testcli then + pass = self:_runtest(_testcli, {action}, "CLI " .. action) + if (not pass) and (not opts.continue) then + break + end + end + end + + print("") + + if (not pass) and (not opts.continue) then + break + end + + end +end + +function Tests:_runtest(testfun, arg, action) + local result + local err + + local ok, contents = xpcall(function() + return testfun(self, unpack(arg)) + end, function(_err) + err = _err .. debug.traceback("", 4) + end) + + if contents == true then + result = {ok = true} + elseif type(contents) == "table" then + result = contents + else + result = json.decode(tostring(contents)) + if type(result) ~= "table" then + contents = tostring(contents) + contents = string.gsub(contents, "\\n", "\n") + contents = string.gsub(contents, "\\\"", '"') + result = {err = err} + end + end + + if result.err then + print(string_format("[%s] \27[31mfailed\27[0m: %s", action, result.err)) + elseif tostring(result.ok) == "true" or tostring(result.result) == "true" then + print(string_format("[%s] \27[32mok\27[0m", action)) + return true + else + print(string_format("[%s] \27[33minvalid result\27[0m: %s", action, contents)) + end +end + +-- private + +_parseargs = function(args) + local opts = { + continue = false, + testsrv = true, + testcli = true, + tests = {} + } + for _, arg in ipairs(string.split(args, " ")) do + if arg == "-h" then + return + elseif arg == "-c" then + opts.continue = true + elseif arg == "-ns" then + opts.testsrv = false + elseif arg == "-nc" then + opts.testcli = false + elseif string.sub(arg, 1, 1) == "-" then + print("Invalid options") + return + else + opts.tests[#opts.tests + 1] = arg + end + end + + return opts +end + +_findtests = function(rootdir) + local cmd = string_format('ls "%s"', rootdir) + local h = io.popen(cmd) + local res = h:read("*a") + h:close() + + local cases = {} + for _, file in ipairs(string.split(res, "\n")) do + if string.sub(file, -10) == "Action.lua" then + cases[#cases + 1] = string.sub(file, 1, -5) + end + end + + table.sort(cases) + + return cases +end + +_testsrv = function(self, action) + local url = string_format(self._url, action) + local cmd = string_format(_CURL_PATTERN, url) + local h = io.popen(cmd) + local res = h:read("*a") + h:close() + return res +end + +_testcli = function(self, action) + local config = table.copy(self._config) + config.app.package = "actions" + local cmd = Factory.create(config, "CommandLineInstance", arg) + return cmd:runAction(action) +end + +_help = function() + print [[ + +$ run_tests.sh [options] [test case name ...] + +options: +-h: show help +-c: continue when test failed +-ns: skip server tests +-nc: skip cli tests + +examples: + +# run JobsTestCase and RedisTestCase +run_tests.sh jobs redis + +]] + +end + +-- bootstrap + +local appKeys = dofile(ROOT_DIR .. "/tmp/app_keys.lua") +local globalConfig = dofile(ROOT_DIR .. "/tmp/config.lua") +local appConfigs = Factory.makeAppConfigs(appKeys, globalConfig, package.path) +local appRootPath = ROOT_DIR .. "/apps/tests" +local appConfig = appConfigs[appRootPath] + +cc.exports.runTests = function(arg) + local tests = Tests:new(appConfig, appRootPath) + tests:run(arg) +end diff --git a/apps/tests/shells/show_nginx_error_log b/apps/tests/shells/show_nginx_error_log new file mode 100755 index 0000000..bba81e9 --- /dev/null +++ b/apps/tests/shells/show_nginx_error_log @@ -0,0 +1,11 @@ +#!/bin/bash + +CUR_DIR=$(cd "$(dirname $0)" && pwd) +ROOT_DIR=$(dirname "$CUR_DIR") +ROOT_DIR=$(dirname "$ROOT_DIR") +ROOT_DIR=$(dirname "$ROOT_DIR") +source "$ROOT_DIR/bin/shell_func.sh" + +if [ $? -ne 0 ] ; then echo "Terminating..." >&2; exit 1; fi + +multitail -csn -cS Apache -ke '^[0-9/]+ [0-9][0-9]:' -ke ' [0-9]+#[0-9]+: ' -ke ' debug\.lua:[0-9]+: _?print[a-z]+\(\):' -ke ', client: .+$' -ke '\[lua\] debug.lua:[0-9]+: dump\(\)' "$ROOT_DIR/logs/nginx-error.log" diff --git a/apps/welcome/WebSocketConnect.lua b/apps/welcome/WebSocketConnect.lua deleted file mode 100644 index efd21ab..0000000 --- a/apps/welcome/WebSocketConnect.lua +++ /dev/null @@ -1,56 +0,0 @@ - -local WebSocketConnectBase = require("server.base.WebSocketConnectBase") -local WebSocketConnect = class("WebSocketConnect", WebSocketConnectBase) - -local ConnectIdService = cc.load("connectid").service -local OnlineService = cc.load("online").service - -function WebSocketConnect:ctor(config) - printInfo("new WebSocketConnect instance") - WebSocketConnect.super.ctor(self, config) -end - -function WebSocketConnect:onUserAdd(event) - local userdata = self.online:get(event.username) - self:sendMessageToSelf({name = "adduser", username = event.username, tag = userdata.tag}) -end - -function WebSocketConnect:onUserRemove(event) - self:sendMessageToSelf({name = "removeuser", username = event.username}) -end - -function WebSocketConnect:afterConnectReady() - -- add user to online list - local session = self:getSession() - local username = session:get("username") - local tag = session:get("tag") - self.online = OnlineService:create(self) - self.online:add(username, {tag = tag}) - - -- register events - self.online:addEventListener(OnlineService.USER_ADD_EVENT, handler(self, self.onUserAdd)) - self.online:addEventListener(OnlineService.USER_REMOVE_EVENT, handler(self, self.onUserRemove)) - self.online:setEventsEnabled(true) - - -- send all users name to client - local all = self.online:getAll() - self:sendMessageToSelf({name = "allusers", users = all}) - - -- set connect tag - local connectId = self:getConnectId() - self.connects = ConnectIdService:create(self:getRedis()) - self.connects:setTag(connectId, tag) -end - -function WebSocketConnect:beforeConnectClose() - -- remove user from online list - local session = self:getSession() - local username = session:get("username") - self.online:remove(username) - - -- remove connect tag - local connectId = self:getConnectId() - self.connects:removeTag(connectId) -end - -return WebSocketConnect diff --git a/apps/welcome/WebSocketInstance.lua b/apps/welcome/WebSocketInstance.lua new file mode 100644 index 0000000..4e265a9 --- /dev/null +++ b/apps/welcome/WebSocketInstance.lua @@ -0,0 +1,87 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local Online = cc.import("#online") +local Session = cc.import("#session") + +local gbc = cc.import("#gbc") +local WebSocketInstance = cc.class("WebSocketInstance", gbc.WebSocketInstanceBase) + +function WebSocketInstance:ctor(config) + WebSocketInstance.super.ctor(self, config) + self._event:bind(WebSocketInstance.EVENT.CONNECTED, cc.handler(self, self.onConnected)) + self._event:bind(WebSocketInstance.EVENT.DISCONNECTED, cc.handler(self, self.onDisconnected)) +end + +function WebSocketInstance:onConnected() + local redis = self:getRedis() + + -- load session + local sid = self:getConnectToken() -- token is session id + local session = Session:new(redis) + session:start(sid) + + -- add user to online users list + local online = Online:new(self) + local username = session:get("username") + online:add(username, self:getConnectId()) + + -- send all usernames to current client + local users = online:getAll() + online:sendMessage(username, {name = "LIST_ALL_USERS", users = users}) + -- subscribe online users event + self:subscribe(online:getChannel()) + + self._username = username + self._session = session + self._online = online +end + +function WebSocketInstance:onDisconnected(event) + if event.reason ~= gbc.Constants.CLOSE_CONNECT then + -- connection interrupted unexpectedly, remove user from online list + cc.printwarn("[websocket:%s] connection interrupted unexpectedly", self:getConnectId()) + local username = self._session:get("username") + self._online:remove(username) + end +end + +function WebSocketInstance:heartbeat() + -- refresh session + self._session:setKeepAlive() +end + +function WebSocketInstance:getUsername() + return self._username +end + +function WebSocketInstance:getSession() + return self._session +end + +function WebSocketInstance:getOnline() + return self._online +end + +return WebSocketInstance diff --git a/apps/welcome/actions/ChatAction.lua b/apps/welcome/actions/ChatAction.lua index fe84d78..9e2a86c 100644 --- a/apps/welcome/actions/ChatAction.lua +++ b/apps/welcome/actions/ChatAction.lua @@ -1,34 +1,44 @@ -local ChatAction = class("ChatAction") +local gbc = cc.import("#gbc") +local ChatAction = cc.class("ChatAction", gbc.ActionBase) -function ChatAction:ctor(connect) - self.connect = connect - self.connects = connect.connects -end +ChatAction.ACCEPTED_REQUEST_TYPE = "websocket" function ChatAction:sendmessageAction(arg) - local tag = arg.tag - if not tag then - throw("not set argument: \"tag\"") + local recipient = arg.recipient + if not recipient then + cc.throw("not set argument: \"recipient\"") end - -- get connect id by tag - local connectId = self.connects:getIdByTag(tag) - if not connectId then - throw("not found connect id by tag \"%s\"", tag) + + local message = arg.message + if not message then + cc.throw("not set argument: \"message\"") end + -- forward message to other client + local instance = self:getInstance() + instance:getOnline():sendMessage(recipient, { + name = "MESSAGE", + sender = instance:getUsername(), + recipient = recipient, + body = message, + }) +end + +function ChatAction:sendmessagetoallAction(arg) local message = arg.message if not message then - throw("not set argument: \"message\"") + cc.throw("not set argument: \"message\"") end - local session = self.connect:getSession() - local data = { - src = session:get("tag"), - username = session:get("username"), - message = message, - } - self.connect:sendMessageToConnect(connectId, data) + -- forward message to all clients + local instance = self:getInstance() + instance:getOnline():sendMessageToAll({ + name = "MESSAGE", + sender = instance:getUsername(), + recipient = recipient, + body = message, + }) end return ChatAction diff --git a/apps/welcome/actions/UserAction.lua b/apps/welcome/actions/UserAction.lua index 14ba437..f9cd38e 100644 --- a/apps/welcome/actions/UserAction.lua +++ b/apps/welcome/actions/UserAction.lua @@ -1,77 +1,127 @@ +--[[ -local UserAction = class("UserAction") +Copyright (c) 2015 gameboxcloud.com -local OnlineService = cc.load("online").service +Permission is hereby granted, free of chargse, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -function UserAction:ctor(connect) - self.connect = connect - self.online = OnlineService:create(connect) -end +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -function UserAction:loginAction(arg) - local username = arg.username - if not username then - throw("not set argument: \"username\"") - end +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] - -- check username is exists - if self.online:isExists(username) then - throw("username \"%s\" is exists", username) +local Online = cc.import("#online") +local Session = cc.import("#session") + +local gbc = cc.import("#gbc") +local UserAction = cc.class("UserAction", gbc.ActionBase) + +local _opensession + +function UserAction:signinAction(args) + local username = args.username + if not username then + cc.throw("not set argsument: \"username\"") end -- start session - local session = self.connect:newSession() + local session = Session:new(self:getInstance():getRedis()) + session:start() session:set("username", username) + session:set("count", 0) + session:save() - -- generating tag by session id - local sid = session:getSid() - local tag = ngx.md5(sid) - session:set("tag", tag) + -- return result + return {sid = session:getSid(), count = 0} +end - -- set count value - local count = 0 +function UserAction:signoutAction(args) + -- remove user from online list + local session = _opensession(self:getInstance(), args) + local online = Online:new(self:getInstance()) + online:remove(session:get("username")) + -- delete session + session:destroy() + return {ok = "ok"} +end + +function UserAction:countAction(args) + -- update count value in session + local session = _opensession(self:getInstance(), args) + local count = session:get("count") + count = count + 1 session:set("count", count) session:save() - -- return result - return {sid = sid, tag = tag, count = count} + return {count = count} end -function UserAction:logoutAction(arg) - local sid = arg.sid +function UserAction:addjobAction(args) + local sid = args.sid if not sid then - throw("not set argument: \"sid\"") + cc.throw("not set argsument: \"sid\"") end - local session = self.connect:openSession(sid) - if not session then - throw("session is expired, or invalid session id") + local instance = self:getInstance() + local redis = instance:getRedis() + local session = Session:new(redis) + if not session:start(sid) then + cc.throw("session is expired, or invalid session id") end - -- close websocket connect - self.connect:closeConnect(session:getConnectId()) - -- destroy session - self.connect:destroySession() - return {ok = "ok"} + local delay = cc.checkint(args.delay) + if delay <= 0 then + delay = 1 + end + local message = args.message + if not message then + cc.throw("not set argument: \"message\"") + end + + -- send message to job + local jobs = instance:getJobs() + local job = { + action = "/jobs/jobs.echo", + delay = delay, + data = { + username = session:get("username"), + message = message, + } + } + local ok, err = jobs:add(job) + if not ok then + return {err = err} + else + return {ok = "ok"} + end end -function UserAction:countAction(arg) - local sid = arg.sid +-- private + +_opensession = function(instance, args) + local sid = args.sid if not sid then - throw("not set argument: \"sid\"") + cc.throw("not set argsument: \"sid\"") end - local session = self.connect:openSession(sid) - if not session then - throw("session is expired, or invalid session id") + local session = Session:new(instance:getRedis()) + if not session:start(sid) then + cc.throw("session is expired, or invalid session id") end - -- update count value - local count = session:get("count") - count = count + 1 - session:set("count", count) - session:save() - return {count = count} + return session end return UserAction diff --git a/apps/welcome/conf/app_entry.conf b/apps/welcome/conf/app_entry.conf new file mode 100644 index 0000000..bce0ea5 --- /dev/null +++ b/apps/welcome/conf/app_entry.conf @@ -0,0 +1,9 @@ + +location / { + root '_APP_ROOT_/public_html'; + index index.html; +} + +location /welcome/ { + content_by_lua 'nginxBootstrap:runapp("_APP_ROOT_")'; +} diff --git a/apps/welcome/jobs/JobsAction.lua b/apps/welcome/jobs/JobsAction.lua new file mode 100644 index 0000000..3f7f8ce --- /dev/null +++ b/apps/welcome/jobs/JobsAction.lua @@ -0,0 +1,21 @@ + +local Online = cc.import("#online") + +local gbc = cc.import("#gbc") +local JobsAction = cc.class("JobsAction", gbc.ActionBase) + +JobsAction.ACCEPTED_REQUEST_TYPE = "worker" + +function JobsAction:echoAction(job) + local username = job.data.username + local message = job.data.message + + local online = Online:new(self:getInstance()) + online:sendMessage(username, { + name = "MESSAGE", + sender = username, + body = string.format("'%s' do a job, message is '%s', delay is %d", username, message, job.delay), + }) +end + +return JobsAction diff --git a/apps/welcome/packages/online/OnlineService.lua b/apps/welcome/packages/online/OnlineService.lua deleted file mode 100644 index 46a2fff..0000000 --- a/apps/welcome/packages/online/OnlineService.lua +++ /dev/null @@ -1,96 +0,0 @@ - -local ngx_null = ngx.null -local table_map = table.map -local json_encode = json.encode -local json_decode = json.decode - -local OnlineService = class("OnlineService") - -OnlineService.USER_ADD_EVENT = "USER_ADD_EVENT" -OnlineService.USER_REMOVE_EVENT = "USER_REMOVE_EVENT" - -local _ONLINE_KEY_PREFIX = "_OL_" -local _ONLINE_SET = "_ONLINES_USERS" -local _ONLINE_CHANNEL = "_ONLINES" - -local _ADD_MESSAGE = "add" -local _REMOVE_MESSAGE = "remove" - -local function _key(username) - return _ONLINE_KEY_PREFIX .. tostring(username) -end - -function OnlineService:ctor(connect) - cc.bind(self, "event") - self.connect = connect - self.redis = connect:getRedis() - self.eventsEnabled = false -end - -function OnlineService:setEventsEnabled(enabled) - if self.eventsEnabled == enabled then return end - - self.eventsEnabled = enabled - if enabled then - self.connect:subscribeChannel(_ONLINE_CHANNEL, function(message) - local message = json_decode(message) - if message.name == _ADD_MESSAGE then - self:dispatchEvent({name = OnlineService.USER_ADD_EVENT, username = message.username}) - elseif message.name == _REMOVE_MESSAGE then - self:dispatchEvent({name = OnlineService.USER_REMOVE_EVENT, username = message.username}) - end - end) - else - self.connect:unsubscribeChannel(_ONLINE_CHANNEL) - end -end - -function OnlineService:isExists(username) - return tostring(self.redis:command("EXISTS", _key(username))) == "1" -end - -function OnlineService:get(username) - local res = self.redis:command("GET", _key(username)) - if not res then return {} end - return json_decode(res) -end - -function OnlineService:getAll() - local usernames = self.redis:command("SMEMBERS", _ONLINE_SET) - if #usernames == 0 then return {} end - - local keys = table_map(usernames, function(v, k) - return _ONLINE_KEY_PREFIX .. v - end) - local all = self.redis:command("MGET", unpack(keys)) - local res = {} - for i, username in ipairs(usernames) do - if all[i] ~= ngx_null then - local userdata = json_decode(all[i]) - res[#res + 1] = {username = username, tag = userdata.tag} - end - end - return res -end - -function OnlineService:getAllUsername() - return self.redis:command("SMEMBERS", _ONLINE_SET) -end - -function OnlineService:add(username, data) - local pipe = self.redis:newPipeline() - pipe:command("SET", _key(username), json_encode(data)) - pipe:command("SADD", _ONLINE_SET, username) - pipe:command("PUBLISH", _ONLINE_CHANNEL, json_encode({name = _ADD_MESSAGE, username = username})) - pipe:commit() -end - -function OnlineService:remove(username) - local pipe = self.redis:newPipeline() - pipe:command("DEL", _key(username)) - pipe:command("SREM", _ONLINE_SET, username) - pipe:command("PUBLISH", _ONLINE_CHANNEL, json_encode({name = _REMOVE_MESSAGE, username = username})) - pipe:commit() -end - -return OnlineService diff --git a/apps/welcome/packages/online/init.lua b/apps/welcome/packages/online/init.lua deleted file mode 100644 index 16a0381..0000000 --- a/apps/welcome/packages/online/init.lua +++ /dev/null @@ -1,6 +0,0 @@ - -local _P = {} - -_P.service = import(".OnlineService") - -return _P diff --git a/apps/welcome/packages/online/online.lua b/apps/welcome/packages/online/online.lua new file mode 100644 index 0000000..1422dbd --- /dev/null +++ b/apps/welcome/packages/online/online.lua @@ -0,0 +1,113 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local string_format = string.format + +local json = cc.import("#json") +local gbc = cc.import("#gbc") + +local Online = cc.class("Online") + +local _ONLINE_SET = "_ONLINE_USERS" +local _ONLINE_CHANNEL = "_ONLINE_CHANNEL" +local _EVENT = table.readonly({ + ADD_USER = "ADD_USER", + REMOVE_USER = "REMOVE_USER", +}) +local _CONNECT_TO_USERNAME = "_CONNECT_TO_USERNAME" +local _USERNAME_TO_CONNECT = "_USERNAME_TO_CONNECT" + +function Online:ctor(instance) + self._instance = instance + self._redis = instance:getRedis() + self._broadcast = gbc.Broadcast:new(self._redis, instance.config.app.websocketMessageFormat) +end + +function Online:getAll() + return self._redis:smembers(_ONLINE_SET) +end + +function Online:add(username, connectId) + local redis = self._redis + redis:initPipeline() + -- map username <-> connect id + redis:hset(_CONNECT_TO_USERNAME, connectId, username) + redis:hset(_USERNAME_TO_CONNECT, username, connectId) + -- add username to set + redis:sadd(_ONLINE_SET, username) + -- send event to all clients + redis:publish(_ONLINE_CHANNEL, json.encode({name = _EVENT.ADD_USER, username = username})) + return redis:commitPipeline() +end + +function Online:remove(username) + local redis = self._redis + local connectId, err = redis:hget(_USERNAME_TO_CONNECT, username) + if not connectId then + return nil, err + end + if connectId == redis.null then + return nil, string_format("not found username '%s'", username) + end + + redis:initPipeline() + -- remove map + redis:hdel(_CONNECT_TO_USERNAME, connectId) + redis:hdel(_USERNAME_TO_CONNECT, username) + -- remove username from set + redis:srem(_ONLINE_SET, username) + redis:publish(_ONLINE_CHANNEL, json.encode({name = _EVENT.REMOVE_USER, username = username})) + local res, err = redis:commitPipeline() + if not res then + return nil, err + end + + return self._broadcast:sendControlMessage(connectId, gbc.Constants.CLOSE_CONNECT) +end + +function Online:getChannel() + return _ONLINE_CHANNEL +end + +function Online:sendMessage(recipient, event) + local redis = self._redis + -- query connect id by recipient + local connectId, err = redis:hget(_USERNAME_TO_CONNECT, recipient) + if not connectId then + return nil, err + end + + if connectId == redis.null then + return nil, string_format("not found recipient '%s'", recipient) + end + + -- send message to connect id + return self._broadcast:sendMessage(connectId, event) +end + +function Online:sendMessageToAll(event) + return self._broadcast:sendMessageToAll(event) +end + +return Online diff --git a/apps/welcome/public_html/css/chartist.custom.css b/apps/welcome/public_html/css/chartist.custom.css deleted file mode 100644 index f274f0c..0000000 --- a/apps/welcome/public_html/css/chartist.custom.css +++ /dev/null @@ -1,13 +0,0 @@ - -.ct-line { - stroke: #c6f43d !important; -} - -.ct-point { - stroke: #79d105 !important; - fill: #79d105 !important; -} - -.ct-area { - fill: #5bf04f !important; -} diff --git a/apps/welcome/public_html/css/chartist.min.css b/apps/welcome/public_html/css/chartist.min.css deleted file mode 100644 index 1e0756d..0000000 --- a/apps/welcome/public_html/css/chartist.min.css +++ /dev/null @@ -1 +0,0 @@ -.ct-chart .ct-label,.ct-chart .ct-label.ct-horizontal{display:block;width:100%;height:100%;fill:rgba(0,0,0,.4);color:rgba(0,0,0,.4);font-size:.50rem;text-align:left}.ct-chart .ct-label.ct-vertical{display:block;width:100%;height:100%;fill:rgba(0,0,0,.4);color:rgba(0,0,0,.4);font-size:.50rem;text-align:right}.ct-chart .ct-grid{stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:2px}.ct-chart .ct-point{stroke-width:6px;stroke-linecap:round}.ct-chart .ct-line{fill:none;stroke-width:1px}.ct-chart .ct-area{stroke:none;fill-opacity:.1}.ct-chart .ct-bar{fill:none;stroke-width:10px}.ct-chart .ct-slice.ct-donut{fill:none;stroke-width:60px}.ct-chart .ct-series.ct-series-a .ct-bar,.ct-chart .ct-series.ct-series-a .ct-line,.ct-chart .ct-series.ct-series-a .ct-point,.ct-chart .ct-series.ct-series-a .ct-slice.ct-donut{stroke:#d70206}.ct-chart .ct-series.ct-series-a .ct-area,.ct-chart .ct-series.ct-series-a .ct-slice:not(.ct-donut){fill:#d70206}.ct-chart .ct-series.ct-series-b .ct-bar,.ct-chart .ct-series.ct-series-b .ct-line,.ct-chart .ct-series.ct-series-b .ct-point,.ct-chart .ct-series.ct-series-b .ct-slice.ct-donut{stroke:#f05b4f}.ct-chart .ct-series.ct-series-b .ct-area,.ct-chart .ct-series.ct-series-b .ct-slice:not(.ct-donut){fill:#f05b4f}.ct-chart .ct-series.ct-series-c .ct-bar,.ct-chart .ct-series.ct-series-c .ct-line,.ct-chart .ct-series.ct-series-c .ct-point,.ct-chart .ct-series.ct-series-c .ct-slice.ct-donut{stroke:#f4c63d}.ct-chart .ct-series.ct-series-c .ct-area,.ct-chart .ct-series.ct-series-c .ct-slice:not(.ct-donut){fill:#f4c63d}.ct-chart .ct-series.ct-series-d .ct-bar,.ct-chart .ct-series.ct-series-d .ct-line,.ct-chart .ct-series.ct-series-d .ct-point,.ct-chart .ct-series.ct-series-d .ct-slice.ct-donut{stroke:#d17905}.ct-chart .ct-series.ct-series-d .ct-area,.ct-chart .ct-series.ct-series-d .ct-slice:not(.ct-donut){fill:#d17905}.ct-chart .ct-series.ct-series-e .ct-bar,.ct-chart .ct-series.ct-series-e .ct-line,.ct-chart .ct-series.ct-series-e .ct-point,.ct-chart .ct-series.ct-series-e .ct-slice.ct-donut{stroke:#453d3f}.ct-chart .ct-series.ct-series-e .ct-area,.ct-chart .ct-series.ct-series-e .ct-slice:not(.ct-donut){fill:#453d3f}.ct-chart .ct-series.ct-series-f .ct-bar,.ct-chart .ct-series.ct-series-f .ct-line,.ct-chart .ct-series.ct-series-f .ct-point,.ct-chart .ct-series.ct-series-f .ct-slice.ct-donut{stroke:#59922b}.ct-chart .ct-series.ct-series-f .ct-area,.ct-chart .ct-series.ct-series-f .ct-slice:not(.ct-donut){fill:#59922b}.ct-chart .ct-series.ct-series-g .ct-bar,.ct-chart .ct-series.ct-series-g .ct-line,.ct-chart .ct-series.ct-series-g .ct-point,.ct-chart .ct-series.ct-series-g .ct-slice.ct-donut{stroke:#0544d3}.ct-chart .ct-series.ct-series-g .ct-area,.ct-chart .ct-series.ct-series-g .ct-slice:not(.ct-donut){fill:#0544d3}.ct-chart .ct-series.ct-series-h .ct-bar,.ct-chart .ct-series.ct-series-h .ct-line,.ct-chart .ct-series.ct-series-h .ct-point,.ct-chart .ct-series.ct-series-h .ct-slice.ct-donut{stroke:#6b0392}.ct-chart .ct-series.ct-series-h .ct-area,.ct-chart .ct-series.ct-series-h .ct-slice:not(.ct-donut){fill:#6b0392}.ct-chart .ct-series.ct-series-i .ct-bar,.ct-chart .ct-series.ct-series-i .ct-line,.ct-chart .ct-series.ct-series-i .ct-point,.ct-chart .ct-series.ct-series-i .ct-slice.ct-donut{stroke:#f05b4f}.ct-chart .ct-series.ct-series-i .ct-area,.ct-chart .ct-series.ct-series-i .ct-slice:not(.ct-donut){fill:#f05b4f}.ct-chart .ct-series.ct-series-j .ct-bar,.ct-chart .ct-series.ct-series-j .ct-line,.ct-chart .ct-series.ct-series-j .ct-point,.ct-chart .ct-series.ct-series-j .ct-slice.ct-donut{stroke:#dda458}.ct-chart .ct-series.ct-series-j .ct-area,.ct-chart .ct-series.ct-series-j .ct-slice:not(.ct-donut){fill:#dda458}.ct-chart .ct-series.ct-series-k .ct-bar,.ct-chart .ct-series.ct-series-k .ct-line,.ct-chart .ct-series.ct-series-k .ct-point,.ct-chart .ct-series.ct-series-k .ct-slice.ct-donut{stroke:#eacf7d}.ct-chart .ct-series.ct-series-k .ct-area,.ct-chart .ct-series.ct-series-k .ct-slice:not(.ct-donut){fill:#eacf7d}.ct-chart .ct-series.ct-series-l .ct-bar,.ct-chart .ct-series.ct-series-l .ct-line,.ct-chart .ct-series.ct-series-l .ct-point,.ct-chart .ct-series.ct-series-l .ct-slice.ct-donut{stroke:#86797d}.ct-chart .ct-series.ct-series-l .ct-area,.ct-chart .ct-series.ct-series-l .ct-slice:not(.ct-donut){fill:#86797d}.ct-chart .ct-series.ct-series-m .ct-bar,.ct-chart .ct-series.ct-series-m .ct-line,.ct-chart .ct-series.ct-series-m .ct-point,.ct-chart .ct-series.ct-series-m .ct-slice.ct-donut{stroke:#b2c326}.ct-chart .ct-series.ct-series-m .ct-area,.ct-chart .ct-series.ct-series-m .ct-slice:not(.ct-donut){fill:#b2c326}.ct-chart .ct-series.ct-series-n .ct-bar,.ct-chart .ct-series.ct-series-n .ct-line,.ct-chart .ct-series.ct-series-n .ct-point,.ct-chart .ct-series.ct-series-n .ct-slice.ct-donut{stroke:#6188e2}.ct-chart .ct-series.ct-series-n .ct-area,.ct-chart .ct-series.ct-series-n .ct-slice:not(.ct-donut){fill:#6188e2}.ct-chart .ct-series.ct-series-o .ct-bar,.ct-chart .ct-series.ct-series-o .ct-line,.ct-chart .ct-series.ct-series-o .ct-point,.ct-chart .ct-series.ct-series-o .ct-slice.ct-donut{stroke:#a748ca}.ct-chart .ct-series.ct-series-o .ct-area,.ct-chart .ct-series.ct-series-o .ct-slice:not(.ct-donut){fill:#a748ca}.ct-chart.ct-square{display:block;position:relative;width:100%}.ct-chart.ct-square:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:100%}.ct-chart.ct-square:after{content:"";display:table;clear:both}.ct-chart.ct-square>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-minor-second{display:block;position:relative;width:100%}.ct-chart.ct-minor-second:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:93.75%}.ct-chart.ct-minor-second:after{content:"";display:table;clear:both}.ct-chart.ct-minor-second>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-major-second{display:block;position:relative;width:100%}.ct-chart.ct-major-second:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:88.8888888889%}.ct-chart.ct-major-second:after{content:"";display:table;clear:both}.ct-chart.ct-major-second>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-minor-third{display:block;position:relative;width:100%}.ct-chart.ct-minor-third:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:83.3333333333%}.ct-chart.ct-minor-third:after{content:"";display:table;clear:both}.ct-chart.ct-minor-third>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-major-third{display:block;position:relative;width:100%}.ct-chart.ct-major-third:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:80%}.ct-chart.ct-major-third:after{content:"";display:table;clear:both}.ct-chart.ct-major-third>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-perfect-fourth{display:block;position:relative;width:100%}.ct-chart.ct-perfect-fourth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:75%}.ct-chart.ct-perfect-fourth:after{content:"";display:table;clear:both}.ct-chart.ct-perfect-fourth>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-perfect-fifth{display:block;position:relative;width:100%}.ct-chart.ct-perfect-fifth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:66.6666666667%}.ct-chart.ct-perfect-fifth:after{content:"";display:table;clear:both}.ct-chart.ct-perfect-fifth>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-minor-sixth{display:block;position:relative;width:100%}.ct-chart.ct-minor-sixth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:62.5%}.ct-chart.ct-minor-sixth:after{content:"";display:table;clear:both}.ct-chart.ct-minor-sixth>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-golden-section{display:block;position:relative;width:100%}.ct-chart.ct-golden-section:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:61.804697157%}.ct-chart.ct-golden-section:after{content:"";display:table;clear:both}.ct-chart.ct-golden-section>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-major-sixth{display:block;position:relative;width:100%}.ct-chart.ct-major-sixth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:60%}.ct-chart.ct-major-sixth:after{content:"";display:table;clear:both}.ct-chart.ct-major-sixth>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-minor-seventh{display:block;position:relative;width:100%}.ct-chart.ct-minor-seventh:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:56.25%}.ct-chart.ct-minor-seventh:after{content:"";display:table;clear:both}.ct-chart.ct-minor-seventh>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-major-seventh{display:block;position:relative;width:100%}.ct-chart.ct-major-seventh:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:53.3333333333%}.ct-chart.ct-major-seventh:after{content:"";display:table;clear:both}.ct-chart.ct-major-seventh>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-octave{display:block;position:relative;width:100%}.ct-chart.ct-octave:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:50%}.ct-chart.ct-octave:after{content:"";display:table;clear:both}.ct-chart.ct-octave>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-major-tenth{display:block;position:relative;width:100%}.ct-chart.ct-major-tenth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:40%}.ct-chart.ct-major-tenth:after{content:"";display:table;clear:both}.ct-chart.ct-major-tenth>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-major-eleventh{display:block;position:relative;width:100%}.ct-chart.ct-major-eleventh:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:37.5%}.ct-chart.ct-major-eleventh:after{content:"";display:table;clear:both}.ct-chart.ct-major-eleventh>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-major-twelfth{display:block;position:relative;width:100%}.ct-chart.ct-major-twelfth:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:33.3333333333%}.ct-chart.ct-major-twelfth:after{content:"";display:table;clear:both}.ct-chart.ct-major-twelfth>svg{display:block;position:absolute;top:0;left:0}.ct-chart.ct-double-octave{display:block;position:relative;width:100%}.ct-chart.ct-double-octave:before{display:block;float:left;content:"";width:0;height:0;padding-bottom:25%}.ct-chart.ct-double-octave:after{content:"";display:table;clear:both}.ct-chart.ct-double-octave>svg{display:block;position:absolute;top:0;left:0} diff --git a/apps/welcome/public_html/css/custom.css b/apps/welcome/public_html/css/custom.css index dfc9e9f..035cfdd 100644 --- a/apps/welcome/public_html/css/custom.css +++ b/apps/welcome/public_html/css/custom.css @@ -6,3 +6,17 @@ pre { white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ } + +article { + margin-top: 3em; + font-size: 110%; + line-height: 150% +} + +article li { + margin-bottom: 1em; +} + +article blockquote { + font-size: 85%; +} diff --git a/apps/welcome/public_html/css/notify.almost-flat.min.css b/apps/welcome/public_html/css/notify.almost-flat.min.css new file mode 100644 index 0000000..5ad6fca --- /dev/null +++ b/apps/welcome/public_html/css/notify.almost-flat.min.css @@ -0,0 +1,2 @@ +/*! UIkit 2.24.2 | http://www.getuikit.com | (c) 2014 YOOtheme | MIT License */ +.uk-notify{position:fixed;top:10px;left:10px;z-index:1040;box-sizing:border-box;width:350px}.uk-notify-bottom-right,.uk-notify-top-right{left:auto;right:10px}.uk-notify-bottom-center,.uk-notify-top-center{left:50%;margin-left:-175px}.uk-notify-bottom-center,.uk-notify-bottom-left,.uk-notify-bottom-right{top:auto;bottom:10px}@media (max-width:479px){.uk-notify{left:10px;right:10px;width:auto;margin:0}}.uk-notify-message{position:relative;margin-bottom:10px;padding:15px;background:#444;color:#fff;font-size:16px;line-height:22px;cursor:pointer;border:1px solid #444;border-radius:4px}.uk-notify-message>.uk-close{visibility:hidden;float:right}.uk-notify-message:hover>.uk-close{visibility:visible}.uk-notify-message-primary{background:#ebf7fd;color:#2d7091;border-color:rgba(45,112,145,.3)}.uk-notify-message-success{background:#f2fae3;color:#659f13;border-color:rgba(101,159,19,.3)}.uk-notify-message-warning{background:#fffceb;color:#e28327;border-color:rgba(226,131,39,.3)}.uk-notify-message-danger{background:#fff1f0;color:#d85030;border-color:rgba(216,80,48,.3)} \ No newline at end of file diff --git a/apps/welcome/public_html/tests.html b/apps/welcome/public_html/demo.html similarity index 50% rename from apps/welcome/public_html/tests.html rename to apps/welcome/public_html/demo.html index de89781..fec8261 100644 --- a/apps/welcome/public_html/tests.html +++ b/apps/welcome/public_html/demo.html @@ -11,34 +11,33 @@ - - + - - + + -
+
@@ -49,77 +48,90 @@

Sign in GameBox Cloud Core

- - + +
- - - + + +
- - + + +
+
+
+
+
+

Send message to user

+
+
- - + +
- - - + + + +
-

Send message to Connect

+

Send message to Delay Job

- - + + + +     + + +     + +
- - -
- -
- - - + + +
+
-
+
+

Logs


-                
-                
+                
+                
             
-
+
-

GameBox Cloud Core Tests

+

GameBox Cloud Core Demo

- +

@@ -128,10 +140,10 @@

GameBox Cloud Core Tests

+ diff --git a/apps/welcome/public_html/index.html b/apps/welcome/public_html/index.html index 4fa9220..76433c8 100644 --- a/apps/welcome/public_html/index.html +++ b/apps/welcome/public_html/index.html @@ -11,70 +11,98 @@ - - - -
-
-
INIT: GET SERVER STATUS.
-
ERROR: CAN'T GET SERVER STATUS.
-

Node Status
realtime - last 60s

-
-
+
+
-
-
-
-

CPU Load

-
-
-
-
-
-

MEM Usage

-
-
-
-
+
+ +

Welcome to GameBox Cloud Core

+ +

GameBox Cloud Core 为开发者提供一个稳定可靠,可伸缩的服务端架构,让开发者可以使用 Lua 脚本语言快速完成服务端的功能开发。

+ +

主要特征:

+ +
    +
  • +

    稳定可靠、经过验证的高性能游戏服务端架构

    + +

    基于 OpenResty_ 和 LuaJIT_ 架构,得到了包括 CloudFlare 等大型机构的应用,无论是稳定性还是性能都得到了验证。

    + +

    GameBox Cloud Core 在 OpenResty 之上封装了一个 Lua Server Framework,为开发者创建游戏服务端功能提供了一个容易学习、容易扩展的基础架构。

    + + +
  • + +
  • +

    使用 Lua 脚本语言开发服务端功能

    + +

    也许您认为在服务端使用 Lua 脚本显得有点不务正业,但 NodeJS 的流行却证明了合适的基础架构可以让一种语言突破原本的应用场景。更何况相比 NodeJS,OpenResty 提供的同步非阻塞编程模型,可以避免写出大量的嵌套 callback,不管是从开发效率还是维护成本上来说都更胜 NodeJS。

    + +

    用 Lua 脚本语言开发服务端功能还有一个巨大的好处,那就是可以和使用 Cocos2d-Lua(quick-cocos2d-x)的客户端共享大量代码。比如数据 Schema 定义、数据对象、游戏逻辑等等,都可以在客户端和服务端之间共享同一份代码。做过网络游戏的同学一定对如何保持客户端和服务端代码在数据接口上的一致头疼过。现在使用 GameBox Cloud Core,这些问题统统消失不见。

    +
  • + +
  • +

    支持短连接和长连接,满足从异步网络到实时网络的各种需求

    + +

    GameBox Cloud Core 支持 HTTP 和 WebSocket 两种连接方式,分别对应短连接和长连接,满足了异步和实时网络游戏的需求。

    + +
    +

    WebSocket 是一种通讯协议。在连接时通过 HTTP 协议进行。在客户端和服务端连接成功后,则变成标准的 TCP Socket 通讯。

    + +

    而相比自己实现 TCP Socket,WebSocket 已经内部处理了数据包的拼合、拆分等问题,极大简化了服务端底层的复杂度。而在传输性能、带宽消耗上,WebSocket 相比传统 TCP Socket 没有任何区别。

    +
    + + +
  • +
+ +

Get Started

+ + + +
-
-
-
-

Connects

-
-
-
-
-
-

Jobs

-
@@ -83,10 +111,10 @@

Jobs

  • - Dashboard + Welcome
  • - Tests + Demo
  • Documents diff --git a/apps/welcome/public_html/js/chartist.min.js b/apps/welcome/public_html/js/chartist.min.js deleted file mode 100644 index a77fb6b..0000000 --- a/apps/welcome/public_html/js/chartist.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/* Chartist.js 0.7.3 - * Copyright © 2015 Gion Kunz - * Free to use under the WTFPL license. - * http://www.wtfpl.net/ - */ - -!function(a,b){"function"==typeof define&&define.amd?define([],function(){return a.Chartist=b()}):"object"==typeof exports?module.exports=b():a.Chartist=b()}(this,function(){var a={version:"0.7.3"};return function(a,b,c){"use strict";c.noop=function(a){return a},c.alphaNumerate=function(a){return String.fromCharCode(97+a%26)},c.extend=function(a){a=a||{};var b=Array.prototype.slice.call(arguments,1);return b.forEach(function(b){for(var d in b)a[d]="object"!=typeof b[d]||b[d]instanceof Array?b[d]:c.extend({},a[d],b[d])}),a},c.replaceAll=function(a,b,c){return a.replace(new RegExp(b,"g"),c)},c.stripUnit=function(a){return"string"==typeof a&&(a=a.replace(/[^0-9\+-\.]/g,"")),+a},c.ensureUnit=function(a,b){return"number"==typeof a&&(a+=b),a},c.querySelector=function(a){return a instanceof Node?a:b.querySelector(a)},c.times=function(a){return Array.apply(null,new Array(a))},c.sum=function(a,b){return a+b},c.serialMap=function(a,b){var d=[],e=Math.max.apply(null,a.map(function(a){return a.length}));return c.times(e).forEach(function(c,e){var f=a.map(function(a){return a[e]});d[e]=b.apply(null,f)}),d},c.roundWithPrecision=function(a,b){var d=Math.pow(10,b||c.precision);return Math.round(a*d)/d},c.precision=8,c.escapingMap={"&":"&","<":"<",">":">",'"':""","'":"'"},c.serialize=function(a){return null===a||void 0===a?a:("number"==typeof a?a=""+a:"object"==typeof a&&(a=JSON.stringify({data:a})),Object.keys(c.escapingMap).reduce(function(a,b){return c.replaceAll(a,b,c.escapingMap[b])},a))},c.deserialize=function(a){if("string"!=typeof a)return a;a=Object.keys(c.escapingMap).reduce(function(a,b){return c.replaceAll(a,c.escapingMap[b],b)},a);try{a=JSON.parse(a),a=void 0!==a.data?a.data:a}catch(b){}return a},c.createSvg=function(a,b,d,e){var f;return b=b||"100%",d=d||"100%",Array.prototype.slice.call(a.querySelectorAll("svg")).filter(function(a){return a.getAttribute(c.xmlNs.qualifiedName)}).forEach(function(b){a.removeChild(b)}),f=new c.Svg("svg").attr({width:b,height:d}).addClass(e).attr({style:"width: "+b+"; height: "+d+";"}),a.appendChild(f._node),f},c.reverseData=function(a){a.labels.reverse(),a.series.reverse();for(var b=0;bd;d++)a[c][d]=0;return a},c.getMetaData=function(a,b){var d=a.data?a.data[b]:a[b];return d?c.serialize(d.meta):void 0},c.orderOfMagnitude=function(a){return Math.floor(Math.log(Math.abs(a))/Math.LN10)},c.projectLength=function(a,b,c){return b/c.range*a},c.getAvailableHeight=function(a,b){return Math.max((c.stripUnit(b.height)||a.height())-(b.chartPadding.top+b.chartPadding.bottom)-b.axisX.offset,0)},c.getHighLow=function(a){var b,c,d={high:-Number.MAX_VALUE,low:Number.MAX_VALUE};for(b=0;bd.high&&(d.high=a[b][c]),a[b][c]j;;)if(k&&c.projectLength(a,i.step,i)<=d)i.step*=2;else{if(k||!(c.projectLength(a,i.step/2,i)>=d))break;i.step/=2}for(g=i.min,h=i.max,f=i.min;f<=i.max;f+=i.step)f+i.step=i.high&&(h-=i.step);for(i.min=g,i.max=h,i.range=i.max-i.min,i.values=[],f=i.min;f<=i.max;f+=i.step)i.values.push(c.roundWithPrecision(f));return i},c.polarToCartesian=function(a,b,c,d){var e=(d-90)*Math.PI/180;return{x:a+c*Math.cos(e),y:b+c*Math.sin(e)}},c.createChartRect=function(a,b,d){var e=b.axisY?b.axisY.offset||0:0,f=b.axisX?b.axisX.offset||0:0,g=c.stripUnit(b.width)||a.width(),h=c.stripUnit(b.height)||a.height(),i=c.normalizePadding(b.chartPadding,d);return{x1:i.left+e,y1:Math.max(h-i.bottom-f,i.bottom),x2:Math.max(g-i.right,i.right+e),y2:i.top,width:function(){return this.x2-this.x1},height:function(){return this.y1-this.y2}}},c.createGrid=function(a,b,d,e,f,g,h,i){var j={};j[d.units.pos+"1"]=a.pos,j[d.units.pos+"2"]=a.pos,j[d.counterUnits.pos+"1"]=e,j[d.counterUnits.pos+"2"]=e+f;var k=g.elem("line",j,h.join(" "));i.emit("draw",c.extend({type:"grid",axis:d.units.pos,index:b,group:g,element:k},j))},c.createLabel=function(a,b,d,e,f,g,h,i,j,k){var l,m={};if(m[e.units.pos]=a.pos+g[e.units.pos],m[e.counterUnits.pos]=g[e.counterUnits.pos],m[e.units.len]=a.len,m[e.counterUnits.len]=f,j){var n=''+d[b]+"";l=h.foreignObject(n,c.extend({style:"overflow: visible;"},m))}else l=h.elem("text",m,i.join(" ")).text(d[b]);k.emit("draw",c.extend({type:"label",axis:e,index:b,group:h,element:l,text:d[b]},m))},c.createAxis=function(a,b,d,e,f,g,h,i){var j=h["axis"+a.units.pos.toUpperCase()],k=b.map(a.projectValue.bind(a)).map(a.transform),l=b.map(j.labelInterpolationFnc);k.forEach(function(b,k){(l[k]||0===l[k])&&(j.showGrid&&c.createGrid(b,k,a,a.gridOffset,d[a.counterUnits.len](),e,[h.classNames.grid,h.classNames[a.units.dir]],i),j.showLabel&&c.createLabel(b,k,l,a,j.offset,a.labelOffset,f,[h.classNames.label,h.classNames[a.units.dir]],g,i))})},c.optionsProvider=function(b,d,e){function f(b){var f=h;if(h=c.extend({},j),d)for(i=0;ig;g+=2){var i=[{x:+a[g-2],y:+a[g-1]},{x:+a[g],y:+a[g+1]},{x:+a[g+2],y:+a[g+3]},{x:+a[g+4],y:+a[g+5]}];b?g?h-4===g?i[3]={x:+a[0],y:+a[1]}:h-2===g&&(i[2]={x:+a[0],y:+a[1]},i[3]={x:+a[2],y:+a[3]}):i[0]={x:+a[h-2],y:+a[h-1]}:h-4===g?i[3]=i[2]:g||(i[0]={x:+a[g],y:+a[g+1]}),f.curve(d*(-i[0].x+6*i[1].x+i[2].x)/6+e*i[2].x,d*(-i[0].y+6*i[1].y+i[2].y)/6+e*i[2].y,d*(i[1].x+6*i[2].x-i[3].x)/6+e*i[2].x,d*(i[1].y+6*i[2].y-i[3].y)/6+e*i[2].y,i[2].x,i[2].y)}return f}}}(window,document,a),function(a,b,c){"use strict";c.EventEmitter=function(){function a(a,b){d[a]=d[a]||[],d[a].push(b)}function b(a,b){d[a]&&(b?(d[a].splice(d[a].indexOf(b),1),0===d[a].length&&delete d[a]):delete d[a])}function c(a,b){d[a]&&d[a].forEach(function(a){a(b)}),d["*"]&&d["*"].forEach(function(c){c(a,b)})}var d=[];return{addEventHandler:a,removeEventHandler:b,emit:c}}}(window,document,a),function(a,b,c){"use strict";function d(a){var b=[];if(a.length)for(var c=0;ca.x;return d&&"explode"===c||!d&&"implode"===c?"start":d&&"implode"===c||!d&&"explode"===c?"end":"middle"}function e(a){var b,e,f,h,i=[],j=a.startAngle,k=c.getDataArray(this.data,a.reverseData);this.svg=c.createSvg(this.container,a.width,a.height,a.classNames.chart),b=c.createChartRect(this.svg,a,g.padding),e=Math.min(b.width()/2,b.height()/2),h=a.total||k.reduce(function(a,b){return a+b},0),e-=a.donut?a.donutWidth/2:0,f=a.donut?e:e/2,f+=a.labelOffset;for(var l={x:b.x1+b.width()/2,y:b.y2+b.height()/2},m=1===this.data.series.filter(function(a){return 0!==a}).length,n=0;n=o-j?"0":"1",s=["M",q.x,q.y,"A",e,e,0,r,0,p.x,p.y];a.donut===!1&&s.push("L",l.x,l.y);var t=i[n].elem("path",{d:s.join(" ")},a.classNames.slice+(a.donut?" "+a.classNames.donut:""));if(t.attr({value:k[n]},c.xmlNs.uri),a.donut===!0&&t.attr({style:"stroke-width: "+ +a.donutWidth+"px"}),this.eventEmitter.emit("draw",{type:"slice",value:k[n],totalDataSum:h,index:n,group:i[n],element:t,center:l,radius:e,startAngle:j,endAngle:o}),a.showLabel){var u=c.polarToCartesian(l.x,l.y,f,j+(o-j)/2),v=a.labelInterpolationFnc(this.data.labels?this.data.labels[n]:k[n],n),w=i[n].elem("text",{dx:u.x,dy:u.y,"text-anchor":d(l,u,a.labelDirection)},a.classNames.label).text(""+v);this.eventEmitter.emit("draw",{type:"label",index:n,group:i[n],element:w,text:""+v,x:u.x,y:u.y})}j=o}this.eventEmitter.emit("created",{chartRect:b,svg:this.svg,options:a})}function f(a,b,d,e){c.Pie["super"].constructor.call(this,a,b,g,c.extend({},g,d),e)}var g={width:void 0,height:void 0,chartPadding:5,classNames:{chart:"ct-chart-pie",series:"ct-series",slice:"ct-slice",donut:"ct-donut",label:"ct-label"},startAngle:0,total:void 0,donut:!1,donutWidth:60,showLabel:!0,labelOffset:0,labelInterpolationFnc:c.noop,labelDirection:"neutral",reverseData:!1};c.Pie=c.Base.extend({constructor:f,createChart:e,determineAnchorPosition:d})}(window,document,a),a}); -//# sourceMappingURL=chartist.min.js.map \ No newline at end of file diff --git a/apps/welcome/public_html/js/chartist.min.js.map b/apps/welcome/public_html/js/chartist.min.js.map deleted file mode 100644 index 1b961df..0000000 --- a/apps/welcome/public_html/js/chartist.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"chartist.min.js","sources":["chartist.js"],"names":["root","factory","define","amd","exports","module","this","Chartist","version","window","document","noop","n","alphaNumerate","String","fromCharCode","extend","target","sources","Array","prototype","slice","call","arguments","forEach","source","prop","replaceAll","str","subStr","newSubStr","replace","RegExp","stripUnit","value","ensureUnit","unit","querySelector","query","Node","times","length","apply","sum","previous","current","serialMap","arr","cb","result","Math","max","map","e","index","args","roundWithPrecision","digits","precision","pow","round","escapingMap","&","<",">","\"","'","serialize","data","undefined","JSON","stringify","Object","keys","reduce","key","deserialize","parse","createSvg","container","width","height","className","svg","querySelectorAll","filter","getAttribute","xmlNs","qualifiedName","removeChild","Svg","attr","addClass","style","appendChild","_node","reverseData","labels","reverse","series","i","getDataArray","localData","array","reversed","push","j","normalizePadding","padding","fallback","top","right","bottom","left","normalizeDataArray","dataArray","getMetaData","meta","orderOfMagnitude","floor","log","abs","LN10","projectLength","axisLength","bounds","range","getAvailableHeight","options","chartPadding","axisX","offset","getHighLow","highLow","high","Number","MAX_VALUE","low","getBounds","scaleMinSpace","referenceValue","newMin","newMax","min","valueRange","oom","ceil","step","numberOfSteps","scaleUp","values","polarToCartesian","centerX","centerY","radius","angleInDegrees","angleInRadians","PI","x","cos","y","sin","createChartRect","fallbackPadding","yOffset","axisY","xOffset","w","h","normalizedPadding","x1","y1","x2","y2","createGrid","projectedValue","axis","group","classes","eventEmitter","positionalData","units","pos","counterUnits","gridElement","elem","join","emit","type","element","createLabel","axisOffset","labelOffset","useForeignObject","labelElement","len","content","foreignObject","text","createAxis","chartRect","gridGroup","labelGroup","axisOptions","toUpperCase","projectedValues","projectValue","bind","transform","labelValues","labelInterpolationFnc","showGrid","gridOffset","classNames","grid","dir","showLabel","label","optionsProvider","responsiveOptions","updateCurrentOptions","preventChangedEvent","previousOptions","currentOptions","baseOptions","mql","matchMedia","matches","removeMediaQueryListeners","mediaQueryListeners","removeListener","addListener",{"end":{"file":"chartist.js","comments_before":[],"nlb":false,"endpos":27794,"pos":27780,"col":10,"line":794,"value":"currentOptions","type":"name"},"start":{"file":"chartist.js","comments_before":[],"nlb":false,"endpos":27794,"pos":27780,"col":10,"line":794,"value":"currentOptions","type":"name"},"name":"currentOptions"},"Interpolation","none","pathCoordinates","path","Path","move","line","simple","defaultOptions","divisor","d","prevX","prevY","currX","currY","curve","cardinal","tension","t","c","z","iLen","p","EventEmitter","addEventHandler","event","handler","handlers","removeEventHandler","splice","indexOf","starHandler","listToArray","list","properties","superProtoOverride","superProto","Class","proto","create","cloneDefinitions","constr","instance","fn","constructor","getOwnPropertyNames","propName","defineProperty","getOwnPropertyDescriptor","update","override","initializeTimeoutId","createChart","detach","removeEventListener","resizeListener","on","off","initialize","addEventListener","plugins","plugin","Base","supportsForeignObject","isSupported","supportsAnimations","__chartist__","clearTimeout","setTimeout","Error","name","attributes","parent","insertFirst","SVGElement","createElementNS","svgNs","setAttributeNS","uri","firstChild","insertBefore","ns","getAttributeNS","prefix","setAttribute","parentNode","node","nodeName","selector","foundNode","foundNodes","List","createElement","innerHTML","xhtmlNs","fnObj","createTextNode","empty","remove","newElement","replaceChild","append","trim","split","names","concat","self","removeClass","removedClasses","removeAllClasses","clientHeight","getBBox","clientWidth","animate","animations","guided","attribute","createAnimate","animationDefinition","timeout","easing","attributeProperties","Easing","begin","dur","calcMode","keySplines","keyTimes","fill","from","attributeName","beginElement","err","to","params","SvgList","nodeList","svgElements","prototypeProperty","feature","implementation","hasFeature","easingCubicBeziers","easeInSine","easeOutSine","easeInOutSine","easeInQuad","easeOutQuad","easeInOutQuad","easeInCubic","easeOutCubic","easeInOutCubic","easeInQuart","easeOutQuart","easeInOutQuart","easeInQuint","easeOutQuint","easeInOutQuint","easeInExpo","easeOutExpo","easeInOutExpo","easeInCirc","easeOutCirc","easeInOutCirc","easeInBack","easeOutBack","easeInOutBack","command","pathElements","relative","toLowerCase","forEachParam","pathElement","pathElementIndex","elementDescriptions","paramName","paramIndex","SvgPath","close","position","count","chunks","match","pop","elements","chunk","shift","description","spliceArgs","accuracyMultiplier","accuracy","scale","translate","transformFnc","transformed","clone","m","l","Axis","axisUnits","rectEnd","rectStart","rectOffset","LinearScaleAxis","axisUnit","StepAxis","stepLength","stepCount","stretch","seriesGroups","normalizedData","chart","fullWidth","seriesIndex","series-name","valueIndex","showPoint","point","showLine","showArea","smoothing","lineSmooth","areaBase","areaBaseProjected","areaPath","area","Line","vertical","horizontal","stackBars","serialSums","valueAxis","labelAxis","horizontalBars","fullHeight","zeroPoint","stackedBarValues","biPol","periodHalfLength","bar","previousStack","projected","seriesBarDistance","positions","Bar","determineAnchorPosition","center","direction","toTheRight","labelRadius","totalDataSum","startAngle","total","previousValue","currentValue","donut","donutWidth","hasSingleValInSeries","val","endAngle","start","end","arcSweep","labelPosition","interpolatedValue","dx","dy","text-anchor","labelDirection","Pie"],"mappings":"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","sourcesContent":["(function (root, factory) {\n if (typeof define === 'function' && define.amd) {\n // AMD. Register as an anonymous module unless amdModuleId is set\n define([], function () {\n return (root['Chartist'] = factory());\n });\n } else if (typeof exports === 'object') {\n // Node. Does not work with strict CommonJS, but\n // only CommonJS-like environments that support module.exports,\n // like Node.\n module.exports = factory();\n } else {\n root['Chartist'] = factory();\n }\n}(this, function () {\n\n/* Chartist.js 0.7.3\n * Copyright © 2015 Gion Kunz\n * Free to use under the WTFPL license.\n * http://www.wtfpl.net/\n */\n/**\n * The core module of Chartist that is mainly providing static functions and higher level functions for chart modules.\n *\n * @module Chartist.Core\n */\nvar Chartist = {\n version: '0.7.3'\n};\n\n(function (window, document, Chartist) {\n 'use strict';\n\n /**\n * Helps to simplify functional style code\n *\n * @memberof Chartist.Core\n * @param {*} n This exact value will be returned by the noop function\n * @return {*} The same value that was provided to the n parameter\n */\n Chartist.noop = function (n) {\n return n;\n };\n\n /**\n * Generates a-z from a number 0 to 26\n *\n * @memberof Chartist.Core\n * @param {Number} n A number from 0 to 26 that will result in a letter a-z\n * @return {String} A character from a-z based on the input number n\n */\n Chartist.alphaNumerate = function (n) {\n // Limit to a-z\n return String.fromCharCode(97 + n % 26);\n };\n\n /**\n * Simple recursive object extend\n *\n * @memberof Chartist.Core\n * @param {Object} target Target object where the source will be merged into\n * @param {Object...} sources This object (objects) will be merged into target and then target is returned\n * @return {Object} An object that has the same reference as target but is extended and merged with the properties of source\n */\n Chartist.extend = function (target) {\n target = target || {};\n\n var sources = Array.prototype.slice.call(arguments, 1);\n sources.forEach(function(source) {\n for (var prop in source) {\n if (typeof source[prop] === 'object' && !(source[prop] instanceof Array)) {\n target[prop] = Chartist.extend({}, target[prop], source[prop]);\n } else {\n target[prop] = source[prop];\n }\n }\n });\n\n return target;\n };\n\n /**\n * Replaces all occurrences of subStr in str with newSubStr and returns a new string.\n *\n * @memberof Chartist.Core\n * @param {String} str\n * @param {String} subStr\n * @param {String} newSubStr\n * @return {String}\n */\n Chartist.replaceAll = function(str, subStr, newSubStr) {\n return str.replace(new RegExp(subStr, 'g'), newSubStr);\n };\n\n /**\n * Converts a string to a number while removing the unit if present. If a number is passed then this will be returned unmodified.\n *\n * @memberof Chartist.Core\n * @param {String|Number} value\n * @return {Number} Returns the string as number or NaN if the passed length could not be converted to pixel\n */\n Chartist.stripUnit = function(value) {\n if(typeof value === 'string') {\n value = value.replace(/[^0-9\\+-\\.]/g, '');\n }\n\n return +value;\n };\n\n /**\n * Converts a number to a string with a unit. If a string is passed then this will be returned unmodified.\n *\n * @memberof Chartist.Core\n * @param {Number} value\n * @param {String} unit\n * @return {String} Returns the passed number value with unit.\n */\n Chartist.ensureUnit = function(value, unit) {\n if(typeof value === 'number') {\n value = value + unit;\n }\n\n return value;\n };\n\n /**\n * This is a wrapper around document.querySelector that will return the query if it's already of type Node\n *\n * @memberof Chartist.Core\n * @param {String|Node} query The query to use for selecting a Node or a DOM node that will be returned directly\n * @return {Node}\n */\n Chartist.querySelector = function(query) {\n return query instanceof Node ? query : document.querySelector(query);\n };\n\n /**\n * Functional style helper to produce array with given length initialized with undefined values\n *\n * @memberof Chartist.Core\n * @param length\n * @return {Array}\n */\n Chartist.times = function(length) {\n return Array.apply(null, new Array(length));\n };\n\n /**\n * Sum helper to be used in reduce functions\n *\n * @memberof Chartist.Core\n * @param previous\n * @param current\n * @return {*}\n */\n Chartist.sum = function(previous, current) {\n return previous + current;\n };\n\n /**\n * Map for multi dimensional arrays where their nested arrays will be mapped in serial. The output array will have the length of the largest nested array. The callback function is called with variable arguments where each argument is the nested array value (or undefined if there are no more values).\n *\n * @memberof Chartist.Core\n * @param arr\n * @param cb\n * @return {Array}\n */\n Chartist.serialMap = function(arr, cb) {\n var result = [],\n length = Math.max.apply(null, arr.map(function(e) {\n return e.length;\n }));\n\n Chartist.times(length).forEach(function(e, index) {\n var args = arr.map(function(e) {\n return e[index];\n });\n\n result[index] = cb.apply(null, args);\n });\n\n return result;\n };\n\n /**\n * This helper function can be used to round values with certain precision level after decimal. This is used to prevent rounding errors near float point precision limit.\n *\n * @memberof Chartist.Core\n * @param {Number} value The value that should be rounded with precision\n * @param {Number} [digits] The number of digits after decimal used to do the rounding\n * @returns {number} Rounded value\n */\n Chartist.roundWithPrecision = function(value, digits) {\n var precision = Math.pow(10, digits || Chartist.precision);\n return Math.round(value * precision) / precision;\n };\n\n /**\n * Precision level used internally in Chartist for rounding. If you require more decimal places you can increase this number.\n *\n * @memberof Chartist.Core\n * @type {number}\n */\n Chartist.precision = 8;\n\n /**\n * A map with characters to escape for strings to be safely used as attribute values.\n *\n * @memberof Chartist.Core\n * @type {Object}\n */\n Chartist.escapingMap = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n '\\'': '''\n };\n\n /**\n * This function serializes arbitrary data to a string. In case of data that can't be easily converted to a string, this function will create a wrapper object and serialize the data using JSON.stringify. The outcoming string will always be escaped using Chartist.escapingMap.\n * If called with null or undefined the function will return immediately with null or undefined.\n *\n * @memberof Chartist.Core\n * @param {Number|String|Object} data\n * @return {String}\n */\n Chartist.serialize = function(data) {\n if(data === null || data === undefined) {\n return data;\n } else if(typeof data === 'number') {\n data = ''+data;\n } else if(typeof data === 'object') {\n data = JSON.stringify({data: data});\n }\n\n return Object.keys(Chartist.escapingMap).reduce(function(result, key) {\n return Chartist.replaceAll(result, key, Chartist.escapingMap[key]);\n }, data);\n };\n\n /**\n * This function de-serializes a string previously serialized with Chartist.serialize. The string will always be unescaped using Chartist.escapingMap before it's returned. Based on the input value the return type can be Number, String or Object. JSON.parse is used with try / catch to see if the unescaped string can be parsed into an Object and this Object will be returned on success.\n *\n * @memberof Chartist.Core\n * @param {String} data\n * @return {String|Number|Object}\n */\n Chartist.deserialize = function(data) {\n if(typeof data !== 'string') {\n return data;\n }\n\n data = Object.keys(Chartist.escapingMap).reduce(function(result, key) {\n return Chartist.replaceAll(result, Chartist.escapingMap[key], key);\n }, data);\n\n try {\n data = JSON.parse(data);\n data = data.data !== undefined ? data.data : data;\n } catch(e) {}\n\n return data;\n };\n\n /**\n * Create or reinitialize the SVG element for the chart\n *\n * @memberof Chartist.Core\n * @param {Node} container The containing DOM Node object that will be used to plant the SVG element\n * @param {String} width Set the width of the SVG element. Default is 100%\n * @param {String} height Set the height of the SVG element. Default is 100%\n * @param {String} className Specify a class to be added to the SVG element\n * @return {Object} The created/reinitialized SVG element\n */\n Chartist.createSvg = function (container, width, height, className) {\n var svg;\n\n width = width || '100%';\n height = height || '100%';\n\n // Check if there is a previous SVG element in the container that contains the Chartist XML namespace and remove it\n // Since the DOM API does not support namespaces we need to manually search the returned list http://www.w3.org/TR/selectors-api/\n Array.prototype.slice.call(container.querySelectorAll('svg')).filter(function filterChartistSvgObjects(svg) {\n return svg.getAttribute(Chartist.xmlNs.qualifiedName);\n }).forEach(function removePreviousElement(svg) {\n container.removeChild(svg);\n });\n\n // Create svg object with width and height or use 100% as default\n svg = new Chartist.Svg('svg').attr({\n width: width,\n height: height\n }).addClass(className).attr({\n style: 'width: ' + width + '; height: ' + height + ';'\n });\n\n // Add the DOM node to our container\n container.appendChild(svg._node);\n\n return svg;\n };\n\n\n /**\n * Reverses the series, labels and series data arrays.\n *\n * @memberof Chartist.Core\n * @param data\n */\n Chartist.reverseData = function(data) {\n data.labels.reverse();\n data.series.reverse();\n for (var i = 0; i < data.series.length; i++) {\n if(typeof(data.series[i]) === 'object' && data.series[i].data !== undefined) {\n data.series[i].data.reverse();\n } else {\n data.series[i].reverse();\n }\n }\n };\n\n /**\n * Convert data series into plain array\n *\n * @memberof Chartist.Core\n * @param {Object} data The series object that contains the data to be visualized in the chart\n * @param {Boolean} reverse If true the whole data is reversed by the getDataArray call. This will modify the data object passed as first parameter. The labels as well as the series order is reversed. The whole series data arrays are reversed too.\n * @return {Array} A plain array that contains the data to be visualized in the chart\n */\n Chartist.getDataArray = function (data, reverse) {\n var array = [],\n value,\n localData;\n\n // If the data should be reversed but isn't we need to reverse it\n // If it's reversed but it shouldn't we need to reverse it back\n // That's required to handle data updates correctly and to reflect the responsive configurations\n if(reverse && !data.reversed || !reverse && data.reversed) {\n Chartist.reverseData(data);\n data.reversed = !data.reversed;\n }\n\n for (var i = 0; i < data.series.length; i++) {\n // If the series array contains an object with a data property we will use the property\n // otherwise the value directly (array or number).\n // We create a copy of the original data array with Array.prototype.push.apply\n localData = typeof(data.series[i]) === 'object' && data.series[i].data !== undefined ? data.series[i].data : data.series[i];\n if(localData instanceof Array) {\n array[i] = [];\n Array.prototype.push.apply(array[i], localData);\n } else {\n array[i] = localData;\n }\n\n // Convert object values to numbers\n for (var j = 0; j < array[i].length; j++) {\n value = array[i][j];\n value = value.value === 0 ? 0 : (value.value || value);\n array[i][j] = +value;\n }\n }\n\n return array;\n };\n\n /**\n * Converts a number into a padding object.\n *\n * @memberof Chartist.Core\n * @param {Object|Number} padding\n * @param {Number} [fallback] This value is used to fill missing values if a incomplete padding object was passed\n * @returns {Object} Returns a padding object containing top, right, bottom, left properties filled with the padding number passed in as argument. If the argument is something else than a number (presumably already a correct padding object) then this argument is directly returned.\n */\n Chartist.normalizePadding = function(padding, fallback) {\n fallback = fallback || 0;\n\n return typeof padding === 'number' ? {\n top: padding,\n right: padding,\n bottom: padding,\n left: padding\n } : {\n top: typeof padding.top === 'number' ? padding.top : fallback,\n right: typeof padding.right === 'number' ? padding.right : fallback,\n bottom: typeof padding.bottom === 'number' ? padding.bottom : fallback,\n left: typeof padding.left === 'number' ? padding.left : fallback\n };\n };\n\n /**\n * Adds missing values at the end of the array. This array contains the data, that will be visualized in the chart\n *\n * @memberof Chartist.Core\n * @param {Array} dataArray The array that contains the data to be visualized in the chart. The array in this parameter will be modified by function.\n * @param {Number} length The length of the x-axis data array.\n * @return {Array} The array that got updated with missing values.\n */\n Chartist.normalizeDataArray = function (dataArray, length) {\n for (var i = 0; i < dataArray.length; i++) {\n if (dataArray[i].length === length) {\n continue;\n }\n\n for (var j = dataArray[i].length; j < length; j++) {\n dataArray[i][j] = 0;\n }\n }\n\n return dataArray;\n };\n\n Chartist.getMetaData = function(series, index) {\n var value = series.data ? series.data[index] : series[index];\n return value ? Chartist.serialize(value.meta) : undefined;\n };\n\n /**\n * Calculate the order of magnitude for the chart scale\n *\n * @memberof Chartist.Core\n * @param {Number} value The value Range of the chart\n * @return {Number} The order of magnitude\n */\n Chartist.orderOfMagnitude = function (value) {\n return Math.floor(Math.log(Math.abs(value)) / Math.LN10);\n };\n\n /**\n * Project a data length into screen coordinates (pixels)\n *\n * @memberof Chartist.Core\n * @param {Object} axisLength The svg element for the chart\n * @param {Number} length Single data value from a series array\n * @param {Object} bounds All the values to set the bounds of the chart\n * @return {Number} The projected data length in pixels\n */\n Chartist.projectLength = function (axisLength, length, bounds) {\n return length / bounds.range * axisLength;\n };\n\n /**\n * Get the height of the area in the chart for the data series\n *\n * @memberof Chartist.Core\n * @param {Object} svg The svg element for the chart\n * @param {Object} options The Object that contains all the optional values for the chart\n * @return {Number} The height of the area in the chart for the data series\n */\n Chartist.getAvailableHeight = function (svg, options) {\n return Math.max((Chartist.stripUnit(options.height) || svg.height()) - (options.chartPadding.top + options.chartPadding.bottom) - options.axisX.offset, 0);\n };\n\n /**\n * Get highest and lowest value of data array. This Array contains the data that will be visualized in the chart.\n *\n * @memberof Chartist.Core\n * @param {Array} dataArray The array that contains the data to be visualized in the chart\n * @return {Object} An object that contains the highest and lowest value that will be visualized on the chart.\n */\n Chartist.getHighLow = function (dataArray) {\n var i,\n j,\n highLow = {\n high: -Number.MAX_VALUE,\n low: Number.MAX_VALUE\n };\n\n for (i = 0; i < dataArray.length; i++) {\n for (j = 0; j < dataArray[i].length; j++) {\n if (dataArray[i][j] > highLow.high) {\n highLow.high = dataArray[i][j];\n }\n\n if (dataArray[i][j] < highLow.low) {\n highLow.low = dataArray[i][j];\n }\n }\n }\n\n return highLow;\n };\n\n /**\n * Calculate and retrieve all the bounds for the chart and return them in one array\n *\n * @memberof Chartist.Core\n * @param {Number} axisLength The length of the Axis used for\n * @param {Object} highLow An object containing a high and low property indicating the value range of the chart.\n * @param {Number} scaleMinSpace The minimum projected length a step should result in\n * @param {Number} referenceValue The reference value for the chart.\n * @return {Object} All the values to set the bounds of the chart\n */\n Chartist.getBounds = function (axisLength, highLow, scaleMinSpace, referenceValue) {\n var i,\n newMin,\n newMax,\n bounds = {\n high: highLow.high,\n low: highLow.low\n };\n\n // If high and low are the same because of misconfiguration or flat data (only the same value) we need\n // to set the high or low to 0 depending on the polarity\n if(bounds.high === bounds.low) {\n // If both values are 0 we set high to 1\n if(bounds.low === 0) {\n bounds.high = 1;\n } else if(bounds.low < 0) {\n // If we have the same negative value for the bounds we set bounds.high to 0\n bounds.high = 0;\n } else {\n // If we have the same positive value for the bounds we set bounds.low to 0\n bounds.low = 0;\n }\n }\n\n // Overrides of high / low based on reference value, it will make sure that the invisible reference value is\n // used to generate the chart. This is useful when the chart always needs to contain the position of the\n // invisible reference value in the view i.e. for bipolar scales.\n if (referenceValue || referenceValue === 0) {\n bounds.high = Math.max(referenceValue, bounds.high);\n bounds.low = Math.min(referenceValue, bounds.low);\n }\n\n bounds.valueRange = bounds.high - bounds.low;\n bounds.oom = Chartist.orderOfMagnitude(bounds.valueRange);\n bounds.min = Math.floor(bounds.low / Math.pow(10, bounds.oom)) * Math.pow(10, bounds.oom);\n bounds.max = Math.ceil(bounds.high / Math.pow(10, bounds.oom)) * Math.pow(10, bounds.oom);\n bounds.range = bounds.max - bounds.min;\n bounds.step = Math.pow(10, bounds.oom);\n bounds.numberOfSteps = Math.round(bounds.range / bounds.step);\n\n // Optimize scale step by checking if subdivision is possible based on horizontalGridMinSpace\n // If we are already below the scaleMinSpace value we will scale up\n var length = Chartist.projectLength(axisLength, bounds.step, bounds),\n scaleUp = length < scaleMinSpace;\n\n while (true) {\n if (scaleUp && Chartist.projectLength(axisLength, bounds.step, bounds) <= scaleMinSpace) {\n bounds.step *= 2;\n } else if (!scaleUp && Chartist.projectLength(axisLength, bounds.step / 2, bounds) >= scaleMinSpace) {\n bounds.step /= 2;\n } else {\n break;\n }\n }\n\n // Narrow min and max based on new step\n newMin = bounds.min;\n newMax = bounds.max;\n for (i = bounds.min; i <= bounds.max; i += bounds.step) {\n if (i + bounds.step < bounds.low) {\n newMin += bounds.step;\n }\n\n if (i - bounds.step >= bounds.high) {\n newMax -= bounds.step;\n }\n }\n bounds.min = newMin;\n bounds.max = newMax;\n bounds.range = bounds.max - bounds.min;\n\n bounds.values = [];\n for (i = bounds.min; i <= bounds.max; i += bounds.step) {\n bounds.values.push(Chartist.roundWithPrecision(i));\n }\n\n return bounds;\n };\n\n /**\n * Calculate cartesian coordinates of polar coordinates\n *\n * @memberof Chartist.Core\n * @param {Number} centerX X-axis coordinates of center point of circle segment\n * @param {Number} centerY X-axis coordinates of center point of circle segment\n * @param {Number} radius Radius of circle segment\n * @param {Number} angleInDegrees Angle of circle segment in degrees\n * @return {Number} Coordinates of point on circumference\n */\n Chartist.polarToCartesian = function (centerX, centerY, radius, angleInDegrees) {\n var angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;\n\n return {\n x: centerX + (radius * Math.cos(angleInRadians)),\n y: centerY + (radius * Math.sin(angleInRadians))\n };\n };\n\n /**\n * Initialize chart drawing rectangle (area where chart is drawn) x1,y1 = bottom left / x2,y2 = top right\n *\n * @memberof Chartist.Core\n * @param {Object} svg The svg element for the chart\n * @param {Object} options The Object that contains all the optional values for the chart\n * @param {Number} [fallbackPadding] The fallback padding if partial padding objects are used\n * @return {Object} The chart rectangles coordinates inside the svg element plus the rectangles measurements\n */\n Chartist.createChartRect = function (svg, options, fallbackPadding) {\n var yOffset = options.axisY ? options.axisY.offset || 0 : 0,\n xOffset = options.axisX ? options.axisX.offset || 0 : 0,\n w = Chartist.stripUnit(options.width) || svg.width(),\n h = Chartist.stripUnit(options.height) || svg.height(),\n normalizedPadding = Chartist.normalizePadding(options.chartPadding, fallbackPadding);\n\n return {\n x1: normalizedPadding.left + yOffset,\n y1: Math.max(h - normalizedPadding.bottom - xOffset, normalizedPadding.bottom),\n x2: Math.max(w - normalizedPadding.right, normalizedPadding.right + yOffset),\n y2: normalizedPadding.top,\n width: function () {\n return this.x2 - this.x1;\n },\n height: function () {\n return this.y1 - this.y2;\n }\n };\n };\n\n /**\n * Creates a grid line based on a projected value.\n *\n * @memberof Chartist.Core\n * @param projectedValue\n * @param index\n * @param axis\n * @param offset\n * @param length\n * @param group\n * @param classes\n * @param eventEmitter\n */\n Chartist.createGrid = function(projectedValue, index, axis, offset, length, group, classes, eventEmitter) {\n var positionalData = {};\n positionalData[axis.units.pos + '1'] = projectedValue.pos;\n positionalData[axis.units.pos + '2'] = projectedValue.pos;\n positionalData[axis.counterUnits.pos + '1'] = offset;\n positionalData[axis.counterUnits.pos + '2'] = offset + length;\n\n var gridElement = group.elem('line', positionalData, classes.join(' '));\n\n // Event for grid draw\n eventEmitter.emit('draw',\n Chartist.extend({\n type: 'grid',\n axis: axis.units.pos,\n index: index,\n group: group,\n element: gridElement\n }, positionalData)\n );\n };\n\n /**\n * Creates a label based on a projected value and an axis.\n *\n * @memberof Chartist.Core\n * @param projectedValue\n * @param index\n * @param labels\n * @param axis\n * @param axisOffset\n * @param labelOffset\n * @param group\n * @param classes\n * @param useForeignObject\n * @param eventEmitter\n */\n Chartist.createLabel = function(projectedValue, index, labels, axis, axisOffset, labelOffset, group, classes, useForeignObject, eventEmitter) {\n var labelElement,\n positionalData = {};\n positionalData[axis.units.pos] = projectedValue.pos + labelOffset[axis.units.pos];\n positionalData[axis.counterUnits.pos] = labelOffset[axis.counterUnits.pos];\n positionalData[axis.units.len] = projectedValue.len;\n positionalData[axis.counterUnits.len] = axisOffset;\n\n if(useForeignObject) {\n var content = '' + labels[index] + '';\n labelElement = group.foreignObject(content, Chartist.extend({\n style: 'overflow: visible;'\n }, positionalData));\n } else {\n labelElement = group.elem('text', positionalData, classes.join(' ')).text(labels[index]);\n }\n\n eventEmitter.emit('draw', Chartist.extend({\n type: 'label',\n axis: axis,\n index: index,\n group: group,\n element: labelElement,\n text: labels[index]\n }, positionalData));\n };\n\n /**\n * This function creates a whole axis with its grid lines and labels based on an axis model and a chartRect.\n *\n * @memberof Chartist.Core\n * @param axis\n * @param data\n * @param chartRect\n * @param gridGroup\n * @param labelGroup\n * @param useForeignObject\n * @param options\n * @param eventEmitter\n */\n Chartist.createAxis = function(axis, data, chartRect, gridGroup, labelGroup, useForeignObject, options, eventEmitter) {\n var axisOptions = options['axis' + axis.units.pos.toUpperCase()],\n projectedValues = data.map(axis.projectValue.bind(axis)).map(axis.transform),\n labelValues = data.map(axisOptions.labelInterpolationFnc);\n\n projectedValues.forEach(function(projectedValue, index) {\n // Skip grid lines and labels where interpolated label values are falsey (execpt for 0)\n if(!labelValues[index] && labelValues[index] !== 0) {\n return;\n }\n\n if(axisOptions.showGrid) {\n Chartist.createGrid(projectedValue, index, axis, axis.gridOffset, chartRect[axis.counterUnits.len](), gridGroup, [\n options.classNames.grid,\n options.classNames[axis.units.dir]\n ], eventEmitter);\n }\n\n if(axisOptions.showLabel) {\n Chartist.createLabel(projectedValue, index, labelValues, axis, axisOptions.offset, axis.labelOffset, labelGroup, [\n options.classNames.label,\n options.classNames[axis.units.dir]\n ], useForeignObject, eventEmitter);\n }\n });\n };\n\n /**\n * Provides options handling functionality with callback for options changes triggered by responsive options and media query matches\n *\n * @memberof Chartist.Core\n * @param {Object} options Options set by user\n * @param {Array} responsiveOptions Optional functions to add responsive behavior to chart\n * @param {Object} eventEmitter The event emitter that will be used to emit the options changed events\n * @return {Object} The consolidated options object from the defaults, base and matching responsive options\n */\n Chartist.optionsProvider = function (options, responsiveOptions, eventEmitter) {\n var baseOptions = Chartist.extend({}, options),\n currentOptions,\n mediaQueryListeners = [],\n i;\n\n function updateCurrentOptions(preventChangedEvent) {\n var previousOptions = currentOptions;\n currentOptions = Chartist.extend({}, baseOptions);\n\n if (responsiveOptions) {\n for (i = 0; i < responsiveOptions.length; i++) {\n var mql = window.matchMedia(responsiveOptions[i][0]);\n if (mql.matches) {\n currentOptions = Chartist.extend(currentOptions, responsiveOptions[i][1]);\n }\n }\n }\n\n if(eventEmitter && !preventChangedEvent) {\n eventEmitter.emit('optionsChanged', {\n previousOptions: previousOptions,\n currentOptions: currentOptions\n });\n }\n }\n\n function removeMediaQueryListeners() {\n mediaQueryListeners.forEach(function(mql) {\n mql.removeListener(updateCurrentOptions);\n });\n }\n\n if (!window.matchMedia) {\n throw 'window.matchMedia not found! Make sure you\\'re using a polyfill.';\n } else if (responsiveOptions) {\n\n for (i = 0; i < responsiveOptions.length; i++) {\n var mql = window.matchMedia(responsiveOptions[i][0]);\n mql.addListener(updateCurrentOptions);\n mediaQueryListeners.push(mql);\n }\n }\n // Execute initially so we get the correct options\n updateCurrentOptions(true);\n\n return {\n get currentOptions() {\n return Chartist.extend({}, currentOptions);\n },\n removeMediaQueryListeners: removeMediaQueryListeners\n };\n };\n\n}(window, document, Chartist));\n;/**\n * Chartist path interpolation functions.\n *\n * @module Chartist.Interpolation\n */\n/* global Chartist */\n(function(window, document, Chartist) {\n 'use strict';\n\n Chartist.Interpolation = {};\n\n /**\n * This interpolation function does not smooth the path and the result is only containing lines and no curves.\n *\n * @memberof Chartist.Interpolation\n * @return {Function}\n */\n Chartist.Interpolation.none = function() {\n return function cardinal(pathCoordinates) {\n var path = new Chartist.Svg.Path().move(pathCoordinates[0], pathCoordinates[1]);\n\n for(var i = 3; i < pathCoordinates.length; i += 2) {\n path.line(pathCoordinates[i - 1], pathCoordinates[i]);\n }\n\n return path;\n };\n };\n\n /**\n * Simple smoothing creates horizontal handles that are positioned with a fraction of the length between two data points. You can use the divisor option to specify the amount of smoothing.\n *\n * Simple smoothing can be used instead of `Chartist.Smoothing.cardinal` if you'd like to get rid of the artifacts it produces sometimes. Simple smoothing produces less flowing lines but is accurate by hitting the points and it also doesn't swing below or above the given data point.\n *\n * All smoothing functions within Chartist are factory functions that accept an options parameter. The simple interpolation function accepts one configuration parameter `divisor`, between 1 and ∞, which controls the smoothing characteristics.\n *\n * @example\n * var chart = new Chartist.Line('.ct-chart', {\n * labels: [1, 2, 3, 4, 5],\n * series: [[1, 2, 8, 1, 7]]\n * }, {\n * lineSmooth: Chartist.Interpolation.simple({\n * divisor: 2\n * })\n * });\n *\n *\n * @memberof Chartist.Interpolation\n * @param {Object} options The options of the simple interpolation factory function.\n * @return {Function}\n */\n Chartist.Interpolation.simple = function(options) {\n var defaultOptions = {\n divisor: 2\n };\n options = Chartist.extend({}, defaultOptions, options);\n\n var d = 1 / Math.max(1, options.divisor);\n\n return function simple(pathCoordinates) {\n var path = new Chartist.Svg.Path().move(pathCoordinates[0], pathCoordinates[1]);\n\n for(var i = 2; i < pathCoordinates.length; i += 2) {\n var prevX = pathCoordinates[i - 2],\n prevY = pathCoordinates[i - 1],\n currX = pathCoordinates[i],\n currY = pathCoordinates[i + 1],\n length = (currX - prevX) * d;\n\n path.curve(\n prevX + length,\n prevY,\n currX - length,\n currY,\n currX,\n currY\n );\n }\n\n return path;\n };\n };\n\n /**\n * Cardinal / Catmull-Rome spline interpolation is the default smoothing function in Chartist. It produces nice results where the splines will always meet the points. It produces some artifacts though when data values are increased or decreased rapidly. The line may not follow a very accurate path and if the line should be accurate this smoothing function does not produce the best results.\n *\n * Cardinal splines can only be created if there are more than two data points. If this is not the case this smoothing will fallback to `Chartist.Smoothing.none`.\n *\n * All smoothing functions within Chartist are factory functions that accept an options parameter. The cardinal interpolation function accepts one configuration parameter `tension`, between 0 and 1, which controls the smoothing intensity.\n *\n * @example\n * var chart = new Chartist.Line('.ct-chart', {\n * labels: [1, 2, 3, 4, 5],\n * series: [[1, 2, 8, 1, 7]]\n * }, {\n * lineSmooth: Chartist.Interpolation.cardinal({\n * tension: 1\n * })\n * });\n *\n * @memberof Chartist.Interpolation\n * @param {Object} options The options of the cardinal factory function.\n * @return {Function}\n */\n Chartist.Interpolation.cardinal = function(options) {\n var defaultOptions = {\n tension: 1\n };\n\n options = Chartist.extend({}, defaultOptions, options);\n\n var t = Math.min(1, Math.max(0, options.tension)),\n c = 1 - t;\n\n return function cardinal(pathCoordinates) {\n // If less than two points we need to fallback to no smoothing\n if(pathCoordinates.length <= 4) {\n return Chartist.Interpolation.none()(pathCoordinates);\n }\n\n var path = new Chartist.Svg.Path().move(pathCoordinates[0], pathCoordinates[1]),\n z;\n\n for (var i = 0, iLen = pathCoordinates.length; iLen - 2 * !z > i; i += 2) {\n var p = [\n {x: +pathCoordinates[i - 2], y: +pathCoordinates[i - 1]},\n {x: +pathCoordinates[i], y: +pathCoordinates[i + 1]},\n {x: +pathCoordinates[i + 2], y: +pathCoordinates[i + 3]},\n {x: +pathCoordinates[i + 4], y: +pathCoordinates[i + 5]}\n ];\n if (z) {\n if (!i) {\n p[0] = {x: +pathCoordinates[iLen - 2], y: +pathCoordinates[iLen - 1]};\n } else if (iLen - 4 === i) {\n p[3] = {x: +pathCoordinates[0], y: +pathCoordinates[1]};\n } else if (iLen - 2 === i) {\n p[2] = {x: +pathCoordinates[0], y: +pathCoordinates[1]};\n p[3] = {x: +pathCoordinates[2], y: +pathCoordinates[3]};\n }\n } else {\n if (iLen - 4 === i) {\n p[3] = p[2];\n } else if (!i) {\n p[0] = {x: +pathCoordinates[i], y: +pathCoordinates[i + 1]};\n }\n }\n\n path.curve(\n (t * (-p[0].x + 6 * p[1].x + p[2].x) / 6) + (c * p[2].x),\n (t * (-p[0].y + 6 * p[1].y + p[2].y) / 6) + (c * p[2].y),\n (t * (p[1].x + 6 * p[2].x - p[3].x) / 6) + (c * p[2].x),\n (t * (p[1].y + 6 * p[2].y - p[3].y) / 6) + (c * p[2].y),\n p[2].x,\n p[2].y\n );\n }\n\n return path;\n };\n };\n\n}(window, document, Chartist));\n;/**\n * A very basic event module that helps to generate and catch events.\n *\n * @module Chartist.Event\n */\n/* global Chartist */\n(function (window, document, Chartist) {\n 'use strict';\n\n Chartist.EventEmitter = function () {\n var handlers = [];\n\n /**\n * Add an event handler for a specific event\n *\n * @memberof Chartist.Event\n * @param {String} event The event name\n * @param {Function} handler A event handler function\n */\n function addEventHandler(event, handler) {\n handlers[event] = handlers[event] || [];\n handlers[event].push(handler);\n }\n\n /**\n * Remove an event handler of a specific event name or remove all event handlers for a specific event.\n *\n * @memberof Chartist.Event\n * @param {String} event The event name where a specific or all handlers should be removed\n * @param {Function} [handler] An optional event handler function. If specified only this specific handler will be removed and otherwise all handlers are removed.\n */\n function removeEventHandler(event, handler) {\n // Only do something if there are event handlers with this name existing\n if(handlers[event]) {\n // If handler is set we will look for a specific handler and only remove this\n if(handler) {\n handlers[event].splice(handlers[event].indexOf(handler), 1);\n if(handlers[event].length === 0) {\n delete handlers[event];\n }\n } else {\n // If no handler is specified we remove all handlers for this event\n delete handlers[event];\n }\n }\n }\n\n /**\n * Use this function to emit an event. All handlers that are listening for this event will be triggered with the data parameter.\n *\n * @memberof Chartist.Event\n * @param {String} event The event name that should be triggered\n * @param {*} data Arbitrary data that will be passed to the event handler callback functions\n */\n function emit(event, data) {\n // Only do something if there are event handlers with this name existing\n if(handlers[event]) {\n handlers[event].forEach(function(handler) {\n handler(data);\n });\n }\n\n // Emit event to star event handlers\n if(handlers['*']) {\n handlers['*'].forEach(function(starHandler) {\n starHandler(event, data);\n });\n }\n }\n\n return {\n addEventHandler: addEventHandler,\n removeEventHandler: removeEventHandler,\n emit: emit\n };\n };\n\n}(window, document, Chartist));\n;/**\n * This module provides some basic prototype inheritance utilities.\n *\n * @module Chartist.Class\n */\n/* global Chartist */\n(function(window, document, Chartist) {\n 'use strict';\n\n function listToArray(list) {\n var arr = [];\n if (list.length) {\n for (var i = 0; i < list.length; i++) {\n arr.push(list[i]);\n }\n }\n return arr;\n }\n\n /**\n * Method to extend from current prototype.\n *\n * @memberof Chartist.Class\n * @param {Object} properties The object that serves as definition for the prototype that gets created for the new class. This object should always contain a constructor property that is the desired constructor for the newly created class.\n * @param {Object} [superProtoOverride] By default extens will use the current class prototype or Chartist.class. With this parameter you can specify any super prototype that will be used.\n * @return {Function} Constructor function of the new class\n *\n * @example\n * var Fruit = Class.extend({\n * color: undefined,\n * sugar: undefined,\n *\n * constructor: function(color, sugar) {\n * this.color = color;\n * this.sugar = sugar;\n * },\n *\n * eat: function() {\n * this.sugar = 0;\n * return this;\n * }\n * });\n *\n * var Banana = Fruit.extend({\n * length: undefined,\n *\n * constructor: function(length, sugar) {\n * Banana.super.constructor.call(this, 'Yellow', sugar);\n * this.length = length;\n * }\n * });\n *\n * var banana = new Banana(20, 40);\n * console.log('banana instanceof Fruit', banana instanceof Fruit);\n * console.log('Fruit is prototype of banana', Fruit.prototype.isPrototypeOf(banana));\n * console.log('bananas prototype is Fruit', Object.getPrototypeOf(banana) === Fruit.prototype);\n * console.log(banana.sugar);\n * console.log(banana.eat().sugar);\n * console.log(banana.color);\n */\n function extend(properties, superProtoOverride) {\n var superProto = superProtoOverride || this.prototype || Chartist.Class;\n var proto = Object.create(superProto);\n\n Chartist.Class.cloneDefinitions(proto, properties);\n\n var constr = function() {\n var fn = proto.constructor || function () {},\n instance;\n\n // If this is linked to the Chartist namespace the constructor was not called with new\n // To provide a fallback we will instantiate here and return the instance\n instance = this === Chartist ? Object.create(proto) : this;\n fn.apply(instance, Array.prototype.slice.call(arguments, 0));\n\n // If this constructor was not called with new we need to return the instance\n // This will not harm when the constructor has been called with new as the returned value is ignored\n return instance;\n };\n\n constr.prototype = proto;\n constr.super = superProto;\n constr.extend = this.extend;\n\n return constr;\n }\n\n // Variable argument list clones args > 0 into args[0] and retruns modified args[0]\n function cloneDefinitions() {\n var args = listToArray(arguments);\n var target = args[0];\n\n args.splice(1, args.length - 1).forEach(function (source) {\n Object.getOwnPropertyNames(source).forEach(function (propName) {\n // If this property already exist in target we delete it first\n delete target[propName];\n // Define the property with the descriptor from source\n Object.defineProperty(target, propName,\n Object.getOwnPropertyDescriptor(source, propName));\n });\n });\n\n return target;\n }\n\n Chartist.Class = {\n extend: extend,\n cloneDefinitions: cloneDefinitions\n };\n\n}(window, document, Chartist));\n;/**\n * Base for all chart types. The methods in Chartist.Base are inherited to all chart types.\n *\n * @module Chartist.Base\n */\n/* global Chartist */\n(function(window, document, Chartist) {\n 'use strict';\n\n // TODO: Currently we need to re-draw the chart on window resize. This is usually very bad and will affect performance.\n // This is done because we can't work with relative coordinates when drawing the chart because SVG Path does not\n // work with relative positions yet. We need to check if we can do a viewBox hack to switch to percentage.\n // See http://mozilla.6506.n7.nabble.com/Specyfing-paths-with-percentages-unit-td247474.html\n // Update: can be done using the above method tested here: http://codepen.io/gionkunz/pen/KDvLj\n // The problem is with the label offsets that can't be converted into percentage and affecting the chart container\n /**\n * Updates the chart which currently does a full reconstruction of the SVG DOM\n *\n * @param {Object} [data] Optional data you'd like to set for the chart before it will update. If not specified the update method will use the data that is already configured with the chart.\n * @param {Object} [options] Optional options you'd like to add to the previous options for the chart before it will update. If not specified the update method will use the options that have been already configured with the chart.\n * @param {Boolean} [override] If set to true, the passed options will be used to extend the options that have been configured already. Otherwise the chart default options will be used as the base\n * @memberof Chartist.Base\n */\n function update(data, options, override) {\n if(data) {\n this.data = data;\n // Event for data transformation that allows to manipulate the data before it gets rendered in the charts\n this.eventEmitter.emit('data', {\n type: 'update',\n data: this.data\n });\n }\n\n if(options) {\n this.options = Chartist.extend({}, override ? this.options : this.defaultOptions, options);\n\n // If chartist was not initialized yet, we just set the options and leave the rest to the initialization\n // Otherwise we re-create the optionsProvider at this point\n if(!this.initializeTimeoutId) {\n this.optionsProvider.removeMediaQueryListeners();\n this.optionsProvider = Chartist.optionsProvider(this.options, this.responsiveOptions, this.eventEmitter);\n }\n }\n\n // Only re-created the chart if it has been initialized yet\n if(!this.initializeTimeoutId) {\n this.createChart(this.optionsProvider.currentOptions);\n }\n\n // Return a reference to the chart object to chain up calls\n return this;\n }\n\n /**\n * This method can be called on the API object of each chart and will un-register all event listeners that were added to other components. This currently includes a window.resize listener as well as media query listeners if any responsive options have been provided. Use this function if you need to destroy and recreate Chartist charts dynamically.\n *\n * @memberof Chartist.Base\n */\n function detach() {\n window.removeEventListener('resize', this.resizeListener);\n this.optionsProvider.removeMediaQueryListeners();\n return this;\n }\n\n /**\n * Use this function to register event handlers. The handler callbacks are synchronous and will run in the main thread rather than the event loop.\n *\n * @memberof Chartist.Base\n * @param {String} event Name of the event. Check the examples for supported events.\n * @param {Function} handler The handler function that will be called when an event with the given name was emitted. This function will receive a data argument which contains event data. See the example for more details.\n */\n function on(event, handler) {\n this.eventEmitter.addEventHandler(event, handler);\n return this;\n }\n\n /**\n * Use this function to un-register event handlers. If the handler function parameter is omitted all handlers for the given event will be un-registered.\n *\n * @memberof Chartist.Base\n * @param {String} event Name of the event for which a handler should be removed\n * @param {Function} [handler] The handler function that that was previously used to register a new event handler. This handler will be removed from the event handler list. If this parameter is omitted then all event handlers for the given event are removed from the list.\n */\n function off(event, handler) {\n this.eventEmitter.removeEventHandler(event, handler);\n return this;\n }\n\n function initialize() {\n // Add window resize listener that re-creates the chart\n window.addEventListener('resize', this.resizeListener);\n\n // Obtain current options based on matching media queries (if responsive options are given)\n // This will also register a listener that is re-creating the chart based on media changes\n this.optionsProvider = Chartist.optionsProvider(this.options, this.responsiveOptions, this.eventEmitter);\n // Register options change listener that will trigger a chart update\n this.eventEmitter.addEventHandler('optionsChanged', function() {\n this.update();\n }.bind(this));\n\n // Before the first chart creation we need to register us with all plugins that are configured\n // Initialize all relevant plugins with our chart object and the plugin options specified in the config\n if(this.options.plugins) {\n this.options.plugins.forEach(function(plugin) {\n if(plugin instanceof Array) {\n plugin[0](this, plugin[1]);\n } else {\n plugin(this);\n }\n }.bind(this));\n }\n\n // Event for data transformation that allows to manipulate the data before it gets rendered in the charts\n this.eventEmitter.emit('data', {\n type: 'initial',\n data: this.data\n });\n\n // Create the first chart\n this.createChart(this.optionsProvider.currentOptions);\n\n // As chart is initialized from the event loop now we can reset our timeout reference\n // This is important if the chart gets initialized on the same element twice\n this.initializeTimeoutId = undefined;\n }\n\n /**\n * Constructor of chart base class.\n *\n * @param query\n * @param data\n * @param defaultOptions\n * @param options\n * @param responsiveOptions\n * @constructor\n */\n function Base(query, data, defaultOptions, options, responsiveOptions) {\n this.container = Chartist.querySelector(query);\n this.data = data;\n this.defaultOptions = defaultOptions;\n this.options = options;\n this.responsiveOptions = responsiveOptions;\n this.eventEmitter = Chartist.EventEmitter();\n this.supportsForeignObject = Chartist.Svg.isSupported('Extensibility');\n this.supportsAnimations = Chartist.Svg.isSupported('AnimationEventsAttribute');\n this.resizeListener = function resizeListener(){\n this.update();\n }.bind(this);\n\n if(this.container) {\n // If chartist was already initialized in this container we are detaching all event listeners first\n if(this.container.__chartist__) {\n if(this.container.__chartist__.initializeTimeoutId) {\n // If the initializeTimeoutId is still set we can safely assume that the initialization function has not\n // been called yet from the event loop. Therefore we should cancel the timeout and don't need to detach\n window.clearTimeout(this.container.__chartist__.initializeTimeoutId);\n } else {\n // The timeout reference has already been reset which means we need to detach the old chart first\n this.container.__chartist__.detach();\n }\n }\n\n this.container.__chartist__ = this;\n }\n\n // Using event loop for first draw to make it possible to register event listeners in the same call stack where\n // the chart was created.\n this.initializeTimeoutId = setTimeout(initialize.bind(this), 0);\n }\n\n // Creating the chart base class\n Chartist.Base = Chartist.Class.extend({\n constructor: Base,\n optionsProvider: undefined,\n container: undefined,\n svg: undefined,\n eventEmitter: undefined,\n createChart: function() {\n throw new Error('Base chart type can\\'t be instantiated!');\n },\n update: update,\n detach: detach,\n on: on,\n off: off,\n version: Chartist.version,\n supportsForeignObject: false\n });\n\n}(window, document, Chartist));\n;/**\n * Chartist SVG module for simple SVG DOM abstraction\n *\n * @module Chartist.Svg\n */\n/* global Chartist */\n(function(window, document, Chartist) {\n 'use strict';\n\n var svgNs = 'http://www.w3.org/2000/svg',\n xmlNs = 'http://www.w3.org/2000/xmlns/',\n xhtmlNs = 'http://www.w3.org/1999/xhtml';\n\n Chartist.xmlNs = {\n qualifiedName: 'xmlns:ct',\n prefix: 'ct',\n uri: 'http://gionkunz.github.com/chartist-js/ct'\n };\n\n /**\n * Chartist.Svg creates a new SVG object wrapper with a starting element. You can use the wrapper to fluently create sub-elements and modify them.\n *\n * @memberof Chartist.Svg\n * @constructor\n * @param {String|SVGElement} name The name of the SVG element to create or an SVG dom element which should be wrapped into Chartist.Svg\n * @param {Object} attributes An object with properties that will be added as attributes to the SVG element that is created. Attributes with undefined values will not be added.\n * @param {String} className This class or class list will be added to the SVG element\n * @param {Object} parent The parent SVG wrapper object where this newly created wrapper and it's element will be attached to as child\n * @param {Boolean} insertFirst If this param is set to true in conjunction with a parent element the newly created element will be added as first child element in the parent element\n */\n function Svg(name, attributes, className, parent, insertFirst) {\n // If Svg is getting called with an SVG element we just return the wrapper\n if(name instanceof SVGElement) {\n this._node = name;\n } else {\n this._node = document.createElementNS(svgNs, name);\n\n // If this is an SVG element created then custom namespace\n if(name === 'svg') {\n this._node.setAttributeNS(xmlNs, Chartist.xmlNs.qualifiedName, Chartist.xmlNs.uri);\n }\n\n if(attributes) {\n this.attr(attributes);\n }\n\n if(className) {\n this.addClass(className);\n }\n\n if(parent) {\n if (insertFirst && parent._node.firstChild) {\n parent._node.insertBefore(this._node, parent._node.firstChild);\n } else {\n parent._node.appendChild(this._node);\n }\n }\n }\n }\n\n /**\n * Set attributes on the current SVG element of the wrapper you're currently working on.\n *\n * @memberof Chartist.Svg\n * @param {Object|String} attributes An object with properties that will be added as attributes to the SVG element that is created. Attributes with undefined values will not be added. If this parameter is a String then the function is used as a getter and will return the attribute value.\n * @param {String} ns If specified, the attributes will be set as namespace attributes with ns as prefix.\n * @return {Object|String} The current wrapper object will be returned so it can be used for chaining or the attribute value if used as getter function.\n */\n function attr(attributes, ns) {\n if(typeof attributes === 'string') {\n if(ns) {\n return this._node.getAttributeNS(ns, attributes);\n } else {\n return this._node.getAttribute(attributes);\n }\n }\n\n Object.keys(attributes).forEach(function(key) {\n // If the attribute value is undefined we can skip this one\n if(attributes[key] === undefined) {\n return;\n }\n\n if(ns) {\n this._node.setAttributeNS(ns, [Chartist.xmlNs.prefix, ':', key].join(''), attributes[key]);\n } else {\n this._node.setAttribute(key, attributes[key]);\n }\n }.bind(this));\n\n return this;\n }\n\n /**\n * Create a new SVG element whose wrapper object will be selected for further operations. This way you can also create nested groups easily.\n *\n * @memberof Chartist.Svg\n * @param {String} name The name of the SVG element that should be created as child element of the currently selected element wrapper\n * @param {Object} [attributes] An object with properties that will be added as attributes to the SVG element that is created. Attributes with undefined values will not be added.\n * @param {String} [className] This class or class list will be added to the SVG element\n * @param {Boolean} [insertFirst] If this param is set to true in conjunction with a parent element the newly created element will be added as first child element in the parent element\n * @return {Chartist.Svg} Returns a Chartist.Svg wrapper object that can be used to modify the containing SVG data\n */\n function elem(name, attributes, className, insertFirst) {\n return new Chartist.Svg(name, attributes, className, this, insertFirst);\n }\n\n /**\n * Returns the parent Chartist.SVG wrapper object\n *\n * @return {Chartist.Svg} Returns a Chartist.Svg wrapper around the parent node of the current node. If the parent node is not existing or it's not an SVG node then this function will return null.\n */\n function parent() {\n return this._node.parentNode instanceof SVGElement ? new Chartist.Svg(this._node.parentNode) : null;\n }\n\n /**\n * This method returns a Chartist.Svg wrapper around the root SVG element of the current tree.\n *\n * @return {Chartist.Svg} The root SVG element wrapped in a Chartist.Svg element\n */\n function root() {\n var node = this._node;\n while(node.nodeName !== 'svg') {\n node = node.parentNode;\n }\n return new Chartist.Svg(node);\n }\n\n /**\n * Find the first child SVG element of the current element that matches a CSS selector. The returned object is a Chartist.Svg wrapper.\n *\n * @param {String} selector A CSS selector that is used to query for child SVG elements\n * @return {Chartist.Svg} The SVG wrapper for the element found or null if no element was found\n */\n function querySelector(selector) {\n var foundNode = this._node.querySelector(selector);\n return foundNode ? new Chartist.Svg(foundNode) : null;\n }\n\n /**\n * Find the all child SVG elements of the current element that match a CSS selector. The returned object is a Chartist.Svg.List wrapper.\n *\n * @param {String} selector A CSS selector that is used to query for child SVG elements\n * @return {Chartist.Svg.List} The SVG wrapper list for the element found or null if no element was found\n */\n function querySelectorAll(selector) {\n var foundNodes = this._node.querySelectorAll(selector);\n return foundNodes.length ? new Chartist.Svg.List(foundNodes) : null;\n }\n\n /**\n * This method creates a foreignObject (see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject) that allows to embed HTML content into a SVG graphic. With the help of foreignObjects you can enable the usage of regular HTML elements inside of SVG where they are subject for SVG positioning and transformation but the Browser will use the HTML rendering capabilities for the containing DOM.\n *\n * @memberof Chartist.Svg\n * @param {Node|String} content The DOM Node, or HTML string that will be converted to a DOM Node, that is then placed into and wrapped by the foreignObject\n * @param {String} [attributes] An object with properties that will be added as attributes to the foreignObject element that is created. Attributes with undefined values will not be added.\n * @param {String} [className] This class or class list will be added to the SVG element\n * @param {Boolean} [insertFirst] Specifies if the foreignObject should be inserted as first child\n * @return {Chartist.Svg} New wrapper object that wraps the foreignObject element\n */\n function foreignObject(content, attributes, className, insertFirst) {\n // If content is string then we convert it to DOM\n // TODO: Handle case where content is not a string nor a DOM Node\n if(typeof content === 'string') {\n var container = document.createElement('div');\n container.innerHTML = content;\n content = container.firstChild;\n }\n\n // Adding namespace to content element\n content.setAttribute('xmlns', xhtmlNs);\n\n // Creating the foreignObject without required extension attribute (as described here\n // http://www.w3.org/TR/SVG/extend.html#ForeignObjectElement)\n var fnObj = this.elem('foreignObject', attributes, className, insertFirst);\n\n // Add content to foreignObjectElement\n fnObj._node.appendChild(content);\n\n return fnObj;\n }\n\n /**\n * This method adds a new text element to the current Chartist.Svg wrapper.\n *\n * @memberof Chartist.Svg\n * @param {String} t The text that should be added to the text element that is created\n * @return {Chartist.Svg} The same wrapper object that was used to add the newly created element\n */\n function text(t) {\n this._node.appendChild(document.createTextNode(t));\n return this;\n }\n\n /**\n * This method will clear all child nodes of the current wrapper object.\n *\n * @memberof Chartist.Svg\n * @return {Chartist.Svg} The same wrapper object that got emptied\n */\n function empty() {\n while (this._node.firstChild) {\n this._node.removeChild(this._node.firstChild);\n }\n\n return this;\n }\n\n /**\n * This method will cause the current wrapper to remove itself from its parent wrapper. Use this method if you'd like to get rid of an element in a given DOM structure.\n *\n * @memberof Chartist.Svg\n * @return {Chartist.Svg} The parent wrapper object of the element that got removed\n */\n function remove() {\n this._node.parentNode.removeChild(this._node);\n return this.parent();\n }\n\n /**\n * This method will replace the element with a new element that can be created outside of the current DOM.\n *\n * @memberof Chartist.Svg\n * @param {Chartist.Svg} newElement The new Chartist.Svg object that will be used to replace the current wrapper object\n * @return {Chartist.Svg} The wrapper of the new element\n */\n function replace(newElement) {\n this._node.parentNode.replaceChild(newElement._node, this._node);\n return newElement;\n }\n\n /**\n * This method will append an element to the current element as a child.\n *\n * @memberof Chartist.Svg\n * @param {Chartist.Svg} element The Chartist.Svg element that should be added as a child\n * @param {Boolean} [insertFirst] Specifies if the element should be inserted as first child\n * @return {Chartist.Svg} The wrapper of the appended object\n */\n function append(element, insertFirst) {\n if(insertFirst && this._node.firstChild) {\n this._node.insertBefore(element._node, this._node.firstChild);\n } else {\n this._node.appendChild(element._node);\n }\n\n return this;\n }\n\n /**\n * Returns an array of class names that are attached to the current wrapper element. This method can not be chained further.\n *\n * @memberof Chartist.Svg\n * @return {Array} A list of classes or an empty array if there are no classes on the current element\n */\n function classes() {\n return this._node.getAttribute('class') ? this._node.getAttribute('class').trim().split(/\\s+/) : [];\n }\n\n /**\n * Adds one or a space separated list of classes to the current element and ensures the classes are only existing once.\n *\n * @memberof Chartist.Svg\n * @param {String} names A white space separated list of class names\n * @return {Chartist.Svg} The wrapper of the current element\n */\n function addClass(names) {\n this._node.setAttribute('class',\n this.classes(this._node)\n .concat(names.trim().split(/\\s+/))\n .filter(function(elem, pos, self) {\n return self.indexOf(elem) === pos;\n }).join(' ')\n );\n\n return this;\n }\n\n /**\n * Removes one or a space separated list of classes from the current element.\n *\n * @memberof Chartist.Svg\n * @param {String} names A white space separated list of class names\n * @return {Chartist.Svg} The wrapper of the current element\n */\n function removeClass(names) {\n var removedClasses = names.trim().split(/\\s+/);\n\n this._node.setAttribute('class', this.classes(this._node).filter(function(name) {\n return removedClasses.indexOf(name) === -1;\n }).join(' '));\n\n return this;\n }\n\n /**\n * Removes all classes from the current element.\n *\n * @memberof Chartist.Svg\n * @return {Chartist.Svg} The wrapper of the current element\n */\n function removeAllClasses() {\n this._node.setAttribute('class', '');\n\n return this;\n }\n\n /**\n * Get element height with fallback to svg BoundingBox or parent container dimensions:\n * See [bugzilla.mozilla.org](https://bugzilla.mozilla.org/show_bug.cgi?id=530985)\n *\n * @memberof Chartist.Svg\n * @return {Number} The elements height in pixels\n */\n function height() {\n return this._node.clientHeight || Math.round(this._node.getBBox().height) || this._node.parentNode.clientHeight;\n }\n\n /**\n * Get element width with fallback to svg BoundingBox or parent container dimensions:\n * See [bugzilla.mozilla.org](https://bugzilla.mozilla.org/show_bug.cgi?id=530985)\n *\n * @memberof Chartist.Core\n * @return {Number} The elements width in pixels\n */\n function width() {\n return this._node.clientWidth || Math.round(this._node.getBBox().width) || this._node.parentNode.clientWidth;\n }\n\n /**\n * The animate function lets you animate the current element with SMIL animations. You can add animations for multiple attributes at the same time by using an animation definition object. This object should contain SMIL animation attributes. Please refer to http://www.w3.org/TR/SVG/animate.html for a detailed specification about the available animation attributes. Additionally an easing property can be passed in the animation definition object. This can be a string with a name of an easing function in `Chartist.Svg.Easing` or an array with four numbers specifying a cubic Bézier curve.\n * **An animations object could look like this:**\n * ```javascript\n * element.animate({\n * opacity: {\n * dur: 1000,\n * from: 0,\n * to: 1\n * },\n * x1: {\n * dur: '1000ms',\n * from: 100,\n * to: 200,\n * easing: 'easeOutQuart'\n * },\n * y1: {\n * dur: '2s',\n * from: 0,\n * to: 100\n * }\n * });\n * ```\n * **Automatic unit conversion**\n * For the `dur` and the `begin` animate attribute you can also omit a unit by passing a number. The number will automatically be converted to milli seconds.\n * **Guided mode**\n * The default behavior of SMIL animations with offset using the `begin` attribute is that the attribute will keep it's original value until the animation starts. Mostly this behavior is not desired as you'd like to have your element attributes already initialized with the animation `from` value even before the animation starts. Also if you don't specify `fill=\"freeze\"` on an animate element or if you delete the animation after it's done (which is done in guided mode) the attribute will switch back to the initial value. This behavior is also not desired when performing simple one-time animations. For one-time animations you'd want to trigger animations immediately instead of relative to the document begin time. That's why in guided mode Chartist.Svg will also use the `begin` property to schedule a timeout and manually start the animation after the timeout. If you're using multiple SMIL definition objects for an attribute (in an array), guided mode will be disabled for this attribute, even if you explicitly enabled it.\n * If guided mode is enabled the following behavior is added:\n * - Before the animation starts (even when delayed with `begin`) the animated attribute will be set already to the `from` value of the animation\n * - `begin` is explicitly set to `indefinite` so it can be started manually without relying on document begin time (creation)\n * - The animate element will be forced to use `fill=\"freeze\"`\n * - The animation will be triggered with `beginElement()` in a timeout where `begin` of the definition object is interpreted in milli seconds. If no `begin` was specified the timeout is triggered immediately.\n * - After the animation the element attribute value will be set to the `to` value of the animation\n * - The animate element is deleted from the DOM\n *\n * @memberof Chartist.Svg\n * @param {Object} animations An animations object where the property keys are the attributes you'd like to animate. The properties should be objects again that contain the SMIL animation attributes (usually begin, dur, from, and to). The property begin and dur is auto converted (see Automatic unit conversion). You can also schedule multiple animations for the same attribute by passing an Array of SMIL definition objects. Attributes that contain an array of SMIL definition objects will not be executed in guided mode.\n * @param {Boolean} guided Specify if guided mode should be activated for this animation (see Guided mode). If not otherwise specified, guided mode will be activated.\n * @param {Object} eventEmitter If specified, this event emitter will be notified when an animation starts or ends.\n * @return {Chartist.Svg} The current element where the animation was added\n */\n function animate(animations, guided, eventEmitter) {\n if(guided === undefined) {\n guided = true;\n }\n\n Object.keys(animations).forEach(function createAnimateForAttributes(attribute) {\n\n function createAnimate(animationDefinition, guided) {\n var attributeProperties = {},\n animate,\n timeout,\n easing;\n\n // Check if an easing is specified in the definition object and delete it from the object as it will not\n // be part of the animate element attributes.\n if(animationDefinition.easing) {\n // If already an easing Bézier curve array we take it or we lookup a easing array in the Easing object\n easing = animationDefinition.easing instanceof Array ?\n animationDefinition.easing :\n Chartist.Svg.Easing[animationDefinition.easing];\n delete animationDefinition.easing;\n }\n\n // If numeric dur or begin was provided we assume milli seconds\n animationDefinition.begin = Chartist.ensureUnit(animationDefinition.begin, 'ms');\n animationDefinition.dur = Chartist.ensureUnit(animationDefinition.dur, 'ms');\n\n if(easing) {\n animationDefinition.calcMode = 'spline';\n animationDefinition.keySplines = easing.join(' ');\n animationDefinition.keyTimes = '0;1';\n }\n\n // Adding \"fill: freeze\" if we are in guided mode and set initial attribute values\n if(guided) {\n animationDefinition.fill = 'freeze';\n // Animated property on our element should already be set to the animation from value in guided mode\n attributeProperties[attribute] = animationDefinition.from;\n this.attr(attributeProperties);\n\n // In guided mode we also set begin to indefinite so we can trigger the start manually and put the begin\n // which needs to be in ms aside\n timeout = Chartist.stripUnit(animationDefinition.begin || 0);\n animationDefinition.begin = 'indefinite';\n }\n\n animate = this.elem('animate', Chartist.extend({\n attributeName: attribute\n }, animationDefinition));\n\n if(guided) {\n // If guided we take the value that was put aside in timeout and trigger the animation manually with a timeout\n setTimeout(function() {\n // If beginElement fails we set the animated attribute to the end position and remove the animate element\n // This happens if the SMIL ElementTimeControl interface is not supported or any other problems occured in\n // the browser. (Currently FF 34 does not support animate elements in foreignObjects)\n try {\n animate._node.beginElement();\n } catch(err) {\n // Set animated attribute to current animated value\n attributeProperties[attribute] = animationDefinition.to;\n this.attr(attributeProperties);\n // Remove the animate element as it's no longer required\n animate.remove();\n }\n }.bind(this), timeout);\n }\n\n if(eventEmitter) {\n animate._node.addEventListener('beginEvent', function handleBeginEvent() {\n eventEmitter.emit('animationBegin', {\n element: this,\n animate: animate._node,\n params: animationDefinition\n });\n }.bind(this));\n }\n\n animate._node.addEventListener('endEvent', function handleEndEvent() {\n if(eventEmitter) {\n eventEmitter.emit('animationEnd', {\n element: this,\n animate: animate._node,\n params: animationDefinition\n });\n }\n\n if(guided) {\n // Set animated attribute to current animated value\n attributeProperties[attribute] = animationDefinition.to;\n this.attr(attributeProperties);\n // Remove the animate element as it's no longer required\n animate.remove();\n }\n }.bind(this));\n }\n\n // If current attribute is an array of definition objects we create an animate for each and disable guided mode\n if(animations[attribute] instanceof Array) {\n animations[attribute].forEach(function(animationDefinition) {\n createAnimate.bind(this)(animationDefinition, false);\n }.bind(this));\n } else {\n createAnimate.bind(this)(animations[attribute], guided);\n }\n\n }.bind(this));\n\n return this;\n }\n\n Chartist.Svg = Chartist.Class.extend({\n constructor: Svg,\n attr: attr,\n elem: elem,\n parent: parent,\n root: root,\n querySelector: querySelector,\n querySelectorAll: querySelectorAll,\n foreignObject: foreignObject,\n text: text,\n empty: empty,\n remove: remove,\n replace: replace,\n append: append,\n classes: classes,\n addClass: addClass,\n removeClass: removeClass,\n removeAllClasses: removeAllClasses,\n height: height,\n width: width,\n animate: animate\n });\n\n /**\n * This method checks for support of a given SVG feature like Extensibility, SVG-animation or the like. Check http://www.w3.org/TR/SVG11/feature for a detailed list.\n *\n * @memberof Chartist.Svg\n * @param {String} feature The SVG 1.1 feature that should be checked for support.\n * @return {Boolean} True of false if the feature is supported or not\n */\n Chartist.Svg.isSupported = function(feature) {\n return document.implementation.hasFeature('www.http://w3.org/TR/SVG11/feature#' + feature, '1.1');\n };\n\n /**\n * This Object contains some standard easing cubic bezier curves. Then can be used with their name in the `Chartist.Svg.animate`. You can also extend the list and use your own name in the `animate` function. Click the show code button to see the available bezier functions.\n *\n * @memberof Chartist.Svg\n */\n var easingCubicBeziers = {\n easeInSine: [0.47, 0, 0.745, 0.715],\n easeOutSine: [0.39, 0.575, 0.565, 1],\n easeInOutSine: [0.445, 0.05, 0.55, 0.95],\n easeInQuad: [0.55, 0.085, 0.68, 0.53],\n easeOutQuad: [0.25, 0.46, 0.45, 0.94],\n easeInOutQuad: [0.455, 0.03, 0.515, 0.955],\n easeInCubic: [0.55, 0.055, 0.675, 0.19],\n easeOutCubic: [0.215, 0.61, 0.355, 1],\n easeInOutCubic: [0.645, 0.045, 0.355, 1],\n easeInQuart: [0.895, 0.03, 0.685, 0.22],\n easeOutQuart: [0.165, 0.84, 0.44, 1],\n easeInOutQuart: [0.77, 0, 0.175, 1],\n easeInQuint: [0.755, 0.05, 0.855, 0.06],\n easeOutQuint: [0.23, 1, 0.32, 1],\n easeInOutQuint: [0.86, 0, 0.07, 1],\n easeInExpo: [0.95, 0.05, 0.795, 0.035],\n easeOutExpo: [0.19, 1, 0.22, 1],\n easeInOutExpo: [1, 0, 0, 1],\n easeInCirc: [0.6, 0.04, 0.98, 0.335],\n easeOutCirc: [0.075, 0.82, 0.165, 1],\n easeInOutCirc: [0.785, 0.135, 0.15, 0.86],\n easeInBack: [0.6, -0.28, 0.735, 0.045],\n easeOutBack: [0.175, 0.885, 0.32, 1.275],\n easeInOutBack: [0.68, -0.55, 0.265, 1.55]\n };\n\n Chartist.Svg.Easing = easingCubicBeziers;\n\n /**\n * This helper class is to wrap multiple `Chartist.Svg` elements into a list where you can call the `Chartist.Svg` functions on all elements in the list with one call. This is helpful when you'd like to perform calls with `Chartist.Svg` on multiple elements.\n * An instance of this class is also returned by `Chartist.Svg.querySelectorAll`.\n *\n * @memberof Chartist.Svg\n * @param {Array|NodeList} nodeList An Array of SVG DOM nodes or a SVG DOM NodeList (as returned by document.querySelectorAll)\n * @constructor\n */\n function SvgList(nodeList) {\n var list = this;\n\n this.svgElements = [];\n for(var i = 0; i < nodeList.length; i++) {\n this.svgElements.push(new Chartist.Svg(nodeList[i]));\n }\n\n // Add delegation methods for Chartist.Svg\n Object.keys(Chartist.Svg.prototype).filter(function(prototypeProperty) {\n return ['constructor',\n 'parent',\n 'querySelector',\n 'querySelectorAll',\n 'replace',\n 'append',\n 'classes',\n 'height',\n 'width'].indexOf(prototypeProperty) === -1;\n }).forEach(function(prototypeProperty) {\n list[prototypeProperty] = function() {\n var args = Array.prototype.slice.call(arguments, 0);\n list.svgElements.forEach(function(element) {\n Chartist.Svg.prototype[prototypeProperty].apply(element, args);\n });\n return list;\n };\n });\n }\n\n Chartist.Svg.List = Chartist.Class.extend({\n constructor: SvgList\n });\n}(window, document, Chartist));\n;/**\n * Chartist SVG path module for SVG path description creation and modification.\n *\n * @module Chartist.Svg.Path\n */\n/* global Chartist */\n(function(window, document, Chartist) {\n 'use strict';\n\n /**\n * Contains the descriptors of supported element types in a SVG path. Currently only move, line and curve are supported.\n *\n * @memberof Chartist.Svg.Path\n * @type {Object}\n */\n var elementDescriptions = {\n m: ['x', 'y'],\n l: ['x', 'y'],\n c: ['x1', 'y1', 'x2', 'y2', 'x', 'y']\n };\n\n /**\n * Default options for newly created SVG path objects.\n *\n * @memberof Chartist.Svg.Path\n * @type {Object}\n */\n var defaultOptions = {\n // The accuracy in digit count after the decimal point. This will be used to round numbers in the SVG path. If this option is set to false then no rounding will be performed.\n accuracy: 3\n };\n\n function element(command, params, pathElements, pos, relative) {\n pathElements.splice(pos, 0, Chartist.extend({\n command: relative ? command.toLowerCase() : command.toUpperCase()\n }, params));\n }\n\n function forEachParam(pathElements, cb) {\n pathElements.forEach(function(pathElement, pathElementIndex) {\n elementDescriptions[pathElement.command.toLowerCase()].forEach(function(paramName, paramIndex) {\n cb(pathElement, paramName, pathElementIndex, paramIndex, pathElements);\n });\n });\n }\n\n /**\n * Used to construct a new path object.\n *\n * @memberof Chartist.Svg.Path\n * @param {Boolean} close If set to true then this path will be closed when stringified (with a Z at the end)\n * @param {Object} options Options object that overrides the default objects. See default options for more details.\n * @constructor\n */\n function SvgPath(close, options) {\n this.pathElements = [];\n this.pos = 0;\n this.close = close;\n this.options = Chartist.extend({}, defaultOptions, options);\n }\n\n /**\n * Gets or sets the current position (cursor) inside of the path. You can move around the cursor freely but limited to 0 or the count of existing elements. All modifications with element functions will insert new elements at the position of this cursor.\n *\n * @memberof Chartist.Svg.Path\n * @param {Number} [position] If a number is passed then the cursor is set to this position in the path element array.\n * @return {Chartist.Svg.Path|Number} If the position parameter was passed then the return value will be the path object for easy call chaining. If no position parameter was passed then the current position is returned.\n */\n function position(pos) {\n if(pos !== undefined) {\n this.pos = Math.max(0, Math.min(this.pathElements.length, pos));\n return this;\n } else {\n return this.pos;\n }\n }\n\n /**\n * Removes elements from the path starting at the current position.\n *\n * @memberof Chartist.Svg.Path\n * @param {Number} count Number of path elements that should be removed from the current position.\n * @return {Chartist.Svg.Path} The current path object for easy call chaining.\n */\n function remove(count) {\n this.pathElements.splice(this.pos, count);\n return this;\n }\n\n /**\n * Use this function to add a new move SVG path element.\n *\n * @memberof Chartist.Svg.Path\n * @param {Number} x The x coordinate for the move element.\n * @param {Number} y The y coordinate for the move element.\n * @param {Boolean} relative If set to true the move element will be created with relative coordinates (lowercase letter)\n * @return {Chartist.Svg.Path} The current path object for easy call chaining.\n */\n function move(x, y, relative) {\n element('M', {\n x: +x,\n y: +y\n }, this.pathElements, this.pos++, relative);\n return this;\n }\n\n /**\n * Use this function to add a new line SVG path element.\n *\n * @memberof Chartist.Svg.Path\n * @param {Number} x The x coordinate for the line element.\n * @param {Number} y The y coordinate for the line element.\n * @param {Boolean} relative If set to true the line element will be created with relative coordinates (lowercase letter)\n * @return {Chartist.Svg.Path} The current path object for easy call chaining.\n */\n function line(x, y, relative) {\n element('L', {\n x: +x,\n y: +y\n }, this.pathElements, this.pos++, relative);\n return this;\n }\n\n /**\n * Use this function to add a new curve SVG path element.\n *\n * @memberof Chartist.Svg.Path\n * @param {Number} x1 The x coordinate for the first control point of the bezier curve.\n * @param {Number} y1 The y coordinate for the first control point of the bezier curve.\n * @param {Number} x2 The x coordinate for the second control point of the bezier curve.\n * @param {Number} y2 The y coordinate for the second control point of the bezier curve.\n * @param {Number} x The x coordinate for the target point of the curve element.\n * @param {Number} y The y coordinate for the target point of the curve element.\n * @param {Boolean} relative If set to true the curve element will be created with relative coordinates (lowercase letter)\n * @return {Chartist.Svg.Path} The current path object for easy call chaining.\n */\n function curve(x1, y1, x2, y2, x, y, relative) {\n element('C', {\n x1: +x1,\n y1: +y1,\n x2: +x2,\n y2: +y2,\n x: +x,\n y: +y\n }, this.pathElements, this.pos++, relative);\n return this;\n }\n\n /**\n * Parses an SVG path seen in the d attribute of path elements, and inserts the parsed elements into the existing path object at the current cursor position. Any closing path indicators (Z at the end of the path) will be ignored by the parser as this is provided by the close option in the options of the path object.\n *\n * @memberof Chartist.Svg.Path\n * @param {String} path Any SVG path that contains move (m), line (l) or curve (c) components.\n * @return {Chartist.Svg.Path} The current path object for easy call chaining.\n */\n function parse(path) {\n // Parsing the SVG path string into an array of arrays [['M', '10', '10'], ['L', '100', '100']]\n var chunks = path.replace(/([A-Za-z])([0-9])/g, '$1 $2')\n .replace(/([0-9])([A-Za-z])/g, '$1 $2')\n .split(/[\\s,]+/)\n .reduce(function(result, element) {\n if(element.match(/[A-Za-z]/)) {\n result.push([]);\n }\n\n result[result.length - 1].push(element);\n return result;\n }, []);\n\n // If this is a closed path we remove the Z at the end because this is determined by the close option\n if(chunks[chunks.length - 1][0].toUpperCase() === 'Z') {\n chunks.pop();\n }\n\n // Using svgPathElementDescriptions to map raw path arrays into objects that contain the command and the parameters\n // For example {command: 'M', x: '10', y: '10'}\n var elements = chunks.map(function(chunk) {\n var command = chunk.shift(),\n description = elementDescriptions[command.toLowerCase()];\n\n return Chartist.extend({\n command: command\n }, description.reduce(function(result, paramName, index) {\n result[paramName] = +chunk[index];\n return result;\n }, {}));\n });\n\n // Preparing a splice call with the elements array as var arg params and insert the parsed elements at the current position\n var spliceArgs = [this.pos, 0];\n Array.prototype.push.apply(spliceArgs, elements);\n Array.prototype.splice.apply(this.pathElements, spliceArgs);\n // Increase the internal position by the element count\n this.pos += elements.length;\n\n return this;\n }\n\n /**\n * This function renders to current SVG path object into a final SVG string that can be used in the d attribute of SVG path elements. It uses the accuracy option to round big decimals. If the close parameter was set in the constructor of this path object then a path closing Z will be appended to the output string.\n *\n * @memberof Chartist.Svg.Path\n * @return {String}\n */\n function stringify() {\n var accuracyMultiplier = Math.pow(10, this.options.accuracy);\n\n return this.pathElements.reduce(function(path, pathElement) {\n var params = elementDescriptions[pathElement.command.toLowerCase()].map(function(paramName) {\n return this.options.accuracy ?\n (Math.round(pathElement[paramName] * accuracyMultiplier) / accuracyMultiplier) :\n pathElement[paramName];\n }.bind(this));\n\n return path + pathElement.command + params.join(',');\n }.bind(this), '') + (this.close ? 'Z' : '');\n }\n\n /**\n * Scales all elements in the current SVG path object. There is an individual parameter for each coordinate. Scaling will also be done for control points of curves, affecting the given coordinate.\n *\n * @memberof Chartist.Svg.Path\n * @param {Number} x The number which will be used to scale the x, x1 and x2 of all path elements.\n * @param {Number} y The number which will be used to scale the y, y1 and y2 of all path elements.\n * @return {Chartist.Svg.Path} The current path object for easy call chaining.\n */\n function scale(x, y) {\n forEachParam(this.pathElements, function(pathElement, paramName) {\n pathElement[paramName] *= paramName[0] === 'x' ? x : y;\n });\n return this;\n }\n\n /**\n * Translates all elements in the current SVG path object. The translation is relative and there is an individual parameter for each coordinate. Translation will also be done for control points of curves, affecting the given coordinate.\n *\n * @memberof Chartist.Svg.Path\n * @param {Number} x The number which will be used to translate the x, x1 and x2 of all path elements.\n * @param {Number} y The number which will be used to translate the y, y1 and y2 of all path elements.\n * @return {Chartist.Svg.Path} The current path object for easy call chaining.\n */\n function translate(x, y) {\n forEachParam(this.pathElements, function(pathElement, paramName) {\n pathElement[paramName] += paramName[0] === 'x' ? x : y;\n });\n return this;\n }\n\n /**\n * This function will run over all existing path elements and then loop over their attributes. The callback function will be called for every path element attribute that exists in the current path.\n * The method signature of the callback function looks like this:\n * ```javascript\n * function(pathElement, paramName, pathElementIndex, paramIndex, pathElements)\n * ```\n * If something else than undefined is returned by the callback function, this value will be used to replace the old value. This allows you to build custom transformations of path objects that can't be achieved using the basic transformation functions scale and translate.\n *\n * @memberof Chartist.Svg.Path\n * @param {Function} transformFnc The callback function for the transformation. Check the signature in the function description.\n * @return {Chartist.Svg.Path} The current path object for easy call chaining.\n */\n function transform(transformFnc) {\n forEachParam(this.pathElements, function(pathElement, paramName, pathElementIndex, paramIndex, pathElements) {\n var transformed = transformFnc(pathElement, paramName, pathElementIndex, paramIndex, pathElements);\n if(transformed || transformed === 0) {\n pathElement[paramName] = transformed;\n }\n });\n return this;\n }\n\n /**\n * This function clones a whole path object with all its properties. This is a deep clone and path element objects will also be cloned.\n *\n * @memberof Chartist.Svg.Path\n * @return {Chartist.Svg.Path}\n */\n function clone() {\n var c = new Chartist.Svg.Path(this.close);\n c.pos = this.pos;\n c.pathElements = this.pathElements.slice().map(function cloneElements(pathElement) {\n return Chartist.extend({}, pathElement);\n });\n c.options = Chartist.extend({}, this.options);\n return c;\n }\n\n Chartist.Svg.Path = Chartist.Class.extend({\n constructor: SvgPath,\n position: position,\n remove: remove,\n move: move,\n line: line,\n curve: curve,\n scale: scale,\n translate: translate,\n transform: transform,\n parse: parse,\n stringify: stringify,\n clone: clone\n });\n\n Chartist.Svg.Path.elementDescriptions = elementDescriptions;\n}(window, document, Chartist));\n;/**\n * Axis base class used to implement different axis types\n *\n * @module Chartist.Axis\n */\n/* global Chartist */\n(function (window, document, Chartist) {\n 'use strict';\n\n var axisUnits = {\n x: {\n pos: 'x',\n len: 'width',\n dir: 'horizontal',\n rectStart: 'x1',\n rectEnd: 'x2',\n rectOffset: 'y2'\n },\n y: {\n pos: 'y',\n len: 'height',\n dir: 'vertical',\n rectStart: 'y2',\n rectEnd: 'y1',\n rectOffset: 'x1'\n }\n };\n\n function Axis(units, chartRect, transform, labelOffset, options) {\n this.units = units;\n this.counterUnits = units === axisUnits.x ? axisUnits.y : axisUnits.x;\n this.chartRect = chartRect;\n this.axisLength = chartRect[units.rectEnd] - chartRect[units.rectStart];\n this.gridOffset = chartRect[units.rectOffset];\n this.transform = transform;\n this.labelOffset = labelOffset;\n this.options = options;\n }\n\n Chartist.Axis = Chartist.Class.extend({\n constructor: Axis,\n projectValue: function(value, index, data) {\n throw new Error('Base axis can\\'t be instantiated!');\n }\n });\n\n Chartist.Axis.units = axisUnits;\n\n}(window, document, Chartist));\n;/**\n * The linear scale axis uses standard linear scale projection of values along an axis.\n *\n * @module Chartist.LinearScaleAxis\n */\n/* global Chartist */\n(function (window, document, Chartist) {\n 'use strict';\n\n function LinearScaleAxis(axisUnit, chartRect, transform, labelOffset, options) {\n Chartist.LinearScaleAxis.super.constructor.call(this,\n axisUnit,\n chartRect,\n transform,\n labelOffset,\n options);\n\n this.bounds = Chartist.getBounds(this.axisLength, options.highLow, options.scaleMinSpace, options.referenceValue);\n }\n\n function projectValue(value) {\n return {\n pos: this.axisLength * (value - this.bounds.min) / (this.bounds.range + this.bounds.step),\n len: Chartist.projectLength(this.axisLength, this.bounds.step, this.bounds)\n };\n }\n\n Chartist.LinearScaleAxis = Chartist.Axis.extend({\n constructor: LinearScaleAxis,\n projectValue: projectValue\n });\n\n}(window, document, Chartist));\n;/**\n * Step axis for step based charts like bar chart or step based line chart\n *\n * @module Chartist.StepAxis\n */\n/* global Chartist */\n(function (window, document, Chartist) {\n 'use strict';\n\n function StepAxis(axisUnit, chartRect, transform, labelOffset, options) {\n Chartist.StepAxis.super.constructor.call(this,\n axisUnit,\n chartRect,\n transform,\n labelOffset,\n options);\n\n this.stepLength = this.axisLength / (options.stepCount - (options.stretch ? 1 : 0));\n }\n\n function projectValue(value, index) {\n return {\n pos: this.stepLength * index,\n len: this.stepLength\n };\n }\n\n Chartist.StepAxis = Chartist.Axis.extend({\n constructor: StepAxis,\n projectValue: projectValue\n });\n\n}(window, document, Chartist));\n;/**\n * The Chartist line chart can be used to draw Line or Scatter charts. If used in the browser you can access the global `Chartist` namespace where you find the `Line` function as a main entry point.\n *\n * For examples on how to use the line chart please check the examples of the `Chartist.Line` method.\n *\n * @module Chartist.Line\n */\n/* global Chartist */\n(function(window, document, Chartist){\n 'use strict';\n\n /**\n * Default options in line charts. Expand the code view to see a detailed list of options with comments.\n *\n * @memberof Chartist.Line\n */\n var defaultOptions = {\n // Options for X-Axis\n axisX: {\n // The offset of the labels to the chart area\n offset: 30,\n // Allows you to correct label positioning on this axis by positive or negative x and y offset.\n labelOffset: {\n x: 0,\n y: 0\n },\n // If labels should be shown or not\n showLabel: true,\n // If the axis grid should be drawn or not\n showGrid: true,\n // Interpolation function that allows you to intercept the value from the axis label\n labelInterpolationFnc: Chartist.noop\n },\n // Options for Y-Axis\n axisY: {\n // The offset of the labels to the chart area\n offset: 40,\n // Allows you to correct label positioning on this axis by positive or negative x and y offset.\n labelOffset: {\n x: 0,\n y: 0\n },\n // If labels should be shown or not\n showLabel: true,\n // If the axis grid should be drawn or not\n showGrid: true,\n // Interpolation function that allows you to intercept the value from the axis label\n labelInterpolationFnc: Chartist.noop,\n // This value specifies the minimum height in pixel of the scale steps\n scaleMinSpace: 20\n },\n // Specify a fixed width for the chart as a string (i.e. '100px' or '50%')\n width: undefined,\n // Specify a fixed height for the chart as a string (i.e. '100px' or '50%')\n height: undefined,\n // If the line should be drawn or not\n showLine: true,\n // If dots should be drawn or not\n showPoint: true,\n // If the line chart should draw an area\n showArea: false,\n // The base for the area chart that will be used to close the area shape (is normally 0)\n areaBase: 0,\n // Specify if the lines should be smoothed. This value can be true or false where true will result in smoothing using the default smoothing interpolation function Chartist.Interpolation.cardinal and false results in Chartist.Interpolation.none. You can also choose other smoothing / interpolation functions available in the Chartist.Interpolation module, or write your own interpolation function. Check the examples for a brief description.\n lineSmooth: true,\n // Overriding the natural low of the chart allows you to zoom in or limit the charts lowest displayed value\n low: undefined,\n // Overriding the natural high of the chart allows you to zoom in or limit the charts highest displayed value\n high: undefined,\n // Padding of the chart drawing area to the container element and labels as a number or padding object {top: 5, right: 5, bottom: 5, left: 5}\n chartPadding: 5,\n // When set to true, the last grid line on the x-axis is not drawn and the chart elements will expand to the full available width of the chart. For the last label to be drawn correctly you might need to add chart padding or offset the last label with a draw event handler.\n fullWidth: false,\n // If true the whole data is reversed including labels, the series order as well as the whole series data arrays.\n reverseData: false,\n // Override the class names that get used to generate the SVG structure of the chart\n classNames: {\n chart: 'ct-chart-line',\n label: 'ct-label',\n labelGroup: 'ct-labels',\n series: 'ct-series',\n line: 'ct-line',\n point: 'ct-point',\n area: 'ct-area',\n grid: 'ct-grid',\n gridGroup: 'ct-grids',\n vertical: 'ct-vertical',\n horizontal: 'ct-horizontal'\n }\n };\n\n /**\n * Creates a new chart\n *\n */\n function createChart(options) {\n var seriesGroups = [],\n normalizedData = Chartist.normalizeDataArray(Chartist.getDataArray(this.data, options.reverseData), this.data.labels.length),\n normalizedPadding = Chartist.normalizePadding(options.chartPadding, defaultOptions.padding);\n\n // Create new svg object\n this.svg = Chartist.createSvg(this.container, options.width, options.height, options.classNames.chart);\n\n var chartRect = Chartist.createChartRect(this.svg, options, defaultOptions.padding);\n\n var highLow = Chartist.getHighLow(normalizedData);\n // Overrides of high / low from settings\n highLow.high = +options.high || (options.high === 0 ? 0 : highLow.high);\n highLow.low = +options.low || (options.low === 0 ? 0 : highLow.low);\n\n var axisX = new Chartist.StepAxis(\n Chartist.Axis.units.x,\n chartRect,\n function xAxisTransform(projectedValue) {\n projectedValue.pos = chartRect.x1 + projectedValue.pos;\n return projectedValue;\n },\n {\n x: options.axisX.labelOffset.x,\n y: chartRect.y1 + options.axisX.labelOffset.y + (this.supportsForeignObject ? 5 : 20)\n },\n {\n stepCount: this.data.labels.length,\n stretch: options.fullWidth\n }\n );\n\n var axisY = new Chartist.LinearScaleAxis(\n Chartist.Axis.units.y,\n chartRect,\n function yAxisTransform(projectedValue) {\n projectedValue.pos = chartRect.y1 - projectedValue.pos;\n return projectedValue;\n },\n {\n x: normalizedPadding.left + options.axisY.labelOffset.x + (this.supportsForeignObject ? -10 : 0),\n y: options.axisY.labelOffset.y + (this.supportsForeignObject ? -15 : 0)\n },\n {\n highLow: highLow,\n scaleMinSpace: options.axisY.scaleMinSpace\n }\n );\n\n // Start drawing\n var labelGroup = this.svg.elem('g').addClass(options.classNames.labelGroup),\n gridGroup = this.svg.elem('g').addClass(options.classNames.gridGroup);\n\n Chartist.createAxis(\n axisX,\n this.data.labels,\n chartRect,\n gridGroup,\n labelGroup,\n this.supportsForeignObject,\n options,\n this.eventEmitter\n );\n\n Chartist.createAxis(\n axisY,\n axisY.bounds.values,\n chartRect,\n gridGroup,\n labelGroup,\n this.supportsForeignObject,\n options,\n this.eventEmitter\n );\n\n // Draw the series\n this.data.series.forEach(function(series, seriesIndex) {\n seriesGroups[seriesIndex] = this.svg.elem('g');\n\n // Write attributes to series group element. If series name or meta is undefined the attributes will not be written\n seriesGroups[seriesIndex].attr({\n 'series-name': series.name,\n 'meta': Chartist.serialize(series.meta)\n }, Chartist.xmlNs.uri);\n\n // Use series class from series data or if not set generate one\n seriesGroups[seriesIndex].addClass([\n options.classNames.series,\n (series.className || options.classNames.series + '-' + Chartist.alphaNumerate(seriesIndex))\n ].join(' '));\n\n var pathCoordinates = [];\n\n normalizedData[seriesIndex].forEach(function(value, valueIndex) {\n var p = {\n x: chartRect.x1 + axisX.projectValue(value, valueIndex, normalizedData[seriesIndex]).pos,\n y: chartRect.y1 - axisY.projectValue(value, valueIndex, normalizedData[seriesIndex]).pos\n };\n pathCoordinates.push(p.x, p.y);\n\n //If we should show points we need to create them now to avoid secondary loop\n // Small offset for Firefox to render squares correctly\n if (options.showPoint) {\n var point = seriesGroups[seriesIndex].elem('line', {\n x1: p.x,\n y1: p.y,\n x2: p.x + 0.01,\n y2: p.y\n }, options.classNames.point).attr({\n 'value': value,\n 'meta': Chartist.getMetaData(series, valueIndex)\n }, Chartist.xmlNs.uri);\n\n this.eventEmitter.emit('draw', {\n type: 'point',\n value: value,\n index: valueIndex,\n group: seriesGroups[seriesIndex],\n element: point,\n x: p.x,\n y: p.y\n });\n }\n }.bind(this));\n\n // TODO: Nicer handling of conditions, maybe composition?\n if (options.showLine || options.showArea) {\n var smoothing = typeof options.lineSmooth === 'function' ?\n options.lineSmooth : (options.lineSmooth ? Chartist.Interpolation.cardinal() : Chartist.Interpolation.none()),\n path = smoothing(pathCoordinates);\n\n if(options.showLine) {\n var line = seriesGroups[seriesIndex].elem('path', {\n d: path.stringify()\n }, options.classNames.line, true).attr({\n 'values': normalizedData[seriesIndex]\n }, Chartist.xmlNs.uri);\n\n this.eventEmitter.emit('draw', {\n type: 'line',\n values: normalizedData[seriesIndex],\n path: path.clone(),\n chartRect: chartRect,\n index: seriesIndex,\n group: seriesGroups[seriesIndex],\n element: line\n });\n }\n\n if(options.showArea) {\n // If areaBase is outside the chart area (< low or > high) we need to set it respectively so that\n // the area is not drawn outside the chart area.\n var areaBase = Math.max(Math.min(options.areaBase, axisY.bounds.max), axisY.bounds.min);\n\n // We project the areaBase value into screen coordinates\n var areaBaseProjected = chartRect.y1 - axisY.projectValue(areaBase).pos;\n\n // Clone original path and splice our new area path to add the missing path elements to close the area shape\n var areaPath = path.clone();\n // Modify line path and add missing elements for area\n areaPath.position(0)\n .remove(1)\n .move(chartRect.x1, areaBaseProjected)\n .line(pathCoordinates[0], pathCoordinates[1])\n .position(areaPath.pathElements.length)\n .line(pathCoordinates[pathCoordinates.length - 2], areaBaseProjected);\n\n // Create the new path for the area shape with the area class from the options\n var area = seriesGroups[seriesIndex].elem('path', {\n d: areaPath.stringify()\n }, options.classNames.area, true).attr({\n 'values': normalizedData[seriesIndex]\n }, Chartist.xmlNs.uri);\n\n this.eventEmitter.emit('draw', {\n type: 'area',\n values: normalizedData[seriesIndex],\n path: areaPath.clone(),\n chartRect: chartRect,\n index: seriesIndex,\n group: seriesGroups[seriesIndex],\n element: area\n });\n }\n }\n }.bind(this));\n\n this.eventEmitter.emit('created', {\n bounds: axisY.bounds,\n chartRect: chartRect,\n svg: this.svg,\n options: options\n });\n }\n\n /**\n * This method creates a new line chart.\n *\n * @memberof Chartist.Line\n * @param {String|Node} query A selector query string or directly a DOM element\n * @param {Object} data The data object that needs to consist of a labels and a series array\n * @param {Object} [options] The options object with options that override the default options. Check the examples for a detailed list.\n * @param {Array} [responsiveOptions] Specify an array of responsive option arrays which are a media query and options object pair => [[mediaQueryString, optionsObject],[more...]]\n * @return {Object} An object which exposes the API for the created chart\n *\n * @example\n * // Create a simple line chart\n * var data = {\n * // A labels array that can contain any sort of values\n * labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],\n * // Our series array that contains series objects or in this case series data arrays\n * series: [\n * [5, 2, 4, 2, 0]\n * ]\n * };\n *\n * // As options we currently only set a static size of 300x200 px\n * var options = {\n * width: '300px',\n * height: '200px'\n * };\n *\n * // In the global name space Chartist we call the Line function to initialize a line chart. As a first parameter we pass in a selector where we would like to get our chart created. Second parameter is the actual data object and as a third parameter we pass in our options\n * new Chartist.Line('.ct-chart', data, options);\n *\n * @example\n * // Use specific interpolation function with configuration from the Chartist.Interpolation module\n *\n * var chart = new Chartist.Line('.ct-chart', {\n * labels: [1, 2, 3, 4, 5],\n * series: [\n * [1, 1, 8, 1, 7]\n * ]\n * }, {\n * lineSmooth: Chartist.Interpolation.cardinal({\n * tension: 0.2\n * })\n * });\n *\n * @example\n * // Create a line chart with responsive options\n *\n * var data = {\n * // A labels array that can contain any sort of values\n * labels: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],\n * // Our series array that contains series objects or in this case series data arrays\n * series: [\n * [5, 2, 4, 2, 0]\n * ]\n * };\n *\n * // In adition to the regular options we specify responsive option overrides that will override the default configutation based on the matching media queries.\n * var responsiveOptions = [\n * ['screen and (min-width: 641px) and (max-width: 1024px)', {\n * showPoint: false,\n * axisX: {\n * labelInterpolationFnc: function(value) {\n * // Will return Mon, Tue, Wed etc. on medium screens\n * return value.slice(0, 3);\n * }\n * }\n * }],\n * ['screen and (max-width: 640px)', {\n * showLine: false,\n * axisX: {\n * labelInterpolationFnc: function(value) {\n * // Will return M, T, W etc. on small screens\n * return value[0];\n * }\n * }\n * }]\n * ];\n *\n * new Chartist.Line('.ct-chart', data, null, responsiveOptions);\n *\n */\n function Line(query, data, options, responsiveOptions) {\n Chartist.Line.super.constructor.call(this,\n query,\n data,\n defaultOptions,\n Chartist.extend({}, defaultOptions, options),\n responsiveOptions);\n }\n\n // Creating line chart type in Chartist namespace\n Chartist.Line = Chartist.Base.extend({\n constructor: Line,\n createChart: createChart\n });\n\n}(window, document, Chartist));\n;/**\n * The bar chart module of Chartist that can be used to draw unipolar or bipolar bar and grouped bar charts.\n *\n * @module Chartist.Bar\n */\n/* global Chartist */\n(function(window, document, Chartist){\n 'use strict';\n\n /**\n * Default options in bar charts. Expand the code view to see a detailed list of options with comments.\n *\n * @memberof Chartist.Bar\n */\n var defaultOptions = {\n // Options for X-Axis\n axisX: {\n // The offset of the chart drawing area to the border of the container\n offset: 30,\n // Allows you to correct label positioning on this axis by positive or negative x and y offset.\n labelOffset: {\n x: 0,\n y: 0\n },\n // If labels should be shown or not\n showLabel: true,\n // If the axis grid should be drawn or not\n showGrid: true,\n // Interpolation function that allows you to intercept the value from the axis label\n labelInterpolationFnc: Chartist.noop,\n // This value specifies the minimum width in pixel of the scale steps\n scaleMinSpace: 40\n },\n // Options for Y-Axis\n axisY: {\n // The offset of the chart drawing area to the border of the container\n offset: 40,\n // Allows you to correct label positioning on this axis by positive or negative x and y offset.\n labelOffset: {\n x: 0,\n y: 0\n },\n // If labels should be shown or not\n showLabel: true,\n // If the axis grid should be drawn or not\n showGrid: true,\n // Interpolation function that allows you to intercept the value from the axis label\n labelInterpolationFnc: Chartist.noop,\n // This value specifies the minimum height in pixel of the scale steps\n scaleMinSpace: 20\n },\n // Specify a fixed width for the chart as a string (i.e. '100px' or '50%')\n width: undefined,\n // Specify a fixed height for the chart as a string (i.e. '100px' or '50%')\n height: undefined,\n // Overriding the natural high of the chart allows you to zoom in or limit the charts highest displayed value\n high: undefined,\n // Overriding the natural low of the chart allows you to zoom in or limit the charts lowest displayed value\n low: undefined,\n // Padding of the chart drawing area to the container element and labels as a number or padding object {top: 5, right: 5, bottom: 5, left: 5}\n chartPadding: 5,\n // Specify the distance in pixel of bars in a group\n seriesBarDistance: 15,\n // If set to true this property will cause the series bars to be stacked and form a total for each series point. This will also influence the y-axis and the overall bounds of the chart. In stacked mode the seriesBarDistance property will have no effect.\n stackBars: false,\n // Inverts the axes of the bar chart in order to draw a horizontal bar chart. Be aware that you also need to invert your axis settings as the Y Axis will now display the labels and the X Axis the values.\n horizontalBars: false,\n // If true the whole data is reversed including labels, the series order as well as the whole series data arrays.\n reverseData: false,\n // Override the class names that get used to generate the SVG structure of the chart\n classNames: {\n chart: 'ct-chart-bar',\n label: 'ct-label',\n labelGroup: 'ct-labels',\n series: 'ct-series',\n bar: 'ct-bar',\n grid: 'ct-grid',\n gridGroup: 'ct-grids',\n vertical: 'ct-vertical',\n horizontal: 'ct-horizontal'\n }\n };\n\n /**\n * Creates a new chart\n *\n */\n function createChart(options) {\n var seriesGroups = [],\n normalizedData = Chartist.normalizeDataArray(Chartist.getDataArray(this.data, options.reverseData), this.data.labels.length),\n normalizedPadding = Chartist.normalizePadding(options.chartPadding, defaultOptions.padding),\n highLow;\n\n // Create new svg element\n this.svg = Chartist.createSvg(this.container, options.width, options.height, options.classNames.chart);\n\n if(options.stackBars) {\n // If stacked bars we need to calculate the high low from stacked values from each series\n var serialSums = Chartist.serialMap(normalizedData, function serialSums() {\n return Array.prototype.slice.call(arguments).reduce(Chartist.sum, 0);\n });\n\n highLow = Chartist.getHighLow([serialSums]);\n } else {\n highLow = Chartist.getHighLow(normalizedData);\n }\n // Overrides of high / low from settings\n highLow.high = +options.high || (options.high === 0 ? 0 : highLow.high);\n highLow.low = +options.low || (options.low === 0 ? 0 : highLow.low);\n\n var chartRect = Chartist.createChartRect(this.svg, options, defaultOptions.padding);\n\n var valueAxis,\n labelAxis;\n\n if(options.horizontalBars) {\n labelAxis = new Chartist.StepAxis(\n Chartist.Axis.units.y,\n chartRect,\n function timeAxisTransform(projectedValue) {\n projectedValue.pos = chartRect.y1 - projectedValue.pos;\n return projectedValue;\n },\n {\n x: normalizedPadding.left + options.axisY.labelOffset.x + (this.supportsForeignObject ? -10 : 0),\n y: options.axisY.labelOffset.y - chartRect.height() / this.data.labels.length\n },\n {\n stepCount: this.data.labels.length,\n stretch: options.fullHeight\n }\n );\n\n valueAxis = new Chartist.LinearScaleAxis(\n Chartist.Axis.units.x,\n chartRect,\n function valueAxisTransform(projectedValue) {\n projectedValue.pos = chartRect.x1 + projectedValue.pos;\n return projectedValue;\n },\n {\n x: options.axisX.labelOffset.x,\n y: chartRect.y1 + options.axisX.labelOffset.y + (this.supportsForeignObject ? 5 : 20)\n },\n {\n highLow: highLow,\n scaleMinSpace: options.axisX.scaleMinSpace,\n referenceValue: 0\n }\n );\n } else {\n labelAxis = new Chartist.StepAxis(\n Chartist.Axis.units.x,\n chartRect,\n function timeAxisTransform(projectedValue) {\n projectedValue.pos = chartRect.x1 + projectedValue.pos;\n return projectedValue;\n },\n {\n x: options.axisX.labelOffset.x,\n y: chartRect.y1 + options.axisX.labelOffset.y + (this.supportsForeignObject ? 5 : 20)\n },\n {\n stepCount: this.data.labels.length\n }\n );\n\n valueAxis = new Chartist.LinearScaleAxis(\n Chartist.Axis.units.y,\n chartRect,\n function valueAxisTransform(projectedValue) {\n projectedValue.pos = chartRect.y1 - projectedValue.pos;\n return projectedValue;\n },\n {\n x: normalizedPadding.left + options.axisY.labelOffset.x + (this.supportsForeignObject ? -10 : 0),\n y: options.axisY.labelOffset.y + (this.supportsForeignObject ? -15 : 0)\n },\n {\n highLow: highLow,\n scaleMinSpace: options.axisY.scaleMinSpace,\n referenceValue: 0\n }\n );\n }\n\n // Start drawing\n var labelGroup = this.svg.elem('g').addClass(options.classNames.labelGroup),\n gridGroup = this.svg.elem('g').addClass(options.classNames.gridGroup),\n // Projected 0 point\n zeroPoint = options.horizontalBars ? (chartRect.x1 + valueAxis.projectValue(0).pos) : (chartRect.y1 - valueAxis.projectValue(0).pos),\n // Used to track the screen coordinates of stacked bars\n stackedBarValues = [];\n\n Chartist.createAxis(\n labelAxis,\n this.data.labels,\n chartRect,\n gridGroup,\n labelGroup,\n this.supportsForeignObject,\n options,\n this.eventEmitter\n );\n\n Chartist.createAxis(\n valueAxis,\n valueAxis.bounds.values,\n chartRect,\n gridGroup,\n labelGroup,\n this.supportsForeignObject,\n options,\n this.eventEmitter\n );\n\n // Draw the series\n this.data.series.forEach(function(series, seriesIndex) {\n // Calculating bi-polar value of index for seriesOffset. For i = 0..4 biPol will be -1.5, -0.5, 0.5, 1.5 etc.\n var biPol = seriesIndex - (this.data.series.length - 1) / 2,\n // Half of the period width between vertical grid lines used to position bars\n periodHalfLength = chartRect[labelAxis.units.len]() / normalizedData[seriesIndex].length / 2;\n\n seriesGroups[seriesIndex] = this.svg.elem('g');\n\n // Write attributes to series group element. If series name or meta is undefined the attributes will not be written\n seriesGroups[seriesIndex].attr({\n 'series-name': series.name,\n 'meta': Chartist.serialize(series.meta)\n }, Chartist.xmlNs.uri);\n\n // Use series class from series data or if not set generate one\n seriesGroups[seriesIndex].addClass([\n options.classNames.series,\n (series.className || options.classNames.series + '-' + Chartist.alphaNumerate(seriesIndex))\n ].join(' '));\n\n normalizedData[seriesIndex].forEach(function(value, valueIndex) {\n var projected = {\n x: chartRect.x1 + (options.horizontalBars ? valueAxis : labelAxis).projectValue(value, valueIndex, normalizedData[seriesIndex]).pos,\n y: chartRect.y1 - (options.horizontalBars ? labelAxis : valueAxis).projectValue(value, valueIndex, normalizedData[seriesIndex]).pos\n },\n bar,\n previousStack;\n\n // Offset to center bar between grid lines\n projected[labelAxis.units.pos] += periodHalfLength * (options.horizontalBars ? -1 : 1);\n // Using bi-polar offset for multiple series if no stacked bars are used\n projected[labelAxis.units.pos] += options.stackBars ? 0 : biPol * options.seriesBarDistance * (options.horizontalBars ? -1 : 1);\n\n // Enter value in stacked bar values used to remember previous screen value for stacking up bars\n previousStack = stackedBarValues[valueIndex] || zeroPoint;\n stackedBarValues[valueIndex] = previousStack - (zeroPoint - projected[labelAxis.counterUnits.pos]);\n\n var positions = {};\n positions[labelAxis.units.pos + '1'] = projected[labelAxis.units.pos];\n positions[labelAxis.units.pos + '2'] = projected[labelAxis.units.pos];\n // If bars are stacked we use the stackedBarValues reference and otherwise base all bars off the zero line\n positions[labelAxis.counterUnits.pos + '1'] = options.stackBars ? previousStack : zeroPoint;\n positions[labelAxis.counterUnits.pos + '2'] = options.stackBars ? stackedBarValues[valueIndex] : projected[labelAxis.counterUnits.pos];\n\n bar = seriesGroups[seriesIndex].elem('line', positions, options.classNames.bar).attr({\n 'value': value,\n 'meta': Chartist.getMetaData(series, valueIndex)\n }, Chartist.xmlNs.uri);\n\n this.eventEmitter.emit('draw', Chartist.extend({\n type: 'bar',\n value: value,\n index: valueIndex,\n chartRect: chartRect,\n group: seriesGroups[seriesIndex],\n element: bar\n }, positions));\n }.bind(this));\n }.bind(this));\n\n this.eventEmitter.emit('created', {\n bounds: valueAxis.bounds,\n chartRect: chartRect,\n svg: this.svg,\n options: options\n });\n }\n\n /**\n * This method creates a new bar chart and returns API object that you can use for later changes.\n *\n * @memberof Chartist.Bar\n * @param {String|Node} query A selector query string or directly a DOM element\n * @param {Object} data The data object that needs to consist of a labels and a series array\n * @param {Object} [options] The options object with options that override the default options. Check the examples for a detailed list.\n * @param {Array} [responsiveOptions] Specify an array of responsive option arrays which are a media query and options object pair => [[mediaQueryString, optionsObject],[more...]]\n * @return {Object} An object which exposes the API for the created chart\n *\n * @example\n * // Create a simple bar chart\n * var data = {\n * labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],\n * series: [\n * [5, 2, 4, 2, 0]\n * ]\n * };\n *\n * // In the global name space Chartist we call the Bar function to initialize a bar chart. As a first parameter we pass in a selector where we would like to get our chart created and as a second parameter we pass our data object.\n * new Chartist.Bar('.ct-chart', data);\n *\n * @example\n * // This example creates a bipolar grouped bar chart where the boundaries are limitted to -10 and 10\n * new Chartist.Bar('.ct-chart', {\n * labels: [1, 2, 3, 4, 5, 6, 7],\n * series: [\n * [1, 3, 2, -5, -3, 1, -6],\n * [-5, -2, -4, -1, 2, -3, 1]\n * ]\n * }, {\n * seriesBarDistance: 12,\n * low: -10,\n * high: 10\n * });\n *\n */\n function Bar(query, data, options, responsiveOptions) {\n Chartist.Bar.super.constructor.call(this,\n query,\n data,\n defaultOptions,\n Chartist.extend({}, defaultOptions, options),\n responsiveOptions);\n }\n\n // Creating bar chart type in Chartist namespace\n Chartist.Bar = Chartist.Base.extend({\n constructor: Bar,\n createChart: createChart\n });\n\n}(window, document, Chartist));\n;/**\n * The pie chart module of Chartist that can be used to draw pie, donut or gauge charts\n *\n * @module Chartist.Pie\n */\n/* global Chartist */\n(function(window, document, Chartist) {\n 'use strict';\n\n /**\n * Default options in line charts. Expand the code view to see a detailed list of options with comments.\n *\n * @memberof Chartist.Pie\n */\n var defaultOptions = {\n // Specify a fixed width for the chart as a string (i.e. '100px' or '50%')\n width: undefined,\n // Specify a fixed height for the chart as a string (i.e. '100px' or '50%')\n height: undefined,\n // Padding of the chart drawing area to the container element and labels as a number or padding object {top: 5, right: 5, bottom: 5, left: 5}\n chartPadding: 5,\n // Override the class names that get used to generate the SVG structure of the chart\n classNames: {\n chart: 'ct-chart-pie',\n series: 'ct-series',\n slice: 'ct-slice',\n donut: 'ct-donut',\n label: 'ct-label'\n },\n // The start angle of the pie chart in degrees where 0 points north. A higher value offsets the start angle clockwise.\n startAngle: 0,\n // An optional total you can specify. By specifying a total value, the sum of the values in the series must be this total in order to draw a full pie. You can use this parameter to draw only parts of a pie or gauge charts.\n total: undefined,\n // If specified the donut CSS classes will be used and strokes will be drawn instead of pie slices.\n donut: false,\n // Specify the donut stroke width, currently done in javascript for convenience. May move to CSS styles in the future.\n donutWidth: 60,\n // If a label should be shown or not\n showLabel: true,\n // Label position offset from the standard position which is half distance of the radius. This value can be either positive or negative. Positive values will position the label away from the center.\n labelOffset: 0,\n // An interpolation function for the label value\n labelInterpolationFnc: Chartist.noop,\n // Label direction can be 'neutral', 'explode' or 'implode'. The labels anchor will be positioned based on those settings as well as the fact if the labels are on the right or left side of the center of the chart. Usually explode is useful when labels are positioned far away from the center.\n labelDirection: 'neutral',\n // If true the whole data is reversed including labels, the series order as well as the whole series data arrays.\n reverseData: false\n };\n\n /**\n * Determines SVG anchor position based on direction and center parameter\n *\n * @param center\n * @param label\n * @param direction\n * @return {string}\n */\n function determineAnchorPosition(center, label, direction) {\n var toTheRight = label.x > center.x;\n\n if(toTheRight && direction === 'explode' ||\n !toTheRight && direction === 'implode') {\n return 'start';\n } else if(toTheRight && direction === 'implode' ||\n !toTheRight && direction === 'explode') {\n return 'end';\n } else {\n return 'middle';\n }\n }\n\n /**\n * Creates the pie chart\n *\n * @param options\n */\n function createChart(options) {\n var seriesGroups = [],\n chartRect,\n radius,\n labelRadius,\n totalDataSum,\n startAngle = options.startAngle,\n dataArray = Chartist.getDataArray(this.data, options.reverseData);\n\n // Create SVG.js draw\n this.svg = Chartist.createSvg(this.container, options.width, options.height, options.classNames.chart);\n // Calculate charting rect\n chartRect = Chartist.createChartRect(this.svg, options, defaultOptions.padding);\n // Get biggest circle radius possible within chartRect\n radius = Math.min(chartRect.width() / 2, chartRect.height() / 2);\n // Calculate total of all series to get reference value or use total reference from optional options\n totalDataSum = options.total || dataArray.reduce(function(previousValue, currentValue) {\n return previousValue + currentValue;\n }, 0);\n\n // If this is a donut chart we need to adjust our radius to enable strokes to be drawn inside\n // Unfortunately this is not possible with the current SVG Spec\n // See this proposal for more details: http://lists.w3.org/Archives/Public/www-svg/2003Oct/0000.html\n radius -= options.donut ? options.donutWidth / 2 : 0;\n\n // If a donut chart then the label position is at the radius, if regular pie chart it's half of the radius\n // see https://github.com/gionkunz/chartist-js/issues/21\n labelRadius = options.donut ? radius : radius / 2;\n // Add the offset to the labelRadius where a negative offset means closed to the center of the chart\n labelRadius += options.labelOffset;\n\n // Calculate end angle based on total sum and current data value and offset with padding\n var center = {\n x: chartRect.x1 + chartRect.width() / 2,\n y: chartRect.y2 + chartRect.height() / 2\n };\n\n // Check if there is only one non-zero value in the series array.\n var hasSingleValInSeries = this.data.series.filter(function(val) {\n return val !== 0;\n }).length === 1;\n\n // Draw the series\n // initialize series groups\n for (var i = 0; i < this.data.series.length; i++) {\n seriesGroups[i] = this.svg.elem('g', null, null, true);\n\n // If the series is an object and contains a name we add a custom attribute\n if(this.data.series[i].name) {\n seriesGroups[i].attr({\n 'series-name': this.data.series[i].name,\n 'meta': Chartist.serialize(this.data.series[i].meta)\n }, Chartist.xmlNs.uri);\n }\n\n // Use series class from series data or if not set generate one\n seriesGroups[i].addClass([\n options.classNames.series,\n (this.data.series[i].className || options.classNames.series + '-' + Chartist.alphaNumerate(i))\n ].join(' '));\n\n var endAngle = startAngle + dataArray[i] / totalDataSum * 360;\n // If we need to draw the arc for all 360 degrees we need to add a hack where we close the circle\n // with Z and use 359.99 degrees\n if(endAngle - startAngle === 360) {\n endAngle -= 0.01;\n }\n\n var start = Chartist.polarToCartesian(center.x, center.y, radius, startAngle - (i === 0 || hasSingleValInSeries ? 0 : 0.2)),\n end = Chartist.polarToCartesian(center.x, center.y, radius, endAngle),\n arcSweep = endAngle - startAngle <= 180 ? '0' : '1',\n d = [\n // Start at the end point from the cartesian coordinates\n 'M', end.x, end.y,\n // Draw arc\n 'A', radius, radius, 0, arcSweep, 0, start.x, start.y\n ];\n\n // If regular pie chart (no donut) we add a line to the center of the circle for completing the pie\n if(options.donut === false) {\n d.push('L', center.x, center.y);\n }\n\n // Create the SVG path\n // If this is a donut chart we add the donut class, otherwise just a regular slice\n var path = seriesGroups[i].elem('path', {\n d: d.join(' ')\n }, options.classNames.slice + (options.donut ? ' ' + options.classNames.donut : ''));\n\n // Adding the pie series value to the path\n path.attr({\n 'value': dataArray[i]\n }, Chartist.xmlNs.uri);\n\n // If this is a donut, we add the stroke-width as style attribute\n if(options.donut === true) {\n path.attr({\n 'style': 'stroke-width: ' + (+options.donutWidth) + 'px'\n });\n }\n\n // Fire off draw event\n this.eventEmitter.emit('draw', {\n type: 'slice',\n value: dataArray[i],\n totalDataSum: totalDataSum,\n index: i,\n group: seriesGroups[i],\n element: path,\n center: center,\n radius: radius,\n startAngle: startAngle,\n endAngle: endAngle\n });\n\n // If we need to show labels we need to add the label for this slice now\n if(options.showLabel) {\n // Position at the labelRadius distance from center and between start and end angle\n var labelPosition = Chartist.polarToCartesian(center.x, center.y, labelRadius, startAngle + (endAngle - startAngle) / 2),\n interpolatedValue = options.labelInterpolationFnc(this.data.labels ? this.data.labels[i] : dataArray[i], i);\n\n var labelElement = seriesGroups[i].elem('text', {\n dx: labelPosition.x,\n dy: labelPosition.y,\n 'text-anchor': determineAnchorPosition(center, labelPosition, options.labelDirection)\n }, options.classNames.label).text('' + interpolatedValue);\n\n // Fire off draw event\n this.eventEmitter.emit('draw', {\n type: 'label',\n index: i,\n group: seriesGroups[i],\n element: labelElement,\n text: '' + interpolatedValue,\n x: labelPosition.x,\n y: labelPosition.y\n });\n }\n\n // Set next startAngle to current endAngle. Use slight offset so there are no transparent hairline issues\n // (except for last slice)\n startAngle = endAngle;\n }\n\n this.eventEmitter.emit('created', {\n chartRect: chartRect,\n svg: this.svg,\n options: options\n });\n }\n\n /**\n * This method creates a new pie chart and returns an object that can be used to redraw the chart.\n *\n * @memberof Chartist.Pie\n * @param {String|Node} query A selector query string or directly a DOM element\n * @param {Object} data The data object in the pie chart needs to have a series property with a one dimensional data array. The values will be normalized against each other and don't necessarily need to be in percentage. The series property can also be an array of objects that contain a data property with the value and a className property to override the CSS class name for the series group.\n * @param {Object} [options] The options object with options that override the default options. Check the examples for a detailed list.\n * @param {Array} [responsiveOptions] Specify an array of responsive option arrays which are a media query and options object pair => [[mediaQueryString, optionsObject],[more...]]\n * @return {Object} An object with a version and an update method to manually redraw the chart\n *\n * @example\n * // Simple pie chart example with four series\n * new Chartist.Pie('.ct-chart', {\n * series: [10, 2, 4, 3]\n * });\n *\n * @example\n * // Drawing a donut chart\n * new Chartist.Pie('.ct-chart', {\n * series: [10, 2, 4, 3]\n * }, {\n * donut: true\n * });\n *\n * @example\n * // Using donut, startAngle and total to draw a gauge chart\n * new Chartist.Pie('.ct-chart', {\n * series: [20, 10, 30, 40]\n * }, {\n * donut: true,\n * donutWidth: 20,\n * startAngle: 270,\n * total: 200\n * });\n *\n * @example\n * // Drawing a pie chart with padding and labels that are outside the pie\n * new Chartist.Pie('.ct-chart', {\n * series: [20, 10, 30, 40]\n * }, {\n * chartPadding: 30,\n * labelOffset: 50,\n * labelDirection: 'explode'\n * });\n *\n * @example\n * // Overriding the class names for individual series\n * new Chartist.Pie('.ct-chart', {\n * series: [{\n * data: 20,\n * className: 'my-custom-class-one'\n * }, {\n * data: 10,\n * className: 'my-custom-class-two'\n * }, {\n * data: 70,\n * className: 'my-custom-class-three'\n * }]\n * });\n */\n function Pie(query, data, options, responsiveOptions) {\n Chartist.Pie.super.constructor.call(this,\n query,\n data,\n defaultOptions,\n Chartist.extend({}, defaultOptions, options),\n responsiveOptions);\n }\n\n // Creating pie chart type in Chartist namespace\n Chartist.Pie = Chartist.Base.extend({\n constructor: Pie,\n createChart: createChart,\n determineAnchorPosition: determineAnchorPosition\n });\n\n}(window, document, Chartist));\n\nreturn Chartist;\n\n}));\n"]} \ No newline at end of file diff --git a/apps/welcome/public_html/js/demo.js b/apps/welcome/public_html/js/demo.js new file mode 100644 index 0000000..cd3e26a --- /dev/null +++ b/apps/welcome/public_html/js/demo.js @@ -0,0 +1,447 @@ + +var State = { + IDLE: 0, + SIGNIN: 1, + CONNECTING: 2, + CONNECTED: 3 +}; + +var f2num = function(n, l) { + if (typeof l === "undefined") l = 2; + while (n.length < l) { + n = "0" + n; + } + return n; +} + +var HTTP_ENTRY = "welcome"; +var WEBSOCKET_ENTRY = "welcome"; + +var DemoApp = function (apphtml) { + var self = this; + + self._state = State.IDLE; + self._socket = null; + + self._apphtml = apphtml; +} + +DemoApp.prototype.init = function() { + var self = this; + + var apphtml = self._apphtml; + + // sign in + self._usernameInput = apphtml.find("#username"); + self._serverAddrInput = apphtml.find("#serverAddr"); + self._counterValueInput = apphtml.find("#counterValue"); + self._signInButton = apphtml.find("#signInButton"); + self._addCounterButton = apphtml.find("#addCounterButton"); + + self._serverAddrInput.val(document.location.host); + + self._signInButton.click(function() { + if (self._state === State.IDLE) { + self.signIn(); + } else { + self.signOut(); + } + }); + + self._addCounterButton.click(function() { + self.addCounter(); + }); + + // job + self._selectDelayInput = apphtml.find("input[name=selectDelay]"); self._jobMessageInput = apphtml.find("#jobMessage"); + self._sendJobMessageButton = apphtml.find("#sendJobMessageButton"); + + self._sendJobMessageButton.click(function() { + var delay = apphtml.find("input[name=selectDelay]:checked").val(); + var message = self._jobMessageInput.val(); + self.sendJobMessage(delay, message); + }); + + // chat + self._selectUserInput = apphtml.find("#selectUser"); + self._messageInput = apphtml.find("#message"); + self._sendMessageButton = apphtml.find("#sendMessageButton"); + self._sendMessageToAllButton = apphtml.find("#sendMessageToAllButton"); + + self._sendMessageButton.click(function() { + var recipient = self._selectUserInput.val(); + var message = self._messageInput.val(); + self.sendMessage(recipient, message); + }); + + self._sendMessageToAllButton.click(function() { + var message = self._messageInput.val(); + self.sendMessageToAll(message); + }); + + // log + + self._alertDialogHtml = apphtml.find("#alertDialog"); + self._logHtml = apphtml.find("#log"); + + apphtml.find("#clearLogsButton").click(function() { + self._clearLogs(); + }); + + apphtml.find("#insertMarkButton").click(function() { + self._appendLogMark(); + }); + + // init + self._updateUI(); +} + +DemoApp.prototype.signIn = function() { + var self = this; + + var username = self._usernameInput.val(); + if (username === "") { + self._showError("PLEASE ENTER username"); + return; + } + + var serverAddr = self._serverAddrInput.val(); + if (serverAddr === "") { + self._showError("PLEASE ENTER server addr"); + return; + } + + self._httpServerAddr = "http://" + serverAddr + "/" + HTTP_ENTRY + "/"; + self._websocketServerAddr = "ws://" + serverAddr + "/" + WEBSOCKET_ENTRY + "/"; + + self._state = State.SIGNIN; + + self._appendLogMark(); + self._appendLog("SIGN IN " + serverAddr); + + var values = {"username": username} + self._sendHttpRequest("user.signin", values, function(res) { + if (!self._validateResult(res, ["sid", "count"])) { + self._state = State.IDLE; + self._showError("Get invalid result"); + self._appendLog(res.toString()); + } else { + self._state = State.CONNECTING; + self._sid = res["sid"]; + self._appendLog("GET SESSION ID: " + self._sid); + + var count = parseInt(res["count"]); + self._appendLog("count = " + count.toString()); + self._counterValueInput.val(count); + + self._connectWebSocket(self._sid); + } + + self._updateUI(); + }, function() { + self._state = State.IDLE; + self._updateUI(); + }); + + self._updateUI(); +} + +DemoApp.prototype.signOut = function() { + var self = this; + + self._appendLogMark(); + self._appendLog("SIGN OUT"); + + self._sendHttpRequest("user.signout", {"sid": self._sid}, function(res) { + if (self._socket) { + // will call cleanup() and updateUI() + self._socket.close(); + } else { + self._cleanup(); + self._updateUI(); + } + }); +} + +DemoApp.prototype.addCounter = function() { + var self = this; + + self._sendHttpRequest("user.count", {sid: self._sid}, function(res) { + if (!self._validateResult(res, ["count"])) return; + + var count = parseInt(res["count"]).toString(); + self._appendLog("count = " + count); + self._counterValueInput.val(count); + }); +} + +DemoApp.prototype.sendJobMessage = function(delay, message) { + var self = this; + + if (message === "") { + self._showError("Please enter message."); + } + + self._sendHttpRequest("user.addjob", { + sid: self._sid, + delay: delay, + message: message + }); +} + +DemoApp.prototype.sendMessage = function(recipient, message) { + var self = this; + + if (recipient === "" || recipient === null) { + self._showError("Please choose user from online users list."); + return; + } + + if (message === "") { + self._showError("Please enter message."); + } + + var data = { + action: "chat.sendmessage", + recipient: recipient, + message: message + }; + self._sendWebSocketMessage(data); +} + +DemoApp.prototype.sendMessageToAll = function(message) { + var self = this; + + if (message === "") { + self._showError("Please enter message."); + } + + var data = { + action: "chat.sendmessagetoall", + message: message + }; + self._sendWebSocketMessage(data); +} + +DemoApp.prototype._updateUI = function() { + var self = this; + + var state = self._state; + + // sign in + self._serverAddrInput.prop("disabled", state != State.IDLE); + self._usernameInput.prop("disabled", state != State.IDLE); + self._addCounterButton.prop("disabled", state != State.CONNECTED); + + if (state != State.CONNECTING && state != State.CONNECTED) { + self._counterValueInput.val(""); + } + + if (state === State.IDLE) { + self._signInButton.text("Sign In").prop("disabled", false); + } else if (state === State.SIGNIN || state === State.CONNECTING) { + self._signInButton.text("Connecting").prop("disabled", true); + } else if (state === State.CONNECTED) { + self._signInButton.text("Sign Out").prop("disabled", false); + } else { + self._signInButton.text("-").prop("disabled", true); + } + + // job + self._selectDelayInput.prop("disabled", state != State.CONNECTED); + self._jobMessageInput.prop("disabled", state != State.CONNECTED); + self._sendJobMessageButton.prop("disabled", state != State.CONNECTED); + + // chat + if (state != State.CONNECTING && state != State.CONNECTED) { + self._selectUserInput.empty(); + } + + self._selectUserInput.prop("disabled", state != State.CONNECTED); + self._messageInput.prop("disabled", state != State.CONNECTED); + self._sendMessageButton.prop("disabled", state != State.CONNECTED); + self._sendMessageToAllButton.prop("disabled", state != State.CONNECTED); +} + +DemoApp.prototype._cleanup = function() { + self._state = State.IDLE; + self._socket = null; + self._sid = null; + self._httpServerAddr = null; + self._websocketServerAddr = null; +} + +DemoApp.prototype._validateResult = function(res, fields) { + var err = res["err"]; + if (typeof err !== "undefined") { + return false; + } + + for (var i = 0; i < fields.length; i++) { + var field = fields[i]; + var v = res[field]; + if (typeof v === "undefined") { + return false; + } + } + return true; +} + +DemoApp.prototype._sendHttpRequest = function(action, values, callback, fail) { + var self = this; + + var url = self._httpServerAddr + "?action=" + action; + self._appendLog("HTTP: " + url); + + $.post(url, values, function(res) { + if (res.err) { + var err = "ERR: " + res.err; + self._showError(err); + self._appendLog(err); + } + if (callback) { + callback(res); + } else { + if (res.ok) { + self._appendLog("OK"); + } else { + self._appendLog("ERR, " + res.err); + } + } + }, "json") + .fail(function() { + self._appendLog("HTTP: " + url + " FAILED"); + if (fail) { + fail(); + } + }); +} + +DemoApp.prototype._sendWebSocketMessage = function(data) { + var self = this; + + var str = JSON.stringify(data); + self._socket.send(str); + self._appendLog("WEBSOCKET SEND: " + str); +} + +DemoApp.prototype._connectWebSocket = function(sid) { + var self = this; + + var protocol = "gbc-auth-" + sid; + self._appendLog("CONNECT WEBSOCKET with PROTOCOL: " + protocol); + + var socket = new WebSocket(self._websocketServerAddr, protocol); + socket.onopen = function() { + self._appendLog("WEBSOCKET CONNECTED"); + self._state = State.CONNECTED; + self._updateUI(); + } + + socket.onerror = function(error) { + if (!(error instanceof Event)) { + self._appendLog("ERR: " + error.toString()); + } + } + + socket.onmessage = function(event) { + self._appendLog("WEBSOCKET RECV: " + event.data.toString()); + + var msg = JSON.parse(event.data); + if (typeof msg == "object" && msg.name) { + var handler = self._messageHandlers[msg.name]; + if (handler) { + handler(self, msg); + } + } else { + self._appendLog("INVALID MSG: " + event.data.toString()); + } + } + + socket.onclose = function() { + self._state = State.IDLE; + self._appendLog("WEBSOCKET DISCONNECTED"); + self._cleanup(); + self._updateUI(); + } + + self._socket = socket; +} + +DemoApp.prototype._messageHandlers = { + LIST_ALL_USERS: function(self, data) { + var users = data["users"]; + if (!users) { + return; + } + + self._selectUserInput.empty(); + for (var i = 0; i < users.length; ++i) { + var username = users[i]; + var username_html = $("
    ").text(users[i]).html(); + self._selectUserInput.append($("") + .val(username) + .text(username_html)); + } + self._selectUserInput.prop("selectedIndex", 0); + }, + + ADD_USER: function(self, data) { + var username = data.username; + self._selectUserInput.append($("") + .val(username) + .text(username)); + }, + + REMOVE_USER: function(self, data) { + var username = data.username; + self._selectUserInput.find("> option").each(function() { + if ($(this).val() === username) { + $(this).remove(); + return; + } + }); + }, + + MESSAGE: function(self, data) { + var username = data.sender; + var message = data.body; + UIkit.notify({ + message: "" + username + " say:
    " + message, + status: 'info', + timeout: 5000, + pos: 'bottom-right' + }); + } +} + +DemoApp.prototype._showError = function(message) { + var self = this; + self._alertDialogHtml.find("#alertContents").text(message); + var modal = UIkit.modal(self._alertDialogHtml); + modal.show(); +} + +DemoApp.prototype._appendLogMark = function() { + var self = this; + + self._logHtml.prepend("--------\n"); + self._logHtml.scrollTop(self._logHtml.prop("scrollHeight")); +} + +DemoApp.prototype._appendLog = function(message) { + var self = this; + + var now = new Date(); + var time = f2num(now.getHours().toString()) + + ":" + f2num(now.getMinutes().toString()) + + ":" + f2num(now.getSeconds().toString()); + message = $("
    ").text(message).html(); + message = message.replace("\n", "
    \n"); + self._logHtml.prepend("[" + time + "] " + message + "\n"); + self._logHtml.scrollTop(self._logHtml.prop("scrollHeight")); +} + +DemoApp.prototype._clearLogs = function() { + this._logHtml.empty(); +} diff --git a/apps/welcome/public_html/js/notify.min.js b/apps/welcome/public_html/js/notify.min.js new file mode 100644 index 0000000..193d059 --- /dev/null +++ b/apps/welcome/public_html/js/notify.min.js @@ -0,0 +1,2 @@ +/*! UIkit 2.24.2 | http://www.getuikit.com | (c) 2014 YOOtheme | MIT License */ +!function(t){var e;window.UIkit&&(e=t(UIkit)),"function"==typeof define&&define.amd&&define("uikit-notify",["uikit"],function(){return e||t(UIkit)})}(function(t){"use strict";var e={},i={},s=function(e){return"string"==t.$.type(e)&&(e={message:e}),arguments[1]&&(e=t.$.extend(e,"string"==t.$.type(arguments[1])?{status:arguments[1]}:arguments[1])),new n(e).show()},o=function(t,e){var s;if(t)for(s in i)t===i[s].group&&i[s].close(e);else for(s in i)i[s].close(e)},n=function(s){this.options=t.$.extend({},n.defaults,s),this.uuid=t.Utils.uid("notifymsg"),this.element=t.$(['
    ','',"
    ","
    "].join("")).data("notifyMessage",this),this.content(this.options.message),this.options.status&&(this.element.addClass("uk-notify-message-"+this.options.status),this.currentstatus=this.options.status),this.group=this.options.group,i[this.uuid]=this,e[this.options.pos]||(e[this.options.pos]=t.$('
    ').appendTo("body").on("click",".uk-notify-message",function(){var e=t.$(this).data("notifyMessage");e.element.trigger("manualclose.uk.notify",[e]),e.close()}))};return t.$.extend(n.prototype,{uuid:!1,element:!1,timout:!1,currentstatus:"",group:!1,show:function(){if(!this.element.is(":visible")){var t=this;e[this.options.pos].show().prepend(this.element);var i=parseInt(this.element.css("margin-bottom"),10);return this.element.css({opacity:0,"margin-top":-1*this.element.outerHeight(),"margin-bottom":0}).animate({opacity:1,"margin-top":0,"margin-bottom":i},function(){if(t.options.timeout){var e=function(){t.close()};t.timeout=setTimeout(e,t.options.timeout),t.element.hover(function(){clearTimeout(t.timeout)},function(){t.timeout=setTimeout(e,t.options.timeout)})}}),this}},close:function(t){var s=this,o=function(){s.element.remove(),e[s.options.pos].children().length||e[s.options.pos].hide(),s.options.onClose.apply(s,[]),s.element.trigger("close.uk.notify",[s]),delete i[s.uuid]};this.timeout&&clearTimeout(this.timeout),t?o():this.element.animate({opacity:0,"margin-top":-1*this.element.outerHeight(),"margin-bottom":0},function(){o()})},content:function(t){var e=this.element.find(">div");return t?(e.html(t),this):e.html()},status:function(t){return t?(this.element.removeClass("uk-notify-message-"+this.currentstatus).addClass("uk-notify-message-"+t),this.currentstatus=t,this):this.currentstatus}}),n.defaults={message:"",status:"",timeout:5e3,group:null,pos:"top-center",onClose:function(){}},t.notify=s,t.notify.message=n,t.notify.closeAll=o,s}); \ No newline at end of file diff --git a/apps/welcome/public_html/js/tests.js b/apps/welcome/public_html/js/tests.js deleted file mode 100644 index 5639086..0000000 --- a/apps/welcome/public_html/js/tests.js +++ /dev/null @@ -1,446 +0,0 @@ - -var State = { - IDLE: 0, - SIGNIN: 1, - CONNECTING: 2, - CONNECTED: 3 -}; - -var tests = { - opts: { - server_addr: null, - http_entry: "welcome_api", - http_server_addr: null, - websocket_entry: "welcome_socket", - websocket_server_addr: null - }, - - status: { - state: State.IDLE, - - username: null, - session_id: null, - connect_tag: null, - - socket: null, - msg_id: 0, - callbacks: {} - }, - - events: { - allusers: function(data) { - var users = data["users"]; - if (users) { - var online_users_select = $("#online_users_select"); - online_users_select.empty(); - for (var i = 0; i < users.length; ++i) { - var user = users[i]; - var username = $("
    ").text(user.username).html(); - online_users_select.append($("").val(user.tag).text(user.username)); - } - online_users_select.prop("selectedIndex", -1); - } - }, - - adduser: function(data) { - var online_users_select = $("#online_users_select"); - online_users_select.append($("").val(data.tag).text(data.username)); - }, - - removeuser: function(data) { - var online_users_select_options = $("#online_users_select > option"); - online_users_select_options.each(function() { - if ($(this).text() == data.username) { - $(this).remove(); - // tests.on_destuserchanged(); - return; - } - }); - } - }, - - prepare: function() { - var self = this; - $("#server_addr_input").val(document.location.host); - - $("#sign_button").click(function() { - if (self.status.state == State.IDLE) { - self.signin(); - } else { - self.signout(); - } - return false; - }); - - $("#add_counter_button").click(function() { - self.add_counter(); - return false; - }); - - $("#online_users_select").change(function() { - if (this.selectedIndex >= 0) { - $("#dest_connect_tag_input").val(this.options[this.selectedIndex].value); - } else { - $("#dest_connect_tag_input").val(""); - } - }); - $("#send_message_button").click(function() { - var tag = $("#dest_connect_tag_input").val(); - var message = $("#message_input").val(); - if (tag == "") { - self.show_error("Please enter Connect Tag, or choose user from Online Users list."); - return; - } - if (message == "") { - self.show_error("Please enter message."); - return; - } - - self.send_message(tag, message); - }); - - $("#clear_logs_button").click(function() { - log.clear(); - return false; - }); - $("#insert_mark_button").click(function() { - log.add_mark(); - return false; - }) - - self.update_ui(); - }, - - update_ui: function() { - var self = this; - - var state = self.status.state; - $("#server_addr_input").prop("disabled", state != State.IDLE); - $("#username_input").prop("disabled", state != State.IDLE); - - if (state != State.CONNECTING && state != State.CONNECTED) { - $("#session_id_input").val(""); - $("#connect_tag_input").val(""); - $("#counter_value_input").val(""); - $("#online_users_select").empty(); - $("#dest_connect_tag_input").val(""); - } - - $("#add_counter_button").prop("disabled", state != State.CONNECTED); - $("#online_users_select").prop("disabled", state != State.CONNECTED); - $("#dest_connect_tag_input").prop("disabled", state != State.CONNECTED); - $("#message_input").prop("disabled", state != State.CONNECTED); - $("#send_message_button").prop("disabled", state != State.CONNECTED); - - if (state == State.IDLE) { - $("#sign_button").text("Sign In").prop("disabled", false); - } else if (state == State.SIGNIN || state == State.CONNECTING) { - $("#sign_button").text("Connecting").prop("disabled", true); - } else if (state == State.CONNECTED) { - $("#sign_button").text("Sign Out").prop("disabled", false); - } else { - $("#sign_button").text("-").prop("disabled", true); - } - }, - - cleanup: function() { - var self = this; - var opts = self.opts; - opts.http_server_addr = null; - opts.websocket_server_addr = null; - - var status = self.status; - status.state = State.IDLE; - status.username = null; - status.session_id = null; - status.connect_tag = null; - status.socket = null; - status.msg_id = 0; - status.callbacks = {}; - }, - - signin: function() { - var self = this; - if (self.status.state != State.IDLE) { - return; - } - - var username = $("#username_input").val(); - if (username === "") { - self.show_error("PLEASE ENTER username"); - return; - } - - var opts = self.opts; - opts.server_addr = $("#server_addr_input").val(); - opts.http_server_addr = "http://" + opts.server_addr + "/" + opts.http_entry - opts.websocket_server_addr = "ws://" + opts.server_addr + "/" + opts.websocket_entry - - var status = self.status; - status.state = State.SIGNIN; - status.username = username; - - self.update_ui(); - - var data = {"username": username} - log.add_mark(); - log.add("SIGN IN"); - self.http_request("user.login", data, function(res) { - if (!self.validate_result(res, ["sid", "tag", "count"])) { - status.state = State.IDLE; - } else { - status.state = State.CONNECTING; - status.session_id = res["sid"].toString(); - status.connect_tag = res["tag"].toString(); - log.add("GET SESSION ID: " + status.session_id); - - var count = parseInt(res["count"]); - log.add("count = " + count.toString()); - $("#session_id_input").val(status.session_id); - $("#connect_tag_input").val(status.connect_tag); - $("#counter_value_input").val(count); - - self.connect_websocket(); - } - - self.update_ui(); - }, function() { - status.state = State.IDLE; - self.update_ui(); - }); - }, - - signout: function() { - var self = this; - var status = self.status; - if (status.session_id === null) { - log.add("ALREADY SIGN OUT"); - return; - } - - log.add_mark(); - log.add("SIGN OUT"); - - self.http_request("user.logout", {"sid": status.session_id}, function(res) { - if (status.socket) { - // will call cleanup() and update_ui() - status.socket.close(); - } else { - self.cleanup(); - self.update_ui(); - } - }); - }, - - add_counter: function() { - var self = this; - var status = self.status; - - if (status.session_id === null) { - log.add("SIGN IN FIRST"); - return; - } - - self.http_request("user.count", {"sid": status.session_id}, function(res) { - if (!self.validate_result(res, ["count"])) return; - - var count = parseInt(res["count"]); - log.add("count = " + count.toString()); - $("#counter_value_input").val(count.toString()); - }); - }, - - send_message: function(tag, message) { - var self = this; - - tag = tag.toString(); - message = message.toString(); - - var data = { - "action": "chat.sendmessage", - "tag": tag, - "message": message - }; - self.send_data(data); - }, - - show_error: function(message) { - var modal = UIkit.modal("#alert_dialog"); - modal.show(); - $("#error_alert").text(message); - }, - - validate_result: function(res, fields) { - var err = res["err"]; - if (typeof err !== "undefined") { - return false; - } - - for (var i = 0; i < fields.length; i++) { - var field = fields[i]; - var v = res[field]; - if (typeof v === "undefined") { - return false; - } - } - return true; - }, - - http_request: function(action, data, callback, fail) { - var self = this; - var opts = self.opts; - var url = opts.http_server_addr + "?action=" + action; - log.add("HTTP: " + url); - $.post(url, data, function(res) { - if (res.err) { - var err = "ERR: " + res.err; - self.show_error(err); - log.add(err); - } - callback(res); - }, "json") - .fail(function() { - log.add("HTTP: " + url + " FAILED"); - if (fail) { - fail(); - } - }); - }, - - connect_websocket: function() { - var self = this; - var opts = self.opts; - var status = self.status; - - if (status.socket !== null) { - log.add("ALREADY CONNECTED"); - return; - } - - if (status.session_id === null) { - log.add("SIGN IN FIRST"); - return; - } - - var protocol = "gbc-" + status.session_id; - log.add("CONNECT WEBSOCKET with PROTOCOL: " + protocol.toString()); - - var socket = new WebSocket(opts.websocket_server_addr, protocol); - socket.onopen = function() { - log.add("WEBSOCKET CONNECTED"); - status.state = State.CONNECTED; - self.update_ui(); - }; - socket.onerror = function(error) { - if (!(error instanceof Event)) { - log.add("ERR: " + error.toString()); - } - }; - socket.onmessage = function(event) { - log.add("WEBSOCKET RECV: " + event.data.toString()); - var data = JSON.parse(event.data); - if (data["__id"]) { - var msgid = data["__id"].toString(); - if (typeof status.callbacks[msgid] !== "undefined") { - var callback = status.callbacks[msgid]; - status.callbacks[msgid] = null; - callback(data); - } - } else if (data["name"]) { - var events = self.events; - var name = data["name"].toString(); - if (isfunction(events[name])) { - events[name](data); - } - } - }; - socket.onclose = function() { - log.add("WEBSOCKET DISCONNECTED"); - self.cleanup(); - self.update_ui(); - }; - - status.socket = socket; - }, - - send_data: function(data, callback) { - var self = this; - var status = self.status; - - if (status.socket === null) { - log.add("NOT CONNECTED"); - return; - } - - status.msg_id++; - data["__id"] = status.msg_id; - var json_str = JSON.stringify(data); - - if (isfunction(callback)) { - status.callbacks[status.msg_id.toString()] = callback; - } - - status.socket.send(json_str); - log.add("WEBSOCKET SEND: " + json_str); - } -}; - -// ---- - -var f2num = function(n, l) { - if (typeof l == "undefined") l = 2; - while (n.length < l) { - n = "0" + n; - } - return n; -} - -var isfunction = function(functionToCheck) { - var getType = {}; - return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; -} - -var log = { - opts: { - lasttime: 0 - }, - - add: function(message) { - var self = this; - var opts = self.opts; - - var log = $("#log"); - var now = new Date(); - var nowtime = now.getTime(); - if (opts.lasttime > 0 && nowtime - opts.lasttime > 10000) { // 10s - $("#log").prepend("-------------------------\n"); - } - opts.lasttime = nowtime; - - var time = f2num(now.getHours().toString()) - + ":" + f2num(now.getMinutes().toString()) - + ":" + f2num(now.getSeconds().toString()); - message = $("
    ").text(message).html(); - message = message.replace("\n", "
    \n"); - log.prepend("[" + time + "] " + message + "\n"); - log.scrollTop(log.prop("scrollHeight")); - }, - - add_mark: function() { - var self = this; - var log = $("#log"); - log.prepend("--------\n"); - log.scrollTop(log.prop("scrollHeight")); - }, - - clear: function() { - $('#log').empty(); - } -} - -// ---- - -$(document).ready(function() { - tests.prepare(); -}); - diff --git a/apps/welcome/public_html/js/welcome.js b/apps/welcome/public_html/js/welcome.js deleted file mode 100644 index bad9464..0000000 --- a/apps/welcome/public_html/js/welcome.js +++ /dev/null @@ -1,242 +0,0 @@ - -var dashboard = { - opts: { - interval: 10, - interval_60s_steps: 6, - update_last60s_busy: false, - - chart_opts: { - axisX: { - showLabel: true, - offset: 20 - }, - axisY: { - showLabel: true, - offset: 30, - scaleMinSpace: 30 - }, - showArea: true, - height: 200, - showPoint: false, - lineSmooth: false, - low: 0, - high: 99, - fullWidth: true - }, - - last60s_data_base: { - labels: [], - series: [[]] - } - }, - - server_data: { - cpu_cores: null, - mem_total: null, - disk_total: null, - interval: null - }, - - data: { - }, - - charts: { - }, - - init: function() { - $("#init_alert").show(); - $("#error_alert").hide(); - - var self = this; - - $.getJSON(dashboard.admin_url + "&time_span=1s", function(data) { - self.server_data.cpu_cores = parseInt(data.cpu_cores); - self.server_data.mem_total = parseInt(data.mem_total); - self.server_data.disk_total = parseInt(data.disk_total); - self.server_data.interval = parseInt(data.interval); - - var last60s_cpu_title = $("#last60s_cpu_title"); - last60s_cpu_title.text(last60s_cpu_title.text() + " (" + self.server_data.cpu_cores.toString() + " cores)"); - - var last60s_mem_title = $("#last60s_mem_title"); - last60s_mem_title.text(last60s_mem_title.text() + " (" + Math.ceil(self.server_data.mem_total / 1024).toString() + " MB)"); - - self.prepare_charts(); - self.update_last60s(); - window.setInterval(function() { - self.update_last60s(); - }, 1000 * self.server_data.interval); - - $("#chart_last60s_1").show(); - $("#chart_last60s_2").show(); - $("#init_alert").hide(); - }) - .fail(function() { - $("#init_alert").hide(); - $("#error_alert").show(); - }); - }, - - prepare_charts: function() { - var self = this; - - var interval = self.server_data.interval; - var interval_60s_steps = Math.ceil(60 / interval); - var last60s_data_base = self.opts.last60s_data_base; - for (var i = 0; i < interval_60s_steps; ++i) { - last60s_data_base.labels[i] = ((i * interval) % 10 == 0) ? (60 - (i * interval)).toString() + "s" : ""; - } - - // last60s_cpu - var last60s_cpu_data = $.extend(true, {}, last60s_data_base); - self.data.last60s_cpu_data = last60s_cpu_data; - - var last60s_cpu_opts = $.extend(true, {}, self.opts.chart_opts); - last60s_cpu_opts.axisY.labelInterpolationFnc = function(value) { - return value + '%'; - }; - - // last60s_mem - var last60s_mem_data = $.extend(true, {}, last60s_data_base); - self.data.last60s_mem_data = last60s_mem_data; - - var last60s_mem_opts = $.extend(true, {}, self.opts.chart_opts); - last60s_mem_opts.high = Math.ceil(self.server_data.mem_total / 1024); - last60s_mem_opts.axisY.labelInterpolationFnc = function(value) { - return value + 'M'; - }; - - // last60s_connects - var last60s_connects_data = $.extend(true, {}, last60s_data_base); - self.data.last60s_connects_data = last60s_connects_data; - - var last60s_connects_opts = $.extend(true, {}, self.opts.chart_opts); - last60s_connects_opts.high = null; - - // last60s_jobs - var last60s_jobs_data = $.extend(true, {}, last60s_data_base); - self.data.last60s_jobs_data = last60s_jobs_data; - - var last60s_jobs_opts = $.extend(true, {}, self.opts.chart_opts); - last60s_jobs_opts.high = null; - - // create charts - self.charts.last60s_cpu_chart = new Chartist.Line('#last60s_cpu', last60s_cpu_data, last60s_cpu_opts); - self.charts.last60s_mem_chart = new Chartist.Line('#last60s_mem', last60s_mem_data, last60s_mem_opts); - self.charts.last60s_connects_chart = new Chartist.Line('#last60s_connects', last60s_connects_data, last60s_connects_opts); - self.charts.last60s_jobs_chart = new Chartist.Line('#last60s_jobs', last60s_jobs_data, last60s_jobs_opts); - }, - - update_last60s: function() { - var self = this; - - if (self.opts.update_last60s_busy) { - return; - } - - $.getJSON(dashboard.admin_url + "&time_span=60s", function(data) { - // CPU - var cores = self.server_data.cpu_cores; - var interval = self.server_data.interval; - var interval_60s_steps = Math.ceil(60 / interval); - - var last60s_cpu_data = self.data.last60s_cpu_data; - var last60s_mem_data = self.data.last60s_mem_data; - var last60s_connects_data = self.data.last60s_connects_data; - var last60s_jobs_data = self.data.last60s_jobs_data; - - var loads = {nginx: [], redis: [], beanstalkd: []}; - var mems = {nginx: 0, redis: 0}; - var connects = {nginx: [], redis: []}; - var jobs = {beanstalkd: []}; - - var redis_data = data["REDIS_SERVER"]; - var beanstalkd_data = data["BEANSTALKD"]; - for (var index = 0; index < interval_60s_steps; ++index) { - var nginx_data = self.calc_ngx_data_at_index(data, "last_60s", index); - if (nginx_data === false) { - break; - } - loads.nginx[index] = nginx_data.load; - loads.redis[index] = parseFloat(redis_data.cpu.last_60s[index]); - loads.beanstalkd[index] = parseFloat(beanstalkd_data.cpu.last_60s[index]); - - mems.nginx = nginx_data.mem; - mems.redis = parseInt(data["REDIS_SERVER"].mem.last_60s[index]); - - connects.nginx[index] = parseInt(data["NGINX_MASTER"].conn_num.last_60s[index]); - connects.redis[index] = parseInt(data["REDIS_SERVER"].conn_num.last_60s[index]); - - jobs.beanstalkd[index] = parseInt(data["BEANSTALKD"].total_jobs.last_60s[index]); - } - - var length = loads.nginx.length; - var offset = interval_60s_steps - length; - - if (offset > 0) { - for (var index = 0; index < offset; ++index) { - last60s_cpu_data.series[0][index] = 0; - last60s_mem_data.series[0][index] = 0; - last60s_connects_data.series[0][index] = 0; - last60s_jobs_data.series[0][index] = 0; - } - } - - for (var index = 0; index < length; ++index) { - var idx = offset + index; - var load = (loads.beanstalkd[index] + loads.redis[index] + loads.nginx[index]) / cores; - if (load > 100) { - load = 100; - } - last60s_cpu_data.series[0][idx] = load; - last60s_mem_data.series[0][idx] = Math.ceil((mems.nginx + mems.redis) / 1024); - - last60s_connects_data.series[0][idx] = connects.nginx[index]; - last60s_jobs_data.series[0][idx] = jobs.beanstalkd[index]; - } - self.charts.last60s_cpu_chart.update(last60s_cpu_data); - self.charts.last60s_mem_chart.update(last60s_mem_data); - self.charts.last60s_connects_chart.update(last60s_connects_data); - self.charts.last60s_jobs_chart.update(last60s_jobs_data); - - // MEM - self.opts.update_last60s_busy = false; - $("#error_alert").hide(); - }) - .fail(function() { - $("#error_alert").show(); - }); - }, - - - calc_ngx_data_at_index: function(data, timetype, index) { - var cpu_set = data["NGINX_MASTER"].cpu[timetype]; - var mem_set = data["NGINX_MASTER"].mem[timetype]; - - var load = cpu_set[index]; - if (load === undefined) { - return false; - } - - load = parseFloat(load); - var mem = parseInt(mem_set[index]); - - for (var ngx_index = 1; ngx_index < 100; ++ngx_index) { - var key = "NGINX_WORKER_#" + ngx_index; - var set = data[key]; - if (set === undefined) { - break; - } - load = load + parseFloat(set.cpu[timetype][index]); - mem = mem + parseInt(set.mem[timetype][index]); - } - - return {load: load, mem: mem}; - } -}; - -$(document).ready(function() { - var l = document.location; - dashboard.admin_url = "http://" + l.host + "/admin?action=monitor.getdata" - dashboard.init(); -}); diff --git a/shells/src/getopt_long.c b/bin/getopt_long.c similarity index 100% rename from shells/src/getopt_long.c rename to bin/getopt_long.c diff --git a/bin/shell_func.lua b/bin/shell_func.lua new file mode 100644 index 0000000..82dc82c --- /dev/null +++ b/bin/shell_func.lua @@ -0,0 +1,251 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +if not ROOT_DIR then + print("Not set ROOT_DIR for Lua, exit.") + os.exit(1) +end + +-- globals + +LUA_BIN = ROOT_DIR .. "/bin/openresty/luajit/bin/lua" +NGINX_DIR = ROOT_DIR .. "/bin/openresty/nginx" +REDIS_DIR = ROOT_DIR .. "/bin/redis" +TMP_DIR = ROOT_DIR .. "/tmp" +CONF_DIR = ROOT_DIR .. "/conf" +DB_DIR = ROOT_DIR .. "/db" + +CONF_PATH = CONF_DIR .. "/config.lua" +NGINX_CONF_PATH = CONF_DIR .. "/nginx.conf" +REDIS_CONF_PATH = CONF_DIR .. "/redis.conf" +SUPERVISORD_CONF_PATH = CONF_DIR .. "/supervisord.conf" + +VAR_CONF_PATH = TMP_DIR .. "/config.lua" +VAR_APP_KEYS_PATH = TMP_DIR .. "/app_keys.lua" +VAR_NGINX_CONF_PATH = TMP_DIR .. "/nginx.conf" +VAR_REDIS_CONF_PATH = TMP_DIR .. "/redis.conf" +VAR_BEANS_LOG_PATH = TMP_DIR .. "/beanstalkd.log" +VAR_SUPERVISORD_CONF_PATH = TMP_DIR .. "/supervisord.conf" + +local _getValue, _checkVarConfig, _checkAppKeys +local _updateCoreConfig, _updateNginxConfig +local _updateRedisConfig, _updateSupervisordConfig + +function updateConfigs() + _updateCoreConfig() + _updateNginxConfig() + _updateRedisConfig() + _updateSupervisordConfig() +end + +-- init + +package.path = ROOT_DIR .. '/src/?.lua;' .. package.path + +require("framework.init") + +if tostring(DEBUG) ~= "0" then + cc.DEBUG = cc.DEBUG_VERBOSE + DEBUG = true +else + cc.DEBUG = cc.DEBUG_WARN + DEBUG = false +end + +-- private + +local luamd5 = cc.import("#luamd5") +local Factory = cc.import("#gbc").Factory + +_getValue = function(t, key, def) + local keys = string.split(key, ".") + for _, key in ipairs(keys) do + if t[key] then + t = t[key] + else + if type(def) ~= "nil" then return def end + return nil + end + end + return t +end + +_checkVarConfig = function() + if not io.exists(VAR_CONF_PATH) then + print(string.format("[ERR] Not found file: %s", VAR_CONF_PATH)) + os.exit(1) + end + + local config = dofile(VAR_CONF_PATH) + if type(config) ~= "table" then + print(string.format("[ERR] Invalid config file: %s", VAR_CONF_PATH)) + os.exit(1) + end + + return config +end + +_checkAppKeys = function() + if not io.exists(VAR_APP_KEYS_PATH) then + print(string.format("[ERR] Not found file: %s", VAR_APP_KEYS_PATH)) + os.exit(1) + end + + local appkeys = dofile(VAR_APP_KEYS_PATH) + if type(appkeys) ~= "table" then + print(string.format("[ERR] Invalid app keys file: %s", VAR_APP_KEYS_PATH)) + os.exit(1) + end + + return appkeys +end + +_updateCoreConfig = function() + local contents = io.readfile(CONF_PATH) + contents = string.gsub(contents, "_GBC_CORE_ROOT_", ROOT_DIR) + io.writefile(VAR_CONF_PATH, contents) + + -- update all apps key and index + local config = _checkVarConfig() + local apps = _getValue(config, "apps") + + local names = {} + for name, _ in pairs(apps) do + names[#names + 1] = name + end + table.sort(names) + + local contents = {"", "local keys = {}"} + for index, name in ipairs(names) do + local path = apps[name] + contents[#contents + 1] = string.format('keys["%s"] = {name = "%s", index = %d, key = "%s"}', path, name, index, luamd5.sumhexa(path)) + end + contents[#contents + 1] = "return keys" + contents[#contents + 1] = "" + + io.writefile(VAR_APP_KEYS_PATH, table.concat(contents, "\n")) +end + +_updateNginxConfig = function() + local config = _checkVarConfig() + + local contents = io.readfile(NGINX_CONF_PATH) + contents = string.gsub(contents, "_GBC_CORE_ROOT_", ROOT_DIR) + contents = string.gsub(contents, "listen[ \t]+[0-9]+", string.format("listen %d", _getValue(config, "server.nginx.port", 8088))) + contents = string.gsub(contents, "worker_processes[ \t]+[0-9]+", string.format("worker_processes %d", _getValue(config, "server.nginx.numOfWorkers", 4))) + + if DEBUG then + contents = string.gsub(contents, "cc.DEBUG = [%a_%.]+", "cc.DEBUG = cc.DEBUG_VERBOSE") + contents = string.gsub(contents, "error_log (.+%-error%.log)[ \t%a]*;", "error_log %1 debug;") + contents = string.gsub(contents, "lua_code_cache[ \t]+%a+;", "lua_code_cache off;") + else + contents = string.gsub(contents, "cc.DEBUG = [%a_%.]+", "cc.DEBUG = cc.DEBUG_ERROR") + contents = string.gsub(contents, "error_log (.+%-error%.log)[ \t%a]*;", "error_log %1;") + contents = string.gsub(contents, "lua_code_cache[ \t]+%a+;", "lua_code_cache on;") + end + + -- copy app_entry.conf to tmp/ + local apps = _getValue(config, "apps") + local includes = {} + for name, path in pairs(apps) do + local entryPath = string.format("%s/conf/app_entry.conf", path) + local varEntryPath = string.format("%s/app_%s_entry.conf", TMP_DIR, name) + if io.exists(entryPath) then + local entry = io.readfile(entryPath) + entry = string.gsub(entry, "_GBC_CORE_ROOT_", ROOT_DIR) + entry = string.gsub(entry, "_APP_ROOT_", path) + io.writefile(varEntryPath, entry) + includes[#includes + 1] = string.format(" include %s;", varEntryPath) + end + end + includes = "\n" .. table.concat(includes, "\n") + contents = string.gsub(contents, "\n[ \t]*#[ \t]*_INCLUDE_APPS_ENTRY_", includes) + + io.writefile(VAR_NGINX_CONF_PATH, contents) +end + +_updateRedisConfig = function() + local config = _checkVarConfig() + + local contents = io.readfile(REDIS_CONF_PATH) + contents = string.gsub(contents, "_GBC_CORE_ROOT_", ROOT_DIR) + + local socket = _getValue(config, "server.redis.socket") + if socket then + if string.sub(socket, 1, 5) == "unix:" then + socket = string.sub(socket, 6) + end + contents = string.gsub(contents, "[# \t]*unixsocket[ \t]+[^\n]+", string.format("unixsocket %s", socket)) + contents = string.gsub(contents, "[# \t]*bind[ \t]+[%d\\.]+", "# bind 127.0.0.1") + contents = string.gsub(contents, "[# \t]*port[ \t]+%d+", "port 0") + else + contents = string.gsub(contents, "[# \t]*unixsocket[ \t]+", "# unixsocket") + + local host = _getValue(config, "server.redis.host", "127.0.0.1") + local port = _getValue(config, "server.redis.port", 6379) + contents = string.gsub(contents, "[# \t]*bind[ \t]+[%d\\.]+", "bind " .. host) + contents = string.gsub(contents, "[# \t]*port[ \t]+%d+", "port " .. port) + end + + io.writefile(VAR_REDIS_CONF_PATH, contents) +end + +local _SUPERVISOR_WORKER_PROG_TMPL = [[ +[program:worker-_APP_NAME_] +command=_GBC_CORE_ROOT_/bin/openresty/luajit/bin/lua _GBC_CORE_ROOT_/bin/start_worker.lua _GBC_CORE_ROOT_ _APP_ROOT_PATH_ +process_name=%%(process_num)02d +numprocs=_NUM_PROCESS_ +redirect_stderr=true +stdout_logfile=_GBC_CORE_ROOT_/logs/worker-_APP_NAME_.log + +]] + +_updateSupervisordConfig = function() + local config = _checkVarConfig() + local appkeys = _checkAppKeys() + local appConfigs = Factory.makeAppConfigs(appkeys, config, package.path) + local beanport = _getValue(config, "server.beanstalkd.port") + + local contents = io.readfile(SUPERVISORD_CONF_PATH) + contents = string.gsub(contents, "_GBC_CORE_ROOT_", ROOT_DIR) + contents = string.gsub(contents, "_BEANSTALKD_PORT_", beanport) + + local workers = {} + local apps = _getValue(config, "apps") + for name, path in pairs(apps) do + local prog = string.gsub(_SUPERVISOR_WORKER_PROG_TMPL, "_GBC_CORE_ROOT_", ROOT_DIR) + prog = string.gsub(prog, "_APP_ROOT_PATH_", path) + prog = string.gsub(prog, "_APP_NAME_", name) + + -- get numOfJobWorkers + local appConfig = appConfigs[path] + prog = string.gsub(prog, "_NUM_PROCESS_", appConfig.app.numOfJobWorkers) + + workers[#workers + 1] = prog + end + + contents = string.gsub(contents, ";_WORKERS_", table.concat(workers, "\n")) + + io.writefile(VAR_SUPERVISORD_CONF_PATH, contents) +end diff --git a/bin/shell_func.sh b/bin/shell_func.sh new file mode 100644 index 0000000..202a6ef --- /dev/null +++ b/bin/shell_func.sh @@ -0,0 +1,70 @@ +#/bin/bash + +if [ "$ROOT_DIR" == "" ]; then + echo "Not set ROOT_DIR, exit." + exit 1 +fi + +echo -e "\033[31mROOT_DIR\033[0m=$ROOT_DIR" +echo "" + +cd $ROOT_DIR + +LUA_BIN=$ROOT_DIR/bin/openresty/luajit/bin/lua +TMP_DIR=$ROOT_DIR/tmp +CONF_DIR=$ROOT_DIR/conf +CONF_PATH=$CONF_DIR/config.lua +VAR_SUPERVISORD_CONF_PATH=$TMP_DIR/supervisord.conf + +function getOsType() +{ + if [ `uname -s` == "Darwin" ]; then + echo "MACOS" + else + echo "LINUX" + fi +} + +OS_TYPE=$(getOsType) +if [ $OS_TYPE == "MACOS" ]; then + SED_BIN='sed -i --' +else + SED_BIN='sed -i' +fi + +function updateConfigs() +{ + $LUA_BIN -e "ROOT_DIR='$ROOT_DIR'; DEBUG=$DEBUG; dofile('$ROOT_DIR/bin/shell_func.lua'); updateConfigs()" +} + +function startSupervisord() +{ + echo "[CMD] supervisord -c $VAR_SUPERVISORD_CONF_PATH" + echo "" + cd $ROOT_DIR/bin/python_env/gbc + source bin/activate + $ROOT_DIR/bin/python_env/gbc/bin/supervisord -c $VAR_SUPERVISORD_CONF_PATH + cd $ROOT_DIR + echo "Start supervisord DONE" + echo "" +} + +function stopSupervisord() +{ + echo "[CMD] supervisorctl -c $VAR_SUPERVISORD_CONF_PATH shutdown" + echo "" + cd $ROOT_DIR/bin/python_env/gbc + source bin/activate + $ROOT_DIR/bin/python_env/gbc/bin/supervisorctl -c $VAR_SUPERVISORD_CONF_PATH shutdown + cd $ROOT_DIR + echo "" +} + +function checkStatus() +{ + cd $ROOT_DIR/bin/python_env/gbc + source bin/activate + $ROOT_DIR/bin/python_env/gbc/bin/supervisorctl -c $VAR_SUPERVISORD_CONF_PATH status + cd $ROOT_DIR + echo "" +} diff --git a/src/CLIBootstrap.lua b/bin/start_worker.lua similarity index 66% rename from src/CLIBootstrap.lua rename to bin/start_worker.lua index 9285b52..a7325fa 100644 --- a/src/CLIBootstrap.lua +++ b/bin/start_worker.lua @@ -22,10 +22,32 @@ THE SOFTWARE. ]] -local factory = require("server.base.Factory") - local args = {...} --- SERVER_CONFIG from init_by_lua, see nginx.conf -local app = factory.create(SERVER_CONFIG, "CLI", args) -app:run() +local help = function() + print [[ + +$ lua start_worker.lua + +]] +end + +if #args < 2 then + return help() +end + +ROOT_DIR = args[1] +APP_ROOT_PATH = args[2] + +package.path = ROOT_DIR .. '/src/?.lua;' .. package.path + +require("framework.init") +local appKeys = dofile(ROOT_DIR .. "/tmp/app_keys.lua") +local globalConfig = dofile(ROOT_DIR .. "/tmp/config.lua") + +cc.DEBUG = globalConfig.DEBUG + +local gbc = cc.import("#gbc") +local bootstrap = gbc.WorkerBootstrap:new(appKeys, globalConfig) + +os.exit(bootstrap:runapp(APP_ROOT_PATH)) diff --git a/check_server b/check_server new file mode 100755 index 0000000..5415f93 --- /dev/null +++ b/check_server @@ -0,0 +1,6 @@ +#!/bin/bash + +ROOT_DIR=$(cd "$(dirname $0)" && pwd) +source $ROOT_DIR/bin/shell_func.sh + +checkStatus diff --git a/conf/README.md b/conf/README.md deleted file mode 100644 index 1a9ae40..0000000 --- a/conf/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Configure Files for GameBox Cloud Core - -- redis.conf - used for redis server. - -- nginx.conf - default configure file for nginx in GameBox Cloud Core. - -- nginx.conf.optimized - an example of nginx.conf optimized. - -- supervisord.conf / supervisord.conf.d - supervisor conf file for GameBox Cloud Core. diff --git a/conf/config.lua b/conf/config.lua index cce00fc..4d20fcc 100644 --- a/conf/config.lua +++ b/conf/config.lua @@ -22,60 +22,56 @@ THE SOFTWARE. ]] -_GAMEBOX_CLOUD_CORE_VERSION = "0.7.0" - -_DBG_ERROR = 0 -_DBG_WARN = 1 -_DBG_INFO = 2 -_DBG_DEBUG = 3 - local config = { - -- user app - appRootPath = "_GBC_CORE_ROOT_/", - - numOfWorkers = 4, - - appHttpMessageFormat = "json", - appSocketMessageFormat = "json", - appJobMessageFormat = "json", - appSessionExpiredTime = 60 * 10, -- 10m - - defaultAcceptedRequestType = "http", - - -- GameBox Cloud Core settings - serverRootPath = "_GBC_CORE_ROOT_", - port = 8088, - welcomeEnabled = true, - adminEnabled = true, - websocketsTimeout = 60 * 1000, -- 60s - websocketsMaxPayloadLen = 16 * 1024, -- 16KB - maxSubscribeRetryCount = 10, - - -- internal memory database - redis = { - socket = "unix:_GBC_CORE_ROOT_/tmp/redis.sock", - -- host = "127.0.0.1", - -- port = 6379, - timeout = 10 * 1000, -- 10 seconds + DEBUG = cc.DEBUG_VERBOSE, + + -- all apps + apps = { + welcome = "_GBC_CORE_ROOT_/apps/welcome", + tests = "_GBC_CORE_ROOT_/apps/tests", }, - -- background job server - beanstalkd = { - host = "127.0.0.1", - port = 11300, - jobTube = "jobTube", + -- default app config + app = { + messageFormat = "json", + defaultAcceptedRequestType = "http", + sessionExpiredTime = 60 * 10, -- 10m + + httpEnabled = true, + httpMessageFormat = "json", + + websocketEnabled = true, + websocketMessageFormat = "json", + websocketsTimeout = 60 * 1000, -- 60s + websocketsMaxPayloadLen = 16 * 1024, -- 16KB + + jobMessageFormat = "json", + numOfJobWorkers = 2, + + jobWorkerRequests = 1000, }, - -- internal monitor - monitor = { - process = { - "nginx", - "redis-server", - "beanstalkd", + -- server config + server = { + nginx = { + numOfWorkers = 4, + port = 8088, }, - interval = 2, - }, + -- internal memory database + redis = { + socket = "unix:_GBC_CORE_ROOT_/tmp/redis.sock", + -- host = "127.0.0.1", + -- port = 6379, + timeout = 10 * 1000, -- 10 seconds + }, + + -- background job server + beanstalkd = { + host = "127.0.0.1", + port = 11300, + }, + } } return config diff --git a/conf/nginx.conf b/conf/nginx.conf index ecf468b..25cbaf9 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -1,15 +1,22 @@ +daemon off; # control by supervisord worker_processes 4; -error_log logs/error.log debug; +error_log _GBC_CORE_ROOT_/logs/nginx-error.log; +pid _GBC_CORE_ROOT_/tmp/nginx.pid; events { - worker_connections 1024; - accept_mutex_delay 100ms; + worker_connections 256; } http { include '_GBC_CORE_ROOT_/bin/openresty/nginx/conf/mime.types'; + # logs + log_format compression '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $bytes_sent ' + '"$http_referer" "$http_user_agent" "$gzip_ratio"'; + access_log _GBC_CORE_ROOT_/logs/nginx-access.log compression; + # tmp client_body_temp_path _GBC_CORE_ROOT_/tmp/client_body_temp; fastcgi_temp_path _GBC_CORE_ROOT_/tmp/fastcgi_temp; @@ -27,76 +34,28 @@ http { ssi off; # lua - lua_package_path '_GBC_CORE_ROOT_/src/?.lua;_GBC_CORE_ROOT_/src/lib/?.lua;;'; - lua_shared_dict INDEXES 512k; + lua_check_client_abort on; + lua_socket_log_errors off; + lua_package_path '_GBC_CORE_ROOT_/src/?.lua;;'; + lua_shared_dict _GBC_ 1024k; lua_code_cache off; init_by_lua ' -SERVER_CONFIG = loadfile("_GBC_CORE_ROOT_/conf/config.lua")() -DEBUG = _DBG_DEBUG -require("framework.init") -'; - - server { - listen 8088 so_keepalive=on; - - # user app - location ~ /api { - content_by_lua_file '_GBC_CORE_ROOT_/src/HttpBootstrap.lua'; - } - location = /socket { - lua_socket_log_errors off; - content_by_lua_file '_GBC_CORE_ROOT_/src/WebSocketBootstrap.lua'; - } +require("framework.init") - # docs - location ~ /docs { - root _GBC_CORE_ROOT_; - index index.html; - } +local appKeys = dofile("_GBC_CORE_ROOT_/tmp/app_keys.lua") +local globalConfig = dofile("_GBC_CORE_ROOT_/tmp/config.lua") - # GameBox Cloud welcome - location / { - access_by_lua ' -if not SERVER_CONFIG.welcomeEnabled then - ngx.exit(403) -end'; - root _GBC_CORE_ROOT_/apps/welcome/public_html; - index index.html; - } +cc.DEBUG = globalConfig.DEBUG - location ~ /welcome_api { - access_by_lua ' -if not SERVER_CONFIG.welcomeEnabled then - ngx.exit(403) -else - SERVER_CONFIG.appRootPath = SERVER_CONFIG.serverRootPath .. "/apps/welcome" -end'; - content_by_lua_file '_GBC_CORE_ROOT_/src/HttpBootstrap.lua'; - } +local gbc = cc.import("#gbc") +cc.exports.nginxBootstrap = gbc.NginxBootstrap:new(appKeys, globalConfig) - location = /welcome_socket { - lua_socket_log_errors off; - access_by_lua ' -if not SERVER_CONFIG.welcomeEnabled then - ngx.exit(403) -else - SERVER_CONFIG.appRootPath = SERVER_CONFIG.serverRootPath .. "/apps/welcome" -end'; - content_by_lua_file '_GBC_CORE_ROOT_/src/WebSocketBootstrap.lua'; - } +'; - # GameBox Cloud admin api - location ~ /admin { - access_by_lua ' -if not SERVER_CONFIG.adminEnabled then - ngx.exit(403) -else - SERVER_CONFIG.appRootPath = SERVER_CONFIG.serverRootPath .. "/apps/admin" -end'; - content_by_lua_file '_GBC_CORE_ROOT_/src/HttpBootstrap.lua'; - } + server { + listen 8088 so_keepalive=on; location = /nginx_status { stub_status; @@ -104,5 +63,10 @@ end'; allow 127.0.0.1; deny all; } + + # apps + # DO NOT MODIFY BELOW LINES + #_INCLUDE_APPS_ENTRY_ } + } diff --git a/conf/nginx.conf.optimized b/conf/nginx.conf.optimized deleted file mode 100644 index 2ace1fe..0000000 --- a/conf/nginx.conf.optimized +++ /dev/null @@ -1,112 +0,0 @@ - -worker_processes 4; -error_log logs/error.log; - -worker_rlimit_nofile 65535; - -events { - worker_connections 12000; - accept_mutex_delay 100ms; -} - -http { - include '_GBC_CORE_ROOT_/bin/openresty/nginx/conf/mime.types'; - - # optimize - #keepalive_timeout 60; - #keepalive_requests 10000; - - open_file_cache max=65535 inactive=30s; - open_file_cache_min_uses 2; - open_file_cache_valid 60s; - - # tmp - client_body_temp_path _GBC_CORE_ROOT_/tmp/client_body_temp; - fastcgi_temp_path _GBC_CORE_ROOT_/tmp/fastcgi_temp; - proxy_temp_path _GBC_CORE_ROOT_/tmp/proxy_temp; - scgi_temp_path _GBC_CORE_ROOT_/tmp/scgi_temp; - uwsgi_temp_path _GBC_CORE_ROOT_/tmp/uwsgi_temp; - - # security - client_max_body_size 32k; - server_tokens off; - client_body_buffer_size 16K; - client_header_buffer_size 1k; - large_client_header_buffers 2 1k; - autoindex off; - ssi off; - - # lua - lua_package_path '_GBC_CORE_ROOT_/src/?.lua;_GBC_CORE_ROOT_/src/lib/?.lua;;'; - lua_shared_dict INDEXES 512k; - lua_code_cache on; - - init_by_lua ' -SERVER_CONFIG = loadfile("_GBC_CORE_ROOT_/conf/config.lua")() -DEBUG = _DBG_ERROR -require("framework.init") -'; - - server { - listen 8088; #so_keepalive=on; - - # user app - location ~ /api { - content_by_lua_file '_GBC_CORE_ROOT_/src/HttpBootstrap.lua'; - } - - location = /socket { - lua_socket_log_errors off; - content_by_lua_file '_GBC_CORE_ROOT_/src/WebSocketBootstrap.lua'; - } - - # GameBox Cloud welcome - location / { - access_by_lua ' -if not SERVER_CONFIG.welcomeEnabled then - ngx.exit(403) -end'; - root _GBC_CORE_ROOT_/apps/welcome/public_html; - index index.html; - } - - location ~ /welcome_api { - access_by_lua ' -if not SERVER_CONFIG.welcomeEnabled then - ngx.exit(403) -else - SERVER_CONFIG.appRootPath = SERVER_CONFIG.serverRootPath .. "/apps/welcome" -end'; - content_by_lua_file '_GBC_CORE_ROOT_/src/HttpBootstrap.lua'; - } - - location = /welcome_socket { - lua_socket_log_errors off; - access_by_lua ' -if not SERVER_CONFIG.welcomeEnabled then - ngx.exit(403) -else - SERVER_CONFIG.appRootPath = SERVER_CONFIG.serverRootPath .. "/apps/welcome" -end'; - content_by_lua_file '_GBC_CORE_ROOT_/src/WebSocketBootstrap.lua'; - } - - # GameBox Cloud admin api - location ~ /admin { - access_by_lua ' -if not SERVER_CONFIG.adminEnabled then - ngx.exit(403) -else - SERVER_CONFIG.appRootPath = SERVER_CONFIG.serverRootPath .. "/apps/admin" -end'; - content_by_lua_file '_GBC_CORE_ROOT_/src/HttpBootstrap.lua'; - } - - location = /nginx_status { - stub_status; - access_log off; - allow 127.0.0.1; - deny all; - } - } -} diff --git a/conf/redis.conf b/conf/redis.conf index c01d964..8d434f9 100644 --- a/conf/redis.conf +++ b/conf/redis.conf @@ -14,7 +14,8 @@ # By default Redis does not run as a daemon. Use 'yes' if you need it. # Note that Redis will write a pid file in /var/run/redis.pid when daemonized. -daemonize yes +daemonize no +# control by supervisord # When running daemonized, Redis writes a pid file in /var/run/redis.pid by # default. You can specify a custom pid file location here. @@ -22,13 +23,12 @@ pidfile _GBC_CORE_ROOT_/tmp/redis.pid # Accept connections on the specified port, default is 6379. # If port 0 is specified Redis will not listen on a TCP socket. -# port 6379 port 0 # If you want you can bind a single interface, if the bind option is not # specified all the interfaces will listen for incoming connections. # -# bind 127.0.0.1 +bind 127.0.0.1 # Specify the path for the unix socket that will be used to listen for # incoming connections. There is no default, so Redis will not listen diff --git a/conf/supervisord.conf b/conf/supervisord.conf index ff81eea..a47bfb7 100644 --- a/conf/supervisord.conf +++ b/conf/supervisord.conf @@ -9,23 +9,23 @@ ; - Comments must have a leading space: "a=b ;comment" not "a=b;comment". [unix_http_server] -file=/tmp/supervisor.sock ; (the path to the socket file) +file=_GBC_CORE_ROOT_/tmp/supervisor.sock ; (the path to the socket file) ;chmod=0700 ; socket file mode (default 0700) ;chown=nobody:nogroup ; socket file uid:gid owner -username=user ; (default is no username (open server)) -password=123 ; (default is no password (open server)) +;username=user ; (default is no username (open server)) +;password=123 ; (default is no password (open server)) -[inet_http_server] ; inet (TCP) server disabled by default -port=127.0.0.1:9001 ; (ip_address:port specifier, *:port for all iface) -username=user ; (default is no username (open server)) -password=123 ; (default is no password (open server)) +;[inet_http_server] ; inet (TCP) server disabled by default +;port=127.0.0.1:9001 ; (ip_address:port specifier, *:port for all iface) +;username=user ; (default is no username (open server)) +;password=123 ; (default is no password (open server)) [supervisord] -logfile=/opt/qs/logs/supervisord/supervisord.log ; (main log file;default $CWD/supervisord.log) -logfile_maxbytes=10MB ; (max main logfile bytes b4 rotation;default 50MB) +logfile=_GBC_CORE_ROOT_/logs/supervisord.log ; (main log file;default $CWD/supervisord.log) +logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) logfile_backups=10 ; (num of main logfile rotation backups;default 10) -loglevel=info ; (log level;default info; others: debug,warn,trace) -pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid) +loglevel=warn ; (log level;default info; others: debug,warn,trace) +pidfile=_GBC_CORE_ROOT_/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid) nodaemon=false ; (start in foreground if true;default false) minfds=1024 ; (min. avail startup file descriptors;default 1024) minprocs=200 ; (min. avail process descriptors;default 200) @@ -45,18 +45,111 @@ minprocs=200 ; (min. avail process descriptors;default 200) supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] -;serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket -serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket -username=user ; should be same as http_username if set -password=123 ; should be same as http_password if set -prompt=GameBoxCloud ; cmd line prompt (default "supervisor") +serverurl=unix://_GBC_CORE_ROOT_/tmp/supervisor.sock ; use a unix:// URL for a unix socket +;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket +;username=chris ; should be same as http_username if set +;password=123 ; should be same as http_password if set +;prompt=mysupervisor ; cmd line prompt (default "supervisor") ;history_file=~/.sc_history ; use readline history if available +; The below sample program section shows all possible program subsection values, +; create one or more 'real' program: sections to be able to control them under +; supervisor. + + +;[program:theprogramname] +;command=/bin/cat ; the program (relative uses PATH, can take args) +;process_name=%(program_name)s ; process_name expr (default %(program_name)s) +;numprocs=1 ; number of processes copies to start (def 1) +;directory=/tmp ; directory to cwd to before exec (def no cwd) +;umask=022 ; umask for process (default None) +;priority=999 ; the relative start priority (default 999) +;autostart=true ; start at supervisord start (default: true) +;autorestart=unexpected ; whether/when to restart (default: unexpected) +;startsecs=1 ; number of secs prog must stay running (def. 1) +;startretries=3 ; max # of serial start failures (default 3) +;exitcodes=0,2 ; 'expected' exit codes for process (default 0,2) +;stopsignal=QUIT ; signal used to kill process (default TERM) +;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10) +;stopasgroup=false ; send stop signal to the UNIX process group (default false) +;killasgroup=false ; SIGKILL the UNIX process group (def false) +;user=chrism ; setuid to this UNIX account to run the program +;redirect_stderr=true ; redirect proc stderr to stdout (default false) +;stdout_logfile=/a/path ; stdout log path, NONE for none; default AUTO +;stdout_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) +;stdout_logfile_backups=10 ; # of stdout logfile backups (default 10) +;stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0) +;stdout_events_enabled=false ; emit events on stdout writes (default false) +;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO +;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) +;stderr_logfile_backups=10 ; # of stderr logfile backups (default 10) +;stderr_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0) +;stderr_events_enabled=false ; emit events on stderr writes (default false) +;environment=A="1",B="2" ; process environment additions (def no adds) +;serverurl=AUTO ; override serverurl computation (childutils) + +; The below sample eventlistener section shows all possible +; eventlistener subsection values, create one or more 'real' +; eventlistener: sections to be able to handle event notifications +; sent by supervisor. + +;[eventlistener:theeventlistenername] +;command=/bin/eventlistener ; the program (relative uses PATH, can take args) +;process_name=%(program_name)s ; process_name expr (default %(program_name)s) +;numprocs=1 ; number of processes copies to start (def 1) +;events=EVENT ; event notif. types to subscribe to (req'd) +;buffer_size=10 ; event buffer queue size (default 10) +;directory=/tmp ; directory to cwd to before exec (def no cwd) +;umask=022 ; umask for process (default None) +;priority=-1 ; the relative start priority (default -1) +;autostart=true ; start at supervisord start (default: true) +;autorestart=unexpected ; whether/when to restart (default: unexpected) +;startsecs=1 ; number of secs prog must stay running (def. 1) +;startretries=3 ; max # of serial start failures (default 3) +;exitcodes=0,2 ; 'expected' exit codes for process (default 0,2) +;stopsignal=QUIT ; signal used to kill process (default TERM) +;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10) +;stopasgroup=false ; send stop signal to the UNIX process group (default false) +;killasgroup=false ; SIGKILL the UNIX process group (def false) +;user=chrism ; setuid to this UNIX account to run the program +;redirect_stderr=true ; redirect proc stderr to stdout (default false) +;stdout_logfile=/a/path ; stdout log path, NONE for none; default AUTO +;stdout_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) +;stdout_logfile_backups=10 ; # of stdout logfile backups (default 10) +;stdout_events_enabled=false ; emit events on stdout writes (default false) +;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO +;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) +;stderr_logfile_backups ; # of stderr logfile backups (default 10) +;stderr_events_enabled=false ; emit events on stderr writes (default false) +;environment=A="1",B="2" ; process environment additions +;serverurl=AUTO ; override serverurl computation (childutils) + +; The below sample group section shows all possible group values, +; create one or more 'real' group: sections to create "heterogeneous" +; process groups. + +;[group:thegroupname] +;programs=progname1,progname2 ; each refers to 'x' in [program:x] definitions +;priority=999 ; the relative start priority (default 999) + ; The [include] section can just contain the "files" setting. This ; setting can list multiple files (separated by whitespace or ; newlines). It can also contain wildcards. The filenames are ; interpreted as relative to this file. Included files *cannot* ; include files themselves. -[include] -files = /etc/supervisord.conf.d/*.conf +;[include] +;files = relative/directory/*.ini + + +[program:beanstalkd] +command=_GBC_CORE_ROOT_/bin/beanstalkd/bin/beanstalkd -l 127.0.0.1 -p _BEANSTALKD_PORT_ -b _GBC_CORE_ROOT_/db + +[program:redis] +command=_GBC_CORE_ROOT_/bin/redis/bin/redis-server _GBC_CORE_ROOT_/tmp/redis.conf + +[program:nginx] +command=_GBC_CORE_ROOT_/bin/openresty/nginx/sbin/nginx -c _GBC_CORE_ROOT_/tmp/nginx.conf + +;all workers +;_WORKERS_ diff --git a/conf/supervisord.conf.d/supervisord_beans.conf b/conf/supervisord.conf.d/supervisord_beans.conf deleted file mode 100644 index 75c22d7..0000000 --- a/conf/supervisord.conf.d/supervisord_beans.conf +++ /dev/null @@ -1,11 +0,0 @@ -[program:Beanstalkd] -command=/opt/qs/beanstalkd/bin/beanstalkd -process_name=Beanstalkd -priority=300 -autostart=false -autorestart=true -startretries=10 -redirect_stderr=true -stdout_logfile=/opt/qs/logs/beanstalkd.log -stdout_logfile_maxbytes=10MB -stdout_logfile_backups=10 diff --git a/conf/supervisord.conf.d/supervisord_nginx.conf b/conf/supervisord.conf.d/supervisord_nginx.conf deleted file mode 100644 index 99bec1d..0000000 --- a/conf/supervisord.conf.d/supervisord_nginx.conf +++ /dev/null @@ -1,9 +0,0 @@ -[program:Nginx] -command=nginx -p /opt/qs -c /opt/qs/openresty/nginx/conf/nginx.conf -process_name=Nginx -priority=100 -autostart=false -autorestart=true -startretries=10 -startsecs=0 -stdout_logfile=none diff --git a/conf/supervisord.conf.d/supervisord_redis.conf b/conf/supervisord.conf.d/supervisord_redis.conf deleted file mode 100644 index e303daa..0000000 --- a/conf/supervisord.conf.d/supervisord_redis.conf +++ /dev/null @@ -1,12 +0,0 @@ -[program:Redis] -command=/opt/qs/redis/bin/redis-server /opt/qs/conf/redis.conf -process_name=Redis -priority=200 -autostart=false -autorestart=true -startretries=10 -startsecs=0 -;redirect_stderr=true -stdout_logfile=none -;stdout_logfile_maxbytes=10MB -;stdout_logfile_backups=10 diff --git a/dists/beanstalkd-1.10.tar.gz b/dists/beanstalkd-1.10.tar.gz new file mode 100644 index 0000000..d855daa Binary files /dev/null and b/dists/beanstalkd-1.10.tar.gz differ diff --git a/dists/lua-process-1.5.0.tar.gz b/dists/lua-process-1.5.0.tar.gz new file mode 100644 index 0000000..467e46c Binary files /dev/null and b/dists/lua-process-1.5.0.tar.gz differ diff --git a/dists/luabson-20151114.tar.gz b/dists/luabson-20151114.tar.gz new file mode 100644 index 0000000..8f318f5 Binary files /dev/null and b/dists/luabson-20151114.tar.gz differ diff --git a/dists/luapbc-20150714.tar.gz b/dists/luapbc-20150714.tar.gz new file mode 100644 index 0000000..7dfc447 Binary files /dev/null and b/dists/luapbc-20150714.tar.gz differ diff --git a/installation/luasocket-3.0-rc1.tar.gz b/dists/luasocket-3.0-rc1.tar.gz similarity index 100% rename from installation/luasocket-3.0-rc1.tar.gz rename to dists/luasocket-3.0-rc1.tar.gz diff --git a/dists/openresty-1.9.7.3.tar.gz b/dists/openresty-1.9.7.3.tar.gz new file mode 100644 index 0000000..78c75a7 Binary files /dev/null and b/dists/openresty-1.9.7.3.tar.gz differ diff --git a/dists/redis-3.0.7.tar.gz b/dists/redis-3.0.7.tar.gz new file mode 100644 index 0000000..e654c84 Binary files /dev/null and b/dists/redis-3.0.7.tar.gz differ diff --git a/dists/supervisor-3.2.2.tar.gz b/dists/supervisor-3.2.2.tar.gz new file mode 100644 index 0000000..6827964 Binary files /dev/null and b/dists/supervisor-3.2.2.tar.gz differ diff --git a/dists/virtualenv-15.0.0.tar.gz b/dists/virtualenv-15.0.0.tar.gz new file mode 100644 index 0000000..9cb824f Binary files /dev/null and b/dists/virtualenv-15.0.0.tar.gz differ diff --git a/install.sh b/install.sh deleted file mode 100755 index 36960cd..0000000 --- a/install.sh +++ /dev/null @@ -1,318 +0,0 @@ -#!/bin/bash - -function showHelp() -{ - echo "Usage: [sudo] ./install.sh [--prefix=absolute_path] [OPTIONS]" - echo "Options:" - echo -e "\t -a | --all \t\t install nginx(openresty) and GameBox Cloud Core, redis and beanstalkd" - echo -e "\t -n | --nginx \t\t install nginx(openresty) and GameBox Cloud Core" - echo -e "\t -r | --redis \t\t install redis" - echo -e "\t -b | --beanstalkd \t install beanstalkd" - echo -e "\t -h | --help \t\t show this help" - echo "if the option is not specified, default option is \"--all(-a)\"." - echo "if the \"--prefix\" is not specified, default path is \"/opt/gbc_core\"." -} - -function checkOSType() -{ - type "apt-get" > /dev/null 2> /dev/null - if [ $? -eq 0 ]; then - echo "UBUNTU" - exit 0 - fi - - type "yum" > /dev/null 2> /dev/null - if [ $? -eq 0 ]; then - echo "CENTOS" - exit 0 - fi - - RES=$(uname -s) - if [ $RES == "Darwin" ]; then - echo "MACOS" - exit 0 - fi - - echo "UNKNOW" - exit 1 -} - -if [ $UID -ne 0 ]; then - echo "Superuser privileges are required to run this script." - echo "e.g. \"sudo $0\"" - exit 1 -fi - -OSTYPE=$(checkOSType) -CUR_DIR=$(cd "$(dirname $0)" && pwd) -BUILD_DIR=/tmp/install_gbc_core -DEST_DIR=/opt/gbc_core - -declare -i ALL=0 -declare -i BEANS=0 -declare -i NGINX=0 -declare -i REDIS=0 - -OPENRESTY_VER=1.7.7.1 -LUASOCKET_VER=3.0-rc1 -LUASEC_VER=0.5 -REDIS_VAR=2.6.16 -BEANSTALKD_VER=1.9 - -if [ $OSTYPE == "MACOS" ]; then - gcc -o $CUR_DIR/shells/getopt_long $CUR_DIR/shells/src/getopt_long.c - ARGS=$($CUR_DIR/shells/getopt_long "$@") -else - ARGS=$(getopt -o abrnh --long all,nginx,redis,beanstalkd,help,prefix: -n 'Install GameBox Cloud Core' -- "$@") -fi - -if [ $? != 0 ] ; then - echo "Install GameBox Cloud Core Terminating..." >&2; - exit 1; -fi - -eval set -- "$ARGS" - -if [ $# -eq 1 ] ; then - ALL=1 -fi - -if [ $# -eq 3 ] && [ $1 == "--prefix" ] ; then - ALL=1 -fi - -while true ; do - case "$1" in - --prefix) - DEST_DIR=$2 - shift 2 - ;; - - -a|--all) - ALL=1 - shift - ;; - - -b|--beanstalkd) - BEANS=1 - shift - ;; - - -r|--redis) - REDIS=1 - shift - ;; - - -n|--nginx) - NGINX=1 - shift - ;; - - -h|--help) - showHelp; - exit 0 - ;; - - --) - shift; - break - ;; - - *) - echo "invalid option: $1" - exit 1 - ;; - esac -done - -DEST_BIN_DIR=$DEST_DIR/bin - -if [ $OSTYPE == "UBUNTU" ] ; then - apt-get install -y build-essential libpcre3-dev libssl-dev git-core unzip -elif [ $OSTYPE == "CENTOS" ]; then - yum groupinstall -y "Development Tools" - yum install -y pcre-devel zlib-devel openssl-devel unzip -elif [ $OSTYPE == "MACOS" ]; then - type "brew" > /dev/null 2> /dev/null - if [ $? -ne 0 ]; then - echo "pleas install brew, with this command:" - echo -e "\033[33mruby -e \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\" \033[0m" - exit 0 - else - su $(users) -c "brew install pcre" - fi - - type "gcc" > /dev/null 2> /dev/null - if [ $? -ne 0 ]; then - echo "Please install xcode." - exit 0 - fi -else - echo "Unsupport current OS." - exit 1 -fi - -if [ $OSTYPE == "MACOS" ]; then - SED_BIN='sed -i --' -else - SED_BIN='sed -i' -fi - -set -e - -rm -rf $BUILD_DIR -mkdir -p $BUILD_DIR -cp -f $CUR_DIR/installation/*.tar.gz $BUILD_DIR - -mkdir -p $DEST_DIR -mkdir -p $DEST_BIN_DIR - -mkdir -p $DEST_DIR/logs -mkdir -p $DEST_DIR/tmp -mkdir -p $DEST_DIR/conf -mkdir -p $DEST_DIR/db - -# install nginx and GameBox Cloud Core -if [ $ALL -eq 1 ] || [ $NGINX -eq 1 ] ; then - cd $BUILD_DIR - tar zxf ngx_openresty-$OPENRESTY_VER.tar.gz - cd ngx_openresty-$OPENRESTY_VER - mkdir -p $DEST_BIN_DIR/openresty - - # install openresty - ./configure \ - --prefix=$DEST_BIN_DIR/openresty \ - --with-luajit \ - --with-http_stub_status_module \ - --with-cc-opt="-I/usr/local/include" \ - --with-ld-opt="-L/usr/local/lib" - make - make install - - # install GameBox Cloud Core source and tools - ln -f -s $DEST_BIN_DIR/openresty/luajit/bin/luajit-2.1.0-alpha $DEST_BIN_DIR/openresty/luajit/bin/lua - cp -rf $CUR_DIR/src $DEST_DIR - cp -rf $CUR_DIR/apps $DEST_DIR - mkdir -p $DEST_BIN_DIR/instrument - cp -rf $CUR_DIR/instrument $DEST_BIN_DIR - - # deploy tool script - cd $CUR_DIR/shells/ - cp -f start_server stop_server check_server $DEST_DIR - mkdir -p $DEST_DIR/apps/welcome/tools/actions - mkdir -p $DEST_DIR/apps/welcome/workers/actions - cp -f tools.sh $DEST_DIR/apps/welcome/. - ln -f -s $DEST_BIN_DIR/openresty/nginx/sbin/nginx /usr/bin/nginx - # if it in Mac OS X, getopt_long should be deployed. - if [ $OSTYPE == "MACOS" ]; then - cp -f $CUR_DIR/shells/getopt_long $DEST_DIR/tmp - fi - - # copy nginx and config.lua file - cp -f $CUR_DIR/conf/nginx.conf $DEST_BIN_DIR/openresty/nginx/conf/. - $SED_BIN "s#_GBC_CORE_ROOT_#$DEST_DIR#g" $DEST_BIN_DIR/openresty/nginx/conf/nginx.conf - rm -f $DEST_BIN_DIR/openresty/nginx/conf/nginx.conf-- - - cp -f $CUR_DIR/conf/config.lua $DEST_DIR/conf - $SED_BIN "s#_GBC_CORE_ROOT_#$DEST_DIR#g" $DEST_DIR/conf/config.lua - rm -f $DEST_DIR/conf/config.lua-- - - # modify tools path - $SED_BIN "s#_GBC_CORE_ROOT_#$DEST_DIR#g" $DEST_DIR/apps/welcome/tools.sh - $SED_BIN "s#_GBC_CORE_ROOT_#$DEST_DIR#g" $DEST_BIN_DIR/instrument/start_workers.sh - $SED_BIN "s#_GBC_CORE_ROOT_#$DEST_DIR#g" $DEST_BIN_DIR/instrument/Monitor.lua - $SED_BIN "s#_GBC_CORE_ROOT_#$DEST_DIR#g" $DEST_BIN_DIR/instrument/monitor.sh - rm -f $DEST_DIR/apps/welcome/tools.sh-- - rm -f $DEST_BIN_DIR/instrument/start_workers.sh-- - rm -f $DEST_BIN_DIR/instrument/Monitor.lua-- - rm -f $DEST_BIN_DIR/instrument/monitor.sh-- - - # install luasocket - cd $BUILD_DIR - tar zxf luasocket-$LUASOCKET_VER.tar.gz - cd luasocket-$LUASOCKET_VER - if [ $OSTYPE == "MACOS" ]; then - $SED_BIN "s#PLAT?= linux#PLAT?= macosx#g" makefile - $SED_BIN "s#PLAT?=linux#PLAT?=macosx#g" src/makefile - $SED_BIN "s#LUAPREFIX_macosx?=/opt/local#LUAPREFIX_macosx?=$DEST_BIN_DIR/openresty/luajit#g" src/makefile - $SED_BIN "s#LUAINC_macosx_base?=/opt/local/include#LUAINC_macosx_base?=$DEST_BIN_DIR/openresty/luajit/include#g" src/makefile - $SED_BIN "s#\$(LUAINC_macosx_base)/lua/\$(LUAV)#\$(LUAINC_macosx_base)/luajit-2.1#g" src/makefile - else - $SED_BIN "s#LUAPREFIX_linux?=/usr/local#LUAPREFIX_linux?=$DEST_BIN_DIR/openresty/luajit#g" src/makefile - $SED_BIN "s#LUAINC_linux_base?=/usr/include#LUAINC_linux_base?=$DEST_BIN_DIR/openresty/luajit/include#g" src/makefile - $SED_BIN "s#\$(LUAINC_linux_base)/lua/\$(LUAV)#\$(LUAINC_linux_base)/luajit-2.1#g" src/makefile - fi - make clean && make && make install - cp -f src/serial.so src/unix.so $DEST_BIN_DIR/openresty/luajit/lib/lua/5.1/socket/. - - # install luasec - cd $BUILD_DIR - tar zxf luasec-$LUASEC_VER.tar.gz - cd luasec-$LUASEC_VER - $SED_BIN "s#/usr/share/lua/5.1#$DEST_BIN_DIR/openresty/luajit/share/lua/5.1#g" ./Makefile - $SED_BIN "s#/usr/lib/lua/5.1#$DEST_BIN_DIR/openresty/luajit/lib/lua/5.1#g" ./Makefile - if [ $OSTYPE != "MACOS" ]; then - make clean && make linux && make install - fi - - # install cjson - cp -f $DEST_BIN_DIR/openresty/lualib/cjson.so $DEST_BIN_DIR/openresty/luajit/lib/lua/5.1/. - - # install http client - cd $BUILD_DIR - tar zxf luahttpclient.tar.gz - cp -f httpclient.lua $DEST_BIN_DIR/openresty/luajit/share/lua/5.1/. - cp -rf httpclient $DEST_BIN_DIR/openresty/luajit/share/lua/5.1/. - - # install inspect - cd $BUILD_DIR - tar zxf luainspect.tar.gz - cp -f inspect.lua $DEST_BIN_DIR/openresty/luajit/share/lua/5.1/. - - echo "Install Openresty and GameBox Cloud Core DONE" -fi - -#install redis -if [ $ALL -eq 1 ] || [ $REDIS -eq 1 ] ; then - cd $BUILD_DIR - tar zxf redis-$REDIS_VAR.tar.gz - cd redis-$REDIS_VAR - mkdir -p $DEST_BIN_DIR/redis/bin - - make - cp src/redis-server $DEST_BIN_DIR/redis/bin - cp src/redis-cli $DEST_BIN_DIR/redis/bin - cp src/redis-sentinel $DEST_BIN_DIR/redis/bin - cp src/redis-benchmark $DEST_BIN_DIR/redis/bin - cp src/redis-check-aof $DEST_BIN_DIR/redis/bin - cp src/redis-check-dump $DEST_BIN_DIR/redis/bin - - mkdir -p $DEST_BIN_DIR/redis/conf - cp -f $CUR_DIR/conf/redis.conf $DEST_BIN_DIR/redis/conf/. - $SED_BIN "s#_GBC_CORE_ROOT_#$DEST_DIR#g" $DEST_BIN_DIR/redis/conf/redis.conf - rm -f $DEST_BIN_DIR/redis/conf/redis.conf-- - - echo "Install Redis DONE" -fi - -# install beanstalkd -if [ $ALL -eq 1 ] || [ $BEANS -eq 1 ] ; then - cd $BUILD_DIR - tar zxf beanstalkd-$BEANSTALKD_VER.tar.gz - cd beanstalkd-$BEANSTALKD_VER - mkdir -p $DEST_BIN_DIR/beanstalkd/bin - - make - cp beanstalkd $DEST_BIN_DIR/beanstalkd/bin - - echo "Install Beanstalkd DONE" -fi - -# done - -echo "" -echo "" -echo "" -echo "DONE!" -echo "" -echo "" diff --git a/installation/README.md b/installation/README.md deleted file mode 100644 index fb29900..0000000 --- a/installation/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Packages of GameBox Cloud Core - -- Redis - - redis-2.6.16.tar.gz - -- beanstalkd - - beanstalkd-1.9.tar.gz - -- openresty - - ngx\_openresty-1.7.7.1.tar.gz - -- luahttpclient - - luahttpclient.tar.gz - -- luainspect - - luainspect.tar.gz - -- luasec - - luasec-0.5.tar.gz - -- luasocket - - luasocket-3.0-rc1.tar.gz diff --git a/installation/beanstalkd-1.9.tar.gz b/installation/beanstalkd-1.9.tar.gz deleted file mode 100644 index 7e0dd0a..0000000 Binary files a/installation/beanstalkd-1.9.tar.gz and /dev/null differ diff --git a/installation/luahttpclient.tar.gz b/installation/luahttpclient.tar.gz deleted file mode 100644 index c9045b5..0000000 Binary files a/installation/luahttpclient.tar.gz and /dev/null differ diff --git a/installation/luainspect.tar.gz b/installation/luainspect.tar.gz deleted file mode 100644 index 4196d50..0000000 Binary files a/installation/luainspect.tar.gz and /dev/null differ diff --git a/installation/luasec-0.5.tar.gz b/installation/luasec-0.5.tar.gz deleted file mode 100644 index a621cdd..0000000 Binary files a/installation/luasec-0.5.tar.gz and /dev/null differ diff --git a/installation/ngx_openresty-1.7.7.1.tar.gz b/installation/ngx_openresty-1.7.7.1.tar.gz deleted file mode 100644 index d282bb2..0000000 Binary files a/installation/ngx_openresty-1.7.7.1.tar.gz and /dev/null differ diff --git a/installation/redis-2.6.16.tar.gz b/installation/redis-2.6.16.tar.gz deleted file mode 100644 index a615cdc..0000000 Binary files a/installation/redis-2.6.16.tar.gz and /dev/null differ diff --git a/instrument/Monitor.lua b/instrument/Monitor.lua deleted file mode 100644 index 53831b5..0000000 --- a/instrument/Monitor.lua +++ /dev/null @@ -1,422 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local pcall = pcall -local tostring = tostring -local tonumber = tonumber -local json_decode = json.decode -local string_format = string.format -local string_find = string.find -local string_sub = string.sub -local string_upper = string.upper -local string_split = string.split -local string_match = string.match -local table_insert = table.insert -local table_concat = table.concat -local math_trunc = math.trunc -local io_popen = io.popen -local os_execute = os.execute -local os_time = os.time - -local _RESET_REDIS_CMD = [[_GBC_CORE_ROOT_/bin/redis/bin/redis-server _GBC_CORE_ROOT_/bin/redis/conf/redis.conf]] -local _RESET_NGINX_CMD = [[nginx -p _GBC_CORE_ROOT_ -c _GBC_CORE_ROOT_/bin/openresty/nginx/conf/nginx.conf]] -local _RESET_BEANSTALKD_CMD = [[_GBC_CORE_ROOT_/bin/beanstalkd/bin/beanstalkd > _GBC_CORE_ROOT_/logs/beanstalkd.log &]] - -local _GET_MEM_INFO_CMD = [[cat /proc/meminfo | grep -E "Mem(Free|Total)"]] -local _GET_DISK_INFO_CMD = [[df --total -k | grep "total"]] -local _GET_CPU_INFO_CMD = [[lscpu]] - -local _GET_PID_PATTERN = "pgrep %s" -local _GET_PERFORMANCE_PATTERN = [[top -b -n 1 -p%s]] - -local _MONITOR_PROC_DICT_KEY = "_MONITOR_PROC_DICT" -local _MONITOR_LIST_PATTERN = "_MONITOR_%s_%s_LIST" -local _MONITOR_MEM_INFO_KEY = "_MONITOR_MEM_INFO" -local _MONITOR_CPU_INFO_KEY = "_MONITOR_CPU_INFO" -local _MONITOR_DISK_INFO_KEY = "_MONITOR_DISK_INFO" -local _JOB_HASH = "_JOB_HASH" - --- since this tool is running background as a loop, --- redis connection don't need closing. -local RedisService = cc.load("redis").service - -local BeansService = cc.load("beanstalkd").service - -local JobService = cc.load("job").service - -local httpClient = require("httpclient").new() - -local socket = require("socket") - -local Monitor = class("Monitor") - -function Monitor:ctor(config) - self._config = config - self._process = config.monitor.process - self._interval = config.monitor.interval - self._procData = {} - self._memThreshold = config.mem - self._cpuThreshold = config.cpu - - self._minuteListLen = 0 - self._secListLen = 0 - self._hourListLen = 0 - - self._jobTube = config.beanstalkd.jobTube -end - -function Monitor:watch(arg) - local sock = require("socket") - local elapseSec = 0 - local elapseMin = 0 - local interval = self._interval - - self:_initList() - - self:_getCpuInfo() - self:_getMemInfo() - self:_getDiskInfo() - - while true do - local timer1 = socket.gettime() - self:_getPid() - self:_getPerfomance() - - self:_save(math_trunc(elapseSec/60), math_trunc(elapseMin/60)) - local timer2 = socket.gettime() - - local dTime = timer2 - timer1 - if interval - dTime > 0 then - sock.sleep(interval-dTime) - end - - if elapseSec >= 60 then - elapseSec = elapseSec % 60 - end - if elapseMin >= 60 then - elapseMin = elapseMin % 60 - end - elapseSec = elapseSec + interval - elapseMin = elapseMin + (math_trunc(elapseSec / 60)) - end -end - -function Monitor:_initList() - self:_getPid() - - local pipe = self:_getRedis():newPipeline() - for k, _ in pairs(self._procData) do - pipe:command("DEL", string_format(_MONITOR_LIST_PATTERN, k, "SEC")) - pipe:command("DEL", string_format(_MONITOR_LIST_PATTERN, k, "MINUTE")) - pipe:command("DEL", string_format(_MONITOR_LIST_PATTERN, k, "HOUR")) - end - pipe:command("DEL", _JOB_HASH) - pipe:commit() -end - -function Monitor:_getCpuInfo() - local fout = io_popen(_GET_CPU_INFO_CMD) - local cores = string_match(fout:read("*a"), "CPU%(s%):%s+(%d+)") - fout:close() - - local redis = self:_getRedis() - redis:command("SET", _MONITOR_CPU_INFO_KEY, cores) -end - -function Monitor:_getMemInfo() - local fout = io_popen(_GET_MEM_INFO_CMD) - local total, free = string_match(fout:read("*a"), "MemTotal:%s+(%d+).*MemFree:%s+(%d+)") - fout:close() - - local redis = self:_getRedis() - redis:command("SET", _MONITOR_MEM_INFO_KEY, total .. "|" .. free) -end - -function Monitor:_getDiskInfo() - local fout = io_popen(_GET_DISK_INFO_CMD) - local total, free = string_match(fout:read("*a"), "total%s+(%d+)%s+%d+%s+(%d+).*") - fout:close() - - local redis = self:_getRedis() - redis:command("SET", _MONITOR_DISK_INFO_KEY, total .. "|" .. free) -end - -function Monitor:_save(isUpdateMinList, isUpdateHourList) - local maxSecLen = 60 / self._interval - - local pipe = self:_getRedis():newPipeline() - for k, v in pairs(self._procData) do - local secListLen = self._secListLen - local minuteListLen = self._minuteListLen - local hourListLen = self._hourListLen - local data = v.cpu .. "|" .. v.mem .. "|" .. v.conn - - local list = string_format(_MONITOR_LIST_PATTERN, k, "SEC") - pipe:command("RPUSH", list, data) - if secListLen > maxSecLen then - pipe:command("LPOP", list) - end - - if isUpdateMinList ~= 0 then - list = string_format(_MONITOR_LIST_PATTERN, k, "MINUTE") - pipe:command("RPUSH", list, data) - if minuteListLen > 60 then - pipe:command("LPOP", list) - end - end - - if isUpdateHourList ~= 0 then - list = string_format(_MONITOR_LIST_PATTERN, k, "HOUR") - pipe:command("RPUSH", list, data) - if hourListLen > 24 then - pipe:command("LPOP", list) - end - end - end - pipe:commit() - - if self._secListLen <= maxSecLen then - self._secListLen = self._secListLen + 1 - end - if isUpdateMinList ~= 0 and self._minuteListLen <= 60 then - self._minuteListLen = self._minuteListLen + 1 - end - if isUpdateHourList ~= 0 and self._hourListLen <= 24 then - self._hourListLen = self._hourListLen + 1 - end -end - -function Monitor:_getPerfomance() - -- get cpu%, mem via top - local pids = {} - for _, v in pairs (self._procData) do - table_insert(pids, v.pid) - end - local strPids = table_concat(pids, ",") - - local cmd = string_format(_GET_PERFORMANCE_PATTERN, strPids) - local fout = io_popen(cmd) - local topRes = string_split(fout:read("*a"), "\n") - fout:close() - - local filterRes = {} - for i = #topRes-#pids+1, #topRes do - local t = string_split(topRes[i], " ") - filterRes[t[1]] = {t[9], t[6]} - end - - for k, v in pairs(self._procData) do - local pid = v.pid - if filterRes[pid] then - v.cpu = filterRes[pid][1] - v.mem = filterRes[pid][2] - v.conn = self:_getConnNums(k) - else - v.cpu = "0.0" - v.mem = "0" - v.conn = "0" - end - end - - if DEBUG >= 1 then - for k, v in pairs(self._procData) do - printInfo("%s pid %s: cpu %s, mem %s", k, v.pid, v.cpu, v.mem) - if tonumber(v.cpu) > 100 then - printWarn("cpu usage %s of %s is large than 100", v.cpu, k) - end - end - end -end - -function Monitor:_getPid() - local process = self._process - local pipe = self:_getRedis():newPipeline() - pipe:command("DEL", _MONITOR_PROC_DICT_KEY) - for _, procName in ipairs(process) do - local cmd = string_format(_GET_PID_PATTERN, procName) - local fout = io_popen(cmd) - local res = fout:read("*a") - fout:close() - - local isBeansReseted = false - while res == "" do - res = self:_resetProcess(procName) - if procName == "beanstalkd" then isBeansReseted = true end - end - if isBeansReseted then - self:_recoverJobs() - end - - local pids = string_split(res, "\n") - for i, pid in ipairs(pids) do - local pName = string_upper(procName) - if procName == "nginx" then - if i == 1 then - pName = pName .. "_MASTER" - else - pName = pName .. "_WORKER_#" .. tostring(i-1) - end - end - - self._procData[pName] = {} - self._procData[pName].pid = pid - pipe:command("HSET", _MONITOR_PROC_DICT_KEY, pName, pid) - end - end - pipe:commit() -end - -function Monitor:_resetProcess(procName) - if procName == "nginx" then - os_execute(_RESET_NGINX_CMD) - end - - if procName == "redis-server" then - os_execute(_RESET_REDIS_CMD) - self._redis = nil - end - - if procName == "beanstalkd" then - os_execute(_RESET_BEANSTALKD_CMD) - self._beans = nil - end - - local cmd = string_format(_GET_PID_PATTERN, procName) - local fout = io_popen(cmd) - local res = fout:read("*a") - fout:close() - - return res -end - -function Monitor:_getConnNums(procName) - -- redis connections. - if string_find(procName, "REDIS%-SERVER") then - local redis = self:_getRedis() - res = redis:command("INFO") - return res.clients.connected_clients - end - - -- nginx connections - if string_find(procName, "NGINX_MASTER") then - local res = httpClient:get("http://localhost:" .. tostring(self._config.port) .. "/nginx_status") - if res.body then - return string_match(res.body, "connections: (%d+)") - else - printWarn("access nginx_status failed, err: %s", res.err) - return -1 - end - end - - -- beanstalkd jobs - if string_find(procName, "BEANSTALKD") then - local beans = self:_getBeans() - local ok, res = pcall(beans.command, beans, "stats_tube", self._config.beanstalkd.jobTube) - if not ok then - return "0" - else - local r1 = string_match(res, "current%-jobs%-ready: (%d+)") - local r2 = string_match(res, "current%-jobs%-reserved: (%d+)") - local r3 = string_match(res, "current%-jobs%-delayed: (%d+)") - - return tostring(r1 + r2 + r3) - end - end - - return 0 -end - -function Monitor:_recoverJobs() - local redis = self:_getRedis() - local jobService = JobService:create(self:_getRedis(), self:_getBeans(), self._config) - - local res, err = redis:command("HGETALL", _JOB_HASH) - if not res then - printWarn("recover jobs from db faild: %s", err) - return - end - - for k,v in pairs(res) do - local ok = redis:command("HDEL", _JOB_HASH, k) - if tostring(ok) == "1" then - local job, err = json_decode(v) - if job then - local now = os_time() - if job.joined_time + job.delay <= now then - job.delay = 0 - else - job.delay = job.joined_time + job.delay - now - end - local id - id, err = jobService:add(job.action, job.arg, job.delay, job.priority, job.ttr) - if id then - printInfo("recover job success, old job id: %s, new job id: %s", k, tostring(id)) - end - end - - if err then - printWarn("recover job failed: %s. job id: %s, contents: %s", err, k, v) - end - end - end - - printInfo("recover jobs finished.") -end - -function Monitor:_getBeans() - if not self._beans then - self._beans = self:_newBeans() - end - return self._beans -end - -function Monitor:_newBeans() - local beans = BeansService:create(self._config.beanstalkd) - local ok, err = beans:connect() - if err then - throw("connect internal beanstalkd failed, %s", err) - end - return beans -end - -function Monitor:_getRedis() - if not self._redis then - self._redis = self:_newRedis() - end - return self._redis -end - -function Monitor:_newRedis() - local redis = RedisService:create(self._config.redis) - local ok, err = redis:connect() - if err then - throw("connect internal redis failed, %s", err) - end - return redis -end - -local monitor = Monitor:create(SERVER_CONFIG) -monitor:watch() - -return Monitor diff --git a/instrument/README.md b/instrument/README.md deleted file mode 100644 index e4798a5..0000000 --- a/instrument/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# some instruments for GameBox Cloud Core. - -- monitor.sh - - start monitor process. - -- start\_workers.sh - - start job workers process. diff --git a/instrument/monitor.sh b/instrument/monitor.sh deleted file mode 100755 index 497da20..0000000 --- a/instrument/monitor.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -export LUA_PATH="_GBC_CORE_ROOT_/src/?.lua;_GBC_CORE_ROOT_/src/lib/?.lua;;" - -GBC_CORE_ROOT="_GBC_CORE_ROOT_" -LUABIN=bin/openresty/luajit/bin/lua -SCRIPT=bin/instrument/Monitor.lua - -cd $GBC_CORE_ROOT - -ENV="SERVER_CONFIG=loadfile([[_GBC_CORE_ROOT_/conf/config.lua]])(); DEBUG=_DBG_DEBUG; require([[framework.init]]);" - -$LUABIN -e "$ENV" $GBC_CORE_ROOT/$SCRIPT diff --git a/instrument/start_workers.sh b/instrument/start_workers.sh deleted file mode 100755 index 8eda0b2..0000000 --- a/instrument/start_workers.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -export LUA_PATH="_GBC_CORE_ROOT_/src/?.lua;_GBC_CORE_ROOT_/src/lib/?.lua;;" - -GBC_CORE_ROOT="_GBC_CORE_ROOT_" -LUABIN=bin/openresty/luajit/bin/lua -SCRIPT=WorkerBootstrap.lua - -cd $GBC_CORE_ROOT - -ENV="SERVER_CONFIG=loadfile([[_GBC_CORE_ROOT_/conf/config.lua]])(); DEBUG=_DBG_DEBUG; require([[framework.init]]); SERVER_CONFIG.appRootPath= SERVER_CONFIG.appRootPath;" - -# workers should be restarted by itself. -while true; do - $LUABIN -e "$ENV" $GBC_CORE_ROOT/src/$SCRIPT - if [ $? -ne 0 ]; then - exit $? - fi - sleep 1 -done diff --git a/make.sh b/make.sh new file mode 100755 index 0000000..7353a80 --- /dev/null +++ b/make.sh @@ -0,0 +1,397 @@ +#!/bin/bash + +# https://pypi.python.org/pypi/virtualenv +VIRTUALENV_VER=15.0.0 +# https://pypi.python.org/pypi/supervisor +SUPERVISOR_VER=3.2.2 +# http://openresty.org/ +OPENRESTY_VER=1.9.7.3 +# http://redis.io/ +REDIS_VER=3.0.7 +# http://kr.github.io/beanstalkd/ +BEANSTALKD_VER=1.10 +# https://github.com/diegonehab/luasocket +LUASOCKET_VER=3.0-rc1 +# https://github.com/cloudwu/lua-bson +LUABSON_VER=20151114 +# https://github.com/cloudwu/pbc +LUAPBC_VER=20150714 +# https://github.com/mah0x211/lua-process +LUAPROCESS_VER=1.5.0 + +function showHelp() +{ + echo "Usage: [sudo] ./make.sh [--prefix=absolute_path] [OPTIONS]" + echo "Options:" + echo -e "\t-h | --help\t\t show this help" + echo "if the \"--prefix\" is not specified, default path is \"/opt/gbc-core\"." +} + +function checkOSType() +{ + type "apt-get" > /dev/null 2> /dev/null + if [ $? -eq 0 ]; then + echo "UBUNTU" + exit 0 + fi + + type "yum" > /dev/null 2> /dev/null + if [ $? -eq 0 ]; then + echo "CENTOS" + exit 0 + fi + + RES=$(uname -s) + if [ $RES == "Darwin" ]; then + echo "MACOS" + exit 0 + fi + + echo "UNKNOW" + exit 1 +} + +# if [ $UID -ne 0 ]; then +# echo "Superuser privileges are required to run this script." +# echo "e.g. \"sudo $0\"" +# exit 1 +# fi + +OSTYPE=$(checkOSType) +SRC_DIR=$(cd "$(dirname $0)" && pwd) + +echo "SRC_DIR = $SRC_DIR" + +# default configs +DEST_DIR=$SRC_DIR + +if [ $OSTYPE == "MACOS" ]; then + type "gcc" > /dev/null 2> /dev/null + if [ $? -ne 0 ]; then + echo "Please install xcode." + exit 1 + fi + + gcc -o $SRC_DIR/bin/getopt_long $SRC_DIR/bin/getopt_long.c + ARGS=$($SRC_DIR/bin/getopt_long "$@") +else + ARGS=$(getopt -o h --long help,prefix: -n 'Install GameBox Cloud Core' -- "$@") +fi + +if [ $? -ne 0 ] ; then + echo "Install GameBox Cloud Core Terminating..." >&2; + exit 1; +fi + +eval set -- "$ARGS" + +while true ; do + case "$1" in + --prefix) + DEST_DIR=$2 + shift 2 + ;; + + -h|--help) + showHelp; + exit 0 + ;; + + --) + shift; + break + ;; + + *) + echo "invalid option: $1" + exit 1 + ;; + esac +done + +NEED_COPY_FILES=1 +if [ "$DEST_DIR" == "$SRC_DIR" ]; then + NEED_COPY_FILES=0 +fi + +echo "NEED_COPY_FILES = $NEED_COPY_FILES" + +mkdir -pv $DEST_DIR + +if [ $? -ne 0 ]; then + echo "DEST_DIR = $DEST_DIR" + echo "" + echo "\033[31mCreate install dir failed.\033[0m" + exit 1 +fi + +cd $DEST_DIR +DEST_DIR=`pwd` +echo "DEST_DIR = $DEST_DIR" + +BUILD_DIR=$DEST_DIR/tmp/install +echo "BUILD_DIR = $BUILD_DIR" +echo "" + +DEST_BIN_DIR=$DEST_DIR/bin +OPENRESETY_CONFIGURE_ARGS="" + +if [ $OSTYPE == "UBUNTU" ] ; then + apt-get install -y build-essential libpcre3-dev libssl-dev git-core unzip +elif [ $OSTYPE == "CENTOS" ]; then + yum groupinstall -y "Development Tools" + yum install -y pcre-devel zlib-devel openssl-devel unzip +elif [ $OSTYPE == "MACOS" ]; then + type "brew" > /dev/null 2> /dev/null + if [ $? -ne 0 ]; then + echo "\033[31mPlease install brew, with this command:\033[0m" + echo -e "\033[32mruby -e \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\" \033[0m" + exit 0 + else + echo "" + echo "install pcre openssl" + + if [ $UID -eq 0 ]; then + sudo -u $SUDO_USER brew install pcre openssl + sudo -u $SUDO_USER brew link openssl --force + else + brew install pcre openssl + brew link openssl --force + fi + + echo "" + fi +else + echo "\033[31mUnsupport current OS.\033[0m" + exit 1 +fi + +if [ $OSTYPE == "MACOS" ]; then + SED_BIN='sed -i --' +else + SED_BIN='sed -i' +fi + +set -e + +rm -rf $BUILD_DIR +mkdir -p $BUILD_DIR +cp -f $SRC_DIR/dists/*.tar.gz $BUILD_DIR + +mkdir -p $DEST_DIR +mkdir -p $DEST_BIN_DIR + +mkdir -p $DEST_DIR/logs +mkdir -p $DEST_DIR/tmp +mkdir -p $DEST_DIR/conf +mkdir -p $DEST_DIR/db + +cd $BUILD_DIR + +# ---- +# install virtualenv and supervisor +echo "" +echo -e "[\033[32mINSTALL\033[0m] virtualenv" +tar xfz $SRC_DIR/dists/virtualenv-$VIRTUALENV_VER.tar.gz + +PYTHON_ENV_DIR=$DEST_BIN_DIR/python_env +rm -fr $PYTHON_ENV_DIR +mv virtualenv-$VIRTUALENV_VER $PYTHON_ENV_DIR +cd $PYTHON_ENV_DIR +echo "" >> setup.cfg +echo "[easy_install]" >> setup.cfg +echo "index-url = http://mirrors.aliyun.com/pypi/simple/" >> setup.cfg +echo "" >> setup.cfg +python virtualenv.py gbc +cd gbc +source bin/activate + +echo "" +echo -e "[\033[32mINSTALL\033[0m] supervisor" +cd $BUILD_DIR +tar zxf supervisor-$SUPERVISOR_VER.tar.gz +cd supervisor-$SUPERVISOR_VER +$SED_BIN "/zip_ok = false/a\\ +index-url = http://mirrors.aliyun.com/pypi/simple/" setup.cfg +python setup.py install + +# ---- +# install openresty and lua extensions +echo "" +echo -e "[\033[32mINSTALL\033[0m] openresty" + +cd $BUILD_DIR +tar zxf openresty-$OPENRESTY_VER.tar.gz +cd openresty-$OPENRESTY_VER +mkdir -p $DEST_BIN_DIR/openresty + +echo ./configure $OPENRESETY_CONFIGURE_ARGS \ + --prefix=$DEST_BIN_DIR/openresty \ + --with-luajit \ + --with-http_stub_status_module \ + --with-cc-opt="-I/usr/local/include" \ + --with-ld-opt="-L/usr/local/lib" + +./configure $OPENRESETY_CONFIGURE_ARGS \ + --prefix=$DEST_BIN_DIR/openresty \ + --with-luajit \ + --with-http_stub_status_module \ + --with-cc-opt="-I/usr/local/include" \ + --with-ld-opt="-L/usr/local/lib" +make && make install +ln -f -s $DEST_BIN_DIR/openresty/luajit/bin/luajit-2.1.0-beta1 $DEST_BIN_DIR/openresty/luajit/bin/lua + +# install cjson +echo "" +echo -e "[\033[32mINSTALL\033[0m] cjson" + +cp -f $DEST_BIN_DIR/openresty/lualib/cjson.so $DEST_BIN_DIR/openresty/luajit/lib/lua/5.1 + +# install luasocket +echo "" +echo -e "[\033[32mINSTALL\033[0m] luasocket" + +cd $BUILD_DIR +tar zxf luasocket-$LUASOCKET_VER.tar.gz +cd luasocket-$LUASOCKET_VER +if [ $OSTYPE == "MACOS" ]; then + $SED_BIN "s#PLAT?= linux#PLAT?= macosx#g" makefile + $SED_BIN "s#PLAT?=linux#PLAT?=macosx#g" src/makefile + $SED_BIN "s#LUAPREFIX_macosx?=/opt/local#LUAPREFIX_macosx?=$DEST_BIN_DIR/openresty/luajit#g" src/makefile + $SED_BIN "s#LUAINC_macosx_base?=/opt/local/include#LUAINC_macosx_base?=$DEST_BIN_DIR/openresty/luajit/include#g" src/makefile + $SED_BIN "s#\$(LUAINC_macosx_base)/lua/\$(LUAV)#\$(LUAINC_macosx_base)/luajit-2.1#g" src/makefile +else + $SED_BIN "s#LUAPREFIX_linux?=/usr/local#LUAPREFIX_linux?=$DEST_BIN_DIR/openresty/luajit#g" src/makefile + $SED_BIN "s#LUAINC_linux_base?=/usr/include#LUAINC_linux_base?=$DEST_BIN_DIR/openresty/luajit/include#g" src/makefile + $SED_BIN "s#\$(LUAINC_linux_base)/lua/\$(LUAV)#\$(LUAINC_linux_base)/luajit-2.1#g" src/makefile +fi +make && make install-unix +cp -f src/serial.so src/unix.so $DEST_BIN_DIR/openresty/luajit/lib/lua/5.1/socket/. + +# install luabson +echo "" +echo -e "[\033[32mINSTALL\033[0m] luabson" + +cd $BUILD_DIR +tar zxf luabson-$LUABSON_VER.tar.gz +cd lua-bson +if [ $OSTYPE == "MACOS" ]; then + $SED_BIN "s#-I/usr/local/include -L/usr/local/bin -llua53#-I$DEST_BIN_DIR/openresty/luajit/include/luajit-2.1 -L$DEST_BIN_DIR/openresty/luajit/lib -lluajit-5.1#g" Makefile +else + $SED_BIN "s#-I/usr/local/include -L/usr/local/bin -llua53#-I$DEST_BIN_DIR/openresty/luajit/include/luajit-2.1 -L$DEST_BIN_DIR/openresty/luajit/lib#g" Makefile +fi +make linux + +cp -f bson.so $DEST_BIN_DIR/openresty/lualib +cp -f bson.so $DEST_BIN_DIR/openresty/luajit/lib/lua/5.1 + +#install luapbc +echo "" +echo -e "[\033[32mINSTALL\033[0m] luapbc" + +cd $BUILD_DIR +tar zxf luapbc-$LUAPBC_VER.tar.gz +cd pbc +make lib +cd binding/lua +if [ $OSTYPE == "MACOS" ]; then + $SED_BIN "s#/usr/local/include#$DEST_BIN_DIR/openresty/luajit/include/luajit-2.1 -L$DEST_BIN_DIR/openresty/luajit/lib -lluajit-5.1#g" Makefile +else + $SED_BIN "s#/usr/local/include#$DEST_BIN_DIR/openresty/luajit/include/luajit-2.1#g" Makefile +fi +make + +cp -f protobuf.so $DEST_BIN_DIR/openresty/lualib +cp -f protobuf.lua $DEST_BIN_DIR/openresty/lualib + +cp -f protobuf.so $DEST_BIN_DIR/openresty/luajit/lib/lua/5.1 +cp -f protobuf.lua $DEST_BIN_DIR/openresty/luajit/lib/lua/5.1 + +# install luaprocess +echo "" +echo -e "[\033[32mINSTALL\033[0m] luaprocess" + +cd $BUILD_DIR +tar zxf lua-process-$LUAPROCESS_VER.tar.gz +cd lua-process-$LUAPROCESS_VER +cp Makefile Makefile_ +echo "PACKAGE=process" > Makefile +echo "LIB_EXTENSION=so" >> Makefile +echo "SRCDIR=src" >> Makefile +echo "TMPLDIR=tmpl" >> Makefile +echo "VARDIR=var" >> Makefile +echo "CFLAGS=-Wall -fPIC -O2 -I_GBC_CORE_ROOT_/bin/openresty/luajit/include/luajit-2.1" >> Makefile +echo "LDFLAGS=--shared -Wall -fPIC -O2 -L_GBC_CORE_ROOT_/bin/openresty/luajit/lib" >> Makefile +if [ $OSTYPE == "MACOS" ]; then + echo "LIBS=-lluajit-5.1" >> Makefile +fi +echo "" >> Makefile +cat Makefile_ >> Makefile +rm Makefile_ + +$SED_BIN "s#_GBC_CORE_ROOT_#$DEST_DIR#g" Makefile +$SED_BIN "s#lua ./codegen.lua#$DEST_BIN_DIR/openresty/luajit/bin/lua ./codegen.lua#g" Makefile + +make + +cp -f process.so $DEST_BIN_DIR/openresty/lualib +cp -f process.so $DEST_BIN_DIR/openresty/luajit/lib/lua/5.1 + +# ---- +#install redis +echo "" +echo -e "[\033[32mINSTALL\033[0m] redis" + +cd $BUILD_DIR +tar zxf redis-$REDIS_VER.tar.gz +cd redis-$REDIS_VER +mkdir -p $DEST_BIN_DIR/redis/bin + +make +cp src/redis-server $DEST_BIN_DIR/redis/bin +cp src/redis-cli $DEST_BIN_DIR/redis/bin +cp src/redis-sentinel $DEST_BIN_DIR/redis/bin +cp src/redis-benchmark $DEST_BIN_DIR/redis/bin +cp src/redis-check-aof $DEST_BIN_DIR/redis/bin +cp src/redis-check-dump $DEST_BIN_DIR/redis/bin + +# ---- +# install beanstalkd +echo "" +echo -e "[\033[32mINSTALL\033[0m] beanstalkd" + +cd $BUILD_DIR +tar zxf beanstalkd-$BEANSTALKD_VER.tar.gz +cd beanstalkd-$BEANSTALKD_VER +mkdir -p $DEST_BIN_DIR/beanstalkd/bin + +make +cp beanstalkd $DEST_BIN_DIR/beanstalkd/bin + +# ---- +# install apps +echo "" +echo -e "[\033[32mINSTALL\033[0m] apps" + +if [ $NEED_COPY_FILES -ne 0 ]; then + cp -rf $SRC_DIR/src $DEST_DIR + cp -rf $SRC_DIR/apps $DEST_DIR + + cd $SRC_DIR + cp -f start_server stop_server check_server $DEST_DIR + cd $SRC_DIR/bin + cp -f shell_func.sh shell_func.lua start_worker.lua $DEST_BIN_DIR + + if [ $OSTYPE == "MACOS" ]; then + cp -f getopt_long $DEST_BIN_DIR + fi + + # copy all configuration files + cp -f $SRC_DIR/conf/* $DEST_DIR/conf/ +fi + +# done +# rm -rf $BUILD_DIR + +echo "DONE!" +echo "" diff --git a/shells/README.md b/shells/README.md deleted file mode 100644 index dde678d..0000000 --- a/shells/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# some tools for GameBox Cloud Core. - -- start\_server - - start nginx process. - - start redis sever. - - start beanstalkd process. - - start job worker process. - - start monitor process. - -- stop\_server - - stop nginx process. - - stop redis process. - - stop beanstalkd process. - - stop job worker process. - - stop monitor process. - - reload ngxin conf file and restart nginx only(with --reload option). - -- check\_server - - show nginx process. - - show redis process. - - show beanstalkd process. - - show job worker process. - - show monitor process. - -- tools.sh - - start a server tool written with GameBox Cloud Core. diff --git a/shells/check_server b/shells/check_server deleted file mode 100755 index 198ef31..0000000 --- a/shells/check_server +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -function getVersion() -{ - LUABIN=$1/bin/openresty/luajit/bin/lua - CODE='_C=require("conf.config"); print("GameBox Cloud Core " .. _GAMEBOX_CLOUD_CORE_VERSION);' - - $LUABIN -e "$CODE" -} - -OLDDIR=$(pwd) -CURRDIR=$(cd "$(dirname $0)" && pwd) - -cd $CURRDIR -VERSION=$(getVersion $CURRDIR) - -grep "_DBG_DEBUG" $CURRDIR/bin/instrument/start_workers.sh > /dev/null - -if [ $? -ne 0 ]; then - echo -e "\n$VERSION in \033[32mRELEASE\033[0m mode" -else - echo -e "\n$VERSION in \033[31mDEBUG\033[0m mode" -fi - - -echo -e "\n\033[33m[Nginx] \033[0m" -ps -ef | grep -i "nginx" | grep -v "grep" --color=auto - -echo -e "\n\033[33m[Redis] \033[0m" -ps -ef | grep -i "redis" | grep -v "grep" --color=auto - -echo -e "\n\033[33m[Beanstalkd] \033[0m" -ps -ef | grep -i "beanstalkd" | grep -v "grep" --color=auto - -echo -e "\n\033[33m[Monitor] \033[0m" -ps -ef | grep -i "monitor\.sh" | grep -v "grep" --color=auto | grep -v "lua -e SERVER_CONFIG" --color=auto - -echo -e "\n\033[33m[Job Worker] \033[0m" -ps -ef | grep -i "start_workers\.sh" | grep -v "grep" --color=auto | grep -v "lua -e SERVER_CONFIG" --color=auto - -echo "" - -cd $OLDDIR diff --git a/shells/start_server b/shells/start_server deleted file mode 100755 index 586c685..0000000 --- a/shells/start_server +++ /dev/null @@ -1,231 +0,0 @@ -#!/bin/bash - -function showHelp() -{ - echo "Usage: [sudo] ./start_server.sh [OPTIONS]" - echo "Options:" - echo -e "\t -a , --all \t\t start nginx(release mode), redis and beanstalkd" - echo -e "\t -n , --nginx \t\t start nginx in release mode" - echo -e "\t -r , --redis \t\t start redis" - echo -e "\t -b , --beanstalkd \t start beanstalkd" - echo -e "\t -v , --version \t\t show GameBox Cloud Core version" - echo -e "\t -h , --help \t\t show this help" - echo -e "\t --debug \t\t start GameBox Cloud Core in debug mode." - echo "if the option is not specified, default option is \"--all(-a)\"." - echo "In default, GameBox Cloud Core will start in release mode, or else it will start in debug mode when you specified \"--debug\"." -} - -function getVersion() -{ - LUABIN=$1/bin/openresty/luajit/bin/lua - CODE='_C=require("conf.config"); print("GameBox Cloud Core " .. _GAMEBOX_CLOUD_CORE_VERSION);' - - $LUABIN -e "$CODE" -} - -function getNginxNumOfWorker() -{ - LUABIN=$1/bin/openresty/luajit/bin/lua - CODE='_C=require("conf.config"); print(_C.numOfWorkers);' - - $LUABIN -e "$CODE" -} - -function getNginxPort() -{ - LUABIN=$1/bin/openresty/luajit/bin/lua - CODE='_C=require("conf.config"); print(_C.port);' - - $LUABIN -e "$CODE" -} - -function isMacOs() -{ - TMPRES=$(uname -s) - if [ $TMPRES == "Darwin" ]; then - echo "MACOS" - exit 0 - fi - - echo "LINUX" -} - -OLDDIR=$(pwd) -CURRDIR=$(cd "$(dirname $0)" && pwd) -NGINXDIR=$CURRDIR/bin/openresty/nginx - -cd $CURRDIR -VERSION=$(getVersion $CURRDIR) - -OSTYPE=$(isMacOs) -if [ $OSTYPE == "MACOS" ]; then - SED_BIN='sed -i --' - ARGS=$($CURRDIR/tmp/getopt_long "$@") -else - SED_BIN='sed -i' - ARGS=$(getopt -o abrnvh --long all,nginx,redis,beanstalkd,debug,version,help -n 'Start GameBox Cloud Core' -- "$@") -fi - -if [ $? -ne 0 ] ; then echo "Start GameBox Cloud Core Terminating..." >&2; exit 1; fi - -eval set -- "$ARGS" - -declare -i DEBUG=0 -declare -i ALL=0 -declare -i BEANS=0 -declare -i NGINX=0 -declare -i REDIS=0 -if [ $# -eq 1 ] ; then - ALL=1 -fi - -if [ $# -eq 2 ] && [ $1 == "--debug" ]; then - ALL=1 -fi - -while true ; do - case "$1" in - --debug) - DEBUG=1 - shift - ;; - - -a|--all) - ALL=1 - shift - ;; - - -b|--beanstalkd) - BEANS=1 - shift - ;; - - -r|--redis) - REDIS=1 - shift - ;; - - -n|--nginx) - NGINX=1 - shift - ;; - - -v|--version) - echo $VERSION - exit 0 - ;; - - -h|--help) - showHelp; - exit 0 - ;; - - --) shift; break ;; - - *) - echo "invalid option. $1" - exit 1 - ;; - esac -done - -echo -e "\033[33mStart $VERSION... \033[0m" - -echo "Start $VERSION... " >> $CURRDIR/logs/error.log - -# start redis -if [ $ALL -eq 1 ] || [ $REDIS -eq 1 ]; then - pgrep redis-server > /dev/null - if [ $? -ne 0 ]; then - $CURRDIR/bin/redis/bin/redis-server $CURRDIR/bin/redis/conf/redis.conf - if [ $? -ne 0 ]; then - exit $? - fi - echo "Start Redis DONE" - else - echo "Redis is already started" - fi -fi - -# start beanstalkd -if [ $ALL -eq 1 ] || [ $BEANS -eq 1 ]; then - pgrep beanstalkd > /dev/null - if [ $? -ne 0 ]; then - $CURRDIR/bin/beanstalkd/bin/beanstalkd > $CURRDIR/logs/beanstalkd.log & - if [ $? -ne 0 ]; then - exit $? - fi - echo "Start Beanstalkd DONE" - else - echo "Beanstalkd is already started" - fi -fi - -# start nginx -if [ $ALL -eq 1 ] || [ $NGINX -eq 1 ]; then - pgrep nginx > /dev/null - if [ $? -ne 0 ]; then - PORT=$(getNginxPort $CURRDIR) - $SED_BIN "s#listen [0-9]*#listen $PORT#g" $NGINXDIR/conf/nginx.conf - - NUMOFWORKERS=$(getNginxNumOfWorker $CURRDIR) - $SED_BIN "s#worker_processes [0-9]*#worker_processes $NUMOFWORKERS#g" $NGINXDIR/conf/nginx.conf - - if [ $DEBUG -eq 1 ] ; then - $SED_BIN "s#DEBUG = _DBG_ERROR#DEBUG = _DBG_DEBUG#g" $NGINXDIR/conf/nginx.conf - $SED_BIN "s#error_log logs/error.log;#error_log logs/error.log debug;#g" $NGINXDIR/conf/nginx.conf - $SED_BIN "s#lua_code_cache on#lua_code_cache off#g" $NGINXDIR/conf/nginx.conf - $SED_BIN "s#DEBUG=_DBG_WARN#DEBUG=_DBG_DEBUG#g" $CURRDIR/apps/welcome/tools.sh - $SED_BIN "s#DEBUG=_DBG_WARN#DEBUG=_DBG_DEBUG#g" $CURRDIR/bin/instrument/start_workers.sh - $SED_BIN "s#DEBUG=_DBG_WARN#DEBUG=_DBG_DEBUG#g" $CURRDIR/bin/instrument/monitor.sh - else - $SED_BIN "s#DEBUG = _DBG_DEBUG#DEBUG = _DBG_ERROR#g" $NGINXDIR/conf/nginx.conf - $SED_BIN "s#error_log logs/error.log debug;#error_log logs/error.log;#g" $NGINXDIR/conf/nginx.conf - $SED_BIN "s#lua_code_cache off#lua_code_cache on#g" $NGINXDIR/conf/nginx.conf - $SED_BIN "s#DEBUG=_DBG_DEBUG#DEBUG=_DBG_WARN#g" $CURRDIR/apps/welcome/tools.sh - $SED_BIN "s#DEBUG=_DBG_DEBUG#DEBUG=_DBG_WARN#g" $CURRDIR/bin/instrument/start_workers.sh - $SED_BIN "s#DEBUG=_DBG_DEBUG#DEBUG=_DBG_WARN#g" $CURRDIR/bin/instrument/monitor.sh - fi - rm -f $NGINXDIR/conf/nginx.conf-- - rm -f $CURRDIR/apps/welcome/tools.sh-- - rm -f $CURRDIR/bin/instrument/start_workers.sh-- - rm -f $CURRDIR/bin/instrument/monitor.sh-- - - nginx -p $CURRDIR -c $NGINXDIR/conf/nginx.conf - if [ $? -ne 0 ]; then - exit $? - fi - echo "Start Nginx DONE" - else - echo "Nginx is already started" - fi -fi - -cd $CURRDIR -if [ $ALL -eq 1 ]; then - # start monitor - if [ $OSTYPE != "MACOS" ]; then - ps -ef | grep -i "monitor.*sh" | grep -v "grep" > /dev/null - if [ $? -ne 0 ]; then - $CURRDIR/bin/instrument/monitor.sh > $CURRDIR/logs/monitor.log & - fi - fi - - # start job worker - ps -ef | grep -i "start_workers.*sh" | grep -v "grep" > /dev/null - if [ $? -ne 0 ]; then - I=0 - rm -f $CURRDIR/logs/jobworker.log - while [ $I -lt $NUMOFWORKERS ]; do - $CURRDIR/bin/instrument/start_workers.sh >> $CURRDIR/logs/jobworker.log & - I=$((I+1)) - done - fi - - echo -e "\033[33mStart GameBox Cloud Core DONE! \033[0m" -fi - -sleep 1 -$CURRDIR/check_server - -cd $OLDDIR diff --git a/shells/stop_server b/shells/stop_server deleted file mode 100755 index ee041c3..0000000 --- a/shells/stop_server +++ /dev/null @@ -1,215 +0,0 @@ -#!/bin/bash - -function showHelp() -{ - echo "Usage: [sudo] ./stop_server.sh [OPTIONS] [--reload]" - echo "Options:" - echo -e "\t -a , --all \t\t stop nginx, redis and beanstalkd" - echo -e "\t -n , --nginx \t\t stop nginx" - echo -e "\t -r , --redis \t\t stop redis" - echo -e "\t -b , --beanstalkd \t stop beanstalkd" - echo -e "\t -h , --help \t\t show this help" - echo -e "\t -v , --version \t\t show version" - echo -e "\t --reload \t\t reload GameBox Cloud Core config." - echo "if the option is not specified, default option is \"--all(-a)\"." -} - -function getVersion() -{ - LUABIN=$1/bin/openresty/luajit/bin/lua - CODE='_C=require("conf.config"); print("GameBox Cloud Core " .. _GAMEBOX_CLOUD_CORE_VERSION);' - - $LUABIN -e "$CODE" -} - -function getNginxNumOfWorker() -{ - LUABIN=$1/bin/openresty/luajit/bin/lua - CODE="_C=require([[conf.config]]); print(_C.numOfWorkers);" - - $LUABIN -e "$CODE" -} - -function getNginxPort() -{ - LUABIN=$1/bin/openresty/luajit/bin/lua - CODE="_C=require([[conf.config]]); print(_C.port);" - - $LUABIN -e "$CODE" -} - -function isMacOs() -{ - TMPRES=$(uname -s) - if [ $TMPRES == "Darwin" ]; then - echo "MACOS" - exit 0 - fi - - echo "LINUX" -} - -OLDDIR=$(pwd) -CURRDIR=$(cd "$(dirname $0)" && pwd) -NGINXDIR=$CURRDIR/bin/openresty/nginx/ - -cd $CURRDIR -VERSION=$(getVersion $CURRDIR) - -OSTYPE=$(isMacOs) -if [ $OSTYPE == "MACOS" ]; then - SED_BIN='sed -i --' - ARGS=$($CURRDIR/tmp/getopt_long "$@") -else - SED_BIN='sed -i' - ARGS=$(getopt -o abrnvh --long all,nginx,redis,beanstalkd,reload,version,help -n 'Stop GameBox Cloud Core' -- "$@") -fi - -if [ $? != 0 ] ; then echo "Stop GameBox Cloud Core Terminating..." >&2; exit 1; fi - -eval set -- "$ARGS" - -declare -i RELOAD=0 -declare -i ALL=0 -declare -i BEANS=0 -declare -i NGINX=0 -declare -i REDIS=0 -if [ $# -eq 1 ] ; then - ALL=1 -fi - -while true ; do - case "$1" in - --reload) - RELOAD=1 - shift - ;; - - -a|--all) - ALL=1 - shift - ;; - - -b|--beanstalkd) - BEANS=1 - shift - ;; - - -r|--redis) - REDIS=1 - shift - ;; - - -n|--nginx) - NGINX=1 - shift - ;; - - -v|--version) - echo $VERSION - exit 0 - ;; - - -h|--help) - showHelp; - exit 0 - ;; - - --) shift; break ;; - - *) - echo "invalid option. $1" - exit 1 - ;; - esac -done - -# "--reload" option has no effect on other options, except "--ngxin(-n)". -if [ $RELOAD -ne 0 ]; then - ALL=0 -fi - -# stop monitor and job worker first. -if [ $OSTYPE == "MACOS" ]; then - ps -ef | grep "start_workers" | awk '{print $2}' | xargs kill -9 > /dev/null 2> /dev/null - ps -ef | grep "monitor" | awk '{print $2}' | xargs kill -9 > /dev/null 2> /dev/null -else - killall start_workers.sh > /dev/null 2> /dev/null - killall monitor.sh > /dev/null 2> /dev/null -fi -killall $CURRDIR/bin/openresty/luajit/bin/lua > /dev/null 2> /dev/null - -#stop nginx -if [ $ALL -eq 1 ] || [ $NGINX -eq 1 ] || [ $RELOAD -eq 1 ]; then - if [ $RELOAD -eq 0 ] ; then - pgrep nginx > /dev/null - if [ $? -eq 0 ]; then - nginx -q -p $CURRDIR -c $NGINXDIR/conf/nginx.conf -s stop - if [ $? -ne 0 ]; then - exit $? - fi - fi - - sleep 1 - echo "Stop Nginx DONE" - else - PORT=$(getNginxPort $CURRDIR) - $SED_BIN "s#listen [0-9]*#listen $PORT#g" $NGINXDIR/conf/nginx.conf - - NUMOFWORKERS=$(getNginxNumOfWorker $CURRDIR) - $SED_BIN "s#worker_processes [0-9]*#worker_processes $NUMOFWORKERS#g" $NGINXDIR/conf/nginx.conf - - rm -f $NGINXDIR/conf/nginx.conf-- - - nginx -p $CURRDIR -c $NGINXDIR/conf/nginx.conf -s reload - if [ $? -ne 0 ]; then - exit $? - fi - echo "Reload Nginx conf DONE" - fi -fi - -#stop redis -if [ $ALL -eq 1 ] || [ $REDIS -eq 1 ]; then - pgrep nginx > /dev/null - - while [ $? -eq 0 ]; - do - nginx -q -p $CURRDIR -c $NGINXDIR/conf/nginx.conf -s stop - pgrep nginx > /dev/null - done - - killall redis-server 2> /dev/null - echo "Stop Redis DONE" -fi - -#stop beanstalkd -if [ $ALL -eq 1 ] || [ $BEANS -eq 1 ]; then - killall beanstalkd 2> /dev/null - echo "Stop Beanstalkd DONE" -fi - - -if [ $RELOAD -ne 0 ]; then - if [ $OSTYPE != "MACOS" ]; then - $CURRDIR/bin/instrument/monitor.sh > $CURRDIR/logs/monitor.log & - fi - - # start job worker - I=0 - rm -f $CURRDIR/logs/jobworker.log - while [ $I -lt $NUMOFWORKERS ]; do - $CURRDIR/bin/instrument/start_workers.sh >> $CURRDIR/logs/jobworker.log & - I=$((I+1)) - done -fi - -if [ $ALL -eq 1 ] ; then - echo -e "\033[33mStop $VERSION DONE! \033[0m" - echo "Stop $VERSION DONE!" >> $CURRDIR/logs/error.log -fi - -sleep 3 -$CURRDIR/check_server - -cd $OLDDIR diff --git a/shells/tools.sh b/shells/tools.sh deleted file mode 100755 index 64d53db..0000000 --- a/shells/tools.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -export LUA_PATH="_GBC_CORE_ROOT_/src/?.lua;_GBC_CORE_ROOT_/src/lib/?.lua;;" - -GBC_CORE_ROOT="_GBC_CORE_ROOT_" -LUABIN=bin/openresty/luajit/bin/lua -SCRIPT=CLIBootstrap.lua - -cd $GBC_CORE_ROOT - -ENV="SERVER_CONFIG=loadfile([[_GBC_CORE_ROOT_/conf/config.lua]])();DEBUG=_DBG_DEBUG;require([[framework.init]]);SERVER_CONFIG.appRootPath=SERVER_CONFIG.appRootPath;" - -$LUABIN -e "$ENV" $GBC_CORE_ROOT/src/$SCRIPT $* diff --git a/shells/trim_spaces.sh b/shells/trim_spaces.sh deleted file mode 100755 index 3b59fc5..0000000 --- a/shells/trim_spaces.sh +++ /dev/null @@ -1,9 +0,0 @@ -CURDIR=$(dirname $(readlink -f $0)) - -find $CURDIR/../ -name "*.lua" | xargs sed -i -r 's#[ \t]+$##g' - -find $CURDIR/../ -name "*.sh" | xargs sed -i -r 's#[ \t]+$##g' - -find $CURDIR/../ -name "*.md" | xargs sed -i -r 's#[ \t]+$##g' - -find $CURDIR/../ -name "*.c" | xargs sed -i -r 's#[ \t]+$##g' diff --git a/src/HttpBootstrap.lua b/src/HttpBootstrap.lua deleted file mode 100644 index b79d1cc..0000000 --- a/src/HttpBootstrap.lua +++ /dev/null @@ -1,29 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local Factory = require("server.base.Factory") - --- SERVER_CONFIG from init_by_lua, see nginx.conf -local app = Factory.create(SERVER_CONFIG, "HttpConnect") -app:run() diff --git a/src/WebSocketBootstrap.lua b/src/WebSocketBootstrap.lua deleted file mode 100644 index 97a356e..0000000 --- a/src/WebSocketBootstrap.lua +++ /dev/null @@ -1,29 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local factory = require("server.base.Factory") - --- SERVER_CONFIG from init_by_lua, see nginx.conf -local app = factory.create(SERVER_CONFIG, "WebSocketConnect") -app:run() diff --git a/src/WorkerBootstrap.lua b/src/WorkerBootstrap.lua deleted file mode 100644 index 21fffa2..0000000 --- a/src/WorkerBootstrap.lua +++ /dev/null @@ -1,29 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local factory = require("server.base.Factory") - --- SERVER_CONFIG from init_by_lua, see nginx.conf -local app = factory.create(SERVER_CONFIG, "Worker") -app:run() diff --git a/src/framework/class.lua b/src/framework/class.lua new file mode 100644 index 0000000..36ef6df --- /dev/null +++ b/src/framework/class.lua @@ -0,0 +1,129 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local assert = assert +local error = error +local getmetatable = getmetatable +local rawget = rawget +local setmetatable = setmetatable +local string_format = string.format +local type = type + +local _iskindofinternal = function(mt, classname) + if not mt then return false end + + local index = rawget(mt, "__index") + if not index then return false end + + local cname = rawget(index, "__cname") + if cname == classname then return true end + + return _iskindofinternal(getmetatable(index), classname) +end + +function cc.iskindof(target, classname) + local targetType = type(target) + if targetType ~= "table" then + return false + end + return _iskindofinternal(getmetatable(target), classname) +end + +local _new = function(cls, ...) + local instance = {} + setmetatable(instance, {__index = cls}) + instance.class = cls + instance:ctor(...) + return instance +end + +function cc.class(classname, super) + assert(type(classname) == "string", string_format("cc.class() - invalid class name \"%s\"", tostring(classname))) + + -- create class + local cls = {__cname = classname, new = _new} + + -- set super class + local superType = type(super) + if superType == "table" then + assert(type(super.__cname) == "string", string_format("cc.class() - create class \"%s\" used super class isn't declared by cc.class()", classname)) + cls.super = super + setmetatable(cls, {__index = cls.super}) + elseif superType ~= "nil" then + error(string_format("cc.class() - create class \"%s\" with invalid super type \"%s\"", classname, superType)) + end + + if not cls.ctor then + cls.ctor = function() end -- add default constructor + end + + return cls +end + +function cc.addComponent(target, cls) + if not target.__components then + target.__components = {} + end + local components = target.__components + local name = cls.__cname + if not components[name] then + components[name] = cls:new(target) + end + return components[name] +end + +function cc.getComponent(target, name) + if type(name) == "table" then + name = name.__cname + end + if not target.__components then + return + end + return target.__components[name] +end + +-- name is Class name or Class or Component object +function cc.removeComponent(target, name) + if type(name) == "table" then + -- name is class or object + if name.__cname then + name = name.__cname + else + local mt = getmetatable(name) + local __index = rawget(mt, "__index") + if __index then + name = rawget(__index, "__cname") + end + end + end + if target.__components then + target.__components[name] = nil + end +end + +function cc.handler(target, method) + return function(...) + return method(target, ...) + end +end diff --git a/src/packages/beanstalkd/init.lua b/src/framework/ctype.lua similarity index 73% rename from src/packages/beanstalkd/init.lua rename to src/framework/ctype.lua index 90c296c..2684a9f 100644 --- a/src/packages/beanstalkd/init.lua +++ b/src/framework/ctype.lua @@ -22,8 +22,22 @@ THE SOFTWARE. ]] -local _P = {} +local math_floor = math.floor -_P.service = import(".service") +function cc.checknumber(value, base) + return tonumber(value, base) or 0 +end -return _P +function cc.checkint(value) + value = tonumber(value) or 0 + return math_floor(value + 0.5) +end + +function cc.checkbool(value) + return (value ~= nil and value ~= false) +end + +function cc.checktable(value) + if type(value) ~= "table" then value = {} end + return value +end diff --git a/src/framework/debug.lua b/src/framework/debug.lua new file mode 100644 index 0000000..bc863d5 --- /dev/null +++ b/src/framework/debug.lua @@ -0,0 +1,168 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local ngx = ngx +local ngx_log = nil +if ngx then + ngx_log = ngx.log +end + +local cc = cc +local debug_traceback = debug.traceback +local error = error +local print = print +local string_format = string.format +local string_rep = string.rep +local string_upper = string.upper +local table_concat = table.concat +local tostring = tostring + +function cc.throw(fmt, ...) + local msg + if #{...} == 0 then + msg = fmt + else + msg = string_format(fmt, ...) + end + if cc.DEBUG > cc.DEBUG_WARN then + error(msg, 2) + else + error(msg, 0) + end +end + +local function _dump_value(v) + if type(v) == "string" then + v = "\"" .. v .. "\"" + end + return tostring(v) +end + +function cc.dump(value, desciption, nesting, _print) + if type(nesting) ~= "number" then nesting = 3 end + _print = _print or print + + local lookup = {} + local result = {} + local traceback = string.split(debug_traceback("", 2), "\n") + _print("dump from: " .. string.trim(traceback[2])) + + local function _dump(value, desciption, indent, nest, keylen) + desciption = desciption or "" + local spc = "" + if type(keylen) == "number" then + spc = string_rep(" ", keylen - string.len(_dump_value(desciption))) + end + if type(value) ~= "table" then + result[#result +1 ] = string_format("%s%s%s = %s", indent, _dump_value(desciption), spc, _dump_value(value)) + elseif lookup[tostring(value)] then + result[#result +1 ] = string_format("%s%s%s = *REF*", indent, _dump_value(desciption), spc) + else + lookup[tostring(value)] = true + if nest > nesting then + result[#result +1 ] = string_format("%s%s = *MAX NESTING*", indent, _dump_value(desciption)) + else + result[#result +1 ] = string_format("%s%s = {", indent, _dump_value(desciption)) + local indent2 = indent.." " + local keys = {} + local keylen = 0 + local values = {} + for k, v in pairs(value) do + keys[#keys + 1] = k + local vk = _dump_value(k) + local vkl = string.len(vk) + if vkl > keylen then keylen = vkl end + values[k] = v + end + table.sort(keys, function(a, b) + if type(a) == "number" and type(b) == "number" then + return a < b + else + return tostring(a) < tostring(b) + end + end) + for i, k in ipairs(keys) do + _dump(values[k], k, indent2, nest + 1, keylen) + end + result[#result +1] = string_format("%s}", indent) + end + end + end + _dump(value, desciption, "- ", 1) + + for i, line in ipairs(result) do + _print(line) + end +end + +function cc.printf(fmt, ...) + print(string_format(tostring(fmt), ...)) +end + +function cc.printlog(tag, fmt, ...) + fmt = tostring(fmt) + if ngx_log then + if tag == "ERR" and cc.DEBUG > cc.DEBUG_WARN then + ngx_log(ngx.ERR, string_format(fmt, ...) .. "\n" .. debug_traceback("", 3)) + else + ngx_log(ngx[tag], string_format(fmt, ...)) + end + return + end + + local t = { + "[", + string_upper(tostring(tag)), + "] ", + string_format(fmt, ...) + } + if tag == "ERR" then + table_insert(t, debug_traceback("", 2)) + end + print(table.concat(t)) +end + +local _printlog = cc.printlog + +function cc.printerror(fmt, ...) + _printlog("ERR", fmt, ...) +end + +function cc.printdebug(fmt, ...) + if cc.DEBUG >= cc.DEBUG_VERBOSE then + _printlog("DEBUG", fmt, ...) + end +end + +function cc.printinfo(fmt, ...) + if cc.DEBUG >= cc.DEBUG_INFO then + _printlog("INFO", fmt, ...) + end +end + +function cc.printwarn(fmt, ...) + if cc.DEBUG >= cc.DEBUG_WARN then + _printlog("WARN", fmt, ...) + end +end diff --git a/src/framework/functions.lua b/src/framework/functions.lua deleted file mode 100644 index 8c09f1b..0000000 --- a/src/framework/functions.lua +++ /dev/null @@ -1,748 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local DEBUG = DEBUG -local tostring = tostring -local tonumber = tonumber -local assert = assert -local error = error -local type = type -local pairs = pairs -local ipairs = ipairs -local pcall = pcall -local ngx = ngx -local ngx_log = nil -if ngx then ngx_log = ngx.log end -local table_insert = table.insert -local table_remove = table.remove -local string_format = string.format -local string_upper = string.upper -local string_len = string.len -local string_rep = string.rep -local string_find = string.find -local string_gsub = string.gsub -local string_sub = string.sub -local string_byte = string.byte -local string_char = string.char -local math_floor = math.floor -local math_ceil = math.ceil -local math_random = math.random -local math_randomseed = math.randomseed -local io_open = io.open -local io_close = io.close -local os_time = os.time -local debug_traceback = debug.traceback -local debug_getlocal = debug.getlocal - -if type(DEBUG) ~= "number" then DEBUG = 2 end - -function throw(fmt, ...) - local msg = string.format(fmt, ...) - if DEBUG > 1 then - error(msg, 2) - else - error(msg, 0) - end -end - --- internal function, advise you not to call it directly. -function printLog(tag, fmt, ...) - if ngx_log then - if tag == "ERR" and DEBUG > 1 then - ngx_log(ngx.ERR, string_format(tostring(fmt), ...) .. "\n" .. debug_traceback("", 3)) - else - ngx_log(ngx[tag], string_format(tostring(fmt), ...)) - end - return - end - - local t = { - "[", - string_upper(tostring(tag)), - "] ", - string_format(tostring(fmt), ...) - } - if tag == "ERR" then - table_insert(t, debug_traceback("", 2)) - end - print(table.concat(t)) -end - -function printError(fmt, ...) - printLog("ERR", fmt, ...) -end - -function printDebug(fmt, ...) - if DEBUG < 3 then return end - printLog("DEBUG", fmt, ...) -end - -function printInfo(fmt, ...) - if DEBUG < 2 then return end - printLog("INFO", fmt, ...) -end - -function printWarn(fmt, ...) - if DEBUG < 1 then return end - printLog("WARN", fmt, ...) -end - -local function _dump_value(v) - if type(v) == "string" then - v = "\"" .. v .. "\"" - end - return tostring(v) -end - -function dump(value, desciption, nesting) - if type(nesting) ~= "number" then nesting = 3 end - - local lookup = {} - local result = {} - local traceback = string.split(debug_traceback("", 2), "\n") - print("dump from: " .. string.trim(traceback[3])) - - local function _dump(value, desciption, indent, nest, keylen) - desciption = desciption or "" - local spc = "" - if type(keylen) == "number" then - spc = string_rep(" ", keylen - string_len(_dump_value(desciption))) - end - if type(value) ~= "table" then - result[#result +1 ] = string_format("%s%s%s = %s", indent, _dump_value(desciption), spc, _dump_value(value)) - elseif lookup[tostring(value)] then - result[#result +1 ] = string_format("%s%s%s = *REF*", indent, _dump_value(desciption), spc) - else - lookup[tostring(value)] = true - if nest > nesting then - result[#result +1 ] = string_format("%s%s = *MAX NESTING*", indent, _dump_value(desciption)) - else - result[#result +1 ] = string_format("%s%s = {", indent, _dump_value(desciption)) - local indent2 = indent.." " - local keys = {} - local keylen = 0 - local values = {} - for k, v in pairs(value) do - keys[#keys + 1] = k - local vk = _dump_value(k) - local vkl = string_len(vk) - if vkl > keylen then keylen = vkl end - values[k] = v - end - table.sort(keys, function(a, b) - if type(a) == "number" and type(b) == "number" then - return a < b - else - return tostring(a) < tostring(b) - end - end) - for i, k in ipairs(keys) do - _dump(values[k], k, indent2, nest + 1, keylen) - end - result[#result +1] = string_format("%s}", indent) - end - end - end - _dump(value, desciption, "- ", 1) - - for i, line in ipairs(result) do - print(line) - end -end - -function printf(fmt, ...) - print(string_format(tostring(fmt), ...)) -end - -function checknumber(value, base) - return tonumber(value, base) or 0 -end - -function checkint(value) - return math.round(checknumber(value)) -end - -function checkbool(value) - return (value ~= nil and value ~= false) -end - -function checktable(value) - if type(value) ~= "table" then value = {} end - return value -end - -function isset(hashtable, key) - local t = type(hashtable) - return (t == "table" or t == "userdata") and hashtable[key] ~= nil -end - -local _setmetatableindex -_setmetatableindex = function(t, index) - if type(t) == "userdata" then - local peer = tolua.getpeer(t) - if not peer then - peer = {} - tolua.setpeer(t, peer) - end - _setmetatableindex(peer, index) - else - local mt = getmetatable(t) - if not mt then mt = {} end - if not mt.__index then - mt.__index = index - setmetatable(t, mt) - elseif mt.__index ~= index then - _setmetatableindex(mt, index) - end - end -end -setmetatableindex = _setmetatableindex - -function clone(object) - local lookup = {} - local function _copy(object) - if type(object) ~= "table" then - return object - elseif lookup[object] then - return lookup[object] - end - local newObject = {} - lookup[object] = newObject - for key, value in pairs(object) do - newObject[_copy(key)] = _copy(value) - end - return setmetatable(newObject, getmetatable(object)) - end - return _copy(object) -end - -function class(classname, ...) - local cls = {__cname = classname} - - local supers = {...} - for _, super in ipairs(supers) do - local superType = type(super) - assert(superType == "nil" or superType == "table" or superType == "function", - string_format("class() - create class \"%s\" with invalid super class type \"%s\"", - classname, superType)) - - if superType == "function" then - assert(cls.__create == nil, - string_format("class() - create class \"%s\" with more than one creating function", - classname)); - -- if super is function, set it to __create - cls.__create = super - elseif superType == "table" then - if super[".isclass"] then - -- super is native class - assert(cls.__create == nil, - string_format("class() - create class \"%s\" with more than one creating function or native class", - classname)); - cls.__create = function() return super:create() end - else - -- super is pure lua class - cls.__supers = cls.__supers or {} - cls.__supers[#cls.__supers + 1] = super - if not cls.super then - -- set first super pure lua class as class.super - cls.super = super - end - end - else - error(string_format("class() - create class \"%s\" with invalid super type", - classname), 0) - end - end - - cls.__index = cls - if not cls.__supers or #cls.__supers == 1 then - setmetatable(cls, {__index = cls.super}) - else - setmetatable(cls, {__index = function(_, key) - local supers = cls.__supers - for i = 1, #supers do - local super = supers[i] - if super[key] then return super[key] end - end - end}) - end - - if not cls.ctor then - -- add default constructor - cls.ctor = function() end - end - cls.new = function(...) - local instance - if cls.__create then - instance = cls.__create(...) - else - instance = {} - end - _setmetatableindex(instance, cls) - instance.class = cls - instance:ctor(...) - return instance - end - cls.create = function(_, ...) - return cls.new(...) - end - - return cls -end - -local _iskindof -_iskindof = function(cls, name) - local __index = rawget(cls, "__index") - if type(__index) == "table" and rawget(__index, "__cname") == name then return true end - - if rawget(cls, "__cname") == name then return true end - local __supers = rawget(cls, "__supers") - if not __supers then return false end - for _, super in ipairs(__supers) do - if _iskindof(super, name) then return true end - end - return false -end - -function iskindof(obj, classname) - local t = type(obj) - if t ~= "table" and t ~= "userdata" then return false end - - local mt - if t == "userdata" then - if tolua.iskindof(obj, classname) then return true end - mt = tolua.getpeer(obj) - else - mt = getmetatable(obj) - end - if mt then - return _iskindof(mt, classname) - end - return false -end - -function import(moduleName, currentModuleName) - local currentModuleNameParts - local moduleFullName = moduleName - local offset = 1 - - while true do - if string_byte(moduleName, offset) ~= 46 then -- . - moduleFullName = string_sub(moduleName, offset) - if currentModuleNameParts and #currentModuleNameParts > 0 then - moduleFullName = table.concat(currentModuleNameParts, ".") .. "." .. moduleFullName - end - break - end - offset = offset + 1 - - if not currentModuleNameParts then - if not currentModuleName then - local n,v = debug_getlocal(3, 1) - currentModuleName = v - end - - currentModuleNameParts = string.split(currentModuleName, ".") - end - table_remove(currentModuleNameParts, #currentModuleNameParts) - end - - return require(moduleFullName) -end - -function handler(obj, method) - return function(...) - return method(obj, ...) - end -end - -function math.newrandomseed() - math_randomseed(os_time()) - math_random() - math_random() - math_random() - math_random() -end - -function math.round(value) - value = checknumber(value) - return math_floor(value + 0.5) -end - -local _pi = math.pi -local _piDiv180 = _pi / 180 -function math.angle2radian(angle) - return angle * _piDiv180 -end - -local _piMul180 = _pi * 180 -function math.radian2angle(radian) - return radian / _piMul180 -end - -function math.trunc(x) - if x <= 0 then - return math_ceil(x); - end - if math_ceil(x) == x then - x = math_ceil(x); - else - x = math_ceil(x) - 1; - end - return x; -end - -function math.newrandomseed() - local ok, socket = pcall(function() - return require("socket") - end) - - if ok then - math.randomseed(socket.gettime() * 1000) - else - math.randomseed(os.time()) - end - math.random() - math.random() - math.random() - math.random() -end - -function io.exists(path) - local file = io_open(path, "r") - if file then - io_close(file) - return true - end - return false -end - -function io.readfile(path) - local file = io_open(path, "r") - if file then - local content = file:read("*a") - io_close(file) - return content - end - return nil -end - -function io.writefile(path, content, mode) - mode = mode or "w+b" - local file = io_open(path, mode) - if file then - if file:write(content) == nil then return false end - io_close(file) - return true - else - return false - end -end - -function io.pathinfo(path) - local pos = string_len(path) - local extpos = pos + 1 - while pos > 0 do - local b = string_byte(path, pos) - if b == 46 then -- 46 = char "." - extpos = pos - elseif b == 47 then -- 47 = char "/" - break - end - pos = pos - 1 - end - - local dirname = string_sub(path, 1, pos) - local filename = string_sub(path, pos + 1) - extpos = extpos - pos - local basename = string_sub(filename, 1, extpos - 1) - local extname = string_sub(filename, extpos) - return { - dirname = dirname, - filename = filename, - basename = basename, - extname = extname - } -end - -function io.filesize(path) - local size = false - local file = io_open(path, "r") - if file then - local current = file:seek() - size = file:seek("end") - file:seek("set", current) - io_close(file) - end - return size -end - -function table.nums(t) - local count = 0 - for k, v in pairs(t) do - count = count + 1 - end - return count -end - -function table.keys(hashtable) - local keys = {} - for k, v in pairs(hashtable) do - keys[#keys + 1] = k - end - return keys -end - -function table.values(hashtable) - local values = {} - for k, v in pairs(hashtable) do - values[#values + 1] = v - end - return values -end - -function table.merge(dest, src) - for k, v in pairs(src) do - dest[k] = v - end -end - -function table.insertto(dest, src, begin) - begin = checkint(begin) - if begin <= 0 then - begin = #dest + 1 - end - - local len = #src - for i = 0, len - 1 do - dest[i + begin] = src[i + 1] - end -end - -function table.indexof(array, value, begin) - for i = begin or 1, #array do - if array[i] == value then return i end - end - return false -end - -function table.keyof(hashtable, value) - for k, v in pairs(hashtable) do - if v == value then return k end - end - return nil -end - -function table.removebyvalue(array, value, removeall) - local c, i, max = 0, 1, #array - while i <= max do - if array[i] == value then - table_remove(array, i) - c = c + 1 - i = i - 1 - max = max - 1 - if not removeall then break end - end - i = i + 1 - end - return c -end - -function table.map(t, fn) - local n = {} - for k, v in pairs(t) do - n[k] = fn(v, k) - end - return n -end - -function table.walk(t, fn) - for k,v in pairs(t) do - fn(v, k) - end -end - -function table.filter(t, fn) - local n = {} - for k, v in pairs(t) do - if fn(v, k) then - n[k] = v - end - end - return n -end - -function table.unique(t, isArray) - local check = {} - local n = {} - local idx = 1 - for k, v in pairs(t) do - if not check[v] then - if isArray then - n[idx] = v - idx = idx + 1 - else - n[k] = v - end - check[v] = true - end - end - return n -end - -function table.length(t) - if type(t) ~= "table" then - return 0 - end - - local count = 0 - for _, __ in pairs(t) do - count = count + 1 - end - return count -end - -local _htmlSpecialCharsTable = {} -_htmlSpecialCharsTable["&"] = "&" -_htmlSpecialCharsTable["\""] = """ -_htmlSpecialCharsTable["'"] = "'" -_htmlSpecialCharsTable["<"] = "<" -_htmlSpecialCharsTable[">"] = ">" - -function string.htmlspecialchars(input) - for k, v in pairs(_htmlSpecialCharsTable) do - input = string_gsub(input, k, v) - end - return input -end - -function string.restorehtmlspecialchars(input) - for k, v in pairs(_htmlSpecialCharsTable) do - input = string_gsub(input, v, k) - end - return input -end - -function string.nl2br(input) - return string_gsub(input, "\n", "
    ") -end - -function string.text2html(input) - input = string_gsub(input, "\t", " ") - input = string.htmlspecialchars(input) - input = string_gsub(input, " ", " ") - input = string.nl2br(input) - return input -end - -function string.split(input, delimiter) - input = tostring(input) - delimiter = tostring(delimiter) - if (delimiter=='') then return false end - local pos,arr = 1, {} - for st,sp in function() return string_find(input, delimiter, pos, true) end do - local str = string_sub(input, pos, st - 1) - if str ~= "" then - table_insert(arr, str) - end - pos = sp + 1 - end - if pos <= string_len(input) then - table_insert(arr, string_sub(input, pos)) - end - return arr -end - - -local _trimChars = " \t\n\r" -function string.ltrim(input, chars) - chars = chars or _trimChars - local pattern = "^[" .. chars .. "]+" - return string_gsub(input, pattern, "") -end - -function string.rtrim(input, chars) - chars = chars or _trimChars - local pattern = "[" .. chars .. "]+$" - return string_gsub(input, pattern, "") -end - -function string.trim(input, chars) - chars = chars or _trimChars - local pattern = "^[" .. chars .. "]+" - input = string_gsub(input, pattern, "") - pattern = "[" .. chars .. "]+$" - return string_gsub(input, pattern, "") -end - -function string.ucfirst(input) - return string_upper(string_sub(input, 1, 1)) .. string_sub(input, 2) -end - -local function urlencodechar(char) - return "%" .. string_format("%02X", string_byte(char)) -end -function string.urlencode(input) - input = string_gsub(tostring(input), "\n", "\r\n") - input = string_gsub(input, "([^%w%.%- ])", urlencodechar) - return string_gsub(input, " ", "+") -end - -local _checknumber = checknumber -function string.urldecode(input) - input = string_gsub (input, "+", " ") - input = string_gsub (input, "%%(%x%x)", function(h) return string_char(_checknumber(h, 16)) end) - input = string_gsub (input, "\r\n", "\n") - return input -end - -function string.utf8len(input) - local len = string_len(input) - local left = len - local cnt = 0 - local arr = {0, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc} - while left ~= 0 do - local tmp = string_byte(input, -left) - local i = #arr - while arr[i] do - if tmp >= arr[i] then - left = left - i - break - end - i = i - 1 - end - cnt = cnt + 1 - end - return cnt -end - -function string.formatnumberthousands(num) - local formatted = tostring(checknumber(num)) - local k - while true do - formatted, k = string_gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2') - if k == 0 then break end - end - return formatted -end diff --git a/src/framework/init.lua b/src/framework/init.lua index c4473a8..007d722 100644 --- a/src/framework/init.lua +++ b/src/framework/init.lua @@ -22,17 +22,89 @@ THE SOFTWARE. ]] -if type(DEBUG) ~= "number" then DEBUG = 0 end +local debug_getlocal = debug.getlocal +local string_byte = string.byte +local string_find = string.find +local string_format = string.format +local string_lower = string.lower +local string_sub = string.sub +local table_concat = table.concat --- load framework cc = cc or {} -require("framework.functions") -require("framework.server_functions") -require("framework.package_support") -json = require("framework.json") +socket = {} -- avoid require("socket") warning +-- export global variable +local _g = _G +cc.exports = {} +setmetatable(cc.exports, { + __newindex = function(_, name, value) + rawset(_g, name, value) + end, -cc.server = {VERSION = "GameBox Cloud Core 0.7.0"} + __index = function(_, name) + return rawget(_g, name) + end +}) --- register the build-in packages -cc.register("event", require("framework.packages.event.init")) +-- disable create unexpected global variable +setmetatable(_g, { + __newindex = function(_, name, value) + local msg = string_format("USE \"cc.exports.%s = \" INSTEAD OF SET GLOBAL VARIABLE", name) + print(debug.traceback(msg, 2)) + if not ngx then print("") end + end +}) + +-- + +cc.DEBUG_ERROR = 0 +cc.DEBUG_WARN = 1 +cc.DEBUG_INFO = 2 +cc.DEBUG_VERBOSE = 3 +cc.DEBUG = cc.DEBUG_DEBUG + +local _loaded = {} +-- loader +function cc.import(name, current) + local _name = name + local first = string_byte(name) + if first ~= 46 and _loaded[name] then + return _loaded[name] + end + + if first == 35 --[[ "#" ]] then + name = string_sub(name, 2) + name = string_format("packages.%s.%s", name, name) + end + + if first ~= 46 --[[ "." ]] then + _loaded[_name] = require(name) + return _loaded[_name] + end + + if not current then + local _, v = debug_getlocal(3, 1) + current = v + end + + _name = current .. name + if not _loaded[_name] then + local pos = string_find(current, "%.[^%.]*$") + if pos then + current = string_sub(current, 1, pos - 1) + end + + _loaded[_name] = require(current .. name) + end + return _loaded[_name] +end + +-- load basics modules +require("framework.class") +require("framework.table") +require("framework.string") +require("framework.debug") +require("framework.math") +require("framework.ctype") +require("framework.os") +require("framework.io") diff --git a/src/framework/io.lua b/src/framework/io.lua new file mode 100644 index 0000000..5962f5e --- /dev/null +++ b/src/framework/io.lua @@ -0,0 +1,100 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local string_byte = string.byte +local string_len = string.len +local string_sub = string.sub + +function io.exists(path) + local file = io.open(path, "rb") + if file then + io.close(file) + return true + end + return false +end + +function io.readfile(path) + local file = io.open(path, "rb") + if file then + local content = file:read("*a") + io.close(file) + return content + end + return nil +end + +function io.writefile(path, content, mode) + mode = mode or "w+b" + local file = io.open(path, mode) + if file then + if file:write(content) == nil then + return false + end + io.close(file) + return true + else + return false + end +end + +function io.pathinfo(path) + local pos = string_len(path) + local extpos = pos + 1 + while pos > 0 do + local b = string_byte(path, pos) + if b == 46 then -- 46 = char "." + extpos = pos + elseif b == 47 then -- 47 = char "/" + break + end + pos = pos - 1 + end + + local dirname = string_sub(path, 1, pos) + local filename = string_sub(path, pos + 1) + + extpos = extpos - pos + local basename = string_sub(filename, 1, extpos - 1) + local extname = string_sub(filename, extpos) + + return { + dirname = dirname, + filename = filename, + basename = basename, + extname = extname + } +end + +function io.filesize(path) + local size = false + local file = io.open(path, "r") + if file then + local current = file:seek() + size = file:seek("end") + file:seek("set", current) + io.close(file) + end + return size +end diff --git a/src/server/base/Factory.lua b/src/framework/math.lua similarity index 63% rename from src/server/base/Factory.lua rename to src/framework/math.lua index 7bf0138..08f909d 100644 --- a/src/server/base/Factory.lua +++ b/src/framework/math.lua @@ -22,25 +22,38 @@ THE SOFTWARE. ]] -local Factory = class("Factory") +local math_ceil = math.ceil +local math_floor = math.floor +local ok, socket = pcall(function() + return require("socket") +end) + +function math.round(value) + value = tonumber(value) or 0 + return math_floor(value + 0.5) +end -function Factory.create(config, classNamePrefix, ...) - local path = config.appRootPath .. "/?.lua;" - if not string.find(package.path, path, 1, true) then - package.path = path .. package.path +function math.trunc(x) + if x <= 0 then + return math_ceil(x) end - - local tagretClass - local ok, _tagretClass = pcall(require, classNamePrefix) - if ok then - tagretClass = _tagretClass + if math_ceil(x) == x then + x = math_ceil(x) + else + x = math_ceil(x) - 1 end + return x +end - if not tagretClass then - tagretClass = require("server.base." .. classNamePrefix .. "Base") +function math.newrandomseed() + if socket then + math.randomseed(socket.gettime() * 1000) + else + math.randomseed(os.time()) end - return tagretClass:create(config, ...) + math.random() + math.random() + math.random() + math.random() end - -return Factory diff --git a/src/framework/os.lua b/src/framework/os.lua new file mode 100644 index 0000000..8f96363 --- /dev/null +++ b/src/framework/os.lua @@ -0,0 +1,45 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +function os.gettimezone() + local now = os.time() + return os.difftime(now, os.time(os.date("!*t", now))) / 3600 +end + +function os.gettime(date, utc) + local time = os.time({ + year = date[1], + month = date[2], + day = date[3], + hour = date[4], + min = date[5], + sec = date[6], + }) + if utc ~= false then + local now = os.time() + local offset = os.difftime(now, os.time(os.date("!*t", now))) + time = time + offset + end + return time +end diff --git a/src/framework/package_support.lua b/src/framework/package_support.lua deleted file mode 100644 index 0f313bc..0000000 --- a/src/framework/package_support.lua +++ /dev/null @@ -1,117 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local assert = assert -local type = type -local ipairs = ipairs -local string_format = string.format - -cc.loaded_packages = {} -local loaded_packages = cc.loaded_packages - -function cc.register(name, package) - cc.loaded_packages[name] = package -end - -function cc.load(...) - local names = {...} - assert(#names > 0, "cc.load() - invalid package names") - - local packages = {} - for _, name in ipairs(names) do - assert(type(name) == "string", string_format("cc.load() - invalid package name \"%s\"", tostring(name))) - if not loaded_packages[name] then - local packageName = string_format("packages.%s.init", name) - local cls = require(packageName) - assert(cls, string_format("cc.load() - package class \"%s\" load failed", packageName)) - loaded_packages[name] = cls - - if DEBUG > 1 then - printInfo("cc.load() - load module \"packages.%s.init\"", name) - end - end - packages[#packages + 1] = loaded_packages[name] - end - return unpack(packages) -end - -local load_ = cc.load -local bind_ -bind_ = function(target, ...) - local t = type(target) - assert(t == "table" or t == "userdata", string_format("cc.bind() - invalid target, expected is object, actual is %s", t)) - local names = {...} - assert(#names > 0, "cc.bind() - package names expected") - - load_(...) - if not target.components_ then target.components_ = {} end - for _, name in ipairs(names) do - assert(type(name) == "string" and name ~= "", string_format("cc.bind() - invalid package name \"%s\"", name)) - if not target.components_[name] then - local cls = loaded_packages[name] - for __, depend in ipairs(cls.depends or {}) do - if not target.components_[depend] then - bind_(target, depend) - end - end - local component = cls:create() - target.components_[name] = component - component:bind(target) - end - end - - return target -end -cc.bind = bind_ - -function cc.unbind(target, ...) - if not target.components_ then return end - - local names = {...} - assert(#names > 0, "cc.unbind() - invalid package names") - - for _, name in ipairs(names) do - assert(type(name) == "string" and name ~= "", string_format("cc.unbind() - invalid package name \"%s\"", name)) - local component = target.components_[name] - assert(component, string_format("cc.unbind() - component \"%s\" not found", tostring(name))) - component:unbind(target) - target.components_[name] = nil - end - return target -end - -function cc.setmethods(target, component, methods) - for _, name in ipairs(methods) do - local method = component[name] - target[name] = function(__, ...) - return method(component, ...) - end - end -end - -function cc.unsetmethods(target, methods) - for _, name in ipairs(methods) do - target[name] = nil - end -end diff --git a/src/framework/packages/event/init.lua b/src/framework/packages/event/init.lua deleted file mode 100644 index 6ec1eea..0000000 --- a/src/framework/packages/event/init.lua +++ /dev/null @@ -1,180 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local Event = class("Event") - -local EXPORTED_METHODS = { - "addEventListener", - "dispatchEvent", - "removeEventListener", - "removeEventListenersByTag", - "removeEventListenersByEvent", - "removeAllEventListeners", - "hasEventListener", - "dumpAllEventListeners", -} - -function Event:init_() - self.target_ = nil - self.listeners_ = {} - self.nextListenerHandleIndex_ = 0 -end - -function Event:bind(target) - self:init_() - cc.setmethods(target, self, EXPORTED_METHODS) - self.target_ = target -end - -function Event:unbind(target) - cc.unsetmethods(target, EXPORTED_METHODS) - self:init_() -end - -function Event:on(eventName, listener, tag) - assert(type(eventName) == "string" and eventName ~= "", - "Event:addEventListener() - invalid eventName") - eventName = string.upper(eventName) - if self.listeners_[eventName] == nil then - self.listeners_[eventName] = {} - end - - self.nextListenerHandleIndex_ = self.nextListenerHandleIndex_ + 1 - local handle = tostring(self.nextListenerHandleIndex_) - tag = tag or "" - self.listeners_[eventName][handle] = {listener, tag} - - if DEBUG > 1 then - printInfo("%s [Event] addEventListener() - event: %s, handle: %s, tag: \"%s\"", - tostring(self.target_), eventName, handle, tostring(tag)) - end - - return self.target_, handle -end - -Event.addEventListener = Event.on - -function Event:dispatchEvent(event) - event.name = string.upper(tostring(event.name)) - local eventName = event.name - if DEBUG > 1 then - printInfo("%s [Event] dispatchEvent() - event %s", tostring(self.target_), eventName) - end - - if self.listeners_[eventName] == nil then return end - event.target = self.target_ - event.stop_ = false - event.stop = function(self) - self.stop_ = true - end - - for handle, listener in pairs(self.listeners_[eventName]) do - if DEBUG > 1 then - printInfo("%s [Event] dispatchEvent() - dispatching event %s to listener %s", tostring(self.target_), eventName, handle) - end - -- listener[1] = listener - -- listener[2] = tag - event.tag = listener[2] - listener[1](event) - if event.stop_ then - if DEBUG > 1 then - printInfo("%s [Event] dispatchEvent() - break dispatching for event %s", tostring(self.target_), eventName) - end - break - end - end - - return self.target_ -end - -function Event:removeEventListener(handleToRemove) - for eventName, listenersForEvent in pairs(self.listeners_) do - for handle, _ in pairs(listenersForEvent) do - if handle == handleToRemove then - listenersForEvent[handle] = nil - if DEBUG > 1 then - printInfo("%s [Event] removeEventListener() - remove listener [%s] for event %s", tostring(self.target_), handle, eventName) - end - return self.target_ - end - end - end - - return self.target_ -end - -function Event:removeEventListenersByTag(tagToRemove) - for eventName, listenersForEvent in pairs(self.listeners_) do - for handle, listener in pairs(listenersForEvent) do - -- listener[1] = listener - -- listener[2] = tag - if listener[2] == tagToRemove then - listenersForEvent[handle] = nil - if DEBUG > 1 then - printInfo("%s [Event] removeEventListener() - remove listener [%s] for event %s", tostring(self.target_), handle, eventName) - end - end - end - end - - return self.target_ -end - -function Event:removeEventListenersByEvent(eventName) - self.listeners_[string.upper(eventName)] = nil - if DEBUG > 1 then - printInfo("%s [Event] removeAllEventListenersForEvent() - remove all listeners for event %s", tostring(self.target_), eventName) - end - return self.target_ -end - -function Event:removeAllEventListeners() - self.listeners_ = {} - if DEBUG > 1 then - printInfo("%s [Event] removeAllEventListeners() - remove all listeners", tostring(self.target_)) - end - return self.target_ -end - -function Event:hasEventListener(eventName) - eventName = string.upper(tostring(eventName)) - local t = self.listeners_[eventName] - for _, __ in pairs(t) do - return true - end - return false -end - -function Event:dumpAllEventListeners() - print("---- Event:dumpAllEventListeners() ----") - for name, listeners in pairs(self.listeners_) do - printf("-- event: %s", name) - for handle, listener in pairs(listeners) do - printf("-- listener: %s, handle: %s", tostring(listener[1]), tostring(handle)) - end - end - return self.target_ -end - -return Event diff --git a/src/framework/string.lua b/src/framework/string.lua new file mode 100644 index 0000000..e87e5d0 --- /dev/null +++ b/src/framework/string.lua @@ -0,0 +1,75 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local string_find = string.find +local string_gsub = string.gsub +local string_len = string.len +local string_sub = string.sub +local string_upper = string.upper +local table_insert = table.insert +local tostring = tostring + +function string.split(input, delimiter) + input = tostring(input) + delimiter = tostring(delimiter) + if (delimiter == "") then return false end + local pos,arr = 1, {} + for st, sp in function() return string_find(input, delimiter, pos, true) end do + local str = string_sub(input, pos, st - 1) + if str ~= "" then + table_insert(arr, str) + end + pos = sp + 1 + end + if pos <= string_len(input) then + table_insert(arr, string_sub(input, pos)) + end + return arr +end + +local _TRIM_CHARS = " \t\n\r" + +function string.ltrim(input, chars) + chars = chars or _TRIM_CHARS + local pattern = "^[" .. chars .. "]+" + return string_gsub(input, pattern, "") +end + +function string.rtrim(input, chars) + chars = chars or _TRIM_CHARS + local pattern = "[" .. chars .. "]+$" + return string_gsub(input, pattern, "") +end + +function string.trim(input, chars) + chars = chars or _TRIM_CHARS + local pattern = "^[" .. chars .. "]+" + input = string_gsub(input, pattern, "") + pattern = "[" .. chars .. "]+$" + return string_gsub(input, pattern, "") +end + +function string.ucfirst(input) + return string_upper(string_sub(input, 1, 1)) .. string_sub(input, 2) +end diff --git a/src/framework/table.lua b/src/framework/table.lua new file mode 100644 index 0000000..8057215 --- /dev/null +++ b/src/framework/table.lua @@ -0,0 +1,120 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local string_format = string.format +local pairs = pairs + +local ok, table_new = pcall(require, "table.new") +if not ok or type(table_new) ~= "function" then + function table:new() + return {} + end +end + +local _copy +_copy = function(t, lookup) + if type(t) ~= "table" then + return t + elseif lookup[t] then + return lookup[t] + end + local n = {} + lookup[t] = n + for key, value in pairs(t) do + n[_copy(key, lookup)] = _copy(value, lookup) + end + return n +end + +function table.copy(t) + local lookup = {} + return _copy(t, lookup) +end + +function table.keys(hashtable) + local keys = {} + for k, v in pairs(hashtable) do + keys[#keys + 1] = k + end + return keys +end + +function table.values(hashtable) + local values = {} + for k, v in pairs(hashtable) do + values[#values + 1] = v + end + return values +end + +function table.merge(dest, src) + for k, v in pairs(src) do + dest[k] = v + end +end + +function table.map(t, fn) + local n = {} + for k, v in pairs(t) do + n[k] = fn(v, k) + end + return n +end + +function table.walk(t, fn) + for k,v in pairs(t) do + fn(v, k) + end +end + +function table.filter(t, fn) + local n = {} + for k, v in pairs(t) do + if fn(v, k) then + n[k] = v + end + end + return n +end + +function table.length(t) + local count = 0 + for _, __ in pairs(t) do + count = count + 1 + end + return count +end + +function table.readonly(t, name) + name = name or "table" + setmetatable(t, { + __newindex = function() + error(string_format("<%s:%s> is readonly table", name, tostring(t))) + end, + __index = function(_, key) + error(string_format("<%s:%s> not found key: %s", name, tostring(t), key)) + end + }) + return t +end diff --git a/src/lib/3rd/beanstalkd/haricot.lua b/src/lib/3rd/beanstalkd/haricot.lua deleted file mode 100644 index 6f14a8b..0000000 --- a/src/lib/3rd/beanstalkd/haricot.lua +++ /dev/null @@ -1,325 +0,0 @@ - -local socket = require "socket" - --- NOTES: --- `job` format: {id=...,data=...} - ---- low level - -local DEFAULT_PRI = 2 ^ 32 - -local default_cfg = function() - return { - max_job_size = 2^16, - } -end - -local is_posint = function(x) - return ( (type(x) == "number") and (math.floor(x) == x) and (x >= 0) ) -end - -local hyphen = string.byte("-") -local valid_name = function(x) - local n = #x - return ( - (type(x) == "string") and - (n > 0) and (n <= 200) and - (x:byte() ~= hyphen) and - x:match("^[%w-_+/;.$()]+$") - ) -end - -local getline = function(self) - return self.cnx:receive("*l") or "NOT_CONNECTED" -end - -local mkcmd = function(cmd,...) - return table.concat({cmd,...}," ") .. "\r\n" -end - -local call = function(self,cmd,...) - self.cnx:send(mkcmd(cmd,...)) - return getline(self) -end - -local recv = function(self,bytes) - assert(is_posint(bytes)) - local r = self.cnx:receive(bytes+2) - if r then - return r:sub(1,bytes) - else return nil end -end - -local expect_simple = function(res,s) - if res:match(string.format("^%s$",s)) then - return true - else - return false,res - end -end - -local expect_int = function(res,s) - local id = tonumber(res:match(string.format("^%s (%%d+)$",s))) - if id then - return true,id - else - return false,res - end -end - -local expect_data = function(self,res) - local bytes = tonumber(res:match("^OK (%d+)$")) - if bytes then - local data = recv(self,bytes) - if data then - assert(#data == bytes) - return true,data - else - return false,"NOT_CONNECTED" - end - else - return false,res - end -end - -local expect_job_body = function(self,bytes,id) - local data = recv(self,bytes) - if data then - assert(#data == bytes) - return true,{id=id,data=data} - else - return false,"NOT_CONNECTED" - end -end - ---- methods - --- connection - -local connect = function(self,server,port) - self.cnx = socket.tcp() - self.cnx:connect(server,port) - return true -end - --- producer - -local put = function(self,data,pri,delay,ttr) - pri = pri or DEFAULT_PRI - delay = delay or 0 - ttr = ttr or 120 - local bytes = #data - assert(bytes < self.cfg.max_job_size) - local cmd = mkcmd("put",pri,delay,ttr,bytes) .. data .. "\r\n" - self.cnx:send(cmd) - local res = getline(self) - return expect_int(res,"INSERTED") -end - -local use = function(self,tube) - assert(valid_name(tube)) - local res = call(self,"use",tube) - local ok = res:match("^USING ([%w-_+/;.$()]+)$") - ok = (ok == tube) - if ok then - return true - else - return false,res - end -end - --- consumer - -local reserve = function(self) - local res = call(self,"reserve") - local id,bytes = res:match("^RESERVED (%d+) (%d+)$") - if id --[[and bytes]] then - id,bytes = tonumber(id),tonumber(bytes) - return expect_job_body(self,bytes,id) - else - return false,res - end -end - -local reserve_with_timeout = function(self,timeout) - assert(is_posint(timeout)) - local res = call(self,"reserve-with-timeout",timeout) - local id,bytes = res:match("^RESERVED (%d+) (%d+)$") - if id --[[and bytes]] then - id,bytes = tonumber(id),tonumber(bytes) - return expect_job_body(self,bytes,id) - else - return expect_simple(res,"TIMED_OUT") - end -end - -local delete = function(self,id) - assert(is_posint(id)) - local res = call(self,"delete",id) - return expect_simple(res,"DELETED") -end - -local release = function(self,id,pri,delay) - assert(is_posint(id), "invalid id") - pri = pri or DEFAULT_PRI - delay = delay or 0 - local res = call(self,"release",id,pri,delay) - return(expect_simple(res,"RELEASED")) -end - -local bury = function(self,id,pri) - pri = pri or DEFAULT_PRI - 1 - assert( - is_posint(id) and - is_posint(pri) and (pri < 2^32) - ) - local res = call(self,"bury",id,pri) - return expect_simple(res,"BURIED") -end - -local touch = function(self,id) - assert(is_posint(id)) - local res = call(self,"touch",id) - return expect_simple(res,"TOUCHED") -end - -local watch = function(self,tube) - assert(valid_name(tube)) - local res = call(self,"watch",tube) - return expect_int(res,"WATCHING") -end - -local ignore = function(self,tube) - assert(valid_name(tube)) - local res = call(self,"ignore",tube) - return expect_int(res,"WATCHING") -end - --- other - -local _peek_result = function(self,res) -- private - local id,bytes = res:match("^FOUND (%d+) (%d+)$") - if id --[[and bytes]] then - id,bytes = tonumber(id),tonumber(bytes) - return expect_job_body(self,bytes,id) - else - return expect_simple(res,"NOT_FOUND") - end -end - -local peek = function(self,id) - assert(is_posint(id)) - local res = call(self,"peek",id) - return _peek_result(self,res) -end - -local make_peek = function(state) - return function(self) - local res = call(self,string.format("peek-%s",state)) - return _peek_result(self,res) - end -end - -local kick = function(self,bound) - assert(is_posint(bound)) - local res = call(self,"kick",bound) - return expect_int(res,"KICKED") -end - -local kick_job = function(self,id) - assert(is_posint(id)) - local res = call(self,"kick-job",id) - return expect_simple(res,"KICKED") -end - -local stats_job = function(self,id) - assert(is_posint(id)) - local res = call(self,"stats-job",id) - return expect_data(self,res) -end - -local stats_tube = function(self,tube) - assert(valid_name(tube)) - local res = call(self,"stats-tube",tube) - return expect_data(self,res) -end - -local stats = function(self) - local res = call(self,"stats") - return expect_data(self,res) -end - -local list_tubes = function(self) - local res = call(self,"list-tubes") - return expect_data(self,res) -end - -local list_tube_used = function(self) - local res = call(self,"list-tube-used") - local tube = res:match("^USING ([%w-_+/;.$()]+)$") - if tube then - return true,tube - else - return false,res - end -end - -local list_tubes_watched = function(self) - local res = call(self,"list-tubes-watched") - return expect_data(self,res) -end - -local quit = function(self) - self.cnx:send(mkcmd("quit")) - return true -end - -local pause_tube = function(self,tube,delay) - assert(valid_name(tube) and is_posint(delay)) - local res = call(self,"pause-tube",tube,delay) - return expect_simple(res,"PAUSED") -end - ---- class - -local methods = { - -- connection - connect = connect, -- (server,port) -> ok - -- producer - put = put, -- (pri,delay,ttr,data) -> ok,[id|err] - use = use, -- (tube) -> ok,[err] - -- consumer - reserve = reserve, -- () -> ok,[job|err] - reserve_with_timeout = reserve_with_timeout, -- () -> ok,[job|nil|err] - delete = delete, -- (id) -> ok,[err] - release = release, -- (id,pri,delay) -> ok,[err] - bury = bury, -- (id,pri) -> ok,[err] - touch = touch, -- (id) -> ok,[err] - watch = watch, -- (tube) -> ok,[count|err] - ignore = ignore, -- (tube) -> ok,[count|err] - -- other - peek = peek, -- (id) -> ok,[job|nil|err] - peek_ready = make_peek("ready"), -- () -> ok,[job|nil|err] - peek_delayed = make_peek("delayed"), -- () -> ok,[job|nil|err] - peek_buried = make_peek("buried"), -- () -> ok,[job|nil|err] - kick = kick, -- (bound) -> ok,[count|err] - kick_job = kick_job, -- (id) -> ok,[err] - stats_job = stats_job, -- (id) -> ok,[yaml|err] - stats_tube = stats_tube, -- (tube) -> ok,[yaml|err] - stats = stats, -- () -> ok,[yaml|err] - list_tubes = list_tubes, -- () -> ok,[yaml|err] - list_tube_used = list_tube_used, -- () -> ok,[tube|err] - list_tubes_watched = list_tubes_watched, -- () -> ok,[tube|err] - quit = quit, -- () -> ok - pause_tube = pause_tube, -- (tube,delay) -> ok,[err] -} - -local new = function(server,port) - local r = {cfg = default_cfg()} - connect(r,server,port) - return setmetatable(r,{__index = methods}) -end - -return { - new = new, -} diff --git a/src/lib/3rd/luasql/mysql.so b/src/lib/3rd/luasql/mysql.so deleted file mode 100644 index 4955d1c..0000000 Binary files a/src/lib/3rd/luasql/mysql.so and /dev/null differ diff --git a/src/lib/3rd/redis/redis_lua.lua b/src/lib/3rd/redis/redis_lua.lua deleted file mode 100644 index 29054f9..0000000 --- a/src/lib/3rd/redis/redis_lua.lua +++ /dev/null @@ -1,1145 +0,0 @@ - -local redis = { - _VERSION = 'redis-lua 2.0.5-dev', - _DESCRIPTION = 'A Lua client library for the redis key value storage system.', - _COPYRIGHT = 'Copyright (C) 2009-2012 Daniele Alessandri', -} - --- The following line is used for backwards compatibility in order to keep the `Redis` --- global module name. Using `Redis` is now deprecated so you should explicitly assign --- the module to a local variable when requiring it: `local redis = require('redis')`. --- Redis = redis - -local unpack = _G.unpack or table.unpack -local network, request, response = {}, {}, {} - -local defaults = { - host = '127.0.0.1', - port = 6379, - tcp_nodelay = true, - path = nil, -} - -local function merge_defaults(parameters) - if parameters == nil then - parameters = {} - end - for k, v in pairs(defaults) do - if parameters[k] == nil then - parameters[k] = defaults[k] - end - end - return parameters -end - -local function parse_boolean(v) - if v == '1' or v == 'true' or v == 'TRUE' then - return true - elseif v == '0' or v == 'false' or v == 'FALSE' then - return false - else - return nil - end -end - -local function toboolean(value) return value == 1 end - -local function sort_request(client, command, key, params) - --[[ params = { - by = 'weight_*', - get = 'object_*', - limit = { 0, 10 }, - sort = 'desc', - alpha = true, - } ]] - local query = { key } - - if params then - if params.by then - table.insert(query, 'BY') - table.insert(query, params.by) - end - - if type(params.limit) == 'table' then - -- TODO: check for lower and upper limits - table.insert(query, 'LIMIT') - table.insert(query, params.limit[1]) - table.insert(query, params.limit[2]) - end - - if params.get then - if (type(params.get) == 'table') then - for _, getarg in pairs(params.get) do - table.insert(query, 'GET') - table.insert(query, getarg) - end - else - table.insert(query, 'GET') - table.insert(query, params.get) - end - end - - if params.sort then - table.insert(query, params.sort) - end - - if params.alpha == true then - table.insert(query, 'ALPHA') - end - - if params.store then - table.insert(query, 'STORE') - table.insert(query, params.store) - end - end - - request.multibulk(client, command, query) -end - -local function zset_range_request(client, command, ...) - local args, opts = {...}, { } - - if #args >= 1 and type(args[#args]) == 'table' then - local options = table.remove(args, #args) - if options.withscores then - table.insert(opts, 'WITHSCORES') - end - end - - for _, v in pairs(opts) do table.insert(args, v) end - request.multibulk(client, command, args) -end - -local function zset_range_byscore_request(client, command, ...) - local args, opts = {...}, { } - - if #args >= 1 and type(args[#args]) == 'table' then - local options = table.remove(args, #args) - if options.limit then - table.insert(opts, 'LIMIT') - table.insert(opts, options.limit.offset or options.limit[1]) - table.insert(opts, options.limit.count or options.limit[2]) - end - if options.withscores then - table.insert(opts, 'WITHSCORES') - end - end - - for _, v in pairs(opts) do table.insert(args, v) end - request.multibulk(client, command, args) -end - -local function zset_range_reply(reply, command, ...) - local args = {...} - local opts = args[4] - if opts and (opts.withscores or string.lower(tostring(opts)) == 'withscores') then - local new_reply = { } - for i = 1, #reply, 2 do - table.insert(new_reply, { reply[i], reply[i + 1] }) - end - return new_reply - else - return reply - end -end - -local function zset_store_request(client, command, ...) - local args, opts = {...}, { } - - if #args >= 1 and type(args[#args]) == 'table' then - local options = table.remove(args, #args) - if options.weights and type(options.weights) == 'table' then - table.insert(opts, 'WEIGHTS') - for _, weight in ipairs(options.weights) do - table.insert(opts, weight) - end - end - if options.aggregate then - table.insert(opts, 'AGGREGATE') - table.insert(opts, options.aggregate) - end - end - - for _, v in pairs(opts) do table.insert(args, v) end - request.multibulk(client, command, args) -end - -local function mset_filter_args(client, command, ...) - local args, arguments = {...}, {} - if (#args == 1 and type(args[1]) == 'table') then - for k,v in pairs(args[1]) do - table.insert(arguments, k) - table.insert(arguments, v) - end - else - arguments = args - end - request.multibulk(client, command, arguments) -end - -local function hash_multi_request_builder(builder_callback) - return function(client, command, ...) - local args, arguments = {...}, { } - if #args == 2 then - table.insert(arguments, args[1]) - for k, v in pairs(args[2]) do - builder_callback(arguments, k, v) - end - else - arguments = args - end - request.multibulk(client, command, arguments) - end -end - -local function parse_info(response) - local info = {} - local current = info - - response:gsub('([^\r\n]*)\r\n', function(kv) - if kv == '' then return end - - local section = kv:match('^# (%w+)$') - if section then - current = {} - info[section:lower()] = current - return - end - - local k,v = kv:match(('([^:]*):([^:]*)'):rep(1)) - if k:match('db%d+') then - current[k] = {} - v:gsub(',', function(dbkv) - local dbk,dbv = kv:match('([^:]*)=([^:]*)') - current[k][dbk] = dbv - end) - else - current[k] = v - end - end) - - return info -end - -local function load_methods(proto, commands) - local client = setmetatable ({}, getmetatable(proto)) - - for cmd, fn in pairs(commands) do - if type(fn) ~= 'function' then - redis.error('invalid type for command ' .. cmd .. '(must be a function)') - end - client[cmd] = fn - end - - for i, v in pairs(proto) do - client[i] = v - end - - return client -end - -local function create_client(proto, client_socket, commands) - local client = load_methods(proto, commands) - client.error = redis.error - client.network = { - socket = client_socket, - read = network.read, - write = network.write, - } - client.requests = { - multibulk = request.multibulk, - } - return client -end - --- ############################################################################ - -function network.write(client, buffer) - local _, err = client.network.socket:send(buffer) - if err then client.error(err) end -end - -function network.read(client, len) - if len == nil then len = '*l' end - local line, err = client.network.socket:receive(len) - if not err then return line else client.error('connection error: ' .. err) end -end - --- ############################################################################ - -function response.read(client) - local payload = client.network.read(client) - local prefix, data = payload:sub(1, -#payload), payload:sub(2) - - -- status reply - if prefix == '+' then - if data == 'OK' then - return true - elseif data == 'QUEUED' then - return { queued = true } - else - return data - end - - -- error reply - elseif prefix == '-' then - return client.error('redis error: ' .. data) - - -- integer reply - elseif prefix == ':' then - local number = tonumber(data) - - if not number then - if res == 'nil' then - return nil - end - client.error('cannot parse '..res..' as a numeric response.') - end - - return number - - -- bulk reply - elseif prefix == '$' then - local length = tonumber(data) - - if not length then - client.error('cannot parse ' .. length .. ' as data length') - end - - if length == -1 then - return nil - end - - local nextchunk = client.network.read(client, length + 2) - - return nextchunk:sub(1, -3) - - -- multibulk reply - elseif prefix == '*' then - local count = tonumber(data) - - if count == -1 then - return nil - end - - local list = {} - if count > 0 then - local reader = response.read - for i = 1, count do - list[i] = reader(client) - end - end - return list - - -- unknown type of reply - else - return client.error('unknown response prefix: ' .. prefix) - end -end - --- ############################################################################ - -function request.raw(client, buffer) - local bufferType = type(buffer) - - if bufferType == 'table' then - client.network.write(client, table.concat(buffer)) - elseif bufferType == 'string' then - client.network.write(client, buffer) - else - client.error('argument error: ' .. bufferType) - end -end - -function request.multibulk(client, command, ...) - local args = {...} - local argsn = #args - local buffer = { true, true } - - if argsn == 1 and type(args[1]) == 'table' then - argsn, args = #args[1], args[1] - end - - buffer[1] = '*' .. tostring(argsn + 1) .. "\r\n" - buffer[2] = '$' .. #command .. "\r\n" .. command .. "\r\n" - - local table_insert = table.insert - for _, argument in pairs(args) do - local s_argument = tostring(argument) - table_insert(buffer, '$' .. #s_argument .. "\r\n" .. s_argument .. "\r\n") - end - - client.network.write(client, table.concat(buffer)) -end - --- ############################################################################ - -local function custom(command, send, parse) - command = string.upper(command) - return function(client, ...) - send(client, command, ...) - local reply = response.read(client) - - if type(reply) == 'table' and reply.queued then - reply.parser = parse - return reply - else - if parse then - return parse(reply, command, ...) - end - return reply - end - end -end - -local function command(command, opts) - if opts == nil or type(opts) == 'function' then - return custom(command, request.multibulk, opts) - else - return custom(command, opts.request or request.multibulk, opts.response) - end -end - -local define_command_impl = function(target, name, opts) - local opts = opts or {} - target[string.lower(name)] = custom( - opts.command or string.upper(name), - opts.request or request.multibulk, - opts.response or nil - ) -end - -local undefine_command_impl = function(target, name) - target[string.lower(name)] = nil -end - --- ############################################################################ - -local client_prototype = {} - -client_prototype.raw_cmd = function(client, buffer) - request.raw(client, buffer .. "\r\n") - return response.read(client) -end - --- obsolete -client_prototype.define_command = function(client, name, opts) - define_command_impl(client, name, opts) -end - --- obsolete -client_prototype.undefine_command = function(client, name) - undefine_command_impl(client, name) -end - -client_prototype.quit = function(client) - request.multibulk(client, 'QUIT') - client.network.socket:shutdown() - return true -end - -client_prototype.shutdown = function(client) - request.multibulk(client, 'SHUTDOWN') - client.network.socket:shutdown() -end - --- Command pipelining - -client_prototype.pipeline = function(client, block) - local requests, replies, parsers = {}, {}, {} - local table_insert = table.insert - local socket_write, socket_read = client.network.write, client.network.read - - client.network.write = function(_, buffer) - table_insert(requests, buffer) - end - - -- TODO: this hack is necessary to temporarily reuse the current - -- request -> response handling implementation of redis-lua - -- without further changes in the code, but it will surely - -- disappear when the new command-definition infrastructure - -- will finally be in place. - client.network.read = function() return '+QUEUED' end - - local pipeline = setmetatable({}, { - __index = function(env, name) - local cmd = client[name] - if not cmd then - client.error('unknown redis command: ' .. name, 2) - end - return function(self, ...) - local reply = cmd(client, ...) - table_insert(parsers, #requests, reply.parser) - return reply - end - end - }) - - local success, retval = pcall(block, pipeline) - - client.network.write, client.network.read = socket_write, socket_read - if not success then client.error(retval, 0) end - - client.network.write(client, table.concat(requests, '')) - - for i = 1, #requests do - local reply, parser = response.read(client), parsers[i] - if parser then - reply = parser(reply) - end - table_insert(replies, i, reply) - end - - return replies, #requests -end - --- Publish/Subscribe - -do - local channels = function(channels) - if type(channels) == 'string' then - channels = { channels } - end - return channels - end - - local subscribe = function(client, ...) - request.multibulk(client, 'subscribe', ...) - end - local psubscribe = function(client, ...) - request.multibulk(client, 'psubscribe', ...) - end - local unsubscribe = function(client, ...) - request.multibulk(client, 'unsubscribe') - end - local punsubscribe = function(client, ...) - request.multibulk(client, 'punsubscribe') - end - - local consumer_loop = function(client) - local aborting, subscriptions = false, 0 - - local abort = function() - if not aborting then - unsubscribe(client) - punsubscribe(client) - aborting = true - end - end - - return coroutine.wrap(function() - while true do - local message - local response = response.read(client) - - if response[1] == 'pmessage' then - message = { - kind = response[1], - pattern = response[2], - channel = response[3], - payload = response[4], - } - else - message = { - kind = response[1], - channel = response[2], - payload = response[3], - } - end - - if string.match(message.kind, '^p?subscribe$') then - subscriptions = subscriptions + 1 - end - if string.match(message.kind, '^p?unsubscribe$') then - subscriptions = subscriptions - 1 - end - - if aborting and subscriptions == 0 then - break - end - coroutine.yield(message, abort) - end - end) - end - - client_prototype.pubsub = function(client, subscriptions) - if type(subscriptions) == 'table' then - if subscriptions.subscribe then - subscribe(client, channels(subscriptions.subscribe)) - end - if subscriptions.psubscribe then - psubscribe(client, channels(subscriptions.psubscribe)) - end - end - return consumer_loop(client) - end -end - --- Redis transactions (MULTI/EXEC) - -do - local function identity(...) return ... end - local emptytable = {} - - local function initialize_transaction(client, options, block, queued_parsers) - local table_insert = table.insert - local coro = coroutine.create(block) - - if options.watch then - local watch_keys = {} - for _, key in pairs(options.watch) do - table_insert(watch_keys, key) - end - if #watch_keys > 0 then - client:watch(unpack(watch_keys)) - end - end - - local transaction_client = setmetatable({}, {__index=client}) - transaction_client.exec = function(...) - client.error('cannot use EXEC inside a transaction block') - end - transaction_client.multi = function(...) - coroutine.yield() - end - transaction_client.commands_queued = function() - return #queued_parsers - end - - assert(coroutine.resume(coro, transaction_client)) - - transaction_client.multi = nil - transaction_client.discard = function(...) - local reply = client:discard() - for i, v in pairs(queued_parsers) do - queued_parsers[i]=nil - end - coro = initialize_transaction(client, options, block, queued_parsers) - return reply - end - transaction_client.watch = function(...) - client.error('WATCH inside MULTI is not allowed') - end - setmetatable(transaction_client, { __index = function(t, k) - local cmd = client[k] - if type(cmd) == "function" then - local function queuey(self, ...) - local reply = cmd(client, ...) - assert((reply or emptytable).queued == true, 'a QUEUED reply was expected') - table_insert(queued_parsers, reply.parser or identity) - return reply - end - t[k]=queuey - return queuey - else - return cmd - end - end - }) - client:multi() - return coro - end - - local function transaction(client, options, coroutine_block, attempts) - local queued_parsers, replies = {}, {} - local retry = tonumber(attempts) or tonumber(options.retry) or 2 - local coro = initialize_transaction(client, options, coroutine_block, queued_parsers) - - local success, retval - if coroutine.status(coro) == 'suspended' then - success, retval = coroutine.resume(coro) - else - -- do not fail if the coroutine has not been resumed (missing t:multi() with CAS) - success, retval = true, 'empty transaction' - end - if #queued_parsers == 0 or not success then - client:discard() - assert(success, retval) - return replies, 0 - end - - local raw_replies = client:exec() - if not raw_replies then - if (retry or 0) <= 0 then - client.error("MULTI/EXEC transaction aborted by the server") - else - --we're not quite done yet - return transaction(client, options, coroutine_block, retry - 1) - end - end - - local table_insert = table.insert - for i, parser in pairs(queued_parsers) do - table_insert(replies, i, parser(raw_replies[i])) - end - - return replies, #queued_parsers - end - - client_prototype.transaction = function(client, arg1, arg2) - local options, block - if not arg2 then - options, block = {}, arg1 - elseif arg1 then --and arg2, implicitly - options, block = type(arg1)=="table" and arg1 or { arg1 }, arg2 - else - client.error("Invalid parameters for redis transaction.") - end - - if not options.watch then - watch_keys = { } - for i, v in pairs(options) do - if tonumber(i) then - table.insert(watch_keys, v) - options[i] = nil - end - end - options.watch = watch_keys - elseif not (type(options.watch) == 'table') then - options.watch = { options.watch } - end - - if not options.cas then - local tx_block = block - block = function(client, ...) - client:multi() - return tx_block(client, ...) --can't wrap this in pcall because we're in a coroutine. - end - end - - return transaction(client, options, block) - end -end - --- MONITOR context - -do - local monitor_loop = function(client) - local monitoring = true - - -- Tricky since the payload format changed starting from Redis 2.6. - local pattern = '^(%d+%.%d+)( ?.- ?) ?"(%a+)" ?(.-)$' - - local abort = function() - monitoring = false - end - - return coroutine.wrap(function() - client:monitor() - - while monitoring do - local message, matched - local response = response.read(client) - - local ok = response:gsub(pattern, function(time, info, cmd, args) - message = { - timestamp = tonumber(time), - client = info:match('%d+.%d+.%d+.%d+:%d+'), - database = tonumber(info:match('%d+')) or 0, - command = cmd, - arguments = args:match('.+'), - } - matched = true - end) - - if not matched then - client.error('Unable to match MONITOR payload: '..response) - end - - coroutine.yield(message, abort) - end - end) - end - - client_prototype.monitor_messages = function(client) - return monitor_loop(client) - end -end - --- ############################################################################ - -local function connect_tcp(socket, parameters) - local host, port = parameters.host, tonumber(parameters.port) - if parameters.timeout then - socket:settimeout(parameters.timeout, 't') - end - - local ok, err = socket:connect(host, port) - if not ok then - redis.error('could not connect to '..host..':'..port..' ['..err..']') - end - socket:setoption('tcp-nodelay', parameters.tcp_nodelay) - return socket -end - -local function connect_unix(socket, parameters) - local ok, err = socket:connect(parameters.path) - if not ok then - redis.error('could not connect to '..parameters.path..' ['..err..']') - end - return socket -end - -local function create_connection(parameters) - if parameters.socket then - return parameters.socket - end - - local perform_connection, socket - - if parameters.scheme == 'unix' then - perform_connection, socket = connect_unix, require('socket.unix') - assert(socket, 'your build of LuaSocket does not support UNIX domain sockets') - else - if parameters.scheme then - local scheme = parameters.scheme - assert(scheme == 'redis' or scheme == 'tcp', 'invalid scheme: '..scheme) - end - perform_connection, socket = connect_tcp, require('socket').tcp - end - - return perform_connection(socket(), parameters) -end - --- ############################################################################ - -function redis.error(message, level) - error(message, (level or 1) + 1) -end - -function redis.connect(...) - local args, parameters = {...}, nil - - if #args == 1 then - if type(args[1]) == 'table' then - parameters = args[1] - else - local uri = require('socket.url') - parameters = uri.parse(select(1, ...)) - if parameters.scheme then - if parameters.query then - for k, v in parameters.query:gmatch('([-_%w]+)=([-_%w]+)') do - if k == 'tcp_nodelay' or k == 'tcp-nodelay' then - parameters.tcp_nodelay = parse_boolean(v) - elseif k == 'timeout' then - parameters.timeout = tonumber(v) - end - end - end - else - parameters.host = parameters.path - end - end - elseif #args > 1 then - local host, port, timeout = unpack(args) - parameters = { host = host, port = port, timeout = tonumber(timeout) } - end - - local commands = redis.commands or {} - if type(commands) ~= 'table' then - redis.error('invalid type for the commands table') - end - - local socket = create_connection(merge_defaults(parameters)) - local client = create_client(client_prototype, socket, commands) - - return client -end - -function redis.command(cmd, opts) - return command(cmd, opts) -end - --- obsolete -function redis.define_command(name, opts) - define_command_impl(redis.commands, name, opts) -end - --- obsolete -function redis.undefine_command(name) - undefine_command_impl(redis.commands, name) -end - --- ############################################################################ - --- Commands defined in this table do not take the precedence over --- methods defined in the client prototype table. - -redis.commands = { - -- commands operating on the key space - exists = command('EXISTS', { - response = toboolean - }), - del = command('DEL'), - type = command('TYPE'), - rename = command('RENAME'), - renamenx = command('RENAMENX', { - response = toboolean - }), - expire = command('EXPIRE', { - response = toboolean - }), - pexpire = command('PEXPIRE', { -- >= 2.6 - response = toboolean - }), - expireat = command('EXPIREAT', { - response = toboolean - }), - pexpireat = command('PEXPIREAT', { -- >= 2.6 - response = toboolean - }), - ttl = command('TTL'), - pttl = command('PTTL'), -- >= 2.6 - move = command('MOVE', { - response = toboolean - }), - dbsize = command('DBSIZE'), - persist = command('PERSIST', { -- >= 2.2 - response = toboolean - }), - keys = command('KEYS', { - response = function(response) - if type(response) == 'string' then - -- backwards compatibility path for Redis < 2.0 - local keys = {} - response:gsub('[^%s]+', function(key) - table.insert(keys, key) - end) - response = keys - end - return response - end - }), - randomkey = command('RANDOMKEY'), - sort = command('SORT', { - request = sort_request, - }), - - -- commands operating on string values - set = command('SET'), - setnx = command('SETNX', { - response = toboolean - }), - setex = command('SETEX'), -- >= 2.0 - psetex = command('PSETEX'), -- >= 2.6 - mset = command('MSET', { - request = mset_filter_args - }), - msetnx = command('MSETNX', { - request = mset_filter_args, - response = toboolean - }), - get = command('GET'), - mget = command('MGET'), - getset = command('GETSET'), - incr = command('INCR'), - incrby = command('INCRBY'), - incrbyfloat = command('INCRBYFLOAT', { -- >= 2.6 - response = function(reply, command, ...) - return tonumber(reply) - end, - }), - decr = command('DECR'), - decrby = command('DECRBY'), - append = command('APPEND'), -- >= 2.0 - substr = command('SUBSTR'), -- >= 2.0 - strlen = command('STRLEN'), -- >= 2.2 - setrange = command('SETRANGE'), -- >= 2.2 - getrange = command('GETRANGE'), -- >= 2.2 - setbit = command('SETBIT'), -- >= 2.2 - getbit = command('GETBIT'), -- >= 2.2 - bitop = command('BITOP'), -- >= 2.6 - bitcount = command('BITCOUNT'), -- >= 2.6 - - -- commands operating on lists - rpush = command('RPUSH'), - lpush = command('LPUSH'), - llen = command('LLEN'), - lrange = command('LRANGE'), - ltrim = command('LTRIM'), - lindex = command('LINDEX'), - lset = command('LSET'), - lrem = command('LREM'), - lpop = command('LPOP'), - rpop = command('RPOP'), - rpoplpush = command('RPOPLPUSH'), - blpop = command('BLPOP'), -- >= 2.0 - brpop = command('BRPOP'), -- >= 2.0 - rpushx = command('RPUSHX'), -- >= 2.2 - lpushx = command('LPUSHX'), -- >= 2.2 - linsert = command('LINSERT'), -- >= 2.2 - brpoplpush = command('BRPOPLPUSH'), -- >= 2.2 - - -- commands operating on sets - sadd = command('SADD'), - srem = command('SREM'), - spop = command('SPOP'), - smove = command('SMOVE', { - response = toboolean - }), - scard = command('SCARD'), - sismember = command('SISMEMBER', { - response = toboolean - }), - sinter = command('SINTER'), - sinterstore = command('SINTERSTORE'), - sunion = command('SUNION'), - sunionstore = command('SUNIONSTORE'), - sdiff = command('SDIFF'), - sdiffstore = command('SDIFFSTORE'), - smembers = command('SMEMBERS'), - srandmember = command('SRANDMEMBER'), - - -- commands operating on sorted sets - zadd = command('ZADD'), - zincrby = command('ZINCRBY', { - response = function(reply, command, ...) - return tonumber(reply) - end, - }), - zrem = command('ZREM'), - zrange = command('ZRANGE', { - request = zset_range_request, - response = zset_range_reply, - }), - zrevrange = command('ZREVRANGE', { - request = zset_range_request, - response = zset_range_reply, - }), - zrangebyscore = command('ZRANGEBYSCORE', { - request = zset_range_byscore_request, - response = zset_range_reply, - }), - zrevrangebyscore = command('ZREVRANGEBYSCORE', { -- >= 2.2 - request = zset_range_byscore_request, - response = zset_range_reply, - }), - zunionstore = command('ZUNIONSTORE', { -- >= 2.0 - request = zset_store_request - }), - zinterstore = command('ZINTERSTORE', { -- >= 2.0 - request = zset_store_request - }), - zcount = command('ZCOUNT'), - zcard = command('ZCARD'), - zscore = command('ZSCORE'), - zremrangebyscore = command('ZREMRANGEBYSCORE'), - zrank = command('ZRANK'), -- >= 2.0 - zrevrank = command('ZREVRANK'), -- >= 2.0 - zremrangebyrank = command('ZREMRANGEBYRANK'), -- >= 2.0 - - -- commands operating on hashes - hset = command('HSET', { -- >= 2.0 - response = toboolean - }), - hsetnx = command('HSETNX', { -- >= 2.0 - response = toboolean - }), - hmset = command('HMSET', { -- >= 2.0 - request = hash_multi_request_builder(function(args, k, v) - table.insert(args, k) - table.insert(args, v) - end), - }), - hincrby = command('HINCRBY'), -- >= 2.0 - hincrbyfloat = command('HINCRBYFLOAT', {-- >= 2.6 - response = function(reply, command, ...) - return tonumber(reply) - end, - }), - hget = command('HGET'), -- >= 2.0 - hmget = command('HMGET', { -- >= 2.0 - request = hash_multi_request_builder(function(args, k, v) - table.insert(args, v) - end), - }), - hdel = command('HDEL'), -- >= 2.0 - hexists = command('HEXISTS', { -- >= 2.0 - response = toboolean - }), - hlen = command('HLEN'), -- >= 2.0 - hkeys = command('HKEYS'), -- >= 2.0 - hvals = command('HVALS'), -- >= 2.0 - hgetall = command('HGETALL', { -- >= 2.0 - response = function(reply, command, ...) - local new_reply = { } - for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end - return new_reply - end - }), - - -- connection related commands - ping = command('PING', { - response = function(response) return response == 'PONG' end - }), - echo = command('ECHO'), - auth = command('AUTH'), - select = command('SELECT'), - - -- transactions - multi = command('MULTI'), -- >= 2.0 - exec = command('EXEC'), -- >= 2.0 - discard = command('DISCARD'), -- >= 2.0 - watch = command('WATCH'), -- >= 2.2 - unwatch = command('UNWATCH'), -- >= 2.2 - - -- publish - subscribe - subscribe = command('SUBSCRIBE'), -- >= 2.0 - unsubscribe = command('UNSUBSCRIBE'), -- >= 2.0 - psubscribe = command('PSUBSCRIBE'), -- >= 2.0 - punsubscribe = command('PUNSUBSCRIBE'), -- >= 2.0 - publish = command('PUBLISH'), -- >= 2.0 - - -- redis scripting - eval = command('EVAL'), -- >= 2.6 - evalsha = command('EVALSHA'), -- >= 2.6 - script = command('SCRIPT'), -- >= 2.6 - - -- remote server control commands - bgrewriteaof = command('BGREWRITEAOF'), - config = command('CONFIG', { -- >= 2.0 - response = function(reply, command, ...) - if (type(reply) == 'table') then - local new_reply = { } - for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end - return new_reply - end - - return reply - end - }), - client = command('CLIENT'), -- >= 2.4 - slaveof = command('SLAVEOF'), - save = command('SAVE'), - bgsave = command('BGSAVE'), - lastsave = command('LASTSAVE'), - flushdb = command('FLUSHDB'), - flushall = command('FLUSHALL'), - monitor = command('MONITOR'), - time = command('TIME'), -- >= 2.6 - slowlog = command('SLOWLOG', { -- >= 2.2.13 - response = function(reply, command, ...) - if (type(reply) == 'table') then - local structured = { } - for index, entry in ipairs(reply) do - structured[index] = { - id = tonumber(entry[1]), - timestamp = tonumber(entry[2]), - duration = tonumber(entry[3]), - command = entry[4], - } - end - return structured - end - - return reply - end - }), - info = command('INFO', { - response = parse_info, - }), -} - --- ############################################################################ - -return redis diff --git a/src/lib/resty/README.md b/src/lib/resty/README.md deleted file mode 100644 index 74c57c0..0000000 --- a/src/lib/resty/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# 3rd party resty lib - -- http.lua & url.lua: https://github.com/liseen/lua-resty-http -- beastalkd.lua: https://github.com/smallfish/lua-resty-beanstalkd diff --git a/src/lib/resty/beanstalkd.lua b/src/lib/resty/beanstalkd.lua deleted file mode 100644 index 321b7e3..0000000 --- a/src/lib/resty/beanstalkd.lua +++ /dev/null @@ -1,248 +0,0 @@ --- Copyright (C) 2012 Chen "smallfish" Xiaoyu (陈小玉) - -local tcp = ngx.socket.tcp -local strlen = string.len -local strsub = string.sub -local strmatch = string.match -local tabconcat = table.concat - -local _M = {} - -_M.VERSION = "0.04" - -local mt = { - __index = _M, - -- to prevent use of casual module global variables - __newindex = function(table, key, val) - error('attempt to write to undeclared variable "' .. key .. '"') - end, -} - -function _M.new(self) - local sock, err = tcp() - if not sock then - return nil, err - end - return setmetatable({sock = sock}, mt) -end - -function _M.set_timeout(self, timeout) - local sock = self.sock - if not sock then - return nil, "not initialized" - end - return sock:settimeout(timeout) -end - -function _M.set_keepalive(self, ...) - local sock = self.sock - if not sock then - return nil, "not initialized" - end - return sock:setkeepalive(...) -end - -function _M.connect(self, host, port, ...) - local sock = self.sock - if not sock then - return nil, "not initialized" - end - host = host or "127.0.0.1" - port = port or 11300 - return sock:connect(host, port, ...) -end - -function _M.use(self, tube) - local sock = self.sock - if not sock then - return nil, "not initialized" - end - local cmd = {"use", " ", tube, "\r\n"} - local bytes, err = sock:send(tabconcat(cmd)) - if not bytes then - return nil, "failed to use tube, send data error: " .. err - end - local line, err = sock:receive() - if not line then - return nil, "failed to use tube, receive data error: " .. err - end - return line -end - -function _M.watch(self, tube) - local sock = self.sock - if not sock then - return nil, "not initialized" - end - local cmd = {"watch", " ", tube, "\r\n"} - local bytes, err = sock:send(tabconcat(cmd)) - if not bytes then - return nil, "failed to watch tube, send data error: " .. err - end - local line, err = sock:receive() - if not line then - return nil, "failed to watch tube, receive data error: " .. err - end - local size = strmatch(line, "^WATCHING (%d+)$") - if size then - return size, line - end - return 0, line -end - -function _M.put(self, body, pri, delay, ttr) - local sock = self.sock - if not sock then - return nil, "not initialized" - end - pri = pri or 2 ^ 32 - delay = delay or 0 - ttr = ttr or 120 - local cmd = {"put", " ", pri, " ", delay, " ", ttr, " ", strlen(body), "\r\n", body, "\r\n"} - local bytes, err = sock:send(tabconcat(cmd)) - if not bytes then - return nil, "failed to put, send data error: " .. err - end - local line, err = sock:receive() - if not line then - return nil, "failed to put, receive data error:" .. err - end - local id = strmatch(line, " (%d+)$") - if id then - return id, line - end - return nil, line -end - -function _M.delete(self, id) - local sock = self.sock - if not sock then - return nil, "not initialized" - end - local cmd = {"delete", " ", id, "\r\n"} - local bytes, err = sock:send(tabconcat(cmd)) - if not bytes then - return nil, "failed to delete, send data error: " .. err - end - local line, err = sock:receive() - if not line then - return nil, "failed to delete, receive data error: " .. err - end - if line == "DELETED" then - return true, line - end - return false, line -end - -function _M.reserve(self, timeout) - local sock = self.sock - local cmd = {"reserve", "\r\n"} - if timeout then - cmd = {"reserve-with-timeout", " ", timeout, "\r\n"} - end - local bytes, err = sock:send(tabconcat(cmd)) - if not bytes then - return nil, "failed to reserve, send data error: " .. err - end - local line, err = sock:receive() - if not line then - return nil, "failed to reserve, receive data error: " .. err - end - local id, size = strmatch(line, "^RESERVED (%d+) (%d+)$") - if id and size then -- remove \r\n - local data, err = sock:receive(size+2) - return id, strsub(data, 1, -3) - end - return false, line -end - -function _M.release(self, id, pri, delay) - local sock = self.sock - if not sock then - return nil, "not initialized" - end - pri = pri or 2 ^ 32 - delay = delay or 0 - local cmd = {"release", " ", id, " ", pri, " ", delay, "\r\n"} - local bytes, err = sock:send(tabconcat(cmd)) - if not bytes then - return nil, "failed to release, send data error: " .. err - end - local line, err = sock:receive() - if not line then - return nil, "failed to release, receive data error: " .. err - end - if line == "RELEASED" then - return true, line - end - return false, line -end - -function _M.bury(self, id, pri) - local sock = self.sock - if not sock then - return nil, "not initialized" - end - pri = pri or 2 ^ 32 - local cmd = {"bury", " ", id, " ", pri, "\r\n"} - local bytes, err = sock:send(tabconcat(cmd)) - if not bytes then - return nil, "failed to release, send data error: " .. err - end - local line, err = sock:receive() - if not line then - return nil, "failed to release, receive data error: " .. err - end - if line == "BURIED" then - return true, line - end - return false, line -end - -function _M.kick(self, bound) - local sock = self.sock - if not sock then - return nil, "not initialized" - end - local cmd = {"kick", " ", bound, "\r\n"} - local bytes, err = sock:send(tabconcat(cmd)) - if not bytes then - return nil, "failed to release, send data error: " .. err - end - local line, err = sock:receive() - if not line then - return nil, "failed to release, receive data error: " .. err - end - local count = strmatch(line, "^KICKED (%d+)$") - return count, nil -end - -function _M.peek(self, id) - local sock = self.sock - local cmd = {"peek", " ", id, "\r\n"} - local bytes, err = sock:send(tabconcat(cmd)) - if not bytes then - return nil, "failed to peek, send data error: " .. err - end - local line, err = sock:receive() - if not line then - return nil, "failed to peek, receive data error: " .. err - end - local id, size = strmatch(line, "^FOUND (%d+) (%d+)$") - if id and size then -- remove \r\n - local data, err = sock:receive(size+2) - return id, strsub(data, 1, -3) - end - return false, line -end - -function _M.close(self) - local sock = self.sock - if not sock then - return nil, "not initialized" - end - sock:send("quit\r\n") - return sock:close() -end - -return _M diff --git a/src/lib/resty/http.lua b/src/lib/resty/http.lua deleted file mode 100644 index 60ad7e3..0000000 --- a/src/lib/resty/http.lua +++ /dev/null @@ -1,416 +0,0 @@ -module(..., package.seeall) - -_VERSION = '0.2' - --- constants --- connection timeout in seconds -local TIMEOUT = 60 --- default port for document retrieval -local PORT = 80 --- user agent field sent in request -local USERAGENT = 'resty.http/' .. _VERSION - --- default url parts -local default = { - host = "", - port = PORT, - path ="/", - scheme = "http" -} - - --- global variables -local url = require("3rd.url") - -local mt = { __index = package.loaded[...] } - -local tcp = ngx.socket.tcp -local base64 = ngx.encode_base64 - - -local function adjusturi(reqt) - local u = reqt - -- if there is a proxy, we need the full url. otherwise, just a part. - if not reqt.proxy and not PROXY then - u = { - path = reqt.path, - params = reqt.params, - query = reqt.query, - fragment = reqt.fragment - } - end - return url.build(u) -end - - -local function adjustheaders(reqt) - -- default headers - local lower = { - ["user-agent"] = USERAGENT, - ["host"] = reqt.host, - ["connection"] = "close, TE", - ["te"] = "trailers" - } - -- if we have authentication information, pass it along - if reqt.user and reqt.password then - lower["authorization"] = - "Basic " .. (base64(reqt.user .. ":" .. reqt.password)) - end - -- override with user headers - for i,v in pairs(reqt.headers or lower) do - lower[string.lower(i)] = v - end - return lower -end - - -local function adjustproxy(reqt) - local proxy = reqt.proxy or PROXY - if proxy then - proxy = url.parse(proxy) - return proxy.host, proxy.port or 3128 - else - return reqt.host, reqt.port - end -end - - -local function adjustrequest(reqt) - -- parse url if provided - local nreqt = reqt.url and url.parse(reqt.url, default) or {} - -- explicit components override url - for i,v in pairs(reqt) do nreqt[i] = v end - - if nreqt.port == "" then nreqt.port = 80 end - - -- compute uri if user hasn't overriden - nreqt.uri = reqt.uri or adjusturi(nreqt) - -- ajust host and port if there is a proxy - nreqt.host, nreqt.port = adjustproxy(nreqt) - -- adjust headers in request - nreqt.headers = adjustheaders(nreqt) - - nreqt.timeout = reqt.timeout or TIMEOUT * 1000; - - nreqt.fetch_size = reqt.fetch_size or 16*1024 -- 16k - nreqt.max_body_size = reqt.max_body_size or 1024*1024*1024 -- 1024mb - - if reqt.keepalive then - nreqt.headers['connection'] = 'keep-alive' - end - - return nreqt -end - - -local function receivestatusline(sock) - local status_reader = sock:receiveuntil("\r\n") - - local data, err, partial = status_reader() - if not data then - return nil, "read status line failed " .. err - end - - local t1, t2, code = string.find(data, "HTTP/%d*%.%d* (%d%d%d)") - - return tonumber(code), data -end - - -local function receiveheaders(sock, headers) - local line, name, value, err, tmp1, tmp2 - headers = headers or {} - -- get first line - line, err = sock:receive() - if err then return nil, err end - -- headers go until a blank line is found - while line ~= "" do - -- get field-name and value - tmp1, tmp2, name, value = string.find(line, "^(.-):%s*(.*)") - if not (name and value) then return nil, "malformed reponse headers" end - name = string.lower(name) - -- get next line (value might be folded) - line, err = sock:receive() - if err then return nil, err end - -- unfold any folded values - while string.find(line, "^%s") do - value = value .. line - line = sock:receive() - if err then return nil, err end - end - -- save pair in table - if headers[name] then - if name == "set-cookie" then - headers[name] = headers[name] .. "," .. value - else - headers[name] = headers[name] .. ", " .. value - end - else headers[name] = value end - end - return headers -end - -local function read_body_data(sock, size, fetch_size, callback) - local p_size = fetch_size - while size and size > 0 do - if size < p_size then - p_size = size - end - local data, err, partial = sock:receive(p_size) - if not err then - if data then - callback(data) - end - elseif err == "closed" then - if partial then - callback(partial) - end - return 1 -- 'closed' - else - return nil, err - end - size = size - p_size - end - return 1 -end - -local function receivebody(sock, headers, nreqt) - local t = headers["transfer-encoding"] -- shortcut - local body = '' - local callback = nreqt.body_callback - if not callback then - local function bc(data, chunked_header, ...) - if chunked_header then return end - body = body .. data - end - callback = bc - end - if t and t ~= "identity" then - -- chunked - while true do - local chunk_header = sock:receiveuntil("\r\n") - local data, err, partial = chunk_header() - if not err then - if data == "0" then - return body -- end of chunk - else - local length = tonumber(data, 16) - - -- TODO check nreqt.max_body_size !! - - local ok, err = read_body_data(sock,length, nreqt.fetch_size, callback) - if err then - return nil,err - end - end - end - end - elseif headers["content-length"] ~= nil and tonumber(headers["content-length"]) >= 0 then - -- content length - local length = tonumber(headers["content-length"]) - if length > nreqt.max_body_size then - ngx.log(ngx.INFO, 'content-length > nreqt.max_body_size !! Tail it !') - length = nreqt.max_body_size - end - - local ok, err = read_body_data(sock,length, nreqt.fetch_size, callback) - if not ok then - return nil,err - end - else - -- connection close - local ok, err = read_body_data(sock,nreqt.max_body_size, nreqt.fetch_size, callback) - if not ok then - return nil,err - end - end - return body -end - -local function shouldredirect(reqt, code, headers) - return headers.location and - string.gsub(headers.location, "%s", "") ~= "" and - (reqt.redirect ~= false) and - (code == 301 or code == 302) and - (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") - and (not reqt.nredirects or reqt.nredirects < 5) -end - - -local function shouldreceivebody(reqt, code) - if reqt.method == "HEAD" then return nil end - if code == 204 or code == 304 then return nil end - if code >= 100 and code < 200 then return nil end - return 1 -end - - -function new(self) - return setmetatable({}, mt) -end - -function request(self, reqt) - local code, headers, status, body, bytes, ok, err - - local nreqt = adjustrequest(reqt) - - local sock = tcp() - if not sock then - return nil, "create sock failed" - end - - sock:settimeout(nreqt.timeout) - - -- connect - ok, err = sock:connect(nreqt.host, nreqt.port) - if err then - return nil, "sock connected failed " .. err - end - - -- check type of req_body, maybe string, file, function - local req_body = nreqt.body - local req_body_type = nil - if req_body then - req_body_type = type(req_body) - if req_body_type == 'string' then -- fixed Content-Length - nreqt.headers['content-length'] = #req_body - end - end - - -- send request line and headers - local reqline = string.format("%s %s HTTP/1.1\r\n", nreqt.method or "GET", nreqt.uri) - local h = "" - for i, v in pairs(nreqt.headers) do - -- fix cookie is a table value - if type(v) == "table" then - if i == "cookie" then - v = table.concat(v, "; ") - else - v = table.concat(v, ", ") - end - end - h = i .. ": " .. v .. "\r\n" .. h - end - - h = h .. '\r\n' -- close headers - - bytes, err = sock:send(reqline .. h) - if err then - sock:close() - return nil, err - end - - -- send req_body, if exists - if req_body_type == 'string' then - bytes, err = sock:send(req_body) - if err then - sock:close() - return nil, err - end - elseif req_body_type == 'file' then - local buf = nil - while true do -- TODO chunked maybe better - buf = req_body:read(8192) - if not buf then break end - bytes, err = sock:send(buf) - if err then - sock:close() - return nil, err - end - end - elseif req_body_type == 'function' then - err = req_body(sock) -- as callback(sock) - if err then - return err - end - end - - -- receive status line - code, status = receivestatusline(sock) - if not code then - sock:close() - if not status then - return nil, "read status line failed " - else - return nil, "read status line failed " .. status - end - end - - -- ignore any 100-continue messages - while code == 100 do - headers, err = receiveheaders(sock, {}) - code, status = receivestatusline(sock) - end - - -- notify code_callback - if nreqt.code_callback then - nreqt.code_callback(code) - end - - -- receive headers - headers, err = receiveheaders(sock, {}) - if err then - sock:close() - return nil, "read headers failed " .. err - end - - -- notify header_callback - if nreqt.header_callback then - nreqt.header_callback(headers) - end - - -- TODO rediret check - - -- receive body - if shouldreceivebody(nreqt, code) then - body, err = receivebody(sock, headers, nreqt) - if err then - sock:close() - if code == 200 then - return 1, code, headers, status, nil - end - return nil, "read body failed " .. err - end - end - - if nreqt.keepalive then - sock:setkeepalive(nreqt.keepalive) - else - sock:close() - end - - return 1, code, headers, status, body -end - -function proxy_pass(self, reqt) - local nreqt = {} - for i,v in pairs(reqt) do nreqt[i] = v end - - if not nreqt.code_callback then - nreqt.code_callback = function(code, ...) - ngx.status = code - end - end - - if not nreqt.header_callback then - nreqt.header_callback = function (headers, ...) - for i, v in pairs(headers) do - ngx.header[i] = v - end - end - end - - if not nreqt.body_callback then - nreqt.body_callback = function (data, ...) - ngx.print(data) -- Will auto package as chunked format!! - end - end - return request(self, nreqt) -end - --- to prevent use of casual module global variables -getmetatable(package.loaded[...]).__newindex = function (table, key, val) - error('attempt to write to undeclared variable "' .. key .. '": ' - .. debug.traceback()) -end - diff --git a/src/lib/resty/url.lua b/src/lib/resty/url.lua deleted file mode 100644 index 5eecfdd..0000000 --- a/src/lib/resty/url.lua +++ /dev/null @@ -1,297 +0,0 @@ ------------------------------------------------------------------------------ --- URI parsing, composition and relative URL resolution --- LuaSocket toolkit. --- Author: Diego Nehab --- RCS ID: $Id: url.lua,v 1.38 2006/04/03 04:45:42 diego Exp $ ------------------------------------------------------------------------------ - ------------------------------------------------------------------------------ --- Declare module ------------------------------------------------------------------------------ -local string = require("string") -local base = _G -local table = require("table") -module(..., package.seeall) - ------------------------------------------------------------------------------ --- Module version ------------------------------------------------------------------------------ -_VERSION = "URL 1.0.1" - ------------------------------------------------------------------------------ --- Encodes a string into its escaped hexadecimal representation --- Input --- s: binary string to be encoded --- Returns --- escaped representation of string binary ------------------------------------------------------------------------------ -function escape(s) - return string.gsub(s, "([^A-Za-z0-9_])", function(c) - return string.format("%%%02x", string.byte(c)) - end) -end - ------------------------------------------------------------------------------ --- Protects a path segment, to prevent it from interfering with the --- url parsing. --- Input --- s: binary string to be encoded --- Returns --- escaped representation of string binary ------------------------------------------------------------------------------ -local function make_set(t) - local s = {} - for i,v in base.ipairs(t) do - s[t[i]] = 1 - end - return s -end - --- these are allowed withing a path segment, along with alphanum --- other characters must be escaped -local segment_set = make_set { - "-", "_", ".", "!", "~", "*", "'", "(", - ")", ":", "@", "&", "=", "+", "$", ",", -} - -local function protect_segment(s) - return string.gsub(s, "([^A-Za-z0-9_])", function (c) - if segment_set[c] then return c - else return string.format("%%%02x", string.byte(c)) end - end) -end - ------------------------------------------------------------------------------ --- Encodes a string into its escaped hexadecimal representation --- Input --- s: binary string to be encoded --- Returns --- escaped representation of string binary ------------------------------------------------------------------------------ -function unescape(s) - return string.gsub(s, "%%(%x%x)", function(hex) - return string.char(base.tonumber(hex, 16)) - end) -end - ------------------------------------------------------------------------------ --- Builds a path from a base path and a relative path --- Input --- base_path --- relative_path --- Returns --- corresponding absolute path ------------------------------------------------------------------------------ -local function absolute_path(base_path, relative_path) - if string.sub(relative_path, 1, 1) == "/" then return relative_path end - local path = string.gsub(base_path, "[^/]*$", "") - path = path .. relative_path - path = string.gsub(path, "([^/]*%./)", function (s) - if s ~= "./" then return s else return "" end - end) - path = string.gsub(path, "/%.$", "/") - local reduced - while reduced ~= path do - reduced = path - path = string.gsub(reduced, "([^/]*/%.%./)", function (s) - if s ~= "../../" then return "" else return s end - end) - end - path = string.gsub(reduced, "([^/]*/%.%.)$", function (s) - if s ~= "../.." then return "" else return s end - end) - return path -end - ------------------------------------------------------------------------------ --- Parses a url and returns a table with all its parts according to RFC 2396 --- The following grammar describes the names given to the URL parts --- ::= :///;?# --- ::= @: --- ::= [:] --- :: = {/} --- Input --- url: uniform resource locator of request --- default: table with default values for each field --- Returns --- table with the following fields, where RFC naming conventions have --- been preserved: --- scheme, authority, userinfo, user, password, host, port, --- path, params, query, fragment --- Obs: --- the leading '/' in {/} is considered part of ------------------------------------------------------------------------------ -function parse(url, default) - -- initialize default parameters - local parsed = {} - for i,v in base.pairs(default or parsed) do parsed[i] = v end - -- empty url is parsed to nil - if not url or url == "" then return nil, "invalid url" end - -- remove whitespace - -- url = string.gsub(url, "%s", "") - -- get fragment - url = string.gsub(url, "#(.*)$", function(f) - parsed.fragment = f - return "" - end) - -- get scheme - url = string.gsub(url, "^([%w][%w%+%-%.]*)%:", - function(s) parsed.scheme = s; return "" end) - -- get authority - url = string.gsub(url, "^//([^/]*)", function(n) - parsed.authority = n - return "" - end) - -- get query stringing - url = string.gsub(url, "%?(.*)", function(q) - parsed.query = q - return "" - end) - -- get params - url = string.gsub(url, "%;(.*)", function(p) - parsed.params = p - return "" - end) - -- path is whatever was left - if url ~= "" then parsed.path = url end - local authority = parsed.authority - if not authority then return parsed end - authority = string.gsub(authority,"^([^@]*)@", - function(u) parsed.userinfo = u; return "" end) - authority = string.gsub(authority, ":([^:]*)$", - function(p) parsed.port = p; return "" end) - if authority ~= "" then parsed.host = authority end - local userinfo = parsed.userinfo - if not userinfo then return parsed end - userinfo = string.gsub(userinfo, ":([^:]*)$", - function(p) parsed.password = p; return "" end) - parsed.user = userinfo - return parsed -end - ------------------------------------------------------------------------------ --- Rebuilds a parsed URL from its components. --- Components are protected if any reserved or unallowed characters are found --- Input --- parsed: parsed URL, as returned by parse --- Returns --- a stringing with the corresponding URL ------------------------------------------------------------------------------ -function build(parsed) - local ppath = parse_path(parsed.path or "") - local url = build_path(ppath) - if parsed.params then url = url .. ";" .. parsed.params end - if parsed.query then url = url .. "?" .. parsed.query end - local authority = parsed.authority - if parsed.host then - authority = parsed.host - if parsed.port then authority = authority .. ":" .. parsed.port end - local userinfo = parsed.userinfo - if parsed.user then - userinfo = parsed.user - if parsed.password then - userinfo = userinfo .. ":" .. parsed.password - end - end - if userinfo then authority = userinfo .. "@" .. authority end - end - if authority then url = "//" .. authority .. url end - if parsed.scheme then url = parsed.scheme .. ":" .. url end - if parsed.fragment then url = url .. "#" .. parsed.fragment end - -- url = string.gsub(url, "%s", "") - return url -end - ------------------------------------------------------------------------------ --- Builds a absolute URL from a base and a relative URL according to RFC 2396 --- Input --- base_url --- relative_url --- Returns --- corresponding absolute url ------------------------------------------------------------------------------ -function absolute(base_url, relative_url) - if base.type(base_url) == "table" then - base_parsed = base_url - base_url = build(base_parsed) - else - base_parsed = parse(base_url) - end - local relative_parsed = parse(relative_url) - if not base_parsed then return relative_url - elseif not relative_parsed then return base_url - elseif relative_parsed.scheme then return relative_url - else - relative_parsed.scheme = base_parsed.scheme - if not relative_parsed.authority then - relative_parsed.authority = base_parsed.authority - if not relative_parsed.path then - relative_parsed.path = base_parsed.path - if not relative_parsed.params then - relative_parsed.params = base_parsed.params - if not relative_parsed.query then - relative_parsed.query = base_parsed.query - end - end - else - relative_parsed.path = absolute_path(base_parsed.path or "", - relative_parsed.path) - end - end - return build(relative_parsed) - end -end - ------------------------------------------------------------------------------ --- Breaks a path into its segments, unescaping the segments --- Input --- path --- Returns --- segment: a table with one entry per segment ------------------------------------------------------------------------------ -function parse_path(path) - local parsed = {} - path = path or "" - --path = string.gsub(path, "%s", "") - string.gsub(path, "([^/]+)", function (s) table.insert(parsed, s) end) - for i = 1, #parsed do - parsed[i] = unescape(parsed[i]) - end - if string.sub(path, 1, 1) == "/" then parsed.is_absolute = 1 end - if string.sub(path, -1, -1) == "/" then parsed.is_directory = 1 end - return parsed -end - ------------------------------------------------------------------------------ --- Builds a path component from its segments, escaping protected characters. --- Input --- parsed: path segments --- unsafe: if true, segments are not protected before path is built --- Returns --- path: corresponding path stringing ------------------------------------------------------------------------------ -function build_path(parsed, unsafe) - local path = "" - local n = #parsed - if unsafe then - for i = 1, n-1 do - path = path .. parsed[i] - path = path .. "/" - end - if n > 0 then - path = path .. parsed[n] - if parsed.is_directory then path = path .. "/" end - end - else - for i = 1, n-1 do - path = path .. protect_segment(parsed[i]) - path = path .. "/" - end - if n > 0 then - path = path .. protect_segment(parsed[n]) - if parsed.is_directory then path = path .. "/" end - end - end - if parsed.is_absolute then path = "/" .. path end - return path -end diff --git a/src/packages/beanstalkd/adapter/BeanstalkdHaricotAdapter.lua b/src/packages/beanstalkd/adapter/BeanstalkdHaricotAdapter.lua deleted file mode 100644 index f7cdc26..0000000 --- a/src/packages/beanstalkd/adapter/BeanstalkdHaricotAdapter.lua +++ /dev/null @@ -1,61 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local assert = assert -local type = type -local tostring = tostring -local string_format = string.format -local string_upper = string.upper - -local haricot = require("3rd.beanstalkd.haricot") - -local BeanstalkdHaricotAdapter = class("BeanstalkdHaricotAdapter") - -function BeanstalkdHaricotAdapter:ctor(config) - self._config = config - self._instance = haricot.new(self._config.host, self._config.port) -end - -function BeanstalkdHaricotAdapter:connect() - return true -end - -function BeanstalkdHaricotAdapter:close() - return self._instance:quit() -end - -function BeanstalkdHaricotAdapter:command(command, ...) - local method = self._instance[command] - if type(method) ~= "function" then - local err = string_format("invalid beanstalkd command \"%s\"", string_upper(command)) - printError("%s", err) - return nil, err - end - - local ok, result = method(self._instance, ...) - if ok then return result end - return nil, result -end - -return BeanstalkdHaricotAdapter diff --git a/src/packages/beanstalkd/adapter/RestyBeanstalkdAdapter.lua b/src/packages/beanstalkd/adapter/RestyBeanstalkdAdapter.lua deleted file mode 100644 index 8c2ace9..0000000 --- a/src/packages/beanstalkd/adapter/RestyBeanstalkdAdapter.lua +++ /dev/null @@ -1,70 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local assert = assert -local type = type -local tostring = tostring -local string_format = string.format - -local beanstalkd = require("resty.beanstalkd") - -local RestyBeanstalkdAdapter = class("RestyBeanstalkdAdapter") - -function RestyBeanstalkdAdapter:ctor(config) - self._config = config - self._instance = beanstalkd:new() -end - -function RestyBeanstalkdAdapter:connect() - self._instance:set_timeout(self._config.timeout) - - return self._instance:connect(self._config.host, self._config.port) -end - -function RestyBeanstalkdAdapter:close() - return self._instance:close() -end - -function RestyBeanstalkdAdapter:setKeepAlive(timeout, size) - if size then - return self._instance:set_keepalive(timeout, size) - elseif timeout then - return self._instance:set_keepalive(timeout) - else - return self._instance:set_keepalive() - end -end - -function RestyBeanstalkdAdapter:command(command, ...) - local method = self._instance[command] - if type(method) ~= "function" then - local err = string_format("invalid beanstalkd command \"%s\"", string_upper(command)) - printError("%s", err) - return nil, err - end - - return method(self._instance, ...) -end - -return RestyBeanstalkdAdapter diff --git a/src/packages/beanstalkd/beanstalkd.lua b/src/packages/beanstalkd/beanstalkd.lua new file mode 100644 index 0000000..69536d3 --- /dev/null +++ b/src/packages/beanstalkd/beanstalkd.lua @@ -0,0 +1,1033 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local _tcp, _TIME_MULTIPLY +if ngx and ngx.socket then + _tcp = ngx.socket.tcp + _TIME_MULTIPLY = 1000 +else + local socket = require("socket") + _tcp = socket.tcp + _TIME_MULTIPLY = 1 +end + +local string_byte = string.byte +local string_format = string.format +local string_match = string.match +local string_split = string.split +local string_sub = string.sub +local string_find = string.find +local table_concat = table.concat +local table_new = table.new +local table_remove = table.remove +local type = type + +local Beanstalkd = cc.class("Beanstalkd") + +Beanstalkd.VERSION = "0.5" +Beanstalkd.ERRORS = table.readonly({ + OUT_OF_MEMORY = "OUT_OF_MEMORY", + INTERNAL_ERROR = "INTERNAL_ERROR", + BAD_FORMAT = "BAD_FORMAT", + UNKNOWN_COMMAND = "UNKNOWN_COMMAND", + EXPECTED_CRLF = "EXPECTED_CRLF", + JOB_TOO_BIG = "JOB_TOO_BIG", + DEADLINE_SOON = "DEADLINE_SOON", + TIMED_OUT = "TIMED_OUT", + NOT_FOUND = "NOT_FOUND", + BURIED = "BURIED", + INVALID_YML = "INVALID_YML", +}) + +local ERRORS = Beanstalkd.ERRORS + +local DEFAULT_HOST = "localhost" +local DEFAULT_PORT = 11300 + +local _req, _reqstate, _reqvalue, _reqyml +local _readreply, _getvalue, _getjob, _getyml + +function Beanstalkd:connect(host, port) + local socket = _tcp() + local ok, err = socket:connect(host or DEFAULT_HOST, port or DEFAULT_PORT) + if not ok then + return nil, err + end + self._socket = socket + return 1 +end + +function Beanstalkd:setTimeout(timeout) + local socket = self._socket + if not socket then + return nil, "not initialized" + end + return socket:settimeout(timeout * _TIME_MULTIPLY) +end + +function Beanstalkd:setKeepAlive(...) + local socket = self._socket + if not socket then + return nil, "not initialized" + end + + self._socket = nil + if not ngx then + return socket:close() + else + return socket:setkeepalive(...) + end +end + +function Beanstalkd:getReusedTimes() + local socket = self._socket + if not socket then + return nil, "not initialized" + end + if socket.getreusedtimes then + return socket:getreusedtimes() + else + return 0 + end +end + +function Beanstalkd:close() + local socket = self._socket + if not socket then + return nil, "not initialized" + end + self._socket = nil + return socket:close() +end + +--[[ += Beanstalk Protocol = + +Protocol +-------- + +The beanstalk protocol runs over TCP using ASCII encoding. Clients connect, +send commands and data, wait for responses, and close the connection. For each +connection, the server processes commands serially in the order in which they +were received and sends responses in the same order. All integers in the +protocol are formatted in decimal and (unless otherwise indicated) +nonnegative. + +Names, in this protocol, are ASCII strings. They may contain letters (A-Z and +a-z), numerals (0-9), hyphen ("-"), plus ("+"), slash ("/"), semicolon (";"), +dot ("."), dollar-sign ("$"), and parentheses ("(" and ")"), but they may not +begin with a hyphen. They are terminated by white space (either a space char or +end of line). Each name must be at least one character long. + +The protocol contains two kinds of data: text lines and unstructured chunks of +data. Text lines are used for client commands and server responses. Chunks are +used to transfer job bodies and stats information. Each job body is an opaque +sequence of bytes. The server never inspects or modifies a job body and always +sends it back in its original form. It is up to the clients to agree on a +meaningful interpretation of job bodies. + +There is no command to close the connection -- the client may simply close the +TCP connection when it no longer has use for the server. However, beanstalkd +performs very well with a large number of open connections, so it is usually +better for the client to keep its connection open and reuse it as much as +possible. This also avoids the overhead of establishing new TCP connections. + +If a client violates the protocol (such as by sending a request that is not +well-formed or a command that does not exist) or if the server has an error, +the server will reply with one of the following error messages: + + - "OUT_OF_MEMORY\r\n" The server cannot allocate enough memory for the job. + The client should try again later. + + - "INTERNAL_ERROR\r\n" This indicates a bug in the server. It should never + happen. If it does happen, please report it at + http://groups.google.com/group/beanstalk-talk. + + - "DRAINING\r\n" This means that the server has been put into "drain mode" + and is no longer accepting new jobs. The client should try another server + or disconnect and try again later. + + - "BAD_FORMAT\r\n" The client sent a command line that was not well-formed. + This can happen if the line does not end with \r\n, if non-numeric + characters occur where an integer is expected, if the wrong number of + arguments are present, or if the command line is mal-formed in any other + way. + + - "UNKNOWN_COMMAND\r\n" The client sent a command that the server does not + know. + +These error responses will not be listed in this document for individual +commands in the following sections, but they are implicitly included in the +description of all commands. Clients should be prepared to receive an error +response after any command. + +As a last resort, if the server has a serious error that prevents it from +continuing service to the current client, the server will close the +connection. + +Job Lifecycle +------------- + +A job in beanstalk gets created by a client with the "put" command. During its +life it can be in one of four states: "ready", "reserved", "delayed", or +"buried". After the put command, a job typically starts out ready. It waits in +the ready queue until a worker comes along and runs the "reserve" command. If +this job is next in the queue, it will be reserved for the worker. The worker +will execute the job; when it is finished the worker will send a "delete" +command to delete the job. + +Here is a picture of the typical job lifecycle: + + + put reserve delete + -----> [READY] ---------> [RESERVED] --------> *poof* + + + +Here is a picture with more possibilities: + + + + put with delay release with delay + ----------------> [DELAYED] <------------. + | | + | (time passes) | + | | + put v reserve | delete + -----------------> [READY] ---------> [RESERVED] --------> *poof* + ^ ^ | | + | \ release | | + | `-------------' | + | | + | kick | + | | + | bury | + [BURIED] <---------------' + | + | delete + `--------> *poof* + + +The system has one or more tubes. Each tube consists of a ready queue and a +delay queue. Each job spends its entire life in one tube. Consumers can show +interest in tubes by sending the "watch" command; they can show disinterest by +sending the "ignore" command. This set of interesting tubes is said to be a +consumer's "watch list". When a client reserves a job, it may come from any of +the tubes in its watch list. + +When a client connects, its watch list is initially just the tube named +"default". If it submits jobs without having sent a "use" command, they will +live in the tube named "default". + +Tubes are created on demand whenever they are referenced. If a tube is empty +(that is, it contains no ready, delayed, or buried jobs) and no client refers +to it, it will be deleted. +]] + +-- Producer Commands + +--[[ +The "put" command is for any process that wants to insert a job into the queue. +It comprises a command line followed by the job body: + +put \r\n +\r\n + +It inserts a job into the client's currently used tube (see the "use" command +below). + + - is an integer < 2**32. Jobs with smaller priority values will be + scheduled before jobs with larger priorities. The most urgent priority is 0; + the least urgent priority is 4294967295. + + - is an integer number of seconds to wait before putting the job in + the ready queue. The job will be in the "delayed" state during this time. + + - -- time to run -- is an integer number of seconds to allow a worker + to run this job. This time is counted from the moment a worker reserves + this job. If the worker does not delete, release, or bury the job within + seconds, the job will time out and the server will release the job. + The minimum ttr is 1. If the client sends 0, the server will silently + increase the ttr to 1. + + - is an integer indicating the size of the job body, not including the + trailing "\r\n". This value must be less than max-job-size (default: 2**16). + + - is the job body -- a sequence of bytes of length from the + previous line. + +After sending the command line and body, the client waits for a reply, which +may be: + + - "INSERTED \r\n" to indicate success. + + - is the integer id of the new job + + - "BURIED \r\n" if the server ran out of memory trying to grow the + priority queue data structure. + + - is the integer id of the new job + + - "EXPECTED_CRLF\r\n" The job body must be followed by a CR-LF pair, that is, + "\r\n". These two bytes are not counted in the job size given by the client + in the put command line. + + - "JOB_TOO_BIG\r\n" The client has requested to put a job with a body larger + than max-job-size bytes. +]] +function Beanstalkd:put(data, pri, delay, ttr) + local socket = self._socket + local res, err = _req(socket, data, {"put", pri, delay, ttr, #data}) + if not res then + return nil, err + end + + local id = _getvalue(res, "INSERTED", true) + if id then + return id + end + + id = _getvalue(res, "BURIED", true) + if id then + return id, ERRORS.BURIED + end + + return nil, res +end + +--[[ +The "use" command is for producers. Subsequent put commands will put jobs into +the tube specified by this command. If no use command has been issued, jobs +will be put into the tube named "default". + +use \r\n + + - is a name at most 200 bytes. It specifies the tube to use. If the + tube does not exist, it will be created. + +The only reply is: + +USING \r\n + + - is the name of the tube now being used. +]] +function Beanstalkd:use(tube) + return _reqvalue(self._socket, {"use", tube}, "USING") +end + + +-- Worker Commands + +--[[ +A process that wants to consume jobs from the queue uses "reserve", "delete", +"release", and "bury". The first worker command, "reserve", looks like this: + +reserve\r\n + +Alternatively, you can specify a timeout as follows: + +reserve-with-timeout \r\n + +This will return a newly-reserved job. If no job is available to be reserved, +beanstalkd will wait to send a response until one becomes available. Once a +job is reserved for the client, the client has limited time to run (TTR) the +job before the job times out. When the job times out, the server will put the +job back into the ready queue. Both the TTR and the actual time left can be +found in response to the stats-job command. + +A timeout value of 0 will cause the server to immediately return either a +response or TIMED_OUT. A positive value of timeout will limit the amount of +time the client will block on the reserve request until a job becomes +available. + +During the TTR of a reserved job, the last second is kept by the server as a +safety margin, during which the client will not be made to wait for another +job. If the client issues a reserve command during the safety margin, or if +the safety margin arrives while the client is waiting on a reserve command, +the server will respond with: + +DEADLINE_SOON\r\n + +This gives the client a chance to delete or release its reserved job before +the server automatically releases it. + +TIMED_OUT\r\n + +If a non-negative timeout was specified and the timeout exceeded before a job +became available, the server will respond with TIMED_OUT. + +Otherwise, the only other response to this command is a successful reservation +in the form of a text line followed by the job body: + +RESERVED \r\n +\r\n + + - is the job id -- an integer unique to this job in this instance of + beanstalkd. + + - is an integer indicating the size of the job body, not including + the trailing "\r\n". + + - is the job body -- a sequence of bytes of length from the + previous line. This is a verbatim copy of the bytes that were originally + sent to the server in the put command for this job. +]] +function Beanstalkd:reserve(timeout) + local socket = self._socket + local res, err + if timeout then + res, err = _req(socket, {"reserve-with-timeout", timeout}) + else + res, err = _req(socket, {"reserve"}) + end + if not res then + return nil, err + end + + local data, err = _getjob(socket, res, "RESERVED") + if not data then + return nil, err + end + + return data +end + +--[[ +The delete command removes a job from the server entirely. It is normally used +by the client when the job has successfully run to completion. A client can +delete jobs that it has reserved, ready jobs, and jobs that are buried. The +delete command looks like this: + +delete \r\n + + - is the job id to delete. + +The client then waits for one line of response, which may be: + + - "DELETED\r\n" to indicate success. + + - "NOT_FOUND\r\n" if the job does not exist or is not either reserved by the + client, ready, or buried. This could happen if the job timed out before the + client sent the delete command. +]] +function Beanstalkd:delete(id) + return _reqstate(self._socket, {"delete", id}, "DELETED") +end + +--[[ +The release command puts a reserved job back into the ready queue (and marks +its state as "ready") to be run by any client. It is normally used when the job +fails because of a transitory error. It looks like this: + +release \r\n + + - is the job id to release. + + - is a new priority to assign to the job. + + - is an integer number of seconds to wait before putting the job in + the ready queue. The job will be in the "delayed" state during this time. + +The client expects one line of response, which may be: + + - "RELEASED\r\n" to indicate success. + + - "BURIED\r\n" if the server ran out of memory trying to grow the priority + queue data structure. + + - "NOT_FOUND\r\n" if the job does not exist or is not reserved by the client. +]] +function Beanstalkd:release(id, priority, delay) + return _reqstate(self._socket, {"release", id, priority, delay}, "RELEASED") +end + +--[[ +The bury command puts a job into the "buried" state. Buried jobs are put into a +FIFO linked list and will not be touched by the server again until a client +kicks them with the "kick" command. + +The bury command looks like this: + +bury \r\n + + - is the job id to release. + + - is a new priority to assign to the job. + +There are two possible responses: + + - "BURIED\r\n" to indicate success. + + - "NOT_FOUND\r\n" if the job does not exist or is not reserved by the client. +]] +function Beanstalkd:bury(id, priority) + return _reqstate(self._socket, {"bury", id, priority}, "BURIED") +end + +--[[ +The touch command looks like this: + +touch \r\n + + - is the ID of a job reserved by the current connection. + +There are two possible responses: + + - "TOUCHED\r\n" to indicate success. + + - "NOT_FOUND\r\n" if the job does not exist or is not reserved by the client. +]] +function Beanstalkd:touch(id) + return _reqstate(self._socket, {"touch", id}, "TOUCHED") +end + +--[[ +The "watch" command adds the named tube to the watch list for the current +connection. A reserve command will take a job from any of the tubes in the +watch list. For each new connection, the watch list initially consists of one +tube, named "default". + +watch \r\n + + - is a name at most 200 bytes. It specifies a tube to add to the watch + list. If the tube doesn't exist, it will be created. + +The reply is: + +WATCHING \r\n + + - is the integer number of tubes currently in the watch list. +]] +function Beanstalkd:watch(tube) + return _reqvalue(self._socket, {"watch", tube}, "WATCHING", true) +end + +--[[ +The "ignore" command is for consumers. It removes the named tube from the +watch list for the current connection. + +ignore \r\n + +The reply is one of: + + - "WATCHING \r\n" to indicate success. + + - is the integer number of tubes currently in the watch list. + + - "NOT_IGNORED\r\n" if the client attempts to ignore the only tube in its + watch list. +]] +function Beanstalkd:ignore(tube) + return _reqvalue(self._socket, {"ignore", tube}, "WATCHING", true) +end + +-- Other Commands + +--[[ +The peek commands let the client inspect a job in the system. There are four +variations. All but the first operate only on the currently used tube. + + - "peek \r\n" - return job . + + - "peek-ready\r\n" - return the next ready job. + + - "peek-delayed\r\n" - return the delayed job with the shortest delay left. + + - "peek-buried\r\n" - return the next job in the list of buried jobs. + +There are two possible responses, either a single line: + + - "NOT_FOUND\r\n" if the requested job doesn't exist or there are no jobs in + the requested state. + +Or a line followed by a chunk of data, if the command was successful: + +FOUND \r\n +\r\n + + - is the job id. + + - is an integer indicating the size of the job body, not including + the trailing "\r\n". + + - is the job body -- a sequence of bytes of length from the + previous line. +]] +function Beanstalkd:peek(id) + local socket = self._socket + local res, err + local _id = tonumber(id) + if type(_id) == "number" then + res, err = _req(socket, {"peek", id}) + else + -- id is state + res, err = _req(socket, {"peek-" .. id}) + end + if not res then + return nil, err + end + + local data, err = _getjob(socket, res, "FOUND") + if not data then + return nil, err + end + + return data +end + +--[[ +The kick command applies only to the currently used tube. It moves jobs into +the ready queue. If there are any buried jobs, it will only kick buried jobs. +Otherwise it will kick delayed jobs. It looks like: + +kick \r\n + + - is an integer upper bound on the number of jobs to kick. The server + will kick no more than jobs. + +The response is of the form: + +KICKED \r\n + + - is an integer indicating the number of jobs actually kicked. +]] +function Beanstalkd:kick(bound) + return _reqvalue(self._socket, {"kick", bound}, "KICKED", true) +end + +--[[ +The stats-job command gives statistical information about the specified job if +it exists. Its form is: + +stats-job \r\n + + - is a job id. + +The response is one of: + + - "NOT_FOUND\r\n" if the job does not exist. + + - "OK \r\n\r\n" + + - is the size of the following data section in bytes. + + - is a sequence of bytes of length from the previous line. It + is a YAML file with statistical information represented a dictionary. + +The stats-job data is a YAML file representing a single dictionary of strings +to scalars. It contains these keys: + + - "id" is the job id + + - "tube" is the name of the tube that contains this job + + - "state" is "ready" or "delayed" or "reserved" or "buried" + + - "pri" is the priority value set by the put, release, or bury commands. + + - "age" is the time in seconds since the put command that created this job. + + - "time-left" is the number of seconds left until the server puts this job + into the ready queue. This number is only meaningful if the job is + reserved or delayed. If the job is reserved and this amount of time + elapses before its state changes, it is considered to have timed out. + + - "timeouts" is the number of times this job has timed out during a + reservation. + + - "releases" is the number of times a client has released this job from a + reservation. + + - "buries" is the number of times this job has been buried. + + - "kicks" is the number of times this job has been kicked. + ]] +function Beanstalkd:statsJob(id) + return _reqyml(self._socket, {"stats-job", id}) +end + +--[[ +stats-tube \r\n + + - is a name at most 200 bytes. Stats will be returned for this tube. + +The response is one of: + + - "NOT_FOUND\r\n" if the tube does not exist. + + - "OK \r\n\r\n" + + - is the size of the following data section in bytes. + + - is a sequence of bytes of length from the previous line. It + is a YAML file with statistical information represented a dictionary. + +The stats-tube data is a YAML file representing a single dictionary of strings +to scalars. It contains these keys: + + - "name" is the tube's name. + + - "current-jobs-urgent" is the number of ready jobs with priority < 1024 in + this tube. + + - "current-jobs-ready" is the number of jobs in the ready queue in this tube. + + - "current-jobs-reserved" is the number of jobs reserved by all clients in + this tube. + + - "current-jobs-delayed" is the number of delayed jobs in this tube. + + - "current-jobs-buried" is the number of buried jobs in this tube. + + - "total-jobs" is the cumulative count of jobs created in this tube. + + - "current-waiting" is the number of open connections that have issued a + reserve command while watching this tube but not yet received a response. +]] +function Beanstalkd:statsTube(tube) + return _reqyml(self._socket, {"stats-tube", tube}) +end + +--[[ +The stats command gives statistical information about the system as a whole. +Its form is: + +stats\r\n + +The server will respond: + +OK \r\n +\r\n + + - is the size of the following data section in bytes. + + - is a sequence of bytes of length from the previous line. It + is a YAML file with statistical information represented a dictionary. + +The stats data for the system is a YAML file representing a single dictionary +of strings to scalars. It contains these keys: + + - "current-jobs-urgent" is the number of ready jobs with priority < 1024. + + - "current-jobs-ready" is the number of jobs in the ready queue. + + - "current-jobs-reserved" is the number of jobs reserved by all clients. + + - "current-jobs-delayed" is the number of delayed jobs. + + - "current-jobs-buried" is the number of buried jobs. + + - "cmd-put" is the cumulative number of put commands. + + - "cmd-peek" is the cumulative number of peek commands. + + - "cmd-peek-ready" is the cumulative number of peek-ready commands. + + - "cmd-peek-delayed" is the cumulative number of peek-delayed commands. + + - "cmd-peek-buried" is the cumulative number of peek-buried commands. + + - "cmd-reserve" is the cumulative number of reserve commands. + + - "cmd-use" is the cumulative number of use commands. + + - "cmd-watch" is the cumulative number of watch commands. + + - "cmd-ignore" is the cumulative number of ignore commands. + + - "cmd-delete" is the cumulative number of delete commands. + + - "cmd-release" is the cumulative number of release commands. + + - "cmd-bury" is the cumulative number of bury commands. + + - "cmd-kick" is the cumulative number of kick commands. + + - "cmd-stats" is the cumulative number of stats commands. + + - "cmd-stats-job" is the cumulative number of stats-job commands. + + - "cmd-stats-tube" is the cumulative number of stats-tube commands. + + - "cmd-list-tubes" is the cumulative number of list-tubes commands. + + - "cmd-list-tube-used" is the cumulative number of list-tube-used commands. + + - "cmd-list-tubes-watched" is the cumulative number of list-tubes-watched + commands. + + - "job-timeouts" is the cumulative count of times a job has timed out. + + - "total-jobs" is the cumulative count of jobs created. + + - "max-job-size" is the maximum number of bytes in a job. + + - "current-tubes" is the number of currently-existing tubes. + + - "current-connections" is the number of currently open connections. + + - "current-producers" is the number of open connections that have each + issued at least one put command. + + - "current-workers" is the number of open connections that have each issued + at least one reserve command. + + - "current-waiting" is the number of open connections that have issued a + reserve command but not yet received a response. + + - "total-connections" is the cumulative count of connections. + + - "pid" is the process id of the server. + + - "version" is the version string of the server. + + - "rusage-utime" is the accumulated user CPU time of this process in seconds + and microseconds. + + - "rusage-stime" is the accumulated system CPU time of this process in + seconds and microseconds. + + - "uptime" is the number of seconds since this server started running. + + - "binlog-oldest-index" is the index of the oldest binlog file needed to + store the current jobs + + - "binlog-current-index" is the index of the current binlog file being + written to. If binlog is not active this value will be 0 + + - "binlog-max-size" is the maximum size in bytes a binlog file is allowed + to get before a new binlog file is opened +]] +function Beanstalkd:stats() + return _reqyml(self._socket, {"stats"}) +end + +--[[ +The list-tubes command returns a list of all existing tubes. Its form is: + +list-tubes\r\n + +The response is: + +OK \r\n +\r\n + + - is the size of the following data section in bytes. + + - is a sequence of bytes of length from the previous line. It + is a YAML file containing all tube names as a list of strings. +]] +function Beanstalkd:listTubes() + return _reqyml(self._socket, {"list-tubes"}) +end + +--[[ +The list-tube-used command returns the tube currently being used by the +client. Its form is: + +list-tube-used\r\n + +The response is: + +USING \r\n + + - is the name of the tube being used. +]] +function Beanstalkd:listTubeUsed() + return _reqvalue(self._socket, {"list-tube-used"}, "USING") +end + +--[[ +The list-tubes-watched command returns a list tubes currently being watched by +the client. Its form is: + +list-tubes-watched\r\n + +The response is: + +OK \r\n +\r\n + + - is the size of the following data section in bytes. + + - is a sequence of bytes of length from the previous line. It + is a YAML file containing watched tube names as a list of strings. +]] +function Beanstalkd:listTubesWatched() + return _reqyml(self._socket, {"list-tubes-watched"}) +end + +-- private + +_req = function(socket, data, args) + if type(data) == "table" then + args = data + data = nil + end + + local n = 2 + if data then n = 4 end + local req = table_new(n, 0) + req[1] = table_concat(args, " ") + req[2] = "\r\n" + if data then + req[3] = data + req[4] = "\r\n" + end + + local bytes, err = socket:send(table_concat(req)) + if not bytes then + return nil, err + end + + return _readreply(socket) +end + +_reqstate = function(socket, args, word) + local res, err = _req(socket, args) + if not res then + return nil, err + end + + if res ~= word then + return nil, res + end + + return true +end + +_reqvalue = function(socket, args, word, isnumber) + local res, err = _req(socket, args) + if not res then + return nil, err + end + + local value = _getvalue(res, word, isnumber) + if not value then + return nil, res + end + + return value +end + +_reqyml = function(socket, args) + local res, err = _req(socket, args) + if not res then + return nil, err + end + + local data, err = _getyml(socket, res, "OK") + if not data then + return nil, err + end + + return data +end + +_readreply = function(socket, bytes) + if bytes then + local res, err = socket:receive(bytes + 2) + if not res then + return nil, err + end + + return string_sub(res, 1, -3) + else + local res, err = socket:receive("*l") + if not res then + return nil, err + end + + return res + end +end + +_getvalue = function(res, word, isnumber) + local value = string_match(res, string_format("^%s (.+)$", word)) + if not value then + return nil, res + end + + if isnumber then + value = tonumber(value) + end + + return value +end + +_getjob = function(socket, res, word) + local pattern = string_format("^%s (%%d+) (%%d+)$", word) + local id, bytes = string_match(res, pattern) + if (not id) or (not bytes) then + return nil, res + end + + id, bytes = tonumber(id), tonumber(bytes) + local res, err = _readreply(socket, bytes) + if not res then + return nil, err + end + + return {id = id, data = res} +end + +_getyml = function(socket, res, word) + local pattern = string_format("^%s (%%d+)$", word) + local bytes = string_match(res, pattern) + if not bytes then + return nil, res + end + + local res, err = _readreply(socket, bytes) + if not res then + return nil, err + end + + if string_sub(res, 1, 3) ~= "---" then + return nil, ERRORS.INVALID_YML + end + + -- convert yml to table + local yml = {} + local offset = 5 + while true do + local colon = string_find(res, ":", offset, true) + local nl = string_find(res, "\n", offset, true) + if colon then + yml[string_sub(res, offset, colon - 1)] = string_sub(res, colon + 2, nl - 1) + elseif nl then + if string_byte(res, offset) == 45 then + offset = offset + 2 + end + yml[#yml + 1] = string_sub(res, offset, nl - 1) + else + break + end + offset = nl + 1 + end + + return yml +end + +return Beanstalkd diff --git a/src/packages/beanstalkd/service.lua b/src/packages/beanstalkd/service.lua deleted file mode 100644 index afa7b6b..0000000 --- a/src/packages/beanstalkd/service.lua +++ /dev/null @@ -1,80 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local type = type -local string_lower = string.lower - -local BeanstalkdService = class("BeanstalkdService") - -local BeanstalkdAdapter -if ngx then - BeanstalkdAdapter = import(".adapter.RestyBeanstalkdAdapter") -else - BeanstalkdAdapter = import(".adapter.BeanstalkdHaricotAdapter") -end - -function BeanstalkdService:ctor(config) - if type(config) ~= "table" then - throw("invalid beanstalkd config") - end - self._config = clone(config) - self._beans = BeanstalkdAdapter:create(self._config) -end - -function BeanstalkdService:connect() - local ok, err = self._beans:connect() - if err then - throw("connect to beanstalkd failed, %s", err) - end -end - -function BeanstalkdService:close() - self._beans:close() -end - -function BeanstalkdService:setKeepAlive(timeout, size) - if not ngx then - self:close() - return - end - self._beans:setKeepAlive(timeout, size) -end - -function BeanstalkdService:command(command, ...) - command = string_lower(command) - local res, err = self._beans:command(command, ...) - - -- make Haricot compatible. Such as "delete", it return nil, nil. - if not res and not err then - res = true - end - - if not res then - throw("beanstalkd command \"%s\" failed, %s, %s", command, err, res) - end - - return res -end - -return BeanstalkdService diff --git a/src/packages/connectid/ConnectIdService.lua b/src/packages/connectid/ConnectIdService.lua deleted file mode 100644 index c2f5286..0000000 --- a/src/packages/connectid/ConnectIdService.lua +++ /dev/null @@ -1,75 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local tostring = tostring - -local ConnectIdService = class("ConnectIdService") - -ConnectIdService.CONNECTS_ID_DICT_KEY = "_CONNECTS_ID_DICT" -- id => tag -ConnectIdService.CONNECTS_TAG_DICT_KEY = "_CONNECTS_TAG_DICT" -- tag => id - -function ConnectIdService:ctor(redis) - self._redis = redis -end - -function ConnectIdService:getIdByTag(tag) - if not tag then - throw("get connect id by invalid tag \"%s\"", tostring(tag)) - end - return self._redis:command("HGET", ConnectIdService.CONNECTS_TAG_DICT_KEY, tostring(tag)) -end - -function ConnectIdService:getTagById(connectId) - if not connectId then - throw("get connect tag by invalid id \"%s\"", tostring(connectId)) - end - return self._redis:command("HGET", ConnectIdService.CONNECTS_ID_DICT_KEY, tostring(connectId)) -end - -function ConnectIdService:getTag(connectId) - return self._redis:command("HGET", ConnectIdService.CONNECTS_ID_DICT_KEY, connectId) -end - -function ConnectIdService:setTag(connectId, tag) - connectId = tostring(connectId) - if not tag then - throw("set connect \"%s\" tag with invalid tag", connectId) - end - local pipe = self._redis:newPipeline() - pipe:command("HMSET", ConnectIdService.CONNECTS_ID_DICT_KEY, connectId, tag) - pipe:command("HMSET", ConnectIdService.CONNECTS_TAG_DICT_KEY, tag, connectId) - pipe:commit() -end - -function ConnectIdService:removeTag(connectId) - local tag = self:getTag(connectId) - if tag then - local pipe = self._redis:newPipeline() - pipe:command("HDEL", ConnectIdService.CONNECTS_ID_DICT_KEY, connectId) - pipe:command("HDEL", ConnectIdService.CONNECTS_TAG_DICT_KEY, tag) - pipe:commit() - end -end - -return ConnectIdService diff --git a/src/packages/connectid/init.lua b/src/packages/connectid/init.lua deleted file mode 100644 index 1967739..0000000 --- a/src/packages/connectid/init.lua +++ /dev/null @@ -1,29 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local _P = {} - -_P.service = import(".ConnectIdService") - -return _P diff --git a/src/packages/event/event.lua b/src/packages/event/event.lua new file mode 100644 index 0000000..9b6a5e6 --- /dev/null +++ b/src/packages/event/event.lua @@ -0,0 +1,121 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local cc = cc +local string_upper = string.upper +local tostring = tostring +local type = type + +local Event = cc.class("Event") + +local _newtag = 0 + +function Event:ctor(target) + self._target = target + self._listeners = {} +end + +function Event:bind(name, listener, tag) + local listeners = self._listeners + name = string_upper(tostring(name)) + if not listeners[name] then + listeners[name] = {} + end + + if not tag then + _newtag = _newtag + 1 + tag = "#" .. tostring(_newtag) + end + + listeners[name][tag] = listener + + -- cc.printinfo("[Event:%s] bind event '%s' with listener '%s'", tostring(self._target), name, tag) + + return tag +end + +function Event:unbind(tag) + local listeners = self._listeners + for name, listenersForEvent in pairs(listeners) do + for _tag, _ in pairs(listenersForEvent) do + if tag == _tag then + listenersForEvent[tag] = nil + -- cc.printinfo("[Event:%s] unbind event '%s' listener '%s'", tostring(self._target), name, tag) + return + end + end + end +end + +function Event:trigger(event) + if type(event) ~= "table" then + event = {name = event} + end + event.name = string_upper(tostring(event.name)) + + -- cc.printinfo("[Event:%s] dispatch event '%s'", tostring(self), event.name) + + local listeners = self._listeners + if not listeners[event.name] then + return + end + + event._stop = false + event.target = self._target + event.stop = function() + event._stop = true + end + + for tag, listener in pairs(listeners[event.name]) do + -- cc.printinfo("[Event:%s] trigger event '%s' to listener '%s'", tostring(self._target), event.name, tag) + listener(event) + if event._stop then + cc.printinfo("[Event:%s] break dispatching event '%s'", tostring(self._target), event.name) + break + end + end +end + +function Event:remove(name) + name = string_upper(tostring(name)) + self._listeners[name] = nil + -- cc.printinfo("[Event:%s] remove event '%s' all listeners", tostring(self._target), name) +end + +function Event:removeAll() + self._listeners = {} + -- cc.printinfo("[Event:%s] remove all listeners", tostring(self._target)) +end + +function Event:dump() + cc.printinfo("[Event:%s] dump all listeners", tostring(self._target)) + for name, listeners in pairs(self._listeners) do + cc.printf(" event: %s", name) + for tag, listener in pairs(listeners) do + cc.printf(" %s: %s", tag, tostring(listener)) + end + end +end + +return Event diff --git a/src/framework/server_functions.lua b/src/packages/gbc/ActionBase.lua similarity index 77% rename from src/framework/server_functions.lua rename to src/packages/gbc/ActionBase.lua index 9d6b392..36f8526 100644 --- a/src/framework/server_functions.lua +++ b/src/packages/gbc/ActionBase.lua @@ -22,9 +22,22 @@ THE SOFTWARE. ]] -local string_gsub = string.gsub +local ActionBase = cc.class("ActionBase") -function strip_luafile_paths(str) - str = string_gsub(str, "/.*/(.*lua:%d+:)", "%1") - return string_gsub(str, "'/.*/(.*lua)'", "'%1'") +function ActionBase:ctor(instance) + self._instance = instance + self:init() end + +function ActionBase:getInstance() + return self._instance +end + +function ActionBase:getInstanceConfig() + return self._instance.config +end + +function ActionBase:init() +end + +return ActionBase diff --git a/src/packages/gbc/Broadcast.lua b/src/packages/gbc/Broadcast.lua new file mode 100644 index 0000000..41d9bd9 --- /dev/null +++ b/src/packages/gbc/Broadcast.lua @@ -0,0 +1,90 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local tostring = tostring +local type = type + +local json = cc.import("#json") +local Constants = cc.import(".Constants") + +local json_encode = json.encode + +local Broadcast = cc.class("Broadcast") + +local _formatmsg + +function Broadcast:ctor(redis, messageType, websocketInstance) + self._redis = redis + self._messageType = messageType + if websocketInstance and websocketInstance.getConnectId then + self._websocketInstance = websocketInstance + end +end + +function Broadcast:sendMessage(connectId, message, format) + format = format or self._messageType + message = _formatmsg(message, format) + + if self._websocketInstance and self._websocketInstance:getConnectId() == connectId then + return self._websocketInstance._socket:send_text(tostring(message)) + else + local connectChannel = Constants.CONNECT_CHANNEL_PREFIX .. connectId + local ok, err = self._redis:publish(connectChannel, message) + if not ok then + return nil, err + end + return 1 + end +end + +function Broadcast:sendMessageToAll(message, format) + format = format or self._messageType + message = _formatmsg(message, format) + return self._redis:publish(Constants.BROADCAST_ALL_CHANNEL, message) +end + +function Broadcast:sendControlMessage(connectId, message) + local controlChannel = Constants.CONTROL_CHANNEL_PREFIX .. connectId + local ok, err = self._redis:publish(controlChannel, tostring(message)) + if not ok then + return nil, err + end + return 1 +end + +-- private + +_formatmsg = function(message, format) + format = format or Constants.MESSAGE_FORMAT_JSON + if type(message) == "table" then + if format == Constants.MESSAGE_FORMAT_JSON then + message = json_encode(message) + else + -- TODO: support more message formats + end + end + return tostring(message) +end + +return Broadcast diff --git a/src/packages/redis/RedisPipeline.lua b/src/packages/gbc/CommandLineBootstrap.lua similarity index 70% rename from src/packages/redis/RedisPipeline.lua rename to src/packages/gbc/CommandLineBootstrap.lua index 5457a69..c8c7465 100644 --- a/src/packages/redis/RedisPipeline.lua +++ b/src/packages/gbc/CommandLineBootstrap.lua @@ -22,23 +22,18 @@ THE SOFTWARE. ]] -local RedisPipeline = class("RedisPipeline") +local Factory = cc.import(".Factory") -function RedisPipeline:ctor(service) - self._service = service - self._commands = {} -end +local CommandLineBootstrap = cc.class("CommandLineBootstrap") -function RedisPipeline:command(command, ...) - self._commands[#self._commands + 1] = {command, {...}} +function CommandLineBootstrap:ctor(appKeys, globalConfig) + self._configs = Factory.makeAppConfigs(appKeys, globalConfig, package.path) end -function RedisPipeline:commit() - if #self._commands > 0 then - return self._service._redis:commitPipeline(self._commands) - else - return {} - end +function CommandLineBootstrap:runapp(appRootPath) + local appConfig = self._configs[appRootPath] + local cli = Factory.create(appConfig, "CommandLineInstance") + cli:run() end -return RedisPipeline +return CommandLineBootstrap diff --git a/src/packages/gbc/CommandLineInstanceBase.lua b/src/packages/gbc/CommandLineInstanceBase.lua new file mode 100644 index 0000000..ae8c1a4 --- /dev/null +++ b/src/packages/gbc/CommandLineInstanceBase.lua @@ -0,0 +1,47 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local string_format = string.format + +local Constants = cc.import(".Constants") + +local InstanceBase = cc.import(".InstanceBase") +local CommandLineInstanceBase = cc.class("CommandLineInstanceBase", InstanceBase) + +function CommandLineInstanceBase:ctor(config, arg) + CommandLineInstanceBase.super.ctor(self, config, Constants.CLI_REQUEST_TYPE) + self._requestParameters = cc.checktable(arg) +end + +function CommandLineInstanceBase:run() + local actionName = self._requestParameters.action or "" + local result = self:runAction(actionName, self._requestParameters) + if type(result) ~= "table" then + print(result) + else + cc.dump(result) + end +end + +return CommandLineInstanceBase diff --git a/src/packages/gbc/Constants.lua b/src/packages/gbc/Constants.lua new file mode 100644 index 0000000..e8b06e9 --- /dev/null +++ b/src/packages/gbc/Constants.lua @@ -0,0 +1,37 @@ + +local _M = {} + +-- request type +_M.HTTP_REQUEST_TYPE = "http" +_M.WEBSOCKET_REQUEST_TYPE = "websocket" +_M.CLI_REQUEST_TYPE = "cli" +_M.WORKER_REQUEST_TYPE = "worker" + +-- message type +_M.MESSAGE_FORMAT_JSON = "json" +_M.MESSAGE_FORMAT_TEXT = "text" +_M.DEFAULT_MESSAGE_FORMAT = _M.MESSAGE_FORMAT_JSON + +-- redis keys +_M.NEXT_CONNECT_ID_KEY = "_NEXT_CONNECT_ID" +_M.CONNECT_CHANNEL_PREFIX = "_CN_" +_M.CONTROL_CHANNEL_PREFIX = "_CT_" +_M.BROADCAST_ALL_CHANNEL = "_CN_ALL" + +-- beanstalkd +_M.BEANSTALKD_JOB_TUBE_PATTERN = "job-%s" -- job- + +-- websocket +_M.WEBSOCKET_TEXT_MESSAGE_TYPE = "text" +_M.WEBSOCKET_BINARY_MESSAGE_TYPE = "binary" +_M.WEBSOCKET_SUBPROTOCOL_PATTERN = "gbc%-auth%-([%w%d%-]+)" -- token +_M.WEBSOCKET_DEFAULT_TIME_OUT = 10 * 1000 -- 10s +_M.WEBSOCKET_DEFAULT_MAX_PAYLOAD_LEN = 16 * 1024 -- 16KB + +-- misc +_M.STRIP_LUA_PATH_PATTERN = "[/%.%a%-]+/([%a%-]+%.lua:%d+: )" + +-- control message +_M.CLOSE_CONNECT = "SEND_CLOSE" + +return table.readonly(_M) diff --git a/src/packages/gbc/Factory.lua b/src/packages/gbc/Factory.lua new file mode 100644 index 0000000..552643e --- /dev/null +++ b/src/packages/gbc/Factory.lua @@ -0,0 +1,88 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local _CUR = ... + +local string_find = string.find +local string_format = string.format +local string_sub = string.sub + +local Factory = cc.class("Factory") + +function Factory.create(config, classname, ...) + if config.app.packagePath then + package.path = config.app.packagePath + end + + local ok, cls = pcall(require, classname) + if not ok then + local err = cls + local pos = string_find(err, "not found:", 1, true) + if not pos then + cc.throw(err) + end + + if not string_find(err, string_format("module '%s' not found:", classname), 1, true) then + cc.throw(err) + end + + cls = nil + end + + if not cls then + cls = cc.import("." .. classname .. "Base", _CUR) + end + + return cls:new(config, ...) +end + +function Factory.makeAppConfigs(appKeys, serverConfig, defaultPackagePath) + local appConfigs = {} + + for appRootPath, opts in pairs(appKeys) do + local config = table.copy(serverConfig) + local appConfig = config.app + appConfig.rootPath = appRootPath + appConfig.appKey = opts.key + appConfig.appIndex = opts.index + appConfig.appName = opts.name + + local appPackagePath = appRootPath .. "/?.lua;" + local pattern = string.gsub(appPackagePath, "([.?-])", "%%%1") + defaultPackagePath = string.gsub(defaultPackagePath, pattern, "") + appConfig.packagePath = appPackagePath .. defaultPackagePath + + local appConfigPath = appRootPath .. "/conf/app_config.lua" + if io.exists(appConfigPath) then + local appCustomConfig = dofile(appConfigPath) + table.merge(appConfig, appCustomConfig) + end + + appConfigs[appRootPath] = config + end + + return appConfigs +end + +return Factory diff --git a/src/server/base/HttpConnectBase.lua b/src/packages/gbc/HttpInstanceBase.lua similarity index 68% rename from src/server/base/HttpConnectBase.lua rename to src/packages/gbc/HttpInstanceBase.lua index d779b70..ab074fb 100644 --- a/src/server/base/HttpConnectBase.lua +++ b/src/packages/gbc/HttpInstanceBase.lua @@ -22,34 +22,33 @@ THE SOFTWARE. ]] -local ngx = ngx -local ngx_say = ngx.say -local req_get_headers = ngx.req.get_headers +local ngx = ngx +local ngx_say = ngx.say local req_get_body_data = ngx.req.get_body_data -local req_get_method = ngx.req.get_method -local req_get_uri_args = ngx.req.get_uri_args -local req_read_body = ngx.req.read_body +local req_get_headers = ngx.req.get_headers +local req_get_method = ngx.req.get_method local req_get_post_args = ngx.req.get_post_args -local table_merge = table.merge -local string_gsub = string.gsub -local string_ltrim = string.ltrim -local json_encode = json.encode +local req_get_uri_args = ngx.req.get_uri_args +local req_read_body = ngx.req.read_body +local string_format = string.format +local string_gsub = string.gsub +local string_ltrim = string.ltrim +local table_merge = table.merge -local ConnectBase = import(".ConnectBase") +local json = cc.import("#json") +local Constants = cc.import(".Constants") -local HttpConnectBase = class("HttpConnectBase", ConnectBase) +local InstanceBase = cc.import(".InstanceBase") +local HttpInstanceBase = cc.class("HttpInstanceBase", InstanceBase) -local Constants = import(".Constants") +function HttpInstanceBase:ctor(config) + HttpInstanceBase.super.ctor(self, config, Constants.HTTP_REQUEST_TYPE) -function HttpConnectBase:ctor(config) - HttpConnectBase.super.ctor(self, config) - - if config.appHttpMessageFormat then - self.config.messageFormat = config.appHttpMessageFormat + if config.app.httpMessageFormat then + self.config.app.messageFormat = config.app.httpMessageFormat end - self._requestType = Constants.HTTP_REQUEST_TYPE - self._requestMethod = req_get_method() + self._requestMethod = req_get_method() self._requestParameters = req_get_uri_args() if self._requestMethod == "POST" then @@ -78,7 +77,7 @@ function HttpConnectBase:ctor(config) if body then local data, err = json.decode(body) if err then - printWarn("HttpConnectBase:ctor() - invalid JSON content, %s", err) + cc.printwarn("HttpInstanceBase:ctor() - invalid JSON content, %s", err) else table_merge(self._requestParameters, data) end @@ -89,7 +88,7 @@ function HttpConnectBase:ctor(config) end end -function HttpConnectBase:run() +function HttpInstanceBase:run() local result, err = self:runEventLoop() result, err = self:_genOutput(result, err) if err then @@ -104,11 +103,11 @@ function HttpConnectBase:run() end -- actually it is not a loop, since it is based on HTTP. -function HttpConnectBase:runEventLoop() +function HttpInstanceBase:runEventLoop() local actionName = self._requestParameters.action or "" actionName = tostring(actionName) - if DEBUG > 1 then - printInfo("HTTP request, action: %s, data: %s", actionName, json_encode(self._requestParameters)) + if cc.DEBUG > cc.DEBUG_WARN then + cc.printinfo("HTTP action: %s, data: %s", actionName, json.encode(self._requestParameters)) end local err = nil @@ -116,29 +115,33 @@ function HttpConnectBase:runEventLoop() return self:runAction(actionName, self._requestParameters) end, function(_err) err = _err - if DEBUG > 1 then - printWarn("HTTP request, action: %s, %s", actionName, err .. debug.traceback("", 4)) + if cc.DEBUG > cc.DEBUG_WARN then + err = debug.traceback(err, 3) + cc.printwarn(err) end - -- error message need return to client end) if err then - return nil, string.format("run action \"%s\" error, %s", actionName, err) + return nil, self:_formatError(actionName, err) end return result end -function HttpConnectBase:_genOutput(result, err) +function HttpInstanceBase:_formatError(actionName, err) + return string_format("run action \"%s\" error, %s", actionName, err) +end + +function HttpInstanceBase:_genOutput(result, err) local rtype = type(result) - if self.config.messageFormat == Constants.MESSAGE_FORMAT_JSON then + if self.config.app.messageFormat == Constants.MESSAGE_FORMAT_JSON then if err then - result = {err = strip_luafile_paths(err)} + result = {err = err} elseif rtype == "nil" then result = {} elseif rtype ~= "table" then result = {result = tostring(result)} end - return json_encode(result) - elseif self.config.messageFormat == Constants.MESSAGE_FORMAT_TEXT then + return json.encode(result) + elseif self.config.app.messageFormat == Constants.MESSAGE_FORMAT_TEXT then if err then return nil, err elseif rtype == "nil" then @@ -149,4 +152,4 @@ function HttpConnectBase:_genOutput(result, err) end end -return HttpConnectBase +return HttpInstanceBase diff --git a/src/packages/gbc/InstanceBase.lua b/src/packages/gbc/InstanceBase.lua new file mode 100644 index 0000000..2902a0d --- /dev/null +++ b/src/packages/gbc/InstanceBase.lua @@ -0,0 +1,252 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local pcall = pcall +local string_byte = string.byte +local string_find = string.find +local string_format = string.format +local string_gsub = string.gsub +local string_lower = string.lower +local string_sub = string.sub +local string_trim = string.trim +local string_ucfirst = string.ucfirst +local table_concat = table.concat +local table_remove = table.remove +local type = type + +local sleep + +if ngx then + sleep = ngx.sleep +else + local socket = require("socket") + sleep = socket.sleep +end + +local Redis = cc.import("#redis") +local Beanstalkd = cc.import("#beanstalkd") +local Jobs = cc.import("#jobs") +local Event = cc.import("#event") +local Constants = cc.import(".Constants") + +local InstanceBase = cc.class("InstanceBase") + +local _MODULE_SUFFIX = 'Action' +local _MEHTOD_SUFFIX = 'Action' +local _CLI_PACKAGE_NAME = 'commands' +local _WORKER_PACKAGE_NAME = 'jobs' +local _HTTP_PACKAGE_NAME = 'actions' + +local _normalize, _getpath, _loadmodule, _checkreqtype + +function InstanceBase:ctor(config, requestType) + self.config = table.copy(cc.checktable(config)) + local appConfig = self.config.app + appConfig.messageFormat = appConfig.messageFormat or Constants.MESSAGE_FORMAT + + self._requestType = requestType + self._package = appConfig.package + if not self._package then + if requestType == Constants.CLI_REQUEST_TYPE then + self._package = _CLI_PACKAGE_NAME + elseif requestType == Constants.WORKER_REQUEST_TYPE then + self._package = _WORKER_PACKAGE_NAME + else + self._package = _HTTP_PACKAGE_NAME + end + end + + self._requestParameters = nil + self._modules = {} + self._event = cc.addComponent(self, Event) +end + +function InstanceBase:getRequestType() + return self._requestType +end + +function InstanceBase:runAction(actionName, args) + local appConfig = self.config.app + + local moduleName, methodName, folder = _normalize(actionName) + methodName = methodName .. _MEHTOD_SUFFIX + + local actionModulePath = _getpath(moduleName, folder, self._package) + local action = self._modules[actionModulePath] + if not action then + local actionModule = _loadmodule(actionModulePath) + local acceptedRequestType = actionModule.ACCEPTED_REQUEST_TYPE or appConfig.defaultAcceptedRequestType + local currentRequestType = self:getRequestType() + if not _checkreqtype(currentRequestType, acceptedRequestType) then + cc.throw("can't access this action via request type \"%s\"", currentRequestType) + end + + action = actionModule:new(self) + self._modules[actionModulePath] = action + end + + local method = action[methodName] + if type(method) ~= "function" then + cc.throw("invalid action method \"%s:%s()\"", moduleName, methodName) + end + + if not args then + args = self._requestParameters or {} + end + + return method(action, args) +end + +function InstanceBase:getRedis() + local redis = self._redis + if not redis then + local config = self.config.server.redis + redis = Redis:new() + + local ok, err + if config.socket then + ok, err = redis:connect(config.socket) + else + ok, err = redis:connect(config.host, config.port) + end + if not ok then + cc.throw("InstanceBase:getRedis() - %s", err) + end + + redis:select(self.config.app.appIndex) + self._redis = redis + end + return redis +end + +function InstanceBase:getJobs(opts) + local jobs = self._jobs + if not jobs then + local bean = Beanstalkd:new() + local config = self.config.server.beanstalkd + local try = 3 + while true do + local ok, err = bean:connect(config.host, config.port) + if ok then break end + try = try - 1 + if try == 0 then + cc.throw("InstanceBase:getJobs() - connect to beanstalkd, %s", err) + else + sleep(1.0) + end + end + + local tube = string_format(Constants.BEANSTALKD_JOB_TUBE_PATTERN, tostring(self.config.app.appIndex)) + bean:use(tube) + bean:watch(tube) + bean:ignore("default") + + jobs = Jobs:new(bean, self:getRedis()) + self._jobs = jobs + end + return jobs +end + +-- private + +_normalize = function(actionName) + if not actionName or actionName == "" then + actionName = "index.index" + end + + local folder = "" + if string_byte(actionName) == 47 --[[ / ]] then + local pos = 1 + local offset = 2 + while true do + pos = string_find(actionName, "/", offset) + if not pos then + pos = offset - 1 + break + else + offset = offset + 1 + end + end + + folder = string_trim(string_sub(actionName, 1, pos), "/") + actionName = string_sub(actionName, pos + 1) + end + + actionName = string_lower(actionName) + actionName = string_gsub(actionName, "[^%a./]", "") + actionName = string_gsub(actionName, "^[.]+", "") + actionName = string_gsub(actionName, "[.]+$", "") + + -- demo.hello.say --> {"demo", "hello", "say"] + local parts = string.split(actionName, ".") + local c = #parts + if c == 1 then + return string_ucfirst(parts[1]), "index", folder + end + -- method = "say" + local method = parts[c] + table_remove(parts, c) + c = c - 1 + -- mdoule = "demo.Hello" + parts[c] = string_ucfirst(parts[c]) + return table_concat(parts, "."), method, folder +end + +_getpath = function(moduleName, folder, package) + moduleName = moduleName .. _MODULE_SUFFIX + if folder ~= "" then + return string_format("%s.%s", string.gsub(folder, "/", "."), moduleName) + else + return string_format("%s.%s", package, moduleName) + end +end + +_loadmodule = function(actionModulePath) + if cc.DEBUG >= cc.DEBUG_INFO then + package.loaded[actionModulePath] = nil + end + local ok, actionModule = pcall(require, actionModulePath) + if not ok then + cc.throw("%s", actionModule) -- actionModule is error message + end + return actionModule +end + +_checkreqtype = function(currentRequestType, acceptedRequestType) + if type(acceptedRequestType) == "table" then + for _, v in ipairs(acceptedRequestType) do + if string_lower(v) == currentRequestType then + return true + end + end + elseif type(acceptedRequestType) == "string" then + return currentRequestType == string_lower(acceptedRequestType) + else + cc.throw("invalid ACCEPTED_REQUEST_TYPE of the action.") + end + + return false +end + +return InstanceBase diff --git a/src/packages/gbc/NginxBootstrap.lua b/src/packages/gbc/NginxBootstrap.lua new file mode 100644 index 0000000..9bebdab --- /dev/null +++ b/src/packages/gbc/NginxBootstrap.lua @@ -0,0 +1,57 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local Factory = cc.import(".Factory") + +local NginxBootstrap = cc.class("NginxBootstrap") + +function NginxBootstrap:ctor(appKeys, globalConfig) + self._configs = Factory.makeAppConfigs(appKeys, globalConfig, package.path) +end + +function NginxBootstrap:runapp(appRootPath) + local headers = ngx.req.get_headers() + local upgrade = headers.upgrade + if type(upgrade) == "table" then + upgrade = upgrade[1] + end + + local classNamePrefix = "HttpInstance" + local appConfig = self._configs[appRootPath] + if upgrade and string.lower(upgrade) == "websocket" then + classNamePrefix = "WebSocketInstance" + if not appConfig.app.websocketEnabled then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + else + if not appConfig.app.httpEnabled then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + end + + local connect = Factory.create(appConfig, classNamePrefix) + connect:run() +end + +return NginxBootstrap diff --git a/src/packages/gbc/WebSocketInstanceBase.lua b/src/packages/gbc/WebSocketInstanceBase.lua new file mode 100644 index 0000000..ed77f93 --- /dev/null +++ b/src/packages/gbc/WebSocketInstanceBase.lua @@ -0,0 +1,350 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local ngx = ngx +local ngx_log = ngx.log +local ngx_md5 = ngx.md5 +local ngx_thread_spawn = ngx.thread.spawn +local req_get_headers = ngx.req.get_headers +local req_read_body = ngx.req.read_body +local string_format = string.format +local string_sub = string.sub +local table_concat = table.concat +local table_insert = table.insert +local tostring = tostring +local type = type + +local json = cc.import("#json") +local Constants = cc.import(".Constants") + +local json_encode = json.encode +local json_decode = json.decode + +local InstanceBase = cc.import(".InstanceBase") +local WebSocketInstanceBase = cc.class("WebSocketInstanceBase", InstanceBase) + +local _EVENT = table.readonly({ + CONNECTED = "CONNECTED", + DISCONNECTED = "DISCONNECTED", + CONTROL_MESSAGE = "CONTROL_MESSAGE", +}) + +WebSocketInstanceBase.EVENT = _EVENT + +local _processMessage, _parseMessage, _authConnect + +function WebSocketInstanceBase:ctor(config) + WebSocketInstanceBase.super.ctor(self, config, Constants.WEBSOCKET_REQUEST_TYPE) + + local appConfig = self.config.app + if config.app.websocketMessageFormat then + appConfig.messageFormat = config.app.websocketMessageFormat + end + appConfig.websocketsTimeout = appConfig.websocketsTimeout or Constants.WEBSOCKET_DEFAULT_TIME_OUT + appConfig.websocketsMaxPayloadLen = appConfig.websocketsMaxPayloadLen or Constants.WEBSOCKET_DEFAULT_MAX_PAYLOAD_LEN +end + +function WebSocketInstanceBase:run() + local ok, err = xpcall(function() + self:runEventLoop() + ngx.exit(ngx.OK) + end, function(err) + err = tostring(err) + if cc.DEBUG > cc.DEBUG_WARN then + ngx_log(ngx.ERR, err .. debug.traceback("", 3)) + else + ngx_log(ngx.ERR, err) + end + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.exit(ngx.ERROR) + end) +end + +function WebSocketInstanceBase:runEventLoop() + -- auth client + local token, err = _authConnect() + if not token then + cc.throw(err) + end + self._connectToken = token + + -- generate connect id and channel + local redis = self:getRedis() + local connectId = tostring(redis:incr(Constants.NEXT_CONNECT_ID_KEY)) + self._connectId = connectId + + local connectChannel = Constants.CONNECT_CHANNEL_PREFIX .. tostring(connectId) + self._connectChannel = connectChannel + local controlChannel = Constants.CONTROL_CHANNEL_PREFIX .. tostring(connectId) + self._controlChannel = controlChannel + + -- create websocket server + local server = require("resty.websocket.server") + local socket, err = server:new({ + timeout = self.config.app.websocketsTimeout, + max_payload_len = self.config.app.websocketsMaxPayloadLen, + }) + if err then + cc.throw("[websocket:%s] create websocket server failed, %s", connectId, err) + end + self._socket = socket + + -- tracking socket close reason + local closeReason = "" + + -- create subscribe loop + local sub, err = self:getRedis():makeSubscribeLoop(connectId) + if not sub then + cc.throw(err) + end + + local event = self._event + sub:start(function(channel, msg) + if channel == controlChannel then + event:trigger({ + name = _EVENT.CONTROL_MESSAGE, + channel = channel, + message = msg + }) + if msg == Constants.CLOSE_CONNECT then + closeReason = Constants.CLOSE_CONNECT + socket:send_close() + end + else + socket:send_text(msg) + end + end, controlChannel, connectChannel, Constants.BROADCAST_ALL_CHANNEL) + self._subloop = sub + + -- connected + cc.printinfo("[websocket:%s] connected", connectId) + event:trigger(_EVENT.CONNECTED) + + -- event loop + local frames = {} + local running = true + while running do + self:heartbeat() + + while true do + --[[ + Receives a WebSocket frame from the wire. + + In case of an error, returns two nil values and a string describing the error. + + The second return value is always the frame type, which could be + one of continuation, text, binary, close, ping, pong, or nil (for unknown types). + + For close frames, returns 3 values: the extra status message + (which could be an empty string), the string "close", and a Lua number for + the status code (if any). For possible closing status codes, see + + http://tools.ietf.org/html/rfc6455#section-7.4.1 + + For other types of frames, just returns the payload and the type. + + For fragmented frames, the err return value is the Lua string "again". + ]] + local frame, ftype, err = socket:recv_frame() + if err then + if err == "again" then + frames[#frames + 1] = frame + break -- recv next message + end + + if string_sub(err, -7) == "timeout" then + break -- recv next message + end + + cc.printwarn("[websocket:%s] failed to receive frame, type \"%s\", %s", connectId, ftype, err) + closeReason = ftype + running = false -- stop loop + break + end + + if #frames > 0 then + -- merging fragmented frames + frames[#frames + 1] = frame + frame = table_concat(frames) + frames = {} + end + + if ftype == "close" then + running = false -- stop loop + break + elseif ftype == "ping" then + socket:send_pong() + elseif ftype == "pong" then + -- client ponged + elseif ftype == "text" or ftype == "binary" then + local ok, err = _processMessage(self, frame, ftype) + if err then + cc.printerror("[websocket:%s] process %s message failed, %s", connectId, ftype, err) + end + else + cc.printwarn("[websocket:%s] unknown frame type \"%s\"", connectId, tostring(ftype)) + end + end -- rect next message + end -- loop + + sub:stop() + self._subloop = nil + self._socket = nil + + -- disconnected + event:trigger({name = _EVENT.DISCONNECTED, reason = reason}) + cc.printinfo("[websocket:%s] disconnected", connectId) +end + +function WebSocketInstanceBase:heartbeat() +end + +function WebSocketInstanceBase:getConnectToken() + return self._connectToken +end + +function WebSocketInstanceBase:getConnectId() + return self._connectId +end + +function WebSocketInstanceBase:getConnectChannel() + return self._connectChannel +end + +function WebSocketInstanceBase:getControlChannel() + return self._controlChannel +end + +-- add methods + +local _COMMANDS = { + "subscribe", "unsubscribe", + "psubscribe", "punsubscribe", +} + +for _, cmd in ipairs(_COMMANDS) do + WebSocketInstanceBase[cmd] = function(self, ...) + local subloop = self._subloop + local method = subloop[cmd] + return method(subloop, ...) + end +end + +-- private + +_processMessage = function(self, rawMessage, messageType) + local message = _parseMessage(rawMessage, messageType, self.config.app.websocketMessageFormat) + local msgid = message.__id + local actionName = message.action + local err = nil + local ok, result = xpcall(function() + return self:runAction(actionName, message) + end, function(_err) + err = _err + if cc.DEBUG > cc.DEBUG_WARN then + err = err .. debug.traceback("", 4) + end + end) + if err then + return nil, err + end + + local rtype = type(result) + if rtype == "nil" then + return + end + + if rtype ~= "table" then + if msgid then + cc.printwarn("action \"%s\" return invalid result for message [__id:\"%s\"]", actionName, msgid) + else + cc.printwarn("action \"%s\" return invalid result", actionName) + end + end + + if not msgid then + cc.printwarn("action \"%s\" return unused result", actionName) + return true + end + + if not self._socket then + return nil, string.format("socket removed, action \"%s\"", actionName) + end + + result.__id = msgid + local message = json_encode(result) + local bytes, err = self._socket:send_text(message) + if err then + return nil, string.format("send message to client failed, %s", err) + end + + return true +end + +_parseMessage = function(rawMessage, messageType, messageFormat) + -- TODO: support message type plugin + if messageType ~= Constants.WEBSOCKET_TEXT_MESSAGE_TYPE then + cc.throw("not supported message type \"%s\"", messageType) + end + + -- TODO: support message format plugin + if messageFormat == "json" then + local message = json_decode(rawMessage) + if type(message) == "table" then + return message + else + cc.throw("not supported message format \"%s\"", type(message)) + end + else + cc.throw("not support message format \"%s\"", tostring(messageFormat)) + end +end + +_authConnect = function() + if ngx.headers_sent then + return nil, "response header already sent" + end + + req_read_body() + local headers = ngx.req.get_headers() + local protocols = headers["sec-websocket-protocol"] + if type(protocols) ~= "table" then + protocols = {protocols} + end + if #protocols == 0 then + return nil, "not set header: Sec-WebSocket-Protocol" + end + + local pattern = Constants.WEBSOCKET_SUBPROTOCOL_PATTERN + for _, protocol in ipairs(protocols) do + local token = string.match(protocol, pattern) + if token then + return token + end + end + + return nil, "not found token in header: Sec-WebSocket-Protocol" +end + +return WebSocketInstanceBase diff --git a/src/packages/gbc/WorkerBootstrap.lua b/src/packages/gbc/WorkerBootstrap.lua new file mode 100644 index 0000000..f3946bc --- /dev/null +++ b/src/packages/gbc/WorkerBootstrap.lua @@ -0,0 +1,41 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local process = cc.import("process") +local Factory = cc.import(".Factory") + +local WorkerBootstrap = cc.class("WorkerBootstrap") + +function WorkerBootstrap:ctor(appKeys, globalConfig) + self._configs = Factory.makeAppConfigs(appKeys, globalConfig, package.path) + self._pid = tostring(process.getpid()) +end + +function WorkerBootstrap:runapp(appRootPath) + local appConfig = self._configs[appRootPath] + local worker = Factory.create(appConfig, "WorkerInstance", nil, self._pid) + return worker:run() +end + +return WorkerBootstrap diff --git a/src/packages/gbc/WorkerInstanceBase.lua b/src/packages/gbc/WorkerInstanceBase.lua new file mode 100644 index 0000000..dd7aa85 --- /dev/null +++ b/src/packages/gbc/WorkerInstanceBase.lua @@ -0,0 +1,117 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local io_flush = io.flush +local os_date = os.date +local os_time = os.time +local string_format = string.format +local string_lower = string.lower +local tostring = tostring +local type = type + +local json = cc.import("#json") +local Constants = cc.import(".Constants") + +local InstanceBase = cc.import(".InstanceBase") +local WorkerInstanceBase = cc.class("WorkerInstanceBase", InstanceBase) + +function WorkerInstanceBase:ctor(config, args, tag) + WorkerInstanceBase.super.ctor(self, config, Constants.WORKER_REQUEST_TYPE) + self._tag = tag or "worker" +end + +function WorkerInstanceBase:run() + return self:runEventLoop() +end + +function WorkerInstanceBase:runEventLoop() + local jobWorkerRequests = self.config.app.jobWorkerRequests + local jobs = self:getJobs({try = 3}) + local bean = jobs:getBeanstalkd() + local beanerrs = bean.ERRORS + local appname = self.config.app.appName + local tag = string_format("%s:%s", appname, self._tag) + local running = true + + cc.printinfo("[%s] ready, waiting for job", tag) + + while running do + while true do + io_flush() + + jobWorkerRequests = jobWorkerRequests - 1 + if jobWorkerRequests < 0 then + cc.printinfo("[%s] job worker is done", tag) + running = false -- stop loop + break + end + + local job, err = jobs:getready() + if not job then + if err == beanerrs.TIMED_OUT then + break -- wait next job + end + if err == beanerrs.DEADLINE_SOON then + cc.printinfo("[%s] deadline soon", tag) + break -- wait next job + end + + cc.printwarn("[%s] reserve job failed, %s", tag, err) running = false -- stop loop + break + end + + if not job.id then + cc.printinfo("[%s] get a invalid job", tag) + break -- wait next job + else + cc.printinfo("[%s] get a job %s, action: %s", tag, job.id, job.action) + end + + -- handle the job + local actionName = job.action + local _, res = xpcall(function() + return self:runAction(actionName, job) + end, function(err) + if cc.DEBUG > cc.DEBUG_WARN then + err = debug.traceback(err, 3) + cc.printwarn(err) + end + end) + if res ~= false then + -- delete job + local ok, err = jobs:delete(job.id) + if not ok then + cc.printwarn("[%s] delete job %s failed, %s", tag, job.id, err) + else + cc.printinfo("[%s] job %s done", tag, job.id) + end + end + end -- wait next job + end -- loop + + io_flush() + return 1 +end + +return WorkerInstanceBase diff --git a/src/packages/redis/RedisTransaction.lua b/src/packages/gbc/gbc.lua similarity index 54% rename from src/packages/redis/RedisTransaction.lua rename to src/packages/gbc/gbc.lua index d671a18..de81fe9 100644 --- a/src/packages/redis/RedisTransaction.lua +++ b/src/packages/gbc/gbc.lua @@ -22,37 +22,30 @@ THE SOFTWARE. ]] -local assert = assert +local _CUR = ... -local RedisTransaction = class("RedisTransaction") +local _M = { + VERSION = "0.8.0", -function RedisTransaction:ctor(easy, ...) - self.easy = easy - self.commandsCount = 0 - if #{...} > 0 then self:watch(...) end -end + Constants = cc.import(".Constants", _CUR), + Factory = cc.import(".Factory", _CUR), -function RedisTransaction:watch(...) - assert(self.commandsCount == 0, "RedisTransaction:watch() - WATCH inside MULTI is not allowed") - return self.easy:command("watch", ...) -end + ActionBase = cc.import(".ActionBase", _CUR), + InstanceBase = cc.import(".InstanceBase", _CUR), -function RedisTransaction:command(command, ...) - if self.commandsCount == 0 then - self.easy:command("multi") - end - self.commandsCount = self.commandsCount + 1 - return self.easy:command(command, ...) -end + CommandLineInstanceBase = cc.import(".CommandLineInstanceBase", _CUR), + CommandLineBootstrap = cc.import(".CommandLineBootstrap", _CUR), -function RedisTransaction:commit() - if self.commandsCount == 0 then return true end - return self.easy:command("exec") -end + WorkerInstanceBase = cc.import(".WorkerInstanceBase", _CUR), + WorkerBootstrap = cc.import(".WorkerBootstrap", _CUR), + + Broadcast = cc.import(".Broadcast", _CUR), +} -function RedisTransaction:discard() - if self.commandsCount == 0 then return true end - return self.easy:command("discard") +if ngx then + _M.HttpInstanceBase = cc.import(".HttpInstanceBase", _CUR) + _M.WebSocketInstanceBase = cc.import(".WebSocketInstanceBase", _CUR) + _M.NginxBootstrap = cc.import(".NginxBootstrap", _CUR) end -return RedisTransaction +return _M diff --git a/src/packages/job/JobService.lua b/src/packages/job/JobService.lua deleted file mode 100644 index 1a6644a..0000000 --- a/src/packages/job/JobService.lua +++ /dev/null @@ -1,145 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local pairs = pairs -local tonumber = tonumber -local json_encode = json.encode -local json_decode = json.decode -local os_time = os.time -local string_format = string.format - -local _JOB_KEY = "_JOB_KEY" -local _JOB_HASH = "_JOB_HASH" - -local _JOB_PRIORITY_NORMAL = 5000 - -local JobService = class("JobService") - -function JobService:ctor(redis, beans, config) - if not redis or not beans then - throw("job service is initialized failed: redis or beans is invalid.") - end - if not config then - throw("job service is initialized failed: can't get jobTube from config.") - end - self._redis = redis - self._beans = beans - self._jobTube = config.beanstalkd.jobTube -end - -function JobService:add(action, data, delay, priority, ttr) - local beans = self._beans - local redis = self._redis - - delay = delay or 0 - priority = priority or _JOB_PRIORITY_NORMAL - ttr = ttr or 120 - - if not action then - throw("job service add job failed: job action is null.") - end - - -- jobId means a redis id of this job. - local jobId, err = redis:command("INCR", _JOB_KEY) - if not jobId then - return nil, string_format("job service generate job id failed: %s", err) - end - - local job = {} - job.id = jobId - job.joined_time = os_time() - job.action = action - job.arg = data - job.delay = delay - job.priority = priority - job.ttr = ttr - - -- put job to beanstalkd - beans:command("use", self._jobTube) - - -- jobBid means job id in beanstalkd. - jobBid = beans:command("put", json_encode(job), tonumber(priority), tonumber(delay), tonumber(ttr)) - printInfo("job Bid = %s", jobBid) - - -- store job info to redis for persistence - job.bid = jobBid - local ok, err = redis:command("HSET", _JOB_HASH, jobId, json_encode(job)) - if not ok then - throw("job service newJob() store job into redis failed: %s", err) - end - - return jobId -end - -function JobService:query(jobId) - local redis = self._redis - - local job, err = redis:command("HGET", _JOB_HASH, jobId) - if not job then - return nil, string_format("job service query failed: %s", err) - end - - if ngx and job == ngx.null then - return nil, string_format("job service query failed: job[%d] does not exist.", tonumber(jobId)) - end - - job, err = json_decode(job) - if not job then - return nil, string_format("job service query, the contents of job[%d] is invalid: %s", tonumber(jobId), err) - end - - return job, nil -end - -function JobService:remove(jobId) - local redis = self._redis - local beans = self._beans - - local job, err = redis:command("HGET", _JOB_HASH, jobId) - if not job then - return nil, string_format("job service remove failed: can't get job from db, %s", err) - end - if ngx and jobStr == ngx.null then - return nil, string_format("job service remove failed: job[%d] does not exist.", jobId) - end - - -- delete it from redis - redis:command("HDEL", _JOB_HASH, jobId) - - job, err = json_decode(job) - if not job then - return nil, string_format("job service remove, the contents of job[%d] is invalid.", jobId) - end - - -- delete it from beanstalkd - local bid = job.bid - local ok, err = beans:command("delete", tonumber(bid)) - if not ok then - return nil, string_format("job service remove failed: %s", err) - end - - return true, nil -end - -return JobService diff --git a/src/packages/jobs/jobs.lua b/src/packages/jobs/jobs.lua new file mode 100644 index 0000000..75b3e0c --- /dev/null +++ b/src/packages/jobs/jobs.lua @@ -0,0 +1,118 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local json = cc.import("#json") + +local Jobs = cc.class("Jobs") + +Jobs.DEFAULT_DELAY = 1 +Jobs.DEFAULT_TTR = 10 + +Jobs.DEFAULT_PRIORITY = 2048 +Jobs.NORMAL_PRIORITY = Jobs.DEFAULT_PRIORITY +Jobs.HIGH_PRIORITY = 512 +Jobs.LOW_PRIORITY = 4096 + +function Jobs:ctor(bean) + self._bean = bean + return 1 +end + +function Jobs:getBeanstalkd() + return self._bean +end + +function Jobs:add(job) + local action = job.action + local data = job.data + local delay = job.delay or Jobs.DEFAULT_DELAY + local pri = job.pri or Jobs.DEFAULT_PRIORITY + local ttr = job.ttr or Jobs.DEFAULT_TTR + + local job = { + action = action, + data = data, + delay = delay, + pri = pri, + ttr = ttr, + } + + return self._bean:put(json.encode(job), pri, delay, ttr) +end + +function Jobs:at(job) + local now = os.time() + local at = job.time + if type(at) ~= "number" then + at = now + end + local delay = at - now + job.delay = delay + job.time = nil + return self:add(job) +end + +function Jobs:get(id) + local bean = self._bean + + local jobraw, err = bean:peek(id) + if not jobraw then + return nil, err + end + + local job = json.decode(jobraw.data) + if type(job) ~= "table" then + self:delete(jobraw.id) + return nil, string.format("invalid job %s", jobraw.id) + end + + job.id = jobraw.id + return job +end + +function Jobs:getready(timeout) + local bean = self._bean + + local jobraw, err = bean:reserve(timeout) + if not jobraw then + return nil, err + end + + local id = jobraw.id + local job = json.decode(jobraw.data) + if type(job) ~= "table" or not job.action then + bean:delete(id) + return {id = nil} + end + + job.id = id + return job +end + +function Jobs:delete(id) + return self._bean:delete(id) +end + +return Jobs + diff --git a/src/framework/json.lua b/src/packages/json/json.lua similarity index 74% rename from src/framework/json.lua rename to src/packages/json/json.lua index 7bb1a99..53d84ad 100644 --- a/src/framework/json.lua +++ b/src/packages/json/json.lua @@ -22,25 +22,26 @@ THE SOFTWARE. ]] -local tostring = tostring local pcall = pcall -local debug_traceback = debug.traceback - -local json = {} local cjson = require("cjson") -function json.encode(var) - local ok, result = pcall(cjson.encode, var) - if ok then return result end - return nil, result -end +local cjson_encode = cjson.encode +local cjson_decode = cjson.decode -function json.decode(text) - local ok, result = pcall(cjson.decode, text) - if ok then return result end - return nil, result +local _M = { + null = cjson.null +} + +function _M.encode(var) + local ok, res = pcall(cjson_encode, var) + if ok then return res end + return nil, res -- res is error end -json.null = cjson.null +function _M.decode(text) + local ok, res = pcall(cjson_decode, text) + if ok then return res end + return nil, res -- res is error +end -return json +return _M diff --git a/src/packages/luamd5/luamd5.lua b/src/packages/luamd5/luamd5.lua new file mode 100644 index 0000000..a65088c --- /dev/null +++ b/src/packages/luamd5/luamd5.lua @@ -0,0 +1,225 @@ + +local _md5 = { + _VERSION = "md5.lua 1.0.2", + _DESCRIPTION = "MD5 computation in Lua (5.1-3, LuaJIT)", + _URL = "https://github.com/kikito/md5.lua", + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2013 Enrique García Cota + Adam Baldwin + hanzao + Equi 4 Software + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ]] +} + + +local char, byte, format, rep, sub = + string.char, string.byte, string.format, string.rep, string.sub +local bit_or, bit_and, bit_not, bit_xor, bit_rshift, bit_lshift + +local ok, bit = pcall(require, 'bit') +bit_or, bit_and, bit_not, bit_xor, bit_rshift, bit_lshift = bit.bor, bit.band, bit.bnot, bit.bxor, bit.rshift, bit.lshift + +-- convert little-endian 32-bit int to a 4-char string +local function lei2str(i) + local f=function (s) return char( bit_and( bit_rshift(i, s), 255)) end + return f(0)..f(8)..f(16)..f(24) +end + +-- convert raw string to big-endian int +local function str2bei(s) + local v=0 + for i=1, #s do + v = v * 256 + byte(s, i) + end + return v +end + +-- convert raw string to little-endian int +local function str2lei(s) + local v=0 + for i = #s,1,-1 do + v = v*256 + byte(s, i) + end + return v +end + +-- cut up a string in little-endian ints of given size +local function cut_le_str(s,...) + local o, r = 1, {} + local args = {...} + for i=1, #args do + table.insert(r, str2lei(sub(s, o, o + args[i] - 1))) + o = o + args[i] + end + return r +end + +local swap = function (w) return str2bei(lei2str(w)) end + +local function hex2binaryaux(hexval) + return char(tonumber(hexval, 16)) +end + +local function hex2binary(hex) + local result, _ = hex:gsub('..', hex2binaryaux) + return result +end + +-- An MD5 mplementation in Lua, requires bitlib (hacked to use LuaBit from above, ugh) +-- 10/02/2001 jcw@equi4.com + +local CONSTS = { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, + 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, + 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, + 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476 +} + +local f=function (x,y,z) return bit_or(bit_and(x,y),bit_and(-x-1,z)) end +local g=function (x,y,z) return bit_or(bit_and(x,z),bit_and(y,-z-1)) end +local h=function (x,y,z) return bit_xor(x,bit_xor(y,z)) end +local i=function (x,y,z) return bit_xor(y,bit_or(x,-z-1)) end +local z=function (f,a,b,c,d,x,s,ac) + a=bit_and(a+f(b,c,d)+x+ac,0xFFFFFFFF) + -- be *very* careful that left shift does not cause rounding! + return bit_or(bit_lshift(bit_and(a,bit_rshift(0xFFFFFFFF,s)),s),bit_rshift(a,32-s))+b +end + +local function transform(A,B,C,D,X) + local a,b,c,d=A,B,C,D + local t=CONSTS + + a=z(f,a,b,c,d,X[ 0], 7,t[ 1]) + d=z(f,d,a,b,c,X[ 1],12,t[ 2]) + c=z(f,c,d,a,b,X[ 2],17,t[ 3]) + b=z(f,b,c,d,a,X[ 3],22,t[ 4]) + a=z(f,a,b,c,d,X[ 4], 7,t[ 5]) + d=z(f,d,a,b,c,X[ 5],12,t[ 6]) + c=z(f,c,d,a,b,X[ 6],17,t[ 7]) + b=z(f,b,c,d,a,X[ 7],22,t[ 8]) + a=z(f,a,b,c,d,X[ 8], 7,t[ 9]) + d=z(f,d,a,b,c,X[ 9],12,t[10]) + c=z(f,c,d,a,b,X[10],17,t[11]) + b=z(f,b,c,d,a,X[11],22,t[12]) + a=z(f,a,b,c,d,X[12], 7,t[13]) + d=z(f,d,a,b,c,X[13],12,t[14]) + c=z(f,c,d,a,b,X[14],17,t[15]) + b=z(f,b,c,d,a,X[15],22,t[16]) + + a=z(g,a,b,c,d,X[ 1], 5,t[17]) + d=z(g,d,a,b,c,X[ 6], 9,t[18]) + c=z(g,c,d,a,b,X[11],14,t[19]) + b=z(g,b,c,d,a,X[ 0],20,t[20]) + a=z(g,a,b,c,d,X[ 5], 5,t[21]) + d=z(g,d,a,b,c,X[10], 9,t[22]) + c=z(g,c,d,a,b,X[15],14,t[23]) + b=z(g,b,c,d,a,X[ 4],20,t[24]) + a=z(g,a,b,c,d,X[ 9], 5,t[25]) + d=z(g,d,a,b,c,X[14], 9,t[26]) + c=z(g,c,d,a,b,X[ 3],14,t[27]) + b=z(g,b,c,d,a,X[ 8],20,t[28]) + a=z(g,a,b,c,d,X[13], 5,t[29]) + d=z(g,d,a,b,c,X[ 2], 9,t[30]) + c=z(g,c,d,a,b,X[ 7],14,t[31]) + b=z(g,b,c,d,a,X[12],20,t[32]) + + a=z(h,a,b,c,d,X[ 5], 4,t[33]) + d=z(h,d,a,b,c,X[ 8],11,t[34]) + c=z(h,c,d,a,b,X[11],16,t[35]) + b=z(h,b,c,d,a,X[14],23,t[36]) + a=z(h,a,b,c,d,X[ 1], 4,t[37]) + d=z(h,d,a,b,c,X[ 4],11,t[38]) + c=z(h,c,d,a,b,X[ 7],16,t[39]) + b=z(h,b,c,d,a,X[10],23,t[40]) + a=z(h,a,b,c,d,X[13], 4,t[41]) + d=z(h,d,a,b,c,X[ 0],11,t[42]) + c=z(h,c,d,a,b,X[ 3],16,t[43]) + b=z(h,b,c,d,a,X[ 6],23,t[44]) + a=z(h,a,b,c,d,X[ 9], 4,t[45]) + d=z(h,d,a,b,c,X[12],11,t[46]) + c=z(h,c,d,a,b,X[15],16,t[47]) + b=z(h,b,c,d,a,X[ 2],23,t[48]) + + a=z(i,a,b,c,d,X[ 0], 6,t[49]) + d=z(i,d,a,b,c,X[ 7],10,t[50]) + c=z(i,c,d,a,b,X[14],15,t[51]) + b=z(i,b,c,d,a,X[ 5],21,t[52]) + a=z(i,a,b,c,d,X[12], 6,t[53]) + d=z(i,d,a,b,c,X[ 3],10,t[54]) + c=z(i,c,d,a,b,X[10],15,t[55]) + b=z(i,b,c,d,a,X[ 1],21,t[56]) + a=z(i,a,b,c,d,X[ 8], 6,t[57]) + d=z(i,d,a,b,c,X[15],10,t[58]) + c=z(i,c,d,a,b,X[ 6],15,t[59]) + b=z(i,b,c,d,a,X[13],21,t[60]) + a=z(i,a,b,c,d,X[ 4], 6,t[61]) + d=z(i,d,a,b,c,X[11],10,t[62]) + c=z(i,c,d,a,b,X[ 2],15,t[63]) + b=z(i,b,c,d,a,X[ 9],21,t[64]) + + return A+a,B+b,C+c,D+d +end + +---------------------------------------------------------------- + +function _md5.sumhexa(s) + local msgLen = #s + local padLen = 56 - msgLen % 64 + + if msgLen % 64 > 56 then padLen = padLen + 64 end + + if padLen == 0 then padLen = 64 end + + s = s .. char(128) .. rep(char(0),padLen-1) .. lei2str(8*msgLen) .. lei2str(0) + + assert(#s % 64 == 0) + + local t = CONSTS + local a,b,c,d = t[65],t[66],t[67],t[68] + + for i=1,#s,64 do + local X = cut_le_str(sub(s,i,i+63),4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4) + assert(#X == 16) + X[0] = table.remove(X,1) -- zero based! + a,b,c,d = transform(a,b,c,d,X) + end + + return format("%08x%08x%08x%08x",swap(a),swap(b),swap(c),swap(d)) +end + +function _md5.sum(s) + return hex2binary(md5.sumhexa(s)) +end + +return _md5 diff --git a/src/packages/mysql/MysqlService.lua b/src/packages/mysql/MysqlService.lua deleted file mode 100644 index 99cdeeb..0000000 --- a/src/packages/mysql/MysqlService.lua +++ /dev/null @@ -1,120 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local type = type -local pairs = pairs -local ngx = ngx -local string_format = string.format -local table_concat = table.concat - -local MysqlService = class("MysqlService") - -local MysqlAdapter -if ngx then - MysqlAdapter = import(".adapter.MysqlRestyAdapter") -else - MysqlAdapter = import(".adapter.MysqlLuaAdapter") -end - -function MysqlService:ctor(config) - if not config or type(config) ~= "table" then - throw("invalid mysql config") - end - - self._config = config - self._mysql = MysqlAdapter:create(config) -end - -function MysqlService:close() - return self._mysql:close() -end - -function MysqlService:setKeepAlive(timeout, size) - if not ngx then - return self:close() - end - return self._mysql:setKeepAlive(timeout, size) -end - -function MysqlService:query(queryStr) - return self._mysql:query(queryStr) -end - -function MysqlService:insert(tableName, params) - local fieldNames = {} - local fieldValues = {} - - for name, value in pairs(params) do - fieldNames[#fieldNames + 1] = self:_escapeName(name) - fieldValues[#fieldValues + 1] = self:_escapeValue(value) - end - - local sql = string_format("INSERT INTO %s (%s) VALUES (%s)", - self:_escapeName(tableName), - table_concat(fieldNames, ","), - table_concat(fieldValues, ",")) - return self._mysql:query(sql) -end - -function MysqlService:update(tableName, params, where) - local fields = {} - local whereFields = {} - - for name, value in pairs(params) do - fields[#fields + 1] = self:_escapeName(name) .. "=" .. self:_escapeValue(value) - end - - for name, value in pairs(where) do - whereFields[#whereFields + 1] = self:_escapeName(name) .. "=" .. self:_escapeValue(value) - end - - local sql = string_format("UPDATE %s SET %s WHERE %s", - self:_escapeName(tableName), - table_concat(fields, ","), - table_concat(whereFields, " AND ")) - return self._mysql:query(sql) -end - -function MysqlService:del(tableName, where) - local whereFields = {} - - for name, value in pairs(where) do - whereFields[#whereFields + 1] = self:_escapeName(name) .. "=" .. self:_escapeValue(value) - end - - local sql = string_format("DElETE FROM %s WHERE %s", - self:_escapeName(tableName), - table_concat(whereFields, " AND ")) - return self._mysql:query(sql) -end - -function MysqlService:_escapeValue(value) - return self._mysql:escapeValue(value) -end - -function MysqlService:_escapeName(name) - return string_format([[`%s`]], name) -end - -return MysqlService diff --git a/src/packages/mysql/adapter/MysqlLuaAdapter.lua b/src/packages/mysql/adapter/MysqlLuaAdapter.lua deleted file mode 100644 index e077177..0000000 --- a/src/packages/mysql/adapter/MysqlLuaAdapter.lua +++ /dev/null @@ -1,105 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local assert = assert -local type = type -local pairs = pairs -local strFormat = string.format - -local mysql = require "3rd.luasql.mysql" - -local MysqlLuaAdapter = class("MysqlLuaAdapter") - -function MysqlLuaAdapter:ctor(config) - self.db_ = nil - self.env_ = nil - - local env, err = mysql.mysql() - if err then - printWarn("MysqlLuaAdapter:ctor() - failed to instantiate mysql: %s", err) - return db, err - end - - self.env_ = env - - local con, err = self.env_:connect(config.database, config.user, config.password, config.host, config.port) - self.db_ = con - - self.db_:execute"SET NAMES 'utf8'" -end - -function MysqlLuaAdapter:close() - assert(self.db_ ~= nil, "Not connect to mysql") - - self.db_:close() - self.env_:close() -end - -function MysqlLuaAdapter:query(queryStr) - assert(self.db_ ~= nil, "Not connect to mysql") - - local cur, err = self.db_:execute(queryStr) - if err then - printWarn("MysqlLuaAdapter:query() - failed to query mysql: %s", err) - return cur, err - end - - if type(cur) == "userdata" then - local row, err = cur:fetch ({}, "a") - if err then - printWarn("MysqlLuaAdapter:query() - failed to query mysql: %s", err) - return row, err - end - - local results = {row} - - while true do - row = cur:fetch (row, "a") - - if row == nil then - break - end - - local result = {} - for key, value in pairs(row) do - result[key] = value - end - - results[#results + 1] = result - end - - return results, err - end - - return cur, err -end - - -function MysqlLuaAdapter:escapeValue(value) - assert(self.db_ ~= nil, "Not connect to mysql") - - return strFormat("'%s'", self.db_:escape(value)) -end - -return MysqlLuaAdapter diff --git a/src/packages/mysql/adapter/MysqlRestyAdapter.lua b/src/packages/mysql/adapter/MysqlRestyAdapter.lua deleted file mode 100644 index 5b12268..0000000 --- a/src/packages/mysql/adapter/MysqlRestyAdapter.lua +++ /dev/null @@ -1,77 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local assert = assert -local tostring = tostring -local throw = throw -local ngx_quote_sql_str = ngx.quote_sql_str - -local mysql = require("resty.mysql") - -local MysqlRestyAdapter = class("MysqlRestyAdapter") - -function MysqlRestyAdapter:ctor(config) - self._config = config - - local db, err = mysql:new() - if err then - throw("failed to instantiation mysql: %s", err) - end - - self._db = db - self._db:set_timeout(config.timeout) - local ok, err, errno, sqlstate = db:connect(config) - if err then - throw("mysql connect error [%s] %s, %s", tostring(errno), err, sqlstate) - end - self._db:query("SET NAMES 'utf8'") -end - -function MysqlRestyAdapter:close() - self._db:close() -end - -function MysqlRestyAdapter:setKeepAlive(timeout, size) - if size then - return self._db:set_keepalive(timeout, size) - elseif timeout then - return self._db:set_keepAlive(timeout) - else - return self._db:set_keepalive() - end -end - -function MysqlRestyAdapter:query(queryStr) - local res, err, errno, sqlstate = self._db:query(queryStr) - if err then - throw("mysql query error: [%s] %s, %s", tostring(errno), err, sqlstate) - end - return res -end - -function MysqlRestyAdapter:escapeValue(value) - return ngx_quote_sql_str(value) -end - -return MysqlRestyAdapter diff --git a/src/packages/mysql/init.lua b/src/packages/mysql/init.lua deleted file mode 100644 index bc4aa52..0000000 --- a/src/packages/mysql/init.lua +++ /dev/null @@ -1,29 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local _P = {} - -_P.service = import(".MysqlService") - -return _P diff --git a/src/packages/redis/NginxRedisLoop.lua b/src/packages/redis/NginxRedisLoop.lua new file mode 100644 index 0000000..b4d9003 --- /dev/null +++ b/src/packages/redis/NginxRedisLoop.lua @@ -0,0 +1,217 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local semaphore = require "ngx.semaphore" + +local ipairs = ipairs +local ngx_thread_kill = ngx.thread.kill +local ngx_thread_spawn = ngx.thread.spawn +local string_byte = string.byte +local string_split = string.split +local string_sub = string.sub +local table_concat = table.concat +local table_remove = table.remove +local tostring = tostring +local unpack = unpack + +local NginxRedisLoop = cc.class("NginxRedisLoop") + +local _loop, _cleanup, _onmessage, _onerror + +function NginxRedisLoop:ctor(redis, subredis, id) + self._redis = redis + self._subredis = subredis + self._subredis:setTimeout(5) -- check client connect abort quickly + self._sema = semaphore.new() + + id = id or "" + self._id = id .. "_" .. string_sub(tostring(self), 10) +end + +function NginxRedisLoop:start(onmessage, cmdchannel, ...) + if not self._subredis then + return nil, "not initialized" + end + local onmessage = onmessage or _onmessage + local onerror = _onerror + self._cmdchannel = cmdchannel + + local res, err = self._subredis:subscribe(cmdchannel, ...) + if not res then + return nil, err + end + cc.printinfo("[RedisSub:%s] %s", self._id, table_concat(res, " ")) + + self._thread = ngx_thread_spawn(_loop, self, onmessage, onerror) + return 1 +end + +function NginxRedisLoop:stop() + self._redis:publish(self._cmdchannel, "!STOP") + self._sema:wait(1) + _cleanup(self) +end + +-- add methods + +local _COMMANDS = { + "subscribe", "unsubscribe", + "psubscribe", "punsubscribe", +} + +for _, cmd in ipairs(_COMMANDS) do + NginxRedisLoop[cmd] = function(self, ...) + local args = {"!REDIS", cmd} + for _, arg in ipairs({...}) do + args[#args + 1] = tostring(arg) + end + -- cc.printinfo("[RedisSub:%s] CMD: %s", self._id, table_concat(args, " ")) + local res, err = self._redis:publish(self._cmdchannel, table_concat(args, " ")) + -- wait for command completed + self._sema:wait(1) + return res, err + end +end + +-- private + +local _skipmsgtypes = { + subscribe = true, + unsubscribe = true, + psubscribe = true, + punsubscribe = true, +} + +_loop = function(self, onmessage, onerror) + local cmdchannel = self._cmdchannel + local subredis = self._subredis + local id = self._id + local running = true + local sema = self._sema + local DEBUG = cc.DEBUG > cc.DEBUG_WARN + + local msgtype, channel, msg, pchannel + + cc.printinfo("[RedisSub:%s] ", id) + + while running do + -- cc.printinfo("[RedisSub:%s] ", id) + local res, err = subredis:readReply() + if not res then + if err ~= "timeout" then + onerror(err, id) + running = false -- stop loop + break + end + end + + while res do -- process message + -- cc.printinfo("[RedisSub:%s] %s", id, table_concat(res, " ")) + + msgtype = res[1] + channel = res[2] + msg = res[3] + + if _skipmsgtypes[msgtype] then + -- cc.printinfo("[RedisSub:%s] %s", id, table_concat(res, " ")) + break -- read reply + end + + if channel ~= cmdchannel then + -- general message + if msgtype == "message" then + -- msgtype, channel, msg + onmessage(channel, msg, nil, id) + elseif msgtype == "pmessage" then + pchannel = res[2] + channel = res[3] + msg = res[4] + onmessage(channel, msg, pchannel, id) + else + cc.printwarn("[RedisSub:%s] invalid message, %s", id, table_concat(res, " ")) + end + break -- read reply + end + + if string_byte(msg) ~= 33 --[[ ! ]] then + -- forward control message + onmessage(channel, msg, nil, id) + break -- read reply + end + + -- control message + local parts = string_split(msg, " ") + local cmd = parts[1] + if cmd == "!STOP" then + running = false -- stop loop + break + elseif cmd == "!REDIS" then + table_remove(parts, 1) + res, err = subredis:doCommand(unpack(parts)) + if not res then + cc.printwarn("[RedisSub:%s] redis failed, %s", id, err) + break -- read reply + else + -- cc.printinfo("[RedisSub:%s] %s", id, table_concat(parts, " ")) + -- cc.printinfo("[RedisSub:%s] %s", id, table_concat(res, " ")) + sema:post(1) -- release lock + end + else + -- unknown control message + cc.printwarn("[RedisSub:%s] unknown control message, %s", id, msg) + break -- read reply + end + + end -- read reply + end -- loop + + cc.printinfo("[RedisSub:%s] ", id) + + subredis:unsubscribe() + subredis:setKeepAlive() + + sema:post(1) +end + +_cleanup = function(self) + ngx_thread_kill(self._thread) + self._thread = nil + self._redis = nil + self._subredis = nil + self._id = nil +end + +_onmessage = function(channel, msg, pchannel, id) + if pchannel then + cc.printinfo("[RedisSub:%s] <%s> <%s> %s", id, pchannel, channel, msg) + else + cc.printinfo("[RedisSub:%s] <%s> %s", id, channel, msg) + end +end + +_onerror = function(err, id) + cc.printwarn("[RedisSub:%s] onerror: %s", id, err) +end + +return NginxRedisLoop diff --git a/src/packages/redis/RedisService.lua b/src/packages/redis/RedisService.lua deleted file mode 100644 index 0821f1a..0000000 --- a/src/packages/redis/RedisService.lua +++ /dev/null @@ -1,142 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local type = type -local pairs = pairs -local clone = clone -local string_lower = string.lower -local string_upper = string.upper -local table_concat = table.concat - -local RedisService = class("RedisService") - -local RESULT_CONVERTER = { - exists = { - RedisLuaAdapter = function(self, result) - if result == true then - return 1 - else - return 0 - end - end, - }, - - hgetall = { - RestyRedisAdapter = function(self, result) - return self:arrayToHash(result) - end, - }, -} - -local RedisAdapter -if ngx then - RedisAdapter = import(".adapter.RestyRedisAdapter") -else - RedisAdapter = import(".adapter.RedisLuaAdapter") -end -local RedisTransaction = import(".RedisTransaction") -local RedisPipeline = import(".RedisPipeline") - -function RedisService:ctor(config) - if type(config) ~= "table" then - throw("redis init with invalid config") - end - self._config = clone(config) - self._redis = RedisAdapter:create(self._config) -end - -function RedisService:connect() - local ok, err = self._redis:connect() - if err then - throw("%s", err) - end -end - -function RedisService:close() - local ok, err = self._redis:close() - if err then - throw("%s", err) - end - return true -end - -function RedisService:setKeepAlive(timeout, size) - if not ngx then - self:close() - return - end - self._redis:setKeepAlive(timeout, size) -end - -function RedisService:command(command, ...) - command = string_lower(command) - local res, err = self._redis:command(command, ...) - if err then - throw("%s", err) - end - - -- converting result - local convert = RESULT_CONVERTER[command] - if convert and convert[self._redis.name] then - res = convert[self._redis.name](self, res) - end - - return res -end - -function RedisService:pubsub(subscriptions) - local loop, err = self._redis:pubsub(subscriptions) - if err then - throw("%s", err) - end - return loop -end - -function RedisService:newPipeline() - return RedisPipeline:create(self) -end - -function RedisService:newTransaction(...) - return RedisTransaction:create(self, ...) -end - -function RedisService:hashToArray(hash) - local arr = {} - for k, v in pairs(hash) do - arr[#arr + 1] = k - arr[#arr + 1] = v - end - return arr -end - -function RedisService:arrayToHash(arr) - local c = #arr - local hash = {} - for i = 1, c, 2 do - hash[arr[i]] = arr[i + 1] - end - return hash -end - -return RedisService diff --git a/src/packages/redis/adapter/RedisLuaAdapter.lua b/src/packages/redis/adapter/RedisLuaAdapter.lua deleted file mode 100644 index 69374bb..0000000 --- a/src/packages/redis/adapter/RedisLuaAdapter.lua +++ /dev/null @@ -1,107 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local assert = assert -local pcall = pcall -local type = type -local tostring = tostring -local table_concat = table.concat -local table_walk = table.walk -local string_format = string.format -local string_upper = string.upper -local string_lower = string.lower - -local redis = require("3rd.redis.redis_lua") - -local RedisLuaAdapter = class("RedisLuaAdapter") - -function RedisLuaAdapter:ctor(config) - self._config = config - self.name = "RedisLuaAdapter" -end - -function RedisLuaAdapter:connect() - local ok, result = pcall(function() - if self._config.socket then - self._instance = redis.connect(self._config.socket) - else - self._instance = redis.connect({ - host = self._config.host, - port = self._config.port, - timeout = self._config.timeout - }) - end - end) - if ok then - return true - else - return nil, result - end -end - -function RedisLuaAdapter:close() - return self._instance:quit() -end - -function RedisLuaAdapter:command(command, ...) - command = string_lower(command) - local method = self._instance[command] - assert(type(method) == "function", string_format("RedisLuaAdapter:command() - invalid command %s", tostring(command))) - - if DEBUG > 1 then - local a = {} - table_walk({...}, function(v) a[#a + 1] = tostring(v) end) - printInfo("RedisLuaAdapter:command() - command %s: %s", string_upper(command), table_concat(a, ", ")) - end - - local arg = {...} - local ok, result = pcall(function() - return method(self._instance, unpack(arg)) - end) - if ok then - return result - else - return nil, result - end -end - -function RedisLuaAdapter:pubsub(subscriptions) - return pcall(function() - return self._instance:pubsub(subscriptions) - end) -end - -function RedisLuaAdapter:commitPipeline(commands) - return pcall(function() - self._instance:pipeline(function() - printInfo("RedisLuaAdapter:commitPipeline() - init pipeline") - for _, arg in ipairs(commands) do - self:command(arg[1], unpack(arg[2])) - end - printInfo("RedisLuaAdapter:commitPipeline() - commit pipeline") - end) - end) -end - -return RedisLuaAdapter diff --git a/src/packages/redis/adapter/RestyRedisAdapter.lua b/src/packages/redis/adapter/RestyRedisAdapter.lua deleted file mode 100644 index 7bbad35..0000000 --- a/src/packages/redis/adapter/RestyRedisAdapter.lua +++ /dev/null @@ -1,237 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local assert = assert -local type = type -local ipairs = ipairs -local tostring = tostring -local ngx_null = ngx.null -local ngx_worker_exiting = ngx.worker.exiting -local table_concat = table.concat -local table_remove = table.remove -local table_walk = table.walk -local string_sub = string.sub -local string_lower = string.lower -local string_upper = string.upper -local string_format = string.format -local string_match = string.match - -local redis = require("resty.redis") - -local RestyRedisAdapter = class("RestyRedisAdapter") - -function RestyRedisAdapter:ctor(config) - self._config = config - self._instance = redis:new() - self.name = "RestyRedisAdapter" -end - -function RestyRedisAdapter:connect() - self._instance:set_timeout(self._config.timeout) - local ok, err - if self._config.socket then - ok, err = self._instance:connect(self._config.socket) - else - ok, err = self._instance:connect(self._config.host, self._config.port) - end - if err then - err = string_format("%s connect, %s", self:_instancename(), err) - end - return ok, err -end - -function RestyRedisAdapter:close() - local ok, err = self._instance:close() - if err then - err = string_format("%s close, %s", self:_instancename(), err) - end - return ok, err -end - -function RestyRedisAdapter:setKeepAlive(timeout, size) - if size then - return self._instance:set_keepalive(timeout, size) - elseif timeout then - return self._instance:set_keepalive(timeout) - else - return self._instance:set_keepalive() - end -end - -local function _formatCommandArgs(args) - local result = {} - table_walk(args, function(v) result[#result + 1] = tostring(v) end) - return table_concat(result, ", ") -end - -function RestyRedisAdapter:command(command, ...) - command = string_lower(command) - local method = self._instance[command] - if type(method) ~= "function" then - local err = string_format("%s invalid command \"%s\"", self:_instancename(), string_upper(command)) - return nil, err - end - - if DEBUG > 1 then - printInfo("%s command \"%s\": %s", self:_instancename(), string_upper(command), _formatCommandArgs({...})) - end - - local res, err = method(self._instance, ...) - if res == ngx_null then res = nil end - - if err then - err = string_format("%s command \"%s\" failed, %s", self:_instancename(), string_upper(command), err) - elseif DEBUG > 1 then - printInfo("%s command \"%s\", result = %s", self:_instancename(), string_upper(command), tostring(res)) - end - - return res, err -end - -function RestyRedisAdapter:pubsub(subscriptions) - if type(subscriptions) ~= "table" then - return nil, string.format("%s invalid subscriptions argument", self:_instancename()) - end - - if type(subscriptions.subscribe) == "string" then - subscriptions.subscribe = {subscriptions.subscribe} - end - if type(subscriptions.psubscribe) == "string" then - subscriptions.psubscribe = {subscriptions.psubscribe} - end - subscriptions.exit = false - - local subscribeMessages = {} - - local function _subscribe(f, channels, command) - for _, channel in ipairs(channels) do - if DEBUG > 1 then - printInfo("%s command \"%s\": %s", self:_instancename(), string_upper(command), channel) - end - local res, err = f(self._instance, channel) - if err then - printWarn("%s command \"%s\" failed, %s", self:_instancename(), string_upper(command), err) - else - subscribeMessages[#subscribeMessages + 1] = res - if DEBUG > 1 then - printInfo("%s command \"%s\", result = %s", self:_instancename(), string_upper(command), _formatCommandArgs(res)) - end - end - end - end - - local function _unsubscribe(f, channels, command) - for _, channel in ipairs(channels) do - if DEBUG > 1 then - printInfo("%s command \"%s\": %s", self:_instancename(), string_upper(command), channel) - end - f(self._instance, channel) - end - end - - local subscriptionsCount = 0 - local function _abort() - if subscriptions.subscribe then - _unsubscribe(self._instance.unsubscribe, subscriptions.subscribe, "UNSUBSCRIBE") - end - if subscriptions.psubscribe then - _unsubscribe(self._instance.punsubscribe, subscriptions.psubscribe, "PUNSUBSCRIBE") - end - end - - if subscriptions.subscribe then - _subscribe(self._instance.subscribe, subscriptions.subscribe, "SUBSCRIBE") - end - if subscriptions.psubscribe then - _subscribe(self._instance.psubscribe, subscriptions.psubscribe, "PSUBSCRIBE") - end - - return coroutine.wrap(function() - while true do - if ngx_worker_exiting() then - _abort() - break - end - local message, result, err - if #subscribeMessages > 0 then - result = subscribeMessages[1] - table_remove(subscribeMessages, 1) - else - result, err = self._instance:read_reply() - if err then - if err ~= "timeout" then - _abort() - break - else - -- err == timeout - message = {kind = "timeout"} - end - end - end - - if not message and result then - if result[1] == "pmessage" then - message = { - kind = result[1], - pattern = result[2], - channel = result[3], - payload = result[4], - } - else - message = { - kind = result[1], - channel = result[2], - payload = result[3], - } - end - - if string_match(message.kind, '^p?subscribe$') then - subscriptionsCount = subscriptionsCount + 1 - end - if string_match(message.kind, '^p?unsubscribe$') then - subscriptionsCount = subscriptionsCount - 1 - end - - if subscriptionsCount == 0 then - break - end - end - coroutine.yield(message, _abort) - end - end) -end - -function RestyRedisAdapter:commitPipeline(commands) - self._instance:init_pipeline() - for _, arg in ipairs(commands) do - self:command(arg[1], unpack(arg[2])) - end - return self._instance:commit_pipeline() -end - -function RestyRedisAdapter:_instancename() - return "redis *" .. string_sub(tostring(self._instance), 10) -end - -return RestyRedisAdapter diff --git a/src/packages/redis/init.lua b/src/packages/redis/init.lua deleted file mode 100644 index 491a4b3..0000000 --- a/src/packages/redis/init.lua +++ /dev/null @@ -1,29 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local _P = {} - -_P.service = import(".RedisService") - -return _P diff --git a/src/packages/redis/redis.lua b/src/packages/redis/redis.lua new file mode 100644 index 0000000..8199d5f --- /dev/null +++ b/src/packages/redis/redis.lua @@ -0,0 +1,473 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local _tcp, _null, _unix_socket, _TIME_MULTIPLY +if ngx and ngx.socket then + _tcp = ngx.socket.tcp + _null = ngx.null + _TIME_MULTIPLY = 1000 +else + local socket = require("socket") + _tcp = socket.tcp + _null = function() return nil end + _unix_socket = require("socket.unix") + _TIME_MULTIPLY = 1 +end + +local _COMMANDS = { + "cluster", + + "auth", "echo", "ping", + "quit", "select", + + "geoadd", "geohash", "geopos", + "geodist", "georadius", "georadiusbymember", + + "hdel", "hexists", "hget", + "hgetall", "hincrby", "hincrbyfloat", + "hkeys", "hlen", "hmget", + "hmset", "hset", "hsetnx", + "hstrlen", "hvals", "hscan", + + "pfadd", "pfcount", "pfmerge", + + "del", "dump", "exists", + "expire", "expireat", "keys", + "migrate", "move", "object", + "persist", "pexpire", "pexpireat", + "pttl", "randomkey", "rename", + "renamenx", "restore", "sort", + "ttl", "type", "wait", + "scan", + + "blpop", "brpop", "brpoplpush", + "lindex", "linsert", "llen", + "lpop", "lpush", "lpushx", + "lrange", "lrem", "lset", + "ltrim", "rpop", "rpoplpush", + "rpush", "rpushx", + + --[["psubscribe",]] "pubsub", "publish", + --[["punsubscribe",]] --[["subscribe",]] --[["unsubscribe",]] + + "eval", "evalsha", "script", + + "bgrewriteaof", "bgsave", "client", + "command", "config", "dbsize", + "debug", "flushall", "flushdb", + "info", "lastsave", "monitor", + "role", "save", "shutdown", + "slaveof", "slowlog", "sync", + "time", + + "sadd", "scard", "sdiff", + "sdiffstore", "sinter", "sinterstore", + "sismember", "smembers", "smove", + "spop", "srandmember", "srem", + "sunion", "sunionstore", "sscan", + + "zadd", "zcard", "zcount", + "zincrby", "zinterstore", "zlexcount", + "zrange", "zrangebylex", "zrevrangebylex", + "zrangebyscore", "zrank", "zrem", + "zremrangebylex", "zremrangebyrank", "zremrangebyscore", + "zrevrange", "zrevrangebyscore", "zrevrank", + "zscore", "zunionstore", "zscan", + + "append", "bitcount", "bitop", + "bitpos", "decr", "decrby", + "get", "getbit", "getrange", + "getset", "incr", "incrby", + "incrbyfloat", "mget", "mset", + "msetnx", "psetex", "set", + "setbit", "setex", "setnx", + "setrange", "strlen", + + "discard", "exec", "multi", + "unwatch", "watch", +} + +local _SUB_COMMANDS = { + "subscribe", "psubscribe", +} + +local _UNSUB_COMMANDS = { + "unsubscribe", "punsubscribe", +} + +local pairs = pairs +local string_byte = string.byte +local string_format = string.format +local string_lower = string.lower +local string_sub = string.sub +local string_upper = string.upper +local table_concat = table.concat +local table_new = table.new +local tonumber = tonumber +local tostring = tostring +local type = type + +local Loop +if ngx then + Loop = cc.import(".NginxRedisLoop") +end + +local Redis = cc.class("Redis") + +Redis.VERSION = "0.6" +Redis.null = _null + +local DEFAULT_HOST = "localhost" +local DEFAULT_PORT = 6379 + +local _genreq, _readreply, _checksub + +function Redis:ctor() + self._config = {} +end + +function Redis:connect(host, port) + local socket_file, socket, ok, err + host = host or DEFAULT_HOST + if string_sub(host, 1, 5) == "unix:" then + socket_file = host + if _unix_socket then + socket_file = string_sub(host, 6) + socket = _unix_socket() + else + socket = _tcp() + end + ok, err = socket:connect(socket_file) + else + socket = _tcp() + ok, err = socket:connect(host, port or DEFAULT_PORT) + end + + if not ok then + return nil, err + end + + self._config = {host = host, port = port} + self._socket = socket + return 1 +end + +function Redis:setTimeout(timeout) + local socket = self._socket + if not socket then + return nil, "not initialized" + end + return socket:settimeout(timeout * _TIME_MULTIPLY) +end + +function Redis:setKeepAlive(...) + local socket = self._socket + if not socket then + return nil, "not initialized" + end + + self._socket = nil + if not ngx then + return socket:close() + else + return socket:setkeepalive(...) + end +end + +function Redis:getReusedTimes() + local socket = self._socket + if not socket then + return nil, "not initialized" + end + if socket.getreusedtimes then + return socket:getreusedtimes() + else + return 0 + end +end + +function Redis:close() + local socket = self._socket + if not socket then + return nil, "not initialized" + end + self._socket = nil + return socket:close() +end + +function Redis:doCommand(...) + local args = {...} + local cmd = args[1] or "" + + -- cc.printinfo("[Redis:%s] %s", string.sub(tostring(self), 8), table.concat(args, " ")) + + local socket = self._socket + if not socket then + return nil, string_format('"%s" failed, not initialized', cmd) + end + + local req = _genreq(args) + local reqs = self._reqs + if reqs then + reqs[#reqs + 1] = req + return "OK" + end + + local bytes, err = socket:send(req) + if not bytes then + return nil, string_format('"%s" failed, %s', cmd, err) + end + + local res, err = _readreply(self, socket) + if not res then + return nil, string_format('"%s" failed, %s', cmd, err) + end + + return res +end + +function Redis:initPipeline(numberOfCommands) + self._reqs = table_new(numberOfCommands or 4, 0) +end + +function Redis:cancelPipeline() + self._reqs = nil +end + +function Redis:commitPipeline() + local socket = self._socket + if not socket then + return nil, "not initialized" + end + + local reqs = self._reqs + if not reqs then + return nil, "no pipeline" + end + self._reqs = nil + + local bytes, err = socket:send(table_concat(reqs)) + if not bytes then + return nil, err + end + + local nvals = 0 + local nreqs = #reqs + local vals = table_new(nreqs, 0) + for i = 1, nreqs do + local res, err = _readreply(self, socket) + if res then + nvals = nvals + 1 + vals[nvals] = res + elseif res == nil then + if err == "timeout" then + self:close() + end + return nil, err + else + -- be a valid redis error value + nvals = nvals + 1 + vals[nvals] = {false, err} + end + end + + return vals +end + +function Redis:readReply() + local res, err = _readreply(self, self._socket) + if not res then + return nil, err + end + + _checksub(self, res) + return res +end + +function Redis:makeSubscribeLoop(id) + if not Loop then + return nil, "not support subscribe loop in current platform" + end + + local subredis = Redis:new() + local ok, err = subredis:connect(self._config.host, self._config.port) + if not ok then + return nil, err + end + return Loop:new(self, subredis, id) +end + +function Redis:hashToArray(hash) + local arr = {} + local i = 0 + for k, v in pairs(hash) do + arr[i + 1] = k + arr[i + 2] = v + i = i + 2 + end + return arr +end + +function Redis:arrayToHash(arr) + local c = #arr + local hash = table_new(0, c / 2) + for i = 1, c, 2 do + hash[arr[i]] = arr[i + 1] + end + return hash +end + +-- private + +_genreq = function(args) + local nargs = #args + local req = table_new(nargs + 1, 0) + req[1] = "*" .. nargs .. "\r\n" + local nbits = 1 + + for i = 1, nargs do + local arg = args[i] + nbits = nbits + 1 + + if not arg then + req[nbits] = "$-1\r\n" + else + if type(arg) ~= "string" then + arg = tostring(arg) + end + req[nbits] = "$" .. #arg .. "\r\n" .. arg .. "\r\n" + end + end + + -- it is faster to do string concatenation on the Lua land + return table_concat(req) +end + +_readreply = function(self, socket) + local line, err = socket:receive() + if not line then + if err == "timeout" and not self._subscribed then + socket:close() + end + return nil, err + end + + local prefix = string_byte(line) + + if prefix == 36 then -- char '$' + -- print("bulk reply") + local size = tonumber(string_sub(line, 2)) + if size < 0 then + return _null + end + + local data, err = socket:receive(size) + if not data then + if err == "timeout" then + socket:close() + end + return nil, err + end + + local dummy, err = socket:receive(2) -- ignore CRLF + if not dummy then + return nil, err + end + + return data + + elseif prefix == 43 then -- char '+' + -- print("status reply") + return string_sub(line, 2) + + elseif prefix == 42 then -- char '*' + local n = tonumber(string_sub(line, 2)) + -- print("multi-bulk reply: ", n) + if n < 0 then + return _null + end + + local vals = table_new(n, 0) + local nvals = 0 + for i = 1, n do + local res, err = _readreply(self, socket) + if res then + nvals = nvals + 1 + vals[nvals] = res + elseif res == nil then + return nil, err + else + -- be a valid redis error value + nvals = nvals + 1 + vals[nvals] = {false, err} + end + end + + return vals + + elseif prefix == 58 then -- char ':' + -- print("integer reply") + return tonumber(string_sub(line, 2)) + + elseif prefix == 45 then -- char '-' + -- print("error reply: ", n) + return false, string_sub(line, 2) + + else + return nil, "unknown prefix: \"" .. prefix .. "\"" + end +end + +_checksub = function(self, res) + if type(res) == "table" + and (res[1] == "unsubscribe" or res[1] == "punsubscribe") + and res[3] == 0 then + self._subscribed = nil + end +end + +-- add commands + +for _, cmd in ipairs(_COMMANDS) do + Redis[cmd] = function(self, ...) + return self:doCommand(cmd, ...) + end +end + +for _, cmd in ipairs(_SUB_COMMANDS) do + Redis[cmd] = function(self, ...) + self._subscribed = true + return self:doCommand(cmd, ...) + end +end + +for _, cmd in ipairs(_UNSUB_COMMANDS) do + Redis[cmd] = function(self, ...) + local res = self:doCommand(cmd, ...) + _checksub(self, res) + return res + end +end + +return Redis diff --git a/src/packages/session/session.lua b/src/packages/session/session.lua new file mode 100644 index 0000000..42ce3f9 --- /dev/null +++ b/src/packages/session/session.lua @@ -0,0 +1,210 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local checkint = cc.checkint +local checktable = cc.checktable +local clone = clone +local string_format = string.format +local tostring = tostring +local type = type +local md5 +if ngx then + md5 = ngx.md5 +else + local luamd5 = cc.import("#luamd5") + md5 = luamd5.sumhexa +end + +local json = cc.import("#json") + +local Session = cc.class("Session") + +local _DEFAULT_EXPIRED = 60 * 5 -- 5m +local _DEFAULT_SID_KEY_PREFIX = "_SID_" +local _DEFAULT_SECRET = "1b876ea6" + +local _gensid + +function Session:ctor(redis, config) + config = config or {} + self._expired = config.expired or _DEFAULT_EXPIRED + self._prefix = config.prefix or _DEFAULT_SID_KEY_PREFIX + self._secret = config.secret or _DEFAULT_SECRET + self._redis = redis +end + +function Session:start(sid) + local create = sid == nil + if type(sid) == "nil" then + sid = _gensid(self._secret) + elseif type(sid) ~= "string" or sid == "" then + cc.throw("[Session] invalid sid '%s'", tostring(sid)) + end + + local key = self._prefix .. sid + + if create then + self._values = {} + self._sid = sid + self._key = key + else + local redis = self._redis + local res, err = self._redis:get(key) + if not res then + return false, err + end + if res == redis.null then + return false, string_format("not found session '%s'", sid) + end + + self._values = checktable(json.decode(res)) + self._sid = sid + self._key = key + self:setKeepAlive() + end + + return true +end + +function Session:getSid() + return self._sid +end + +function Session:getExpired() + return self._expired +end + +function Session:get(key) + if not self._values then + cc.throw("[Session] get key '%s' failed, not initialized", key) + end + + if type(key) ~= "string" or key == "" then + cc.throw("[Session] invalid get key '%s'", tostring(key)) + end + + return self._values[key] +end + +function Session:set(key, value) + if not self._values then + cc.throw("[Session] set key '%s' failed, not initialized", key) + end + + if type(key) ~= "string" or key == "" then + cc.throw("[Session] invalid set key '%s'", tostring(key)) + end + + self._values[key] = value +end + +function Session:save() + if not self._values then + cc.throw("[Session] save failed, not initialized") + end + + local jsonstr = json.encode(self._values) + if type(jsonstr) ~= "string" then + return false, "serializing failed" + end + + local ok, err = self._redis:set(self._key, jsonstr, "EX", self._expired) + if not ok then + return false, err + end + + return true +end + +function Session:setKeepAlive(expired) + if not self._values then + cc.throw("[Session] set keep alive failed, not initialized") + end + + if expired then + self._expired = expired + end + local ok, err = self._redis:expire(self._key, self._expired) + if not ok then + return false, err + end + + return true +end + +function Session:isAlive() + if not self._values then + cc.throw("[Session] check alive failed, not initialized") + end + + local res, err = self._redis:exists(self._key) + if not res then + return false, err + end + + if tostring(res) == "1" then + return true + end + + return false, string_format("not found session '%s'", self._sid) +end + +function Session:destroy() + if not self._values then + cc.throw("[Session] destroy failed, not initialized") + end + + local ok, err = self._redis:del(self._key) + self._values = nil + self._redis = nil + self._sid = nil + self._key = nil + + if not ok then + return false, err + end + return true +end + +-- private + +_gensid = function(secret) + math.newrandomseed() + + local random = math.random() * 100000000000000 + local now + if ngx then + local addr = ngx.var.remote_addr + now = ngx.now() + else + local addr = "127.0.0.1" + now = os.time() + end + + local mask = string.format("%0.5f|%0.10f|%s", now, random, secret) + local origin = string.format("%s|%s", addr, mask) + return md5(origin) +end + +return Session diff --git a/src/packages/tests/Check.lua b/src/packages/tests/Check.lua new file mode 100644 index 0000000..f0d18b3 --- /dev/null +++ b/src/packages/tests/Check.lua @@ -0,0 +1,220 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local rawget = rawget +local rawequal = rawequal +local string_format = string.format +local _empty +local _equals +local _contains, _containsintable +local _dumpresult +local _dumpresultarr +local _formatmsg + +local _M = {} + +-- nil, "", {} is empty +function _M.empty(v, msg) + if _empty(v) then return end + cc.throw("expected is empty, actual is '%s'%s", tostring(v), _formatmsg(msg)) +end + +function _M.notEmpty(v, msg) + if not _empty(v) then return end + cc.throw("expected is not empty, actual is '%s'%s", tostring(v), _formatmsg(msg)) +end + +function _M.isTrue(v, msg) + if v == true then return end + cc.throw("expected is true, actual is '%s'%s", tostring(v), _formatmsg(msg)) +end + +function _M.isFalse(v, msg) + if v == false then return end + cc.throw("expected is false, actual is '%s'%s", tostring(v), _formatmsg(msg)) +end + +function _M.isNil(v, msg) + if type(v) == "nil" then return end + cc.throw("expected is nil, actual is '%s'%s", type(v), _formatmsg(msg)) +end + +function _M.isFunction(v, msg) + if type(v) == "function" then return end + cc.throw("expected is function, actual is '%s'%s", type(v), _formatmsg(msg)) +end + +function _M.isTable(v, msg) + if type(v) == "table" then return end + cc.throw("expected is table, actual is '%s'%s", type(v), _formatmsg(msg)) +end + +function _M.isInt(v, msg) + if type(v) == "number" and math.floor(v) == v then return end + cc.throw("expected is integer, actual is '%s'%s", tostring(v), _formatmsg(msg)) +end + +function _M.isPosInt(v, msg) + if type(v) == "number" and math.floor(v) == v and v >= 0 then return end + cc.throw("expected is positive integer, actual is '%s'%s", tostring(v), _formatmsg(msg)) +end + +function _M.isString(v, msg) + if type(v) == "string" then return end + cc.throw("expected is string, actual is '%s'%s", tostring(v), _formatmsg(msg)) +end + +function _M.greaterThan(actual, expected, msg) + if type(actual) == "number" and type(expected) == "number" and actual > expected then return end + cc.throw("expected is '%s' > '%s'%s", tostring(actual), tostring(expected), _formatmsg(msg)) +end + +function _M.equals(actual, expected, msg) + if _equals(actual, expected) then return end + local msgs = { + "should be equals" .. _formatmsg(msg), + _dumpresult(actual, "actual"), + _dumpresult(expected, "expected"), + } + cc.throw(table.concat(msgs, "\n")) +end + +function _M.notEquals(actual, expected, msg) + if not _equals(actual, expected) then return end + local msgs = { + "should be not equals" .. _formatmsg(msg), + _dumpresult(actual, "actual"), + _dumpresult(expected, "expected"), + } + cc.throw(table.concat(msgs, "\n")) +end + +function _M.contains(actual, expected, msg) + if _contains(actual, expected) then return end + local msgs = { + string_format("expected contains '%s'%s", tostring(expected), _formatmsg(msg)), + _dumpresult(actual, "actual"), + _dumpresult(expected, "expected"), + } + cc.throw(table.concat(msgs, "\n")) +end + +function _M.notContains(actual, expected, msg) + if not _contains(actual, expected) then return end + local msgs = { + string_format("expected not contains '%s'%s", tostring(needle), _formatmsg(msg)), + _dumpresult(actual, "actual"), + _dumpresult(expected, "expected"), + } + cc.throw(table.concat(msgs, "\n")) +end + +-- private + +_empty = function(v) + local t = type(v) + local test = true + while true do + if t == "nil" then break end + if t == "string" and v == "" then break end + if t ~= "table" then + test = false + break + end + + for k, v in pairs(v) do + test = false + break + end + + break + end + + return test +end + +_equals = function(actual, expected) + local at = type(actual) + local et = type(expected) + if at ~= et then return false end + + if at == "table" then + local akeys = {} + -- check all values in actual exists in expected + for k, v in pairs(actual) do + akeys[k] = true + if not _equals(v, rawget(expected, k)) then return false end + end + -- check expected not have more keys + for k, v in pairs(expected) do + if akeys[k] ~= true then return false end + end + return true + elseif at == "number" then + return tostring(actual) == tostring(expected) + else + return actual == expected + end +end + +_contains = function(actual, expected) + if type(actual) == "table" then + return _containsintable(actual, expected) + end + return string.find(tostring(actual), tostring(expected), 1, true) +end + +_containsintable = function(arr, needle) + for _, v in pairs(arr) do + if needle == v then return true end + end + return false +end + +_dumpresult = function(value, label) + return table.concat(_dumpresultarr(value, label), "\n") +end + +_dumpresultarr = function(value, label) + local result = {} + local first = true + cc.dump(value, label, 99, function(s) + if first then + first = false + else + result[#result + 1] = s + end + end) + return result +end + +_formatmsg = function(msg) + if msg then + return ", " .. tostring(msg) + else + return "" + end +end + +return _M diff --git a/src/packages/tests/TestCase.lua b/src/packages/tests/TestCase.lua new file mode 100644 index 0000000..4d8eff9 --- /dev/null +++ b/src/packages/tests/TestCase.lua @@ -0,0 +1,55 @@ +--[[ + +Copyright (c) 2015 gameboxcloud.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +]] + +local string_sub = string.sub + +local gbc = cc.import("#gbc") + +local TestCase = cc.class("TestCase", gbc.ActionBase) + +TestCase.ACCEPTED_REQUEST_TYPE = {"http", "cli"} + +function TestCase:init() + local mt = getmetatable(self) + for name, method in pairs(mt.__index) do + if type(method) == "function" and string_sub(name, -6) == "Action" then + self[name] = function(...) + self:setup() + local res = {method(...)} + self:teardown() + return unpack(res) + end + end + end + + math.newrandomseed() +end + +function TestCase:setup() +end + +function TestCase:teardown() +end + +return TestCase diff --git a/src/packages/job/init.lua b/src/packages/tests/tests.lua similarity index 91% rename from src/packages/job/init.lua rename to src/packages/tests/tests.lua index f8456dc..41370a7 100644 --- a/src/packages/job/init.lua +++ b/src/packages/tests/tests.lua @@ -22,8 +22,9 @@ THE SOFTWARE. ]] -local _P = {} +local _M = {} -_P.service = import(".JobService") +_M.TestCase = cc.import(".TestCase") +_M.Check = cc.import(".Check") -return _P +return _M diff --git a/src/server/base/ActionDispatcher.lua b/src/server/base/ActionDispatcher.lua deleted file mode 100644 index 966672c..0000000 --- a/src/server/base/ActionDispatcher.lua +++ /dev/null @@ -1,140 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local pcall = pcall -local string_lower = string.lower -local string_ucfirst = string.ucfirst -local string_gsub = string.gsub -local string_format = string.format -local string_find = string.find -local string_sub = string.sub -local table_concat = table.concat -local table_remove = table.remove - -local Constants = import(".Constants") - -local ActionDispatcher = class("ActionDispatcher") - -function ActionDispatcher:ctor(config) - self.config = clone(checktable(config)) - - self.config.appRootPath = self.config.appRootPath - self.config.actionPackage = self.config.actionPackage or Constants.ACTION_PACKAGE_NAME - self.config.actionModuleSuffix = config.actionModuleSuffix or Constants.DEFAULT_ACTION_MODULE_SUFFIX - - self._actionModules = {} - self._requestParameters = nil -end - -function ActionDispatcher:runAction(actionName, data) - -- parse actionName - local actionModuleName, actionMethodName = self:normalizeActionName(actionName) - actionMethodName = actionMethodName .. self.config.actionModuleSuffix - - local action -- instance - -- check registered action module before load module - local actionModule = self._actionModules[actionModuleName] - local actionModulePath - if not actionModule then - actionModulePath = self:getActionModulePath(actionModuleName) - if DEBUG >= _DBG_INFO then - package.loaded[actionModulePath] = false - end - local ok, _actionModule = pcall(require, actionModulePath) - if ok then - actionModule = _actionModule - else - local err = _actionModule - throw(err) - end - end - - local t = type(actionModule) - if t ~= "table" and t ~= "userdata" then - throw("failed to load action module \"%s\"", actionModulePath or actionModuleName) - end - - local acceptedRequestType = rawget(actionModule, "ACCEPTED_REQUEST_TYPE") or self.config.defaultAcceptedRequestType - local currentRequestType = self:getRequestType() - if currentRequestType ~= acceptedRequestType then - throw("can't access this action via \"%s\"", currentRequestType) - end - - action = actionModule:create(self) - - local method = action[actionMethodName] - if type(method) ~= "function" then - throw("invalid action method \"%s:%s()\"", actionModuleName, actionMethodName) - end - - if not data then - data = self._requestParameters or {} - end - - return method(action, data) -end - -function ActionDispatcher:getActionModulePath(actionModuleName) - return string_format("%s.%s%s", self.config.actionPackage, actionModuleName, self.config.actionModuleSuffix) -end - -function ActionDispatcher:registerActionModule(actionModuleName, actionModule) - if type(actionModuleName) ~= "string" then - throw("invalid action module name \"%s\"", actionModuleName) - end - if type(actionModule) ~= "table" or type(actionModule) ~= "userdata" then - throw("invalid action module \"%s\"", actionModuleName) - end - - local action = actionModuleName .. ".index" - local actionModuleName, _ = self:normalizeActionName(actionName) - self._actionModules[actionModuleName] = actionModule -end - -function ActionDispatcher:normalizeActionName(actionName) - local actionName = actionName - if not actionName or actionName == "" then - actionName = "index.index" - end - actionName = string_lower(actionName) - actionName = string_gsub(actionName, "[^%a.]", "") - actionName = string_gsub(actionName, "^[.]+", "") - actionName = string_gsub(actionName, "[.]+$", "") - - -- demo.hello.say --> {"demo", "hello", "say"] - local parts = string.split(actionName, ".") - local c = #parts - if c == 1 then - return string_ucfirst(parts[1]), "index" - end - -- method = "say" - method = parts[c] - table_remove(parts, c) - c = c - 1 - -- mdoule = "demo.Hello" - parts[c] = string_ucfirst(parts[c]) - return table_concat(parts, "."), method -end - -return ActionDispatcher diff --git a/src/server/base/CLIBase.lua b/src/server/base/CLIBase.lua deleted file mode 100644 index 3b96499..0000000 --- a/src/server/base/CLIBase.lua +++ /dev/null @@ -1,80 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local type = type - -local ActionDispatcher = import(".ActionDispatcher") -local Constants = import(".Constants") - -local CLIBase = class("CLIBase", ActionDispatcher) - -function CLIBase:ctor(config, arg) - CLIBase.super.ctor(self, config) - - self._requestType = Constants.CLI_REQUEST_TYPE - self._requestParameters = checktable(arg) -end - -function CLIBase:getRequestType() - return self._requestType or "unknow" -end - -function CLIBase:run() - local ok, res = xpcall(function() - return self:runEventLoop() - end, function(err) - err = tostring(err) - printError(err) - end) - - if ok then - if type(res) ~= "table" then - printf(res) - else - dump(res) - end - end - - printInfo("DONE") -end - -function CLIBase:runEventLoop() - local actionName = self._requestParameters[1] - if actionName == nil or actionName == "help" then - self:_showHelp() - return - end - - return self:runAction(actionName, self._requestParameters) -end - -function CLIBase:getActionModulePath(actionModuleName) - return string_format("%s.%s%s", "tools.actions", actionModuleName, self.config.actionModuleSuffix) -end - -function CLIBase:_showHelp() - printf("usage: tools [ActionModule.Action] [args]") -end - -return CLIBase diff --git a/src/server/base/ConnectBase.lua b/src/server/base/ConnectBase.lua deleted file mode 100644 index c7f0934..0000000 --- a/src/server/base/ConnectBase.lua +++ /dev/null @@ -1,149 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local clone = clone -local checktable = checktable -local ngx = ngx -local ngx_now = ngx.now -local ngx_md5 = ngx.md5 -local json_encode = json.encode - -local Constants = import(".Constants") -local SessionService = import(".SessionService") -local RedisService = cc.load("redis").service - -local ActionDispatcher = import(".ActionDispatcher") -local ConnectBase = class("ConnectBase", ActionDispatcher) - -function ConnectBase:ctor(config) - ConnectBase.super.ctor(self, config) - self.config.messageFormat = self.config.messageFormat or Constants.DEFAULT_MESSAGE_FORMAT -end - -function ConnectBase:getRequestType() - return self._requestType or "unknow" -end - -function ConnectBase:run() - throw("ConnectBase:run() - must override in inherited class") -end - -function ConnectBase:runEventLoop() - throw("ConnectBase:runEventLoop() - must override in inherited class") -end - -function ConnectBase:getSession() - return self._session -end - -function ConnectBase:openSession(sid) - if self._session then - throw("session \"%s\" already exists, disallow open an other session", self._session:getSid()) - end - if type(sid) ~= "string" or sid == "" then - throw("open session with invalid sid") - end - self._session = self:_loadSession(sid) - return self._session -end - -function ConnectBase:newSession() - if self._session then - throw("session \"%s\" already exists, disallow start a new session", self._session:getSid()) - end - self._session = self:_genSession() - return self._session -end - -function ConnectBase:destroySession() - if self._session then - self._session:destroy() - self._session = nil - end -end - -function ConnectBase:closeConnect(connectId) - if not connectId then - throw("invalid connect id \"%s\"", tostring(connectId)) - end - self:sendMessageToConnect(connectId, "QUIT") -end - -function ConnectBase:sendMessageToConnect(connectId, message) - if not connectId then - throw("send message to connect with invalid id \"%s\"", tostring(connectId)) - end - local channelName = Constants.CONNECT_CHANNEL_PREFIX .. tostring(connectId) - self:sendMessageToChannel(channelName, message) -end - -function ConnectBase:sendMessageToChannel(channelName, message) - if not channelName or not message then - throw("send message to channel with invalid channel name \"%s\" or invalid message", tostring(channelName)) - end - if self.config.messageFormat == Constants.MESSAGE_FORMAT_JSON and type(message) == "table" then - message = json_encode(message) - end - local redis = self:getRedis() - redis:command("PUBLISH", channelName, tostring(message)) -end - -function ConnectBase:getRedis() - if not self._redis then - self._redis = self:_newRedis() - end - return self._redis -end - -function ConnectBase:_loadSession(sid) - local redis = self:getRedis() - local session = SessionService.load(redis, sid, self.config.appSessionExpiredTime, ngx.var.remote_addr) - if session then - session:setKeepAlive() - printInfo("load session \"%s\"", sid) - end - return session -end - -function ConnectBase:_genSession() - local addr = ngx.var.remote_addr - local now = ngx_now() - math.newrandomseed() - local random = math.random() * 100000000000000 - local mask = string.format("%0.5f|%0.10f|%s", now, random, self._secret) - local origin = string.format("%s|%s", addr, ngx_md5(mask)) - local sid = ngx_md5(origin) - return SessionService:create(self:getRedis(), sid, self.config.appSessionExpiredTime, addr) -end - -function ConnectBase:_newRedis() - local redis = RedisService:create(self.config.redis) - local ok, err = redis:connect() - if err then - throw("connect internal redis failed, %s", err) - end - return redis -end - -return ConnectBase diff --git a/src/server/base/Constants.lua b/src/server/base/Constants.lua deleted file mode 100644 index 6dd3324..0000000 --- a/src/server/base/Constants.lua +++ /dev/null @@ -1,30 +0,0 @@ - -local Constants = {} - --- request type -Constants.HTTP_REQUEST_TYPE = "http" -Constants.WEBSOCKET_REQUEST_TYPE = "websocket" -Constants.CLI_REQUEST_TYPE = "cli" -Constants.WORKER_REQUEST_TYPE = "worker" - --- action -Constants.ACTION_PACKAGE_NAME = 'actions' -Constants.DEFAULT_ACTION_MODULE_SUFFIX = 'Action' -Constants.MESSAGE_FORMAT_JSON = "json" -Constants.MESSAGE_FORMAT_TEXT = "text" -Constants.DEFAULT_MESSAGE_FORMAT = Constants.MESSAGE_FORMAT_JSON - --- redis keys -Constants.NEXT_CONNECT_ID_KEY = "_NEXT_CONNECT_ID" -Constants.CONNECT_CHANNEL_PREFIX = "_C" - --- websocket -Constants.WEBSOCKET_TEXT_MESSAGE_TYPE = "text" -Constants.WEBSOCKET_BINARY_MESSAGE_TYPE = "binary" -Constants.WEBSOCKET_SUBPROTOCOL_PATTERN = "gbc%-([%w%d%-]+)" -Constants.WEBSOCKET_DEFAULT_TIME_OUT = 10 * 1000 -- 10s -Constants.WEBSOCKET_DEFAULT_MAX_PAYLOAD_LEN = 16 * 1024 -- 16KB -Constants.WEBSOCKET_DEFAULT_MAX_RETRY_COUNT = 5 -- 5 times -Constants.WEBSOCKET_DEFAULT_MAX_SUB_RETRY_COUNT = 10 -- 10 times - -return Constants diff --git a/src/server/base/SessionService.lua b/src/server/base/SessionService.lua deleted file mode 100644 index ffc0d7f..0000000 --- a/src/server/base/SessionService.lua +++ /dev/null @@ -1,135 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local tostring = tostring -local type = type -local clone = clone -local checkint = checkint -local checktable = checktable -local json_encode = json.encode -local json_decode = json.decode -local ngx_null = ngx.null - -local SessionService = class("SessionService") - -local _DEFAULT_EXPIRED = 60 * 5 -- 5m -local _SID_KEY_PREFIX = "_SID_" - -function SessionService.load(redis, sid, expired, remoteAddr) - local key = _SID_KEY_PREFIX .. sid - local data = redis:command("GET", key) - if data == "" or data == nil or data == ngx_null then return end - - data = json_decode(data) - if type(data) == "table" then - if sid ~= data.__id then -- or remoteAddr ~= data.__addr - throw("load session with invalid sid \"%s\"", sid) - end - data.__id = nil - data.__addr = nil - return SessionService:create(redis, sid, expired, remoteAddr, data) - else - printWarn("found invalid session by sid \"%s\"", sid) - redis:command("DEL", key) - end -end - -function SessionService:ctor(redis, sid, expired, remoteAddr, data) - self._redis = redis - self._sid = tostring(sid) - self._key = _SID_KEY_PREFIX .. self._sid - self._expired = checkint(expired) - if self._expired < 0 then - self._expired = _DEFAULT_EXPIRED - end - self._remoteAddr = tostring(remoteAddr) - if data and data.__cid then - self._connectId = data.__cid - data.__cid = nil - end - self._data = clone(checktable(data)) -end - -function SessionService:getSid() - return self._sid -end - -function SessionService:getExpired() - return self._expired -end - -function SessionService:getConnectId() - return self._connectId -end - -function SessionService:setConnectId(connectId) - self._connectId = connectId -end - -function SessionService:get(key) - if type(key) ~= "string" then - error("invalid session read key \"%s\" type", tostring(key)) - end - return self._data[key] -end - -function SessionService:set(key, value) - if type(key) ~= "string" then - error("invalid session save key \"%s\" type", tostring(key)) - end - local vtype = type(value) - if vtype ~= "number" and vtype ~= "boolean" and vtype ~= "string" then - error("invalid session value type for key \"%s\"", key) - end - self._data[tostring(key)] = value -end - -function SessionService:save() - self._redis:command("SET", self._key, json_encode(self:vardump()), "EX", self._expired) -end - -function SessionService:setKeepAlive() - self._redis:command("EXPIRE", self._key, self._expired) -end - -function SessionService:checkAlive() - local res = self._redis:command("EXISTS", self._key) - return tostring(res) == "1" -end - -function SessionService:destroy() - self._redis:command("DEL", self._key) -end - -function SessionService:vardump() - local v = clone(self._data) - v.__id = self._sid - v.__addr = self._remoteAddr - if self._connectId then - v.__cid = self._connectId - end - return v -end - -return SessionService diff --git a/src/server/base/WebSocketConnectBase.lua b/src/server/base/WebSocketConnectBase.lua deleted file mode 100644 index 22afc49..0000000 --- a/src/server/base/WebSocketConnectBase.lua +++ /dev/null @@ -1,422 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local DEBUG = DEBUG -local type = type -local tostring = tostring -local ngx = ngx -local ngx_log = ngx.log -local ngx_thread_spawn = ngx.thread.spawn -local ngx_md5 = ngx.md5 -local req_read_body = ngx.req.read_body -local req_get_headers = ngx.req.get_headers -local table_insert = table.insert -local table_concat = table.concat -local string_format = string.format -local string_sub = string.sub -local json_encode = json.encode - -local ConnectBase = import(".ConnectBase") - -local WebSocketConnectBase = class("WebSocketConnectBase", ConnectBase) - -local Constants = import(".Constants") - -function WebSocketConnectBase:ctor(config) - WebSocketConnectBase.super.ctor(self, config) - - if config.appSocketMessageFormat then - self.config.messageFormat = config.appSocketMessageFormat - end - - self.config.websocketsTimeout = self.config.websocketsTimeout or Constants.WEBSOCKET_DEFAULT_TIME_OUT - self.config.websocketsMaxPayloadLen = self.config.websocketsMaxPayloadLen or Constants.WEBSOCKET_DEFAULT_MAX_PAYLOAD_LEN - self.config.maxSubscribeRetryCount = self.config.maxSubscribeRetryCount or Constants.WEBSOCKET_DEFAULT_MAX_SUB_RETRY_COUNT - - self._requestType = Constants.WEBSOCKET_REQUEST_TYPE - self._subscribeChannels = {} -end - -function WebSocketConnectBase:run() - local ok, err = xpcall(function() - self:_authConnect() - self:runEventLoop() - ngx.exit(ngx.OK) - end, function(err) - err = tostring(err) - if DEBUG > 1 then - ngx_log(ngx.ERR, err .. debug.traceback("", 4)) - else - ngx_log(ngx.ERR, strip_luafile_paths(err)) - end - ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR - ngx.exit(ngx.ERROR) - end) -end - -function WebSocketConnectBase:runEventLoop() - printInfo("websocket [beforeConnectReady]") - self:beforeConnectReady() - - local server = require("resty.websocket.server") - local socket, err = server:new({ - timeout = self.config.websocketsTimeout, - max_payload_len = self.config.websocketsMaxPayloadLen, - }) - if err then - throw("failed to create websocket server, %s", err) - end - - -- ready - self._socket = socket - - -- spawn a thread to subscribe redis channel for broadcast - self:subscribeChannel(self._connectChannel, function(payload) - if payload == "QUIT" then - -- ubsubscribe - if self._socket then - self._socket:send_close() - end - return false - end - -- forward message to connect - self._socket:send_text(payload) - end) - - -- event callback - self:afterConnectReady() - printInfo("websocket [afterConnectReady], connect id: %s", tostring(self._connectId)) - - -- event loop - local retryCount = 0 - local framesPool = {} - while true do - --[[ - Receives a WebSocket frame from the wire. - - In case of an error, returns two nil values and a string describing the error. - - The second return value is always the frame type, which could be - one of continuation, text, binary, close, ping, pong, or nil (for unknown types). - - For close frames, returns 3 values: the extra status message - (which could be an empty string), the string "close", and a Lua number for - the status code (if any). For possible closing status codes, see - - http://tools.ietf.org/html/rfc6455#section-7.4.1 - - For other types of frames, just returns the payload and the type. - - For fragmented frames, the err return value is the Lua string "again". - ]] - local frame, ftype, err = socket:recv_frame() - -- check session - if not self._session:checkAlive() then - printWarn("session is lost") - break - end - - if err then - if err == "again" then - framesPool[#framesPool + 1] = frame - goto recv_next_message - end - - if string_sub(err, -7) == "timeout" then - goto recv_next_message - end - - printWarn("failed to receive frame, type \"%s\", %s", ftype, err) - break - end - - if #framesPool > 0 then - -- merging fragmented frames - framesPool[#framesPool + 1] = frame - frame = table.concat(framesPool) - framesPool = {} - end - - if ftype == "close" then - break -- exit event loop - elseif ftype == "ping" then - local bytes, err = socket:send_pong() - if err then - printWarn("failed to send pong, %s", err) - end - elseif ftype == "pong" then - -- client ponged - elseif ftype == "text" or ftype == "binary" then - local ok, err = self:_processMessage(frame, ftype) - if err then - printError("process %s message failed, %s", ftype, err) - end - else - printWarn("unknwon frame type \"%s\"", tostring(ftype)) - end - -::recv_next_message:: - - end -- while - - -- end the subscribe thread - self:_unsubscribeChannel() - printInfo("websocket [beforeConnectClose]") - self:beforeConnectClose() - - -- close connect - self._socket:send_close() - self._socket = nil - - self:afterConnectClose() - printInfo("websocket [afterConnectClose]") -end - -function WebSocketConnectBase:getConnectId() - if not self._connectId then - local redis = self:getRedis() - self._connectId = tostring(redis:command("INCR", Constants.NEXT_CONNECT_ID_KEY)) - end - return self._connectId -end - -function WebSocketConnectBase:sendMessageToSelf(message) - if self.config.messageFormat == Constants.MESSAGE_FORMAT_JSON and type(message) == "table" then - message = json_encode(message) - end - self._socket:send_text(tostring(message)) -end - -function WebSocketConnectBase:subscribeChannel(channelName, callback) - local sub = self._subscribeChannels[channelName] - if not sub then - sub = { - name = channelName, - enabled = false, - running = false, - retryCount = 0, - } - self._subscribeChannels[channelName] = sub - end - - if sub.enabled then - printWarn("already subscribed channel \"%s\"", sub.name) - return - end - - local function _subscribe() - sub.enabled = true - sub.running = true - - -- pubsub thread need separated redis connect - local redis = self:_newRedis() - local channel = sub.name - local loop, err = redis:pubsub({subscribe = channel}) - if not loop then - throw("subscribe channel \"%s\" failed, %s", channel, err) - end - - for msg, abort in loop do - if not sub.running then - abort() - break - end - - if msg then - if msg.kind == "subscribe" then - printInfo("channel \"%s\" subscribed", channel) - elseif msg.kind == "message" then - local payload = msg.payload - printInfo("channel \"%s\" message \"%s\"", channel, payload) - if callback(payload) == false then - sub.running = false - abort() - break - end - end - end - end - - -- when error occured or exit normally, - -- connect will auto close, channel will be unsubscribed - sub.enabled = false - redis:setKeepAlive() - if not sub.running then - self._subscribeChannels[channel] = nil - else - -- if an error leads to an exiting, retry to subscribe channel - if sub.retryCount < self.config.maxSubscribeRetryCount then - sub.retryCount = sub.retryCount + 1 - printWarn("subscribe channel \"%s\" loop ended, try [%d]", channel, sub.retryCount) - self:subscribeChannel(channel, callback) - else - printWarn("subscribe channel \"%s\" loop ended, max try", channel) - self._subscribeChannels[channel] = nil - end - end - end - - local thread = ngx_thread_spawn(_subscribe) - printInfo("spawn subscribe thread \"%s\"", tostring(thread)) -end - -function WebSocketConnectBase:unsubscribeChannel(channelName) - local sub = self._subscribeChannels[channelName] - if not sub then - printInfo("not subscribe channel \"%s\"", channelName) - else - printInfo("unsubscribe channel \"%s\"", channelName) - sub.running = false - end -end - --- events - -function WebSocketConnectBase:beforeConnectReady() -end - -function WebSocketConnectBase:afterConnectReady() -end - -function WebSocketConnectBase:beforeConnectClose() -end - -function WebSocketConnectBase:afterConnectClose() -end - -function WebSocketConnectBase:convertTokenToSessionId(token) - return token -end - --- private methods - -function WebSocketConnectBase:_processMessage(rawMessage, messageType) - local message = self:_parseMessage(rawMessage, messageType) - local msgid = message.__id - local actionName = message.action - local err = nil - local ok, result = xpcall(function() - return self:runAction(actionName, message) - end, function(_err) - err = _err - if DEBUG > 1 then - err = err .. debug.traceback("", 4) - end - end) - if err then - return nil, err - end - - local rtype = type(result) - if rtype == "nil" then return end - if rtype ~= "table" then - if msgid then - printWarn("action \"%s\" return invalid result for message [__id:\"%s\"]", actionName, msgid) - else - printWarn("action \"%s\" return invalid result", actionName) - end - end - - if not msgid then - printWarn("action \"%s\" return unused result", actionName) - return true - end - - if not self._socket then - return nil, string.format("socket removed, action \"%s\"", actionName) - end - - result.__id = msgid - local message = json.encode(result) - local bytes, err = self._socket:send_text(message) - if err then - return nil, string.format("send message to client failed, %s", err) - end - - return true -end - -function WebSocketConnectBase:_parseMessage(rawMessage, messageType) - -- TODO: support message type plugin - if messageType ~= Constants.WEBSOCKET_TEXT_MESSAGE_TYPE then - throw("not supported message type \"%s\"", messageType) - end - - -- TODO: support message format plugin - if self.config.appSocketMessageFormat == "json" then - local message = json.decode(rawMessage) - if type(message) == "table" then - return message - else - throw("not supported message format \"%s\"", type(message)) - end - else - throw("not support message format \"%s\"", tostring(self.config.appSocketMessageFormat)) - end -end - -function WebSocketConnectBase:_unsubscribeChannel() - local redis = self:getRedis() - redis:command("PUBLISH", self._connectChannel, "QUIT") -end - -function WebSocketConnectBase:_authConnect() - if ngx.headers_sent then - throw("response header already sent") - end - - req_read_body() - local headers = ngx.req.get_headers() - local protocols = headers["sec-websocket-protocol"] - if type(protocols) == "table" then - protocols = protocols[1] - end - if not protocols then - throw("not set header: Sec-WebSocket-Protocol") - end - - local token = string.match(protocols, Constants.WEBSOCKET_SUBPROTOCOL_PATTERN) - if not token then - throw("not found token in header: Sec-WebSocket-Protocol") - end - - -- convert token to session id - local sid = self:convertTokenToSessionId(token) - if not sid then - throw("convertTokenToSessionId() return invalid sid") - end - - local session = self:openSession(sid) - if not session then - throw("not set valid session id in header: Sec-WebSocket-Protocol") - end - - -- save connect id in session - local connectId = self:getConnectId() - session:setConnectId(connectId) - session:save() - self._connectChannel = Constants.CONNECT_CHANNEL_PREFIX .. connectId -end - -return WebSocketConnectBase diff --git a/src/server/base/WorkerBase.lua b/src/server/base/WorkerBase.lua deleted file mode 100644 index 4786ab4..0000000 --- a/src/server/base/WorkerBase.lua +++ /dev/null @@ -1,145 +0,0 @@ ---[[ - -Copyright (c) 2015 gameboxcloud.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -]] - -local assert = assert -local type = type -local string_lower = string.lower -local string_format = string.format -local json_decode = json.decode -local json_encode = json.encode -local tostring = tostring -local os_date = os.date -local os_time = os.time -local io_flush = io.flush - -local _JOB_HASH = "_JOB_HASH" - -local RedisService = cc.load("redis").service -local BeansService = cc.load("beanstalkd").service -local JobService = cc.load("job").service - -local CLIBase = import(".CLIBase") -local Constants = import(".Constants") - -local WorkerBase = class("WorkerBase", CLIBase) - -function WorkerBase:ctor(config) - WorkerBase.super.ctor(self, config) - - self._requestType = Constants.WORKER_REQUEST_TYPE - self._jobTube = config.beanstalkd.jobTube - self._jobService = JobService:create(self:_getRedis(), self:_getBeans(), config) -end - -function WorkerBase:run() - local ok, err = xpcall(function() - self:runEventLoop() - end, function(err) - err = tostring(err) - printError(err) - end) -end - -function WorkerBase:runEventLoop() - local beans = self:_getBeans() - local redis = self:_getRedis() - local jobService = self._jobService - - beans:command("watch", self._jobTube) - - while true do - local job, err = beans:command("reserve") - if not job then - printWarn("reserve beanstalkd job failed: %s", err) - if err == "NOT_CONNECTED" then - throw("beanstalkd NOT_CONNECTED") - end - goto reserve_next_job - end - - local data, err = json_decode(job.data) - if not data then - printWarn("job bid: %s, contents: \"%s\" is invalid: %s", job.id, job.data, err) - beans:command("delete", job.id) - goto reserve_next_job - end - - printInfo("get a job, jobId: %s, contents: %s", tostring(data.id), job.data) - - -- remove redis data, which is related to this job - jobService:remove(data.id) - - -- handle this job - local jobAction = data.action - res = self:runAction(jobAction, data.arg) - if self.config.appJobMessageFormat == "json" then - res = json_encode(res) - end - - printInfo("finish job, jobId: %s, joined_time: %s, reserved_time:%s, result: %s", tostring(data.id), os_date("%Y-%m-%d %H:%M:%S", data.joined_time), os_date("%Y-%m-%d %H:%M:%S"), res) - - io_flush() -::reserve_next_job:: - end - - printInfo("DONE") -end - -function WorkerBase:getActionModulePath(actionModuleName) - return string_format("%s.%s%s", "workers.actions", actionModuleName, self.config.actionModuleSuffix) -end - -function WorkerBase:_getBeans() - if not self._beans then - self._beans = self:_newBeans() - end - return self._beans -end - -function WorkerBase:_newBeans() - local beans = BeansService:create(self.config.beanstalkd) - local ok, err = beans:connect() - if err then - throw("connect internal beanstalkd failed, %s", err) - end - return beans -end - -function WorkerBase:_getRedis() - if not self._redis then - self._redis = self:_newRedis() - end - return self._redis -end - -function WorkerBase:_newRedis() - local redis = RedisService:create(self.config.redis) - local ok, err = redis:connect() - if err then - throw("connect internal redis failed, %s", err) - end - return redis -end - -return WorkerBase diff --git a/start_server b/start_server new file mode 100755 index 0000000..1c68900 --- /dev/null +++ b/start_server @@ -0,0 +1,62 @@ +#!/bin/bash + +function showHelp() +{ + echo "Usage: [sudo] ./start_server.sh [OPTIONS]" + echo "Options:" + echo -e "\t -v , --version \t\t show GameBox Cloud Core version" + echo -e "\t -h , --help \t\t show this help" + echo -e "\t --debug \t\t start GameBox Cloud Core in debug mode." + echo "In default, GameBox Cloud Core will start in release mode, or else it will start in debug mode when you specified \"--debug\"." + echo "" +} + +ROOT_DIR=$(cd "$(dirname $0)" && pwd) +source $ROOT_DIR/bin/shell_func.sh + +if [ $? -ne 0 ] ; then echo "Terminating..." >&2; exit 1; fi + +if [ $OS_TYPE == "MACOS" ]; then + ARGS=$($ROOT_DIR/bin/getopt_long "$@") +else + ARGS=$(getopt -o vh --long debug,version,help -n 'Start GameBox Cloud Core' -- "$@") +fi + +eval set -- "$ARGS" + +declare -i DEBUG=0 + +while true ; do + case "$1" in + --debug) + DEBUG=1 + shift + ;; + + -v|--version) + echo $VERSION + echo "" + exit 0 + ;; + + -h|--help) + showHelp; + echo "" + exit 0 + ;; + + --) shift; break ;; + + *) + echo "invalid option. $1" + exit 1 + ;; + esac +done + +echo -e "\033[33mStart GameBox Cloud Core $VERSION\033[0m" +echo "" + +updateConfigs +startSupervisord +checkStatus diff --git a/stop_server b/stop_server new file mode 100755 index 0000000..f0cfda2 --- /dev/null +++ b/stop_server @@ -0,0 +1,6 @@ +#!/bin/bash + +ROOT_DIR=$(cd "$(dirname $0)" && pwd) +source $ROOT_DIR/bin/shell_func.sh + +stopSupervisord diff --git a/vagrant-support/bootstrap.sh b/vagrant-support/bootstrap.sh new file mode 100644 index 0000000..4639f28 --- /dev/null +++ b/vagrant-support/bootstrap.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +PROJECT_ROOT_DIR=/vagrant +BOOTSTRAP_DIR=/vagrant/vagrant-bootstrap + +function setup() +{ + # setup packages + cd /etc/apt/ + cp sources.list sources.list.origin + cat sources.list.origin | sed s/archive.ubuntu.com/mirrors.aliyun.com/ > sources.list + + debconf-set-selections <<< 'mysql-server mysql-server/root_password password gamebox' + debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password gamebox' + + apt-get update -y + apt-get upgrade -y + apt-get install -y mysql-server mysql-client + + # fix locale warnings + apt-get install -y language-pack-en + echo "" >> /home/vagrant/.profile + echo "export LC_CTYPE=\"en_US.UTF-8\"" >> /home/vagrant/.profile + echo "export LANG=\"en_US.UTF-8\"" >> /home/vagrant/.profile + chown vagrant:vagrant /home/vagrant/.profile + + export LC_CTYPE="en_US.UTF-8" + export LANG="en_US.UTF-8" + + cd /vagrant/ + sudo ./make.sh --prefix=/opt/gbc-core + + sudo rm -fr /opt/gbc-core/apps + sudo rm -fr /opt/gbc-core/conf + sudo rm -fr /opt/gbc-core/src + + sudo rm -f /opt/gbc-core/start_server + sudo rm -f /opt/gbc-core/stop_server + sudo rm -f /opt/gbc-core/check_server + + sudo rm -f /opt/gbc-core/bin/start_worker.lua + sudo rm -f /opt/gbc-core/bin/shell_func.sh + sudo rm -f /opt/gbc-core/bin/shell_func.lua + + ln -s /vagrant/apps /opt/gbc-core/apps + ln -s /vagrant/conf /opt/gbc-core/conf + ln -s /vagrant/src /opt/gbc-core/src + + ln -s /vagrant/start_server /opt/gbc-core/start_server + ln -s /vagrant/stop_server /opt/gbc-core/stop_server + ln -s /vagrant/check_server /opt/gbc-core/check_server + + ln -s /vagrant/bin/start_worker.lua /opt/gbc-core/bin/start_worker.lua + ln -s /vagrant/bin/shell_func.sh /opt/gbc-core/bin/shell_func.sh + ln -s /vagrant/bin/shell_func.lua /opt/gbc-core/bin/shell_func.lua + + echo "" + ls -lh /opt/gbc-core + + echo "" + echo "INSTALL COMPLETED." + echo "" + echo "" +} + +if [ ! -f /opt/gbc-core/start_server ]; then + setup +fi + +# done +/opt/gbc-core/start_server --debug +echo "" +echo "waiting 5 seconds..." +sleep 5 +echo "" +echo "" +/opt/gbc-core/check_server + +echo "" +echo "" +echo ALL DONE. please use browser open http://localhost:8088/ +echo ""