├── spec ├── dummy │ ├── .keep │ ├── log │ │ └── .keep │ └── tmp │ │ ├── .keep │ │ ├── cache │ │ └── .keep │ │ └── pids │ │ └── .keep ├── support │ └── service_support.rb ├── spec_helper.rb ├── cli_spec.rb ├── service │ ├── callbacks_spec.rb │ ├── base_spec.rb │ └── interface_spec.rb └── sails_spec.rb ├── lib ├── sails │ ├── templates │ │ ├── config │ │ │ ├── locales │ │ │ │ ├── en.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ └── rails.zh-CN.yml │ │ │ ├── environments │ │ │ │ ├── test.rb │ │ │ │ ├── production.rb │ │ │ │ └── development.rb │ │ │ ├── boot.rb │ │ │ ├── database.yml.tt │ │ │ ├── initializers │ │ │ │ └── active_record.rb │ │ │ └── application.rb.tt │ │ ├── app │ │ │ └── services │ │ │ │ ├── handler.rb.tt │ │ │ │ ├── gen-rb │ │ │ │ ├── %app_name%_constants.rb.tt │ │ │ │ ├── %app_name%_types.rb.tt │ │ │ │ └── %app_name%.rb.tt │ │ │ │ └── application_service.rb.tt │ │ ├── Gemfile │ │ ├── Rakefile.tt │ │ ├── %app_name%.thrift.tt │ │ ├── db │ │ │ └── seeds.rb │ │ └── lib │ │ │ └── tasks │ │ │ └── client.rake.tt │ ├── version.rb │ ├── service.rb │ ├── service │ │ ├── exception.rb │ │ ├── callbacks.rb │ │ ├── base.rb │ │ └── interface.rb │ ├── config.rb │ ├── console.rb │ ├── log_subscriber.rb │ ├── tasks.rb │ ├── cli.rb │ ├── daemon.rb │ └── base.rb └── sails.rb ├── Gemfile ├── .travis.yml ├── Rakefile ├── .gitignore ├── bin └── sails ├── sails.gemspec ├── LICENSE ├── Gemfile.lock ├── CHANGELOG.md └── README.md /spec/dummy/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/tmp/cache/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/tmp/pids/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/sails/templates/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | hello: "Hello" -------------------------------------------------------------------------------- /lib/sails/templates/config/locales/zh-CN.yml: -------------------------------------------------------------------------------- 1 | zh-CN: 2 | hello: "你好" 3 | 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | gem 'dalli' 5 | gem 'rspec' -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.3.2 5 | 6 | script: bundle exec rspec spec 7 | -------------------------------------------------------------------------------- /lib/sails/version.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | def self.version 3 | "0.2.3" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/sails/templates/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | config.cache_classes = false 3 | end -------------------------------------------------------------------------------- /lib/sails/templates/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | config.cache_classes = true 3 | end -------------------------------------------------------------------------------- /lib/sails/templates/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | config.cache_classes = false 3 | end -------------------------------------------------------------------------------- /lib/sails/templates/app/services/handler.rb.tt: -------------------------------------------------------------------------------- 1 | $:.unshift Sails.root.join("app/services/gen-rb") 2 | 3 | require "<%= app_name %>" -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | task default: :spec 6 | -------------------------------------------------------------------------------- /lib/sails/templates/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | $:.unshift File.expand_path('../', __FILE__) 5 | require "application" 6 | 7 | Sails.start!("nonblocking") -------------------------------------------------------------------------------- /lib/sails/templates/config/database.yml.tt: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: sqlite3 3 | database: db/<%= app_name %>.sqlite 4 | 5 | development: 6 | <<: *default 7 | 8 | test: 9 | <<: *default 10 | 11 | production: 12 | <<: *default 13 | -------------------------------------------------------------------------------- /lib/sails/service.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | module Service 3 | extend ActiveSupport::Autoload 4 | 5 | autoload :Base 6 | autoload :Config 7 | autoload :Exception 8 | autoload :Interface 9 | autoload :Callbacks 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/sails/templates/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sails' 4 | gem 'activerecord' 5 | gem 'sqlite3' 6 | gem 'eventmachine' 7 | gem 'thrift' 8 | 9 | group :development, :test do 10 | # gem 'rspec' 11 | # gem 'factory_girl' 12 | # gem 'capistrano' 13 | end -------------------------------------------------------------------------------- /lib/sails/templates/app/services/gen-rb/%app_name%_constants.rb.tt: -------------------------------------------------------------------------------- 1 | # 2 | # Autogenerated by Thrift Compiler (0.9.2) 3 | # 4 | # DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING 5 | # 6 | 7 | require 'thrift' 8 | require '<%= app_name %>_types' 9 | 10 | module Thrift 11 | end 12 | -------------------------------------------------------------------------------- /lib/sails/templates/Rakefile.tt: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "rubygems" 3 | require File.expand_path('../config/application.rb', __FILE__) 4 | require "sails/tasks" 5 | Dir.glob('lib/tasks/*.rake').each { |r| import r } 6 | 7 | task :environment do 8 | require File.expand_path('../config/application.rb', __FILE__) 9 | end -------------------------------------------------------------------------------- /lib/sails/templates/config/initializers/active_record.rb: -------------------------------------------------------------------------------- 1 | require "active_record" 2 | ActiveRecord::Base.raise_in_transactional_callbacks = true 3 | 4 | config_file = Sails.root.join("config/database.yml") 5 | database_config = YAML.load_file(config_file)[Sails.env] 6 | ActiveRecord::Base.establish_connection(database_config) -------------------------------------------------------------------------------- /spec/support/service_support.rb: -------------------------------------------------------------------------------- 1 | module ServiceSupport 2 | extend ActiveSupport::Concern 3 | 4 | def service 5 | Sails.service 6 | end 7 | 8 | # Insert a instance into Sails.service 9 | def insert_service(instance) 10 | service.services << simple 11 | service.send(:define_service_methods!) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | log/* 12 | .dat* 13 | build/ 14 | /.yardoc/ 15 | /_yardoc/ 16 | /doc/ 17 | /rdoc/ 18 | /.bundle/ 19 | /lib/bundler/man/ 20 | .rvmrc 21 | spec/dummy/log/*.log 22 | spec/dummy/log/*.log 23 | -------------------------------------------------------------------------------- /lib/sails/templates/%app_name%.thrift.tt: -------------------------------------------------------------------------------- 1 | namespace rb Thrift 2 | 3 | struct Session { 4 | 1:i64 user_id, 5 | 2:string access_token 6 | } 7 | 8 | exception OperationFailed { 9 | 1: i32 code, 10 | 2: string message 11 | } 12 | 13 | service <%= @app_name.capitalize %> { 14 | 15 | // ping <-> pong 16 | string ping() throws (1:OperationFailed failure) 17 | } -------------------------------------------------------------------------------- /lib/sails/templates/app/services/application_service.rb.tt: -------------------------------------------------------------------------------- 1 | class ApplicationService < Sails::Service::Base 2 | # def require_user!(user_id, access_token) 3 | # end 4 | 5 | # def raise_error(code, msg = nil) 6 | # raise YouCustomThriftException.new(code: code, message: msg) 7 | # end 8 | 9 | def ping 10 | return "Pong #{Time.now}" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/sails/templates/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /bin/sails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | git_path = File.expand_path('../../.git', __FILE__) 3 | 4 | if File.exist?(git_path) 5 | sails_path = File.expand_path('../../lib', __FILE__) 6 | $:.unshift(sails_path) 7 | require_relative "../lib/sails/cli" 8 | end 9 | 10 | source_dir = Dir.pwd 11 | if File.exist?(File.join(source_dir,"config/application.rb")) 12 | $:.unshift(source_dir) 13 | require "config/application" 14 | require "sails" 15 | else 16 | require "sails/cli" 17 | end 18 | 19 | Sails::CLI.start(ARGV) 20 | -------------------------------------------------------------------------------- /lib/sails/service/exception.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | module Service 3 | # Thrift Exception 4 | class Exception < ::Thrift::Exception 5 | include ::Thrift::Struct, ::Thrift::Struct_Union 6 | CODE = 1 7 | MESSAGE = 2 8 | 9 | FIELDS = { 10 | CODE => {:type => ::Thrift::Types::I32, :name => 'code'}, 11 | MESSAGE => {:type => ::Thrift::Types::STRING, :name => 'message'} 12 | } 13 | 14 | def struct_fields; FIELDS; end 15 | 16 | def validate 17 | end 18 | 19 | ::Thrift::Struct.generate_accessors self 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /lib/sails/service/callbacks.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | module Service 3 | module Callbacks 4 | extend ActiveSupport::Concern 5 | 6 | include ActiveSupport::Callbacks 7 | 8 | included do 9 | define_callbacks :action 10 | 11 | set_callback :action, :before do |object| 12 | end 13 | 14 | set_callback :action, :after do |object| 15 | end 16 | end 17 | 18 | module ClassMethods 19 | def before_action(*names, &blk) 20 | names.each do |name| 21 | set_callback(:action, :before, name, &blk) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/sails/templates/config/application.rb.tt: -------------------------------------------------------------------------------- 1 | require 'sails' 2 | $LOAD_PATH.unshift Sails.root 3 | 4 | Bundler.require() 5 | 6 | require "app/services/handler" 7 | 8 | module Sails 9 | config.app_name = '<%= app_name %>' 10 | # Thrift Configs 11 | config.port = 4000 12 | 13 | # Thrift protocol allows: :binary, :compact, default: :binary 14 | # config.protocol = :binary 15 | 16 | # Number of threads, default: 20 17 | # config.thread_size = 20 18 | 19 | config.processor = Thrift::<%= app_name.capitalize %>::Processor 20 | 21 | # config.autoload_paths += %W(app/workers) 22 | 23 | config.i18n.default_locale = 'zh-CN' 24 | 25 | # cache store 26 | # config.cache_store = [:dalli_store, '127.0.0.1'] 27 | end 28 | 29 | Sails.init() 30 | -------------------------------------------------------------------------------- /lib/sails/templates/lib/tasks/client.rake.tt: -------------------------------------------------------------------------------- 1 | $:.unshift Sails.root.join("app/services") 2 | require "handler" 3 | 4 | class TestClient 5 | def self.instance 6 | @transport ||= Thrift::FramedTransport.new(::Thrift::Socket.new('127.0.0.1', 4000, 10)) 7 | @protocol ||= Thrift::BinaryProtocol.new(@transport) 8 | @client ||= Thrift::<%= @app_name.capitalize %>::Client.new(@protocol) 9 | @transport.open() if !@transport.open? 10 | @client 11 | end 12 | 13 | def self.method_missing(name, *args) 14 | puts "> #{name}" 15 | puts self.instance.send(name, *args).inspect 16 | puts "" 17 | end 18 | end 19 | 20 | namespace :client do 21 | desc "client ping test" 22 | task :ping do 23 | TestClient.ping() 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/sails/config.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | class Config 3 | include ActiveSupport::Configurable 4 | 5 | def initialize 6 | init_defaults! 7 | end 8 | 9 | def init_defaults! 10 | config.app_name = "Sails" 11 | config.cache_store = [:memory_store] 12 | config.autoload_paths = %w(app/models app/models/concerns app/workers app/services app/services/concerns lib) 13 | config.i18n = I18n 14 | config.i18n.load_path += Dir[Sails.root.join('config', 'locales', '*.{rb,yml}').to_s] 15 | config.i18n.default_locale = :en 16 | config.cache_classes = false 17 | 18 | config.port = 4000 19 | config.thread_port = 4001 20 | config.processor = nil 21 | config.thread_size = 20 22 | config.protocol = :binary 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative "../lib/sails" 2 | 3 | # Force set Sails root to spec/dummy 4 | Sails.root = File.expand_path("../dummy", __FILE__) 5 | $LOAD_PATH.unshift Sails.root 6 | 7 | # for test Sails config 8 | I18n.config.enforce_available_locales = false 9 | module Sails 10 | config.app_name = 'hello' 11 | config.host = '1.1.1.1' 12 | config.port = 1000 13 | config.i18n.default_locale = 'zh-TW' 14 | config.autoload_paths += %W(app/bar) 15 | config.cache_store = [:dalli_store, '127.0.0.1'] 16 | end 17 | 18 | Dir["./spec/support/**/*.rb"].each { |f| require f } 19 | 20 | RSpec.configure do |config| 21 | config.include ServiceSupport, file_path: /spec\/service/ 22 | 23 | config.before(:each) do 24 | end 25 | 26 | config.after(:each) do 27 | end 28 | 29 | config.raise_errors_for_deprecations! 30 | end 31 | -------------------------------------------------------------------------------- /lib/sails/console.rb: -------------------------------------------------------------------------------- 1 | require "irb" 2 | require "irb/ext/loader" 3 | 4 | module Sails 5 | module ConsoleMethods 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | end 10 | 11 | def service 12 | Sails.service 13 | end 14 | 15 | def reload! 16 | puts "Reloading..." 17 | Sails.reload!(force: true) 18 | true 19 | end 20 | end 21 | 22 | class Console 23 | class << self 24 | def start(app_path) 25 | new(app_path).start 26 | end 27 | end 28 | 29 | def initialize(app_path) 30 | @app_path = app_path 31 | end 32 | 33 | def start 34 | puts "Loading #{Sails.env} environment (Sails #{Sails.version})" 35 | IRB.conf[:IRB_NAME] = "Sails console" 36 | require @app_path 37 | ARGV.clear 38 | if defined?(IRB::ExtendCommandBundle) 39 | IRB::ExtendCommandBundle.send :include, Sails::ConsoleMethods 40 | end 41 | IRB.start 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /sails.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require File.expand_path('lib/sails/version') 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "sails" 7 | s.version = Sails.version 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Jason Lee", "P.S.V.R", "wxianfeng", "sapronlee","qhwa"] 10 | s.email = ["huacnlee@gmail.com", "pmq2001@gmail.com", "wang.fl1429@gmail.com", "sapronlee@gmail.com","qhwa@163.com"] 11 | s.homepage = "https://github.com/huacnlee/sails" 12 | s.summary = %q{Sails, Help you to create Rails style Thrift Server} 13 | s.description = %q{Sails, Help you to create Rails style Thrift Server} 14 | s.files = `git ls-files`.split("\n") 15 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 16 | s.executables = ["sails"] 17 | s.require_paths = ["lib"] 18 | s.license = 'MIT' 19 | 20 | s.add_dependency "activesupport", ["> 3.2.0"] 21 | s.add_dependency "thrift", [">= 0.9.0"] 22 | s.add_dependency "thor" 23 | end 24 | -------------------------------------------------------------------------------- /spec/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Sails::CLI' do 4 | let(:cli) { Sails::CLI.new } 5 | describe '.start' do 6 | it { expect(cli).to respond_to(:start) } 7 | it { 8 | expect(Sails::Daemon).to receive(:init) 9 | expect(Sails::Daemon).to receive(:start_process) 10 | cli.start 11 | } 12 | end 13 | 14 | describe '.stop' do 15 | it { expect(cli).to respond_to(:stop) } 16 | it { 17 | expect(Sails::Daemon).to receive(:init) 18 | expect(Sails::Daemon).to receive(:stop_process) 19 | cli.stop 20 | } 21 | end 22 | 23 | describe '.restart' do 24 | it { expect(cli).to respond_to(:restart) } 25 | it { 26 | # expect(Sails::Daemon).to receive(:init) 27 | expect(Sails::Daemon).to receive(:restart_process) 28 | cli.restart 29 | } 30 | end 31 | 32 | describe '.new' do 33 | it { expect(cli).to respond_to(:new) } 34 | end 35 | 36 | describe '.console' do 37 | it { expect(cli).to respond_to(:console) } 38 | end 39 | 40 | describe '.version' do 41 | it { expect(cli).to respond_to(:version) } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jason Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/sails.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'active_support/all' 4 | require 'i18n' 5 | require 'thrift' 6 | require 'yaml' 7 | require "sails/base" 8 | 9 | # Sails 10 | # 11 | # You can custom Sails configs in config/application.rb 12 | # 13 | # module Sails 14 | # config.app_name = 'you_app_name' 15 | # config.thrift.port = 7075 16 | # config.thrift.processor = Thrift::YouAppName::Processor 17 | # 18 | # # Thrift Protocols can be use [:binary, :compact, :json] 19 | # # http://jnb.ociweb.com/jnb/jnbJun2009.html#protocols 20 | # config.thrift.procotol = :binary 21 | # 22 | # config.autoload_paths += %W(app/workers) 23 | # 24 | # config.i18n.default_locale = 'zh-CN' 25 | # 26 | # # cache store 27 | # config.cache_store = [:dalli_store, '127.0.0.1' }] 28 | # end 29 | # 30 | module Sails 31 | extend ActiveSupport::Autoload 32 | 33 | autoload :Config 34 | autoload :Version 35 | autoload :Service 36 | autoload :CLI 37 | autoload :Daemon 38 | autoload :Console 39 | 40 | eager_autoload do 41 | autoload :LogSubscriber 42 | end 43 | end 44 | 45 | Sails.eager_load! 46 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | sails (0.2.3) 5 | activesupport (> 3.2.0) 6 | thor 7 | thrift (>= 0.9.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | activesupport (4.2.6) 13 | i18n (~> 0.7) 14 | json (~> 1.7, >= 1.7.7) 15 | minitest (~> 5.1) 16 | thread_safe (~> 0.3, >= 0.3.4) 17 | tzinfo (~> 1.1) 18 | dalli (2.7.1) 19 | diff-lcs (1.2.5) 20 | i18n (0.7.0) 21 | json (1.8.3) 22 | minitest (5.9.1) 23 | rspec (3.1.0) 24 | rspec-core (~> 3.1.0) 25 | rspec-expectations (~> 3.1.0) 26 | rspec-mocks (~> 3.1.0) 27 | rspec-core (3.1.7) 28 | rspec-support (~> 3.1.0) 29 | rspec-expectations (3.1.2) 30 | diff-lcs (>= 1.2.0, < 2.0) 31 | rspec-support (~> 3.1.0) 32 | rspec-mocks (3.1.3) 33 | rspec-support (~> 3.1.0) 34 | rspec-support (3.1.2) 35 | thor (0.19.1) 36 | thread_safe (0.3.5) 37 | thrift (0.9.3.0) 38 | tzinfo (1.2.2) 39 | thread_safe (~> 0.1) 40 | 41 | PLATFORMS 42 | ruby 43 | 44 | DEPENDENCIES 45 | dalli 46 | rspec 47 | sails! 48 | 49 | BUNDLED WITH 50 | 1.13.6 51 | -------------------------------------------------------------------------------- /lib/sails/templates/app/services/gen-rb/%app_name%_types.rb.tt: -------------------------------------------------------------------------------- 1 | # 2 | # Autogenerated by Thrift Compiler (0.9.2) 3 | # 4 | # DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING 5 | # 6 | 7 | require 'thrift' 8 | 9 | module Thrift 10 | class Session 11 | include ::Thrift::Struct, ::Thrift::Struct_Union 12 | USER_ID = 1 13 | ACCESS_TOKEN = 2 14 | 15 | FIELDS = { 16 | USER_ID => {:type => ::Thrift::Types::I64, :name => 'user_id'}, 17 | ACCESS_TOKEN => {:type => ::Thrift::Types::STRING, :name => 'access_token'} 18 | } 19 | 20 | def struct_fields; FIELDS; end 21 | 22 | def validate 23 | end 24 | 25 | ::Thrift::Struct.generate_accessors self 26 | end 27 | 28 | class OperationFailed < ::Thrift::Exception 29 | include ::Thrift::Struct, ::Thrift::Struct_Union 30 | CODE = 1 31 | MESSAGE = 2 32 | 33 | FIELDS = { 34 | CODE => {:type => ::Thrift::Types::I32, :name => 'code'}, 35 | MESSAGE => {:type => ::Thrift::Types::STRING, :name => 'message'} 36 | } 37 | 38 | def struct_fields; FIELDS; end 39 | 40 | def validate 41 | end 42 | 43 | ::Thrift::Struct.generate_accessors self 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /spec/service/callbacks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Service' do 4 | describe 'Callbacks' do 5 | context 'callback with method_name' do 6 | class CallbackTest1Service < Sails::Service::Base 7 | before_action :bar, :dar 8 | def bar; end 9 | def dar; end 10 | def foo; end 11 | end 12 | 13 | let(:simple) { CallbackTest1Service.new } 14 | 15 | before do 16 | insert_service(simple) 17 | end 18 | 19 | it 'should work' do 20 | expect(simple).to receive(:bar).once 21 | expect(simple).to receive(:dar).once 22 | service.foo 23 | end 24 | end 25 | 26 | describe 'params' do 27 | class CallbackTest3Service < Sails::Service::Base 28 | def foo(a, b) 29 | return params 30 | end 31 | end 32 | 33 | let(:simple) { CallbackTest3Service.new } 34 | 35 | before do 36 | insert_service(simple) 37 | end 38 | 39 | it "should work" do 40 | params = service.foo(1,2) 41 | expect(params).to include(:a, :b) 42 | expect(params[:a]).to eq 1 43 | expect(params[:b]).to eq 2 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/service/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Service' do 4 | describe 'Base' do 5 | class SimpleBaseTestService < Sails::Service::Base 6 | def foo 7 | end 8 | 9 | def bar(a, b) 10 | end 11 | 12 | private 13 | def dar 14 | end 15 | end 16 | 17 | let(:simple) { SimpleBaseTestService.new } 18 | 19 | describe '.raise_error' do 20 | it { expect(simple).to respond_to(:raise_error) } 21 | it { 22 | expect { simple.raise_error(11,'foo') }.to raise_error do |error| 23 | expect(error).to be_a(Sails::Service::Exception) 24 | expect(error.code).to eq 11 25 | expect(error.message).to eq 'foo' 26 | end 27 | } 28 | end 29 | 30 | describe '.action_methods' do 31 | it { expect(simple.action_methods).to include("foo", "bar") } 32 | it { expect(simple.action_methods.size).to eq 2 } 33 | it { expect(simple.action_methods).not_to include("dar") } 34 | end 35 | 36 | describe '.logger' do 37 | it { expect(simple.logger).to eq Sails.logger } 38 | end 39 | 40 | describe '.params' do 41 | it { expect(simple.params).to eq({}) } 42 | it "should set val" do 43 | simple.params[:foo] = 111 44 | expect(simple.params).to eq({ foo: 111 }) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/service/interface_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Service' do 4 | describe 'Interface' do 5 | class SimpleBaseTestService < Sails::Service::Base 6 | def bar(a, b) 7 | end 8 | 9 | def foo(c, d) 10 | 11 | end 12 | end 13 | 14 | module Sails 15 | module Service 16 | class Interface 17 | attr_accessor :services 18 | 19 | def initialize 20 | @services = [] 21 | @services << SimpleBaseTestService.new 22 | define_service_methods! 23 | end 24 | end 25 | end 26 | end 27 | 28 | let(:interface) { Sails::Service::Interface.new } 29 | 30 | describe 'set params' do 31 | it "should set val" do 32 | simple = interface.services.first 33 | interface.bar(1, 2) 34 | expect(simple.params.size).to eq(3) 35 | expect(simple.params[:method_name]).to eq('bar') 36 | expect(simple.params[:a]).to eq(1) 37 | expect(simple.params[:b]).to eq(2) 38 | end 39 | it "shoud clear val before set" do 40 | simple = interface.services.first 41 | interface.bar(1, 2) 42 | interface.foo(3, 4) 43 | expect(simple.params.size).to eq(3) 44 | expect(simple.params[:method_name]).to eq('foo') 45 | expect(simple.params[:c]).to eq(3) 46 | expect(simple.params[:d]).to eq(4) 47 | end 48 | end 49 | end 50 | end -------------------------------------------------------------------------------- /lib/sails/log_subscriber.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | class LogSubscriber < ActiveSupport::LogSubscriber 3 | INTERNAL_PARAMS = %w(controller action format _method only_path) 4 | 5 | def start_processing(event) 6 | return unless logger.info? 7 | 8 | payload = event.payload 9 | params = payload[:params] 10 | 11 | info "" 12 | info "Processing by #{payload[:controller]}##{payload[:action]} at #{Time.now}" 13 | info " Parameters: #{params.except(:method_name).inspect}" unless params.empty? 14 | 15 | ActiveRecord::LogSubscriber.reset_runtime if defined?(ActiveRecord::LogSubscriber) 16 | end 17 | 18 | def process_action(event) 19 | info do 20 | payload = event.payload 21 | additions = [] 22 | 23 | payload.each_key do |key| 24 | key_s = key.to_s 25 | if key_s.include?("_runtime") 26 | next if payload[key].blank? 27 | runtime_name = key_s.sub("_runtime","").capitalize 28 | additions << ("#{runtime_name}: %.1fms" % payload[key].to_f) 29 | end 30 | end 31 | 32 | status = payload[:status] 33 | payload[:runtime] = event.duration 34 | 35 | message = "Completed #{status} in #{event.duration.round(2)}ms" 36 | message << " (#{additions.join(" | ")})" 37 | message 38 | end 39 | end 40 | 41 | def logger 42 | Sails.logger 43 | end 44 | end 45 | end 46 | 47 | Sails::LogSubscriber.attach_to :sails 48 | -------------------------------------------------------------------------------- /lib/sails/tasks.rb: -------------------------------------------------------------------------------- 1 | if defined?(ActiveRecord::Base) 2 | load 'active_record/railties/databases.rake' 3 | 4 | class << ActiveRecord::Tasks::DatabaseTasks 5 | def env 6 | @env ||= Sails.env 7 | end 8 | 9 | def migrations_paths 10 | [Sails.root.join("db/migrate")] 11 | end 12 | 13 | def database_configuration 14 | YAML.load open(Sails.root.join('config/database.yml')).read 15 | end 16 | 17 | def db_dir 18 | Sails.root.join("db") 19 | end 20 | end 21 | end 22 | 23 | namespace :db do 24 | namespace :migrate do 25 | desc "Create new migration file. use `rake db:migrate:create NAME=create_users`" 26 | task :create do 27 | name = ENV['NAME'] 28 | abort("no NAME specified. use `rake db:migrate:create NAME=create_users`") if !name 29 | 30 | migrations_dir = File.join("db", "migrate") 31 | version = ENV["VERSION"] || Time.now.utc.strftime("%Y%m%d%H%M%S") 32 | filename = "#{version}_#{name}.rb" 33 | migration_name = name.gsub(/_(.)/) { $1.upcase }.gsub(/^(.)/) { $1.upcase } 34 | 35 | FileUtils.mkdir_p(migrations_dir) 36 | 37 | open(File.join(migrations_dir, filename), 'w') do |f| 38 | f << (<<-EOS).gsub(" ", "") 39 | class #{migration_name} < ActiveRecord::Migration 40 | def self.change 41 | end 42 | end 43 | EOS 44 | end 45 | puts filename 46 | end 47 | end 48 | end 49 | 50 | desc "Generate code from thrift IDL file" 51 | task :generate do 52 | puts "Generating Thrift code..." 53 | out = Sails.root.join("app/services/gen-rb") 54 | 55 | cmd = "thrift --gen rb -out #{out} -strict #{Sails.config.app_name}.thrift" 56 | puts "> #{cmd}" 57 | system cmd 58 | puts "[Done]" 59 | end 60 | 61 | task :gen => :generate 62 | -------------------------------------------------------------------------------- /lib/sails/service/base.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | module Service 3 | # Like ActionController::Base 4 | class Base 5 | include Callbacks 6 | 7 | class << self 8 | def internal_methods 9 | controller = self.superclass 10 | controller.public_instance_methods(true) 11 | end 12 | 13 | def action_methods 14 | @action_methods ||= begin 15 | # All public instance methods of this class, including ancestors 16 | methods = (public_instance_methods(true) - 17 | # Except for public instance methods of Base and its ancestors 18 | internal_methods + 19 | # Be sure to include shadowed public instance methods of this class 20 | public_instance_methods(false)).uniq.map(&:to_s) 21 | 22 | # Clear out AS callback method pollution 23 | Set.new(methods.reject { |method| method =~ /_one_time_conditions/ }) 24 | end 25 | end 26 | end 27 | 28 | # action params to Hash 29 | # 30 | # example: 31 | # 32 | # class FooService < Sails::Service::Base 33 | # def foo(name, age) 34 | # # you can use params in any instance methods 35 | # puts params[:name] 36 | # puts params[:age] 37 | # end 38 | # end 39 | # 40 | def params 41 | @params ||= {} 42 | end 43 | 44 | # Raise a Sails::Service::Exception (Thrift::Exception) 45 | # if you want custom error you can override this method in you ApplicationService 46 | def raise_error(code, msg = nil) 47 | raise Exception.new(code: code, message: msg) 48 | end 49 | 50 | def action_methods 51 | self.class.action_methods 52 | end 53 | 54 | # Sails.logger 55 | # You can use this method in app/services 56 | def logger 57 | Sails.logger 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.3 2 | 3 | - clear service instance params before set 4 | 5 | ## 0.2.2 6 | 7 | - Remove Sails.init in base.rb (#5) 8 | 9 | ## 0.2.1 10 | 11 | - Allow ActiveSupport > 5.0. 12 | 13 | ## 0.2.0 14 | 15 | - Remove define `Rails`; 16 | 17 | ## 0.1.5 18 | 19 | - Fix stdout error in daemon mode. 20 | - Clear ActiveRecord connections after method call in ensure. 21 | - Do not output exception info when raise 404. 22 | - Add more default code with new project template. 23 | - Add `rake client` task for thrift client connect test, and you can edit client test code in lib/tasks/client.rake. 24 | - Move ActiveRecord migration rake tasks into Sails. 25 | - Add `rake db:migrate:create` command to generate a new migration file. 26 | - Add default ActiveRecord initialize code. 27 | - Default use SQLite. 28 | - Use ActiveSupport::Notifications to output service action logs. 29 | 30 | ## 0.1.4 / 2014-12-24 31 | 32 | - Sails.root use Linux pwd command, to support symlink dir, for example like Capistrano's project/current path. 33 | 34 | ## 0.1.3 / 2014-12-22 35 | 36 | - Fix zombie process on restart in some Linux systems. 37 | 38 | ## 0.1.2 / 2014-12-17 39 | 40 | - `sails restart` use kill -USR2 signal, not kill master process. 41 | - Use tail log file to instead of direct stdout. 42 | - Add `before_action` support for Service layout; 43 | - Add `params` like same name in ActionController for Service layout; 44 | 45 | ## 0.1.1 / 2014-12-10 46 | 47 | - Add `sails s`, `sails c` commands. 48 | - Refactor service layer, use class to instead module. 49 | - Support to custom Thrift protocol with `config.protocol = :binary`. 50 | - Fix ThriftServer::OperationFailed not found error. 51 | - Implement Master Process to manage and protect Child Process, keep it running/restart/stop, like Unicorn. 52 | - Add Sails.reload! to reload cache classes. 53 | - Sails console start with IRB class, and support `reload!`,`service` methods in console. 54 | 55 | ## 0.1.0 / 2014-12-9 56 | 57 | - First version release. 58 | -------------------------------------------------------------------------------- /lib/sails/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'sails/version' 3 | 4 | module Sails 5 | class CLI < Thor 6 | include Thor::Actions 7 | 8 | map '-v' => :version 9 | map 's' => :start 10 | map 'c' => :console 11 | 12 | def self.source_root 13 | __dir__ 14 | end 15 | 16 | no_commands do 17 | def app_name 18 | @app_name 19 | end 20 | end 21 | 22 | # sails start 23 | # 24 | # Start a thrift app server 25 | option :daemon, type: :boolean, default: false 26 | option :mode, default: 'nonblocking' 27 | desc "start", "Start Thrift server" 28 | def start 29 | Sails::Daemon.init(mode: options[:mode], daemon: options[:daemon]) 30 | Sails::Daemon.start_process 31 | end 32 | 33 | # sails stop 34 | # 35 | # Stop thrift app server 36 | option :mode, default: 'nonblocking' 37 | desc "stop", "Stop Thrift server" 38 | def stop 39 | Sails::Daemon.init(mode: options[:mode]) 40 | Sails::Daemon.stop_process 41 | end 42 | 43 | # sails restart 44 | # 45 | # Restart thrift app server 46 | option :mode, default: 'nonblocking' 47 | desc "restart", "Restart Thrift server" 48 | def restart 49 | Sails::Daemon.init(mode: options[:mode]) 50 | Sails::Daemon.restart_process 51 | end 52 | 53 | desc "new APP_NAME", "Create a project" 54 | def new(name) 55 | require 'fileutils' 56 | 57 | app_dir = File.expand_path File.join(Dir.pwd, name) 58 | @rel_dir = name 59 | @app_name = File.basename app_dir 60 | 61 | directory 'templates', name 62 | %W(log tmp/pids tmp/cache lib/tasks app/models/concerns config/initializers log).each do |dir_name| 63 | empty_directory File.join(app_dir,dir_name) 64 | end 65 | puts '' 66 | ensure 67 | @app_name = nil 68 | @rel_dir = nil 69 | end 70 | 71 | desc "console", "Enter Sails console" 72 | def console 73 | Sails::Console.start(Sails.root.join("config/application.rb")) 74 | end 75 | 76 | desc "version", "Show Sails version" 77 | def version 78 | puts "Sails #{Sails.version}" 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/sails/templates/app/services/gen-rb/%app_name%.rb.tt: -------------------------------------------------------------------------------- 1 | # 2 | # Autogenerated by Thrift Compiler (0.9.2) 3 | # 4 | # DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING 5 | # 6 | 7 | require 'thrift' 8 | require '<%= @app_name %>_types' 9 | 10 | module Thrift 11 | module <%= @app_name.capitalize %> 12 | class Client 13 | include ::Thrift::Client 14 | 15 | def ping() 16 | send_ping() 17 | return recv_ping() 18 | end 19 | 20 | def send_ping() 21 | send_message('ping', Ping_args) 22 | end 23 | 24 | def recv_ping() 25 | result = receive_message(Ping_result) 26 | return result.success unless result.success.nil? 27 | raise result.failure unless result.failure.nil? 28 | raise ::Thrift::ApplicationException.new(::Thrift::ApplicationException::MISSING_RESULT, 'ping failed: unknown result') 29 | end 30 | 31 | end 32 | 33 | class Processor 34 | include ::Thrift::Processor 35 | 36 | def process_ping(seqid, iprot, oprot) 37 | args = read_args(iprot, Ping_args) 38 | result = Ping_result.new() 39 | begin 40 | result.success = @handler.ping() 41 | rescue ::Thrift::OperationFailed => failure 42 | result.failure = failure 43 | end 44 | write_result(result, oprot, 'ping', seqid) 45 | end 46 | 47 | end 48 | 49 | # HELPER FUNCTIONS AND STRUCTURES 50 | 51 | class Ping_args 52 | include ::Thrift::Struct, ::Thrift::Struct_Union 53 | 54 | FIELDS = { 55 | 56 | } 57 | 58 | def struct_fields; FIELDS; end 59 | 60 | def validate 61 | end 62 | 63 | ::Thrift::Struct.generate_accessors self 64 | end 65 | 66 | class Ping_result 67 | include ::Thrift::Struct, ::Thrift::Struct_Union 68 | SUCCESS = 0 69 | FAILURE = 1 70 | 71 | FIELDS = { 72 | SUCCESS => {:type => ::Thrift::Types::STRING, :name => 'success'}, 73 | FAILURE => {:type => ::Thrift::Types::STRUCT, :name => 'failure', :class => ::Thrift::OperationFailed} 74 | } 75 | 76 | def struct_fields; FIELDS; end 77 | 78 | def validate 79 | end 80 | 81 | ::Thrift::Struct.generate_accessors self 82 | end 83 | 84 | end 85 | 86 | end 87 | -------------------------------------------------------------------------------- /lib/sails/service/interface.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | module Service 3 | class Interface 4 | attr_accessor :services 5 | 6 | def initialize 7 | @services = [] 8 | Dir["#{Sails.root.join("app/services")}/*_service.rb"].each do |f| 9 | if File.basename(f) =~ /^(.*)_service.rb$/ 10 | require f 11 | mtd = $1.dup 12 | klass_name = "#{mtd.camelize}Service" 13 | @services << klass_name.constantize.new 14 | end 15 | end 16 | define_service_methods! 17 | end 18 | 19 | private 20 | def define_service_methods! 21 | @services.each do |instance| 22 | instance.action_methods.each do |method_name| 23 | self.class.send(:define_method, method_name) do |*args, &block| 24 | run_action(instance, method_name, *args, &block) 25 | end 26 | end 27 | end 28 | end 29 | 30 | def set_params_with_method_args(instance, method_name, args) 31 | instance.params.clear 32 | method_args = instance.method(method_name.to_sym).parameters.map { |arg| arg[1] } 33 | instance.params[:method_name] = method_name 34 | method_args.each_with_index do |arg, idx| 35 | instance.params[arg] = args[idx] 36 | end 37 | end 38 | 39 | def run_action(instance, method_name, *args, &block) 40 | set_params_with_method_args(instance, method_name, args) 41 | instance.run_callbacks :action do 42 | raw_payload = { 43 | controller: instance.class.to_s, 44 | action: method_name, 45 | params: instance.params, 46 | } 47 | 48 | ActiveSupport::Notifications.instrument("start_processing.sails", raw_payload.dup) 49 | ActiveSupport::Notifications.instrument("process_action.sails", raw_payload) do |payload| 50 | begin 51 | res = instance.send(method_name, *args, &block) 52 | payload[:status] = 200 53 | return res 54 | rescue Thrift::Exception => e 55 | payload[:status] = e.try(:code) 56 | raise e 57 | rescue => e 58 | if e.class.to_s == "ActiveRecord::RecordNotFound" 59 | payload[:status] = 404 60 | else 61 | payload[:status] = 500 62 | 63 | Sails.logger.info " ERROR #{e.inspect} backtrace: \n #{e.backtrace.join("\n ")}" 64 | end 65 | 66 | instance.raise_error(payload[:status]) 67 | ensure 68 | ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord::Base) 69 | if defined?(ActiveRecord::RuntimeRegistry) 70 | payload[:db_runtime] = ActiveRecord::RuntimeRegistry.sql_runtime || 0 71 | else 72 | payload[:db_runtime] = nil 73 | end 74 | end 75 | end 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/sails_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'Sails' do 4 | it { expect(Sails.version).not_to be_nil } 5 | 6 | describe '#root' do 7 | it 'should be a Pathname class' do 8 | expect(Sails.root).to be_a(Pathname) 9 | end 10 | 11 | it 'should support Sails.root.join' do 12 | expect(Sails.root.join("aaa").to_s).to eq File.join(Dir.pwd, "spec/dummy/aaa") 13 | end 14 | 15 | it 'should work' do 16 | expect(Sails.root.to_s).to eq File.join(Dir.pwd, "spec/dummy") 17 | end 18 | end 19 | 20 | describe '#init' do 21 | it 'should work' do 22 | Sails.instance_variable_set(:@inited, false) 23 | expect(Sails).to receive(:check_create_dirs).once 24 | expect(Sails).to receive(:load_initialize).once 25 | Sails.init 26 | expect(Sails.instance_variable_get(:@inited)).to eq true 27 | end 28 | end 29 | 30 | describe '#logger' do 31 | it 'should be a Logger class' do 32 | expect(Sails.logger).to be_a(Logger) 33 | end 34 | 35 | it 'should have logger_path' do 36 | expect(Sails.logger_path).to eq Sails.root.join("log/#{Sails.env}.log") 37 | end 38 | end 39 | 40 | describe '#config' do 41 | it 'should work' do 42 | expect(Sails.config).to be_a(Hash) 43 | expect(Sails.config.autoload_paths).to be_a(Array) 44 | end 45 | 46 | describe 'Real config' do 47 | it { expect(Sails.config.app_name).to eq 'hello' } 48 | it { expect(Sails.config.host).to eq '1.1.1.1' } 49 | it { expect(Sails.config.port).to eq 1000 } 50 | it { expect(Sails.config.protocol).to eq :binary } 51 | it { expect(Sails.config.thread_size).to eq 20 } 52 | it { expect(Sails.config.i18n.default_locale).to eq :'zh-TW' } 53 | it { expect(Sails.config.autoload_paths).to include("app/bar") } 54 | end 55 | end 56 | 57 | describe '#cache' do 58 | it { expect(Sails.cache).to be_a(ActiveSupport::Cache::DalliStore) } 59 | it { expect(Sails.cache).to respond_to(:read, :write, :delete, :clear) } 60 | end 61 | 62 | describe '#thrift_protocol_class' do 63 | it 'should work' do 64 | allow(Sails.config).to receive(:protocol).and_return(:binary) 65 | expect(Sails.thrift_protocol_class).to eq ::Thrift::BinaryProtocolFactory 66 | allow(Sails.config).to receive(:protocol).and_return(:compact) 67 | expect(Sails.thrift_protocol_class).to eq ::Thrift::CompactProtocolFactory 68 | allow(Sails.config).to receive(:protocol).and_return(:json) 69 | expect(Sails.thrift_protocol_class).to eq ::Thrift::JsonProtocolFactory 70 | allow(Sails.config).to receive(:protocol).and_return(:xxx) 71 | expect(Sails.thrift_protocol_class).to eq ::Thrift::BinaryProtocolFactory 72 | end 73 | end 74 | 75 | describe '#reload!' do 76 | it 'should work' do 77 | s1 = Sails.service 78 | Sails.reload! 79 | # expect(Sails.service).not_to eq s1 80 | # TODO: test reload autoload_paths 81 | end 82 | end 83 | 84 | describe '#check_create_dirs' do 85 | it 'should work' do 86 | require "fileutils" 87 | log_path = Sails.root.join("log") 88 | FileUtils.rm_r(log_path) if Dir.exist?(log_path) 89 | expect(Dir.exist?(log_path)).to eq false 90 | 91 | Sails.check_create_dirs 92 | %W(log tmp tmp/cache tmp/pids).each do |name| 93 | expect(Dir.exist?(Sails.root.join(name))).to eq true 94 | end 95 | end 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Sails 2 | ===== 3 | 4 | Sails, create [Thrift](http://thrift.apache.org) app server like Rails. 5 | 6 | [![Build Status](https://travis-ci.org/huacnlee/sails.svg)](https://travis-ci.org/huacnlee/sails) [![Gem Version](https://badge.fury.io/rb/sails.svg)](http://badge.fury.io/rb/sails) 7 | 8 | ## Features 9 | 10 | - Rails style Thrift server; 11 | - Nonblocking mode; 12 | - I18n support; 13 | 14 | ## Installation 15 | 16 | ```bash 17 | $ gem install sails 18 | $ sails -h 19 | ENV: development 20 | Commands: 21 | sails help [COMMAND] # Describe available commands or one specific command 22 | sails new APP_NAME # Create a project 23 | sails restart # Restart Thrift server 24 | sails start # Start Thrift server 25 | sails stop # Stop Thrift server 26 | sails version # Show Sails version 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Create new project 32 | 33 | ```bash 34 | $ sails new foo 35 | $ cd foo 36 | $ sails start 37 | ``` 38 | 39 | ### Generate Thrift IDL 40 | 41 | You can edit Thrift IDL in `app_name.thrift`, and then generate it to ruby source code. 42 | 43 | ``` 44 | $ rake generate 45 | ``` 46 | 47 | ## Rake tasks 48 | 49 | ```bash 50 | rake client:ping # client ping test 51 | rake db:create # Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config) 52 | rake db:drop # Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config) 53 | rake db:fixtures:load # Load fixtures into the current environment's database 54 | rake db:migrate # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog) 55 | rake db:migrate:create # Create new migration file 56 | rake db:migrate:status # Display status of migrations 57 | rake db:rollback # Rolls the schema back to the previous version (specify steps w/ STEP=n) 58 | rake db:schema:cache:clear # Clear a db/schema_cache.dump file 59 | rake db:schema:cache:dump # Create a db/schema_cache.dump file 60 | rake db:schema:dump # Create a db/schema.rb file that is portable against any DB supported by AR 61 | rake db:schema:load # Load a schema.rb file into the database 62 | rake db:seed # Load the seed data from db/seeds.rb 63 | rake db:setup # Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first) 64 | rake db:structure:dump # Dump the database structure to db/structure.sql 65 | rake db:structure:load # Recreate the databases from the structure.sql file 66 | rake db:version # Retrieves the current schema version number 67 | rake generate # Generate code from thrift IDL file 68 | ``` 69 | 70 | ## Client connect to test 71 | 72 | You can write test code in `lib/tasks/client.rake` to test your thrift methods. 73 | 74 | And then start sails server, and run rake task to test, for example: 75 | 76 | ```bash 77 | sails s --daemon 78 | rake client:ping 79 | ``` 80 | 81 | 82 | ## Deploy 83 | 84 | ``` 85 | $ sails s --daemon 86 | $ ps aux 87 | jason 2408 0.1 0.2 2648176 13532 s003 S 12:14下午 0:00.02 you_sails_app 88 | jason 2407 0.0 0.0 2604916 1016 s003 S 12:14下午 0:00.00 you_sails_app [master] 89 | $ sails restart 90 | $ sails stop 91 | ``` 92 | 93 | ## API Documents 94 | 95 | http://www.rubydoc.info/github/huacnlee/sails 96 | 97 | 98 | ## TODO 99 | 100 | - [ ] Reload without restart; 101 | - [ ] Scaffold generator; 102 | - [X] Multi processes; 103 | - [ ] Default test case templates; 104 | - [ ] Client rake task to test services; 105 | - [ ] Write more framework test cases. 106 | -------------------------------------------------------------------------------- /lib/sails/templates/config/locales/rails.zh-CN.yml: -------------------------------------------------------------------------------- 1 | zh-CN: 2 | date: 3 | abbr_day_names: 4 | - 日 5 | - 一 6 | - 二 7 | - 三 8 | - 四 9 | - 五 10 | - 六 11 | abbr_month_names: 12 | - 13 | - 1月 14 | - 2月 15 | - 3月 16 | - 4月 17 | - 5月 18 | - 6月 19 | - 7月 20 | - 8月 21 | - 9月 22 | - 10月 23 | - 11月 24 | - 12月 25 | day_names: 26 | - 星期日 27 | - 星期一 28 | - 星期二 29 | - 星期三 30 | - 星期四 31 | - 星期五 32 | - 星期六 33 | formats: 34 | default: ! '%Y-%m-%d' 35 | long: ! '%Y年%b%d日' 36 | short: ! '%b%d日' 37 | month_names: 38 | - 39 | - 一月 40 | - 二月 41 | - 三月 42 | - 四月 43 | - 五月 44 | - 六月 45 | - 七月 46 | - 八月 47 | - 九月 48 | - 十月 49 | - 十一月 50 | - 十二月 51 | order: 52 | - :year 53 | - :month 54 | - :day 55 | datetime: 56 | distance_in_words: 57 | about_x_hours: 58 | one: 大约一小时 59 | other: 大约 %{count} 小时 60 | about_x_months: 61 | one: 大约一个月 62 | other: 大约 %{count} 个月 63 | about_x_years: 64 | one: 大约一年 65 | other: 大约 %{count} 年 66 | almost_x_years: 67 | one: 接近一年 68 | other: 接近 %{count} 年 69 | half_a_minute: 半分钟 70 | less_than_x_minutes: 71 | one: 不到一分钟 72 | other: 不到 %{count} 分钟 73 | less_than_x_seconds: 74 | one: 不到一秒 75 | other: 不到 %{count} 秒 76 | over_x_years: 77 | one: 一年多 78 | other: ! '%{count} 年多' 79 | x_days: 80 | one: 一天 81 | other: ! '%{count} 天' 82 | x_minutes: 83 | one: 一分钟 84 | other: ! '%{count} 分钟' 85 | x_months: 86 | one: 一个月 87 | other: ! '%{count} 个月' 88 | x_seconds: 89 | one: 一秒 90 | other: ! '%{count} 秒' 91 | prompts: 92 | day: 日 93 | hour: 时 94 | minute: 分 95 | month: 月 96 | second: 秒 97 | year: 年 98 | errors: 99 | format: ! '%{attribute}%{message}' 100 | messages: 101 | accepted: 必须是可被接受的 102 | blank: 不能为空字符 103 | present: 必须是空白 104 | confirmation: 与确认值不匹配 105 | empty: 不能留空 106 | equal_to: 必须等于 %{count} 107 | even: 必须为双数 108 | exclusion: 是保留关键字 109 | greater_than: 必须大于 %{count} 110 | greater_than_or_equal_to: 必须大于或等于 %{count} 111 | inclusion: 不包含于列表中 112 | invalid: 是无效的 113 | less_than: 必须小于 %{count} 114 | less_than_or_equal_to: 必须小于或等于 %{count} 115 | not_a_number: 不是数字 116 | not_an_integer: 必须是整数 117 | odd: 必须为单数 118 | record_invalid: ! '验证失败: %{errors}' 119 | restrict_dependent_destroy: 120 | one: 由于 %{record} 需要此记录,所以无法移除记录 121 | many: 由于 %{record} 需要此记录,所以无法移除记录 122 | taken: 已经被使用 123 | too_long: 124 | one: 过长(最长为一个字符) 125 | other: 过长(最长为 %{count} 个字符) 126 | too_short: 127 | one: 过短(最短为一个字符) 128 | other: 过短(最短为 %{count} 个字符) 129 | wrong_length: 130 | one: 长度非法(必须为一个字符) 131 | other: 长度非法(必须为 %{count} 个字符) 132 | other_than: 长度非法(不可为 %{count} 个字符 133 | template: 134 | body: 如下字段出现错误: 135 | header: 136 | one: 有 1 个错误发生导致「%{model}」无法被保存。 137 | other: 有 %{count} 个错误发生导致「%{model}」无法被保存。 138 | helpers: 139 | select: 140 | prompt: 请选择 141 | submit: 142 | create: 新增%{model} 143 | submit: 储存%{model} 144 | update: 更新%{model} 145 | number: 146 | currency: 147 | format: 148 | delimiter: ! ',' 149 | format: ! '%u %n' 150 | precision: 2 151 | separator: . 152 | significant: false 153 | strip_insignificant_zeros: false 154 | unit: CN¥ 155 | format: 156 | delimiter: ! ',' 157 | precision: 3 158 | separator: . 159 | significant: false 160 | strip_insignificant_zeros: false 161 | human: 162 | decimal_units: 163 | format: ! '%n %u' 164 | units: 165 | billion: 十亿 166 | million: 百万 167 | quadrillion: 千兆 168 | thousand: 千 169 | trillion: 兆 170 | unit: '' 171 | format: 172 | delimiter: '' 173 | precision: 1 174 | significant: false 175 | strip_insignificant_zeros: false 176 | storage_units: 177 | format: ! '%n %u' 178 | units: 179 | byte: 180 | one: Byte 181 | other: Bytes 182 | gb: GB 183 | kb: KB 184 | mb: MB 185 | tb: TB 186 | percentage: 187 | format: 188 | delimiter: '' 189 | precision: 190 | format: 191 | delimiter: '' 192 | support: 193 | array: 194 | last_word_connector: ! ', 和 ' 195 | two_words_connector: ! ' 和 ' 196 | words_connector: ! ', ' 197 | time: 198 | am: 上午 199 | formats: 200 | default: ! '%Y年%b%d日 %A %H:%M:%S %Z' 201 | long: ! '%Y年%b%d日 %H:%M' 202 | short: ! '%b%d日 %H:%M' 203 | pm: 下午 204 | -------------------------------------------------------------------------------- /lib/sails/daemon.rb: -------------------------------------------------------------------------------- 1 | module Sails 2 | class Daemon 3 | class << self 4 | attr_accessor :options, :daemon, :mode, :app_name, :pid_file 5 | 6 | def init(opts = {}) 7 | self.app_name = Sails.config.app_name 8 | self.mode = opts[:mode] 9 | self.app_name = "#{Sails::Daemon.app_name}-thread" if self.mode == "thread" 10 | self.pid_file = Sails.root.join("tmp/#{Sails::Daemon.app_name}.pid") 11 | self.daemon = opts[:daemon] 12 | self.options = opts 13 | end 14 | 15 | def read_pid 16 | if !File.exist?(pid_file) 17 | return nil 18 | end 19 | 20 | pid = File.open(pid_file).read.to_i 21 | begin 22 | Process.getpgid(pid) 23 | rescue 24 | pid = nil 25 | end 26 | pid 27 | end 28 | 29 | def start_process 30 | old_pid = read_pid 31 | if !old_pid.nil? 32 | puts colorize("Current have #{app_name} process in running on pid #{old_pid}", :red) 33 | return 34 | end 35 | 36 | # start master process 37 | @master_pid = fork_master_process! 38 | File.open(pid_file, "w+") do |f| 39 | f.puts @master_pid 40 | end 41 | 42 | puts "Started #{app_name} on pid: #{@master_pid}" 43 | # puts "in init: #{Sails.service.object_id}" 44 | 45 | if not self.daemon 46 | Process.waitpid(@master_pid) 47 | else 48 | exit 49 | end 50 | end 51 | 52 | def restart_process(options = {}) 53 | old_pid = read_pid 54 | if old_pid == nil 55 | puts colorize("#{app_name} process not found on pid #{old_pid}", :red) 56 | return 57 | end 58 | 59 | print "Restarting #{app_name}..." 60 | Process.kill("USR2", old_pid) 61 | puts colorize(" [OK]", :green) 62 | end 63 | 64 | def fork_master_process! 65 | fork do 66 | # WARN: DO NOT CALL Sails IN THIS BLOCK! 67 | $PROGRAM_NAME = self.app_name + " [sails master]" 68 | @child_pid = fork_child_process! 69 | 70 | Signal.trap("QUIT") do 71 | Process.kill("QUIT", @child_pid) 72 | exit 73 | end 74 | 75 | Signal.trap("USR2") do 76 | Process.kill("USR2", @child_pid) 77 | end 78 | 79 | loop do 80 | sleep 1 81 | begin 82 | Process.getpgid(@child_pid) 83 | rescue Errno::ESRCH 84 | @child_pid = fork_child_process! 85 | end 86 | end 87 | end 88 | end 89 | 90 | def fork_child_process! 91 | pid = fork do 92 | $PROGRAM_NAME = self.app_name + " [sails child]" 93 | Signal.trap("QUIT") do 94 | exit 95 | end 96 | 97 | Signal.trap("USR2") do 98 | # TODO: reload Sails in current process 99 | exit 100 | end 101 | 102 | if self.daemon == true 103 | redirect_stdout 104 | else 105 | log_to_stdout 106 | end 107 | # puts "in child: #{Sails.service.object_id}" 108 | Sails.start!(self.mode) 109 | end 110 | # http://ruby-doc.org/core-1.9.3/Process.html#detach-method 111 | Process.detach(pid) 112 | pid 113 | end 114 | 115 | def stop_process 116 | pid = read_pid 117 | if pid.nil? 118 | puts colorize("#{app_name} process not found, pid #{pid}", :red) 119 | return 120 | end 121 | 122 | print "Stoping #{app_name}..." 123 | begin 124 | Process.kill("QUIT", pid) 125 | ensure 126 | File.delete(pid_file) 127 | end 128 | puts colorize(" [OK]", :green) 129 | end 130 | 131 | private 132 | # Sync Sails.logger output in file to STDOUT 133 | def log_to_stdout 134 | console = ActiveSupport::Logger.new($stdout) 135 | console.formatter = Sails.logger.formatter 136 | console.level = Sails.logger.level 137 | 138 | Sails.logger.extend(ActiveSupport::Logger.broadcast(console)) 139 | end 140 | 141 | # Redirect stdout, stderr to log file, 142 | # If we not do this, stdout will block sails daemon, for example `puts`. 143 | def redirect_stdout 144 | redirect_io($stdout, Sails.logger_path) 145 | redirect_io($stderr, Sails.logger_path) 146 | end 147 | 148 | def redirect_io(io, path) 149 | File.open(path, 'ab') { |fp| io.reopen(fp) } if path 150 | io.sync = true 151 | end 152 | 153 | def colorize(text, c) 154 | case c 155 | when :red 156 | return ["\033[31m",text,"\033[0m"].join("") 157 | when :green 158 | return ["\033[32m",text,"\033[0m"].join("") 159 | when :blue 160 | return ["\033[34m",text,"\033[0m"].join("") 161 | else 162 | return text 163 | end 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /lib/sails/base.rb: -------------------------------------------------------------------------------- 1 | Bundler.require 2 | 3 | module Sails 4 | extend ActiveSupport::Autoload 5 | 6 | autoload :Config 7 | 8 | class << self 9 | # Sails.config 10 | # 11 | # Configs with Sails 12 | # For example: 13 | # 14 | # Sails.config.app_name 15 | # # => returns "You App Name" 16 | # 17 | # Sails.config.autoload_paths 18 | # # => returns ['app/models','app/models/concerns', 'app/workers', 'app/services'...] 19 | # 20 | # Sails.config.cache_store = [:dalli_store, '127.0.0.1', { pool_size: 100 }] 21 | # Sails.config.i18n.default_locale = 'zh-CN' 22 | # 23 | def config 24 | return @config if defined?(@config) 25 | @config = Config.new.config 26 | end 27 | 28 | # Sails.cache 29 | # 30 | # An abstract cache store class. There are multiple cache store 31 | # implementations, each having its own additional features. See the classes 32 | # under the ActiveSupport::Cache module, e.g. 33 | # ActiveSupport::Cache::MemCacheStore. MemCacheStore is currently the most 34 | # popular cache store for large production websites. 35 | # 36 | # Some implementations may not support all methods beyond the basic cache 37 | # methods of +fetch+, +write+, +read+, +exist?+, and +delete+. 38 | # 39 | # ActiveSupport::Cache::Store can store any serializable Ruby object. 40 | # 41 | # Sails.cache.read('city') # => nil 42 | # Sails.cache.write('city', "Duckburgh") 43 | # Sails.cache.read('city') # => "Duckburgh" 44 | # 45 | # Keys are always translated into Strings and are case sensitive. When an 46 | # object is specified as a key and has a +cache_key+ method defined, this 47 | # method will be called to define the key. Otherwise, the +to_param+ 48 | # method will be called. Hashes and Arrays can also be used as keys. The 49 | # elements will be delimited by slashes, and the elements within a Hash 50 | # will be sorted by key so they are consistent. 51 | # 52 | # Sails.cache.read('city') == Sails.cache.read(:city) # => true 53 | # 54 | # Nil values can be cached. 55 | def cache 56 | return @cache if defined?(@cache) 57 | @cache = ActiveSupport::Cache.lookup_store(self.config.cache_store) 58 | end 59 | 60 | # Sails.root 61 | # 62 | # This method returns a Pathname object which handles paths starting with a / as absolute (starting from the root of the filesystem). Compare: 63 | # 64 | # For example: 65 | # >> Sails.root 66 | # => # 67 | # >> Sails.root + "file" 68 | # => # 69 | # 70 | def root 71 | return @root if defined?(@root) 72 | path = `pwd -L`.sub(/\n/,'') rescue Dir.pwd 73 | @root ||= Pathname.new(path) 74 | end 75 | 76 | def root=(root) 77 | @root = Pathname.new(root) 78 | end 79 | 80 | # Sails.env 81 | # 82 | # returns a string representing the current Sails environment. 83 | # This will read from ENV['RAILS_ENV'] like Rails 84 | # 85 | # For example: 86 | # 87 | # Sails.env # in development mode 88 | # => "development" 89 | # Sails.env.development? 90 | # => true 91 | # 92 | def env 93 | @env ||= ActiveSupport::StringInquirer.new(ENV['RAILS_ENV'].presence || 'development') 94 | end 95 | 96 | # Sails.logger 97 | # 98 | # returns a Logger class 99 | # For example: 100 | # 101 | # Sails.logger.info "Hello world" 102 | # Sails.logger.error "Hello world" 103 | # 104 | def logger 105 | return @logger if defined?(@logger) 106 | @logger = Logger.new(logger_path) 107 | @logger.formatter = proc { |severity, datetime, progname, msg| 108 | "#{msg}\n" 109 | } 110 | @logger 111 | end 112 | 113 | def logger_path 114 | @logger_path ||= Sails.root.join("log/#{self.env}.log") 115 | end 116 | 117 | def init 118 | # init root 119 | return false if @inited == true 120 | $:.unshift self.root.join("lib") 121 | 122 | self.root 123 | self.check_create_dirs 124 | 125 | ActiveSupport::Dependencies.autoload_paths += Sails.config.autoload_paths 126 | 127 | env_file = self.root.join('config/environments/',Sails.env + ".rb") 128 | if File.exist?(env_file) 129 | require env_file 130 | end 131 | 132 | require "sails/service" 133 | 134 | self.load_initialize 135 | @inited = true 136 | end 137 | 138 | # Sails.service 139 | # 140 | # return a instance of Sails Service layer 141 | # 142 | # for example: 143 | # 144 | # class UsersService < Sails::Service::Base 145 | # def check_name_exist?(name) 146 | # User.check_name_exist?(name) 147 | # end 148 | # end 149 | # 150 | # class UsersServiceTest 151 | # def test_check_name_exist? 152 | # assert_equal(Sails.service.check_name_exist?(name), true) 153 | # end 154 | # end 155 | # 156 | def service 157 | @service ||= Sails::Service::Interface.new 158 | end 159 | 160 | # Force reload Sails cache classes in config.autoload_paths 161 | def reload!(opts = {}) 162 | force = opts[:force] || false 163 | if force || config.cache_classes == false 164 | # @service = nil 165 | ActiveSupport::Dependencies.clear 166 | # reload_server! 167 | end 168 | true 169 | end 170 | 171 | def reload_server! 172 | if @server 173 | new_processor = config.processor.new(self.service) 174 | @server.instance_variable_set(:@processor, new_processor) 175 | end 176 | end 177 | 178 | def start!(type) 179 | logger.info "ENV: #{Sails.env}" 180 | 181 | @server_type = type 182 | if @server_type == "thread" 183 | start_thread_pool_server! 184 | else 185 | start_non_blocking_server! 186 | end 187 | end 188 | 189 | def thrift_protocol_class 190 | case config.protocol 191 | when :compact 192 | return ::Thrift::CompactProtocolFactory 193 | when :json 194 | return ::Thrift::JsonProtocolFactory 195 | else 196 | return ::Thrift::BinaryProtocolFactory 197 | end 198 | end 199 | 200 | # Start Thrift Server with Threaded mode 201 | # 202 | def start_thread_pool_server! 203 | transport = ::Thrift::ServerSocket.new(nil, config.thread_port) 204 | transport_factory = ::Thrift::BufferedTransportFactory.new 205 | protocol_factory = thrift_protocol_class.new 206 | processor = config.processor.new(self.service) 207 | @server = ::Thrift::ThreadPoolServer.new(processor, transport, transport_factory, protocol_factory, config.thread_size) 208 | 209 | logger.info "Boot on: #{Sails.root}" 210 | logger.info "[#{Time.now}] Starting the Sails with ThreadPool size: #{Setting.pool_size}..." 211 | logger.info "serve: 127.0.0.1:#{config.thread_port}" 212 | 213 | begin 214 | @server.serve 215 | rescue => e 216 | puts "Start thrift server exception! \n #{e.inspect}" 217 | puts e.backtrace 218 | end 219 | end 220 | 221 | # Start Thrift Server with Event drive mode 222 | def start_non_blocking_server! 223 | transport = ::Thrift::ServerSocket.new(nil, config.port) 224 | transport_factory = ::Thrift::FramedTransportFactory.new 225 | protocol_factory = thrift_protocol_class.new 226 | processor = config.processor.new(self.service) 227 | @server = ::Thrift::NonblockingServer.new(processor, transport, transport_factory, protocol_factory, config.thread_size) 228 | 229 | logger.info "Boot on: #{Sails.root}" 230 | logger.info "[#{Time.now}] Starting the Sails with NonBlocking..." 231 | logger.info "Protocol: #{thrift_protocol_class.name}" 232 | logger.info "serve: 127.0.0.1:#{config.port}" 233 | 234 | begin 235 | @server.serve 236 | rescue => e 237 | puts "Start thrift server exception! \n #{e.inspect}" 238 | puts e.backtrace 239 | end 240 | end 241 | 242 | def load_initialize 243 | Dir["#{Sails.root}/config/initializers/*.rb"].each do |f| 244 | require f 245 | end 246 | end 247 | 248 | def check_create_dirs 249 | %w(log tmp tmp/cache tmp/pids).each do |name| 250 | if not Dir.exist? Sails.root.join(name) 251 | require "fileutils" 252 | dir_path = Sails.root.join(name) 253 | FileUtils.mkdir_p dir_path 254 | FileUtils.touch [dir_path,".keep"].join("/") 255 | end 256 | end 257 | end 258 | end 259 | end 260 | --------------------------------------------------------------------------------