lib/cpee/implementation.rb in cpee-1.5.27 vs lib/cpee/implementation.rb in cpee-2.0

- old
+ new

@@ -11,288 +11,249 @@ # You should have received a copy of the GNU General Public License along with # CPEE (file COPYING in the main directory). If not, see # <http://www.gnu.org/licenses/>. require 'fileutils' +require 'redis' require 'riddl/server' require 'riddl/client' -require 'riddl/utils/notifications_producer' -require 'riddl/utils/properties' -require_relative 'controller' +require_relative 'message' +require_relative 'persistence' +require_relative 'statemachine' +require_relative 'implementation_properties' +require_relative 'implementation_notifications' +require_relative 'implementation_callbacks' -require 'ostruct' -class ParaStruct < OpenStruct - def to_json(*a) - table.to_json - end -end -def →(a); ParaStruct.new(a); end -def ⭐(a); ParaStruct.new(a); end - module CPEE SERVER = File.expand_path(File.join(__dir__,'..','cpee.xml')) - + PROPERTIES_PATHS_FULL = %w{ + /p:properties/p:handlerwrapper + /p:properties/p:positions/p:* + /p:properties/p:positions/p:*/@* + /p:properties/p:dataelements/p:* + /p:properties/p:endpoints/p:* + /p:properties/p:attributes/p:* + /p:properties/p:transformation/p:* + /p:properties/p:transformation/p:*/@* + /p:properties/p:description + /p:properties/p:dslx + /p:properties/p:dsl + /p:properties/p:status/p:id + /p:properties/p:status/p:message + /p:properties/p:state/@changed + /p:properties/p:state + } + PROPERTIES_PATHS_INDEX_UNORDERED = %w{ + /p:properties/p:positions/p:* + } + PROPERTIES_PATHS_INDEX_ORDERED = %w{ + /p:properties/p:dataelements/p:* + /p:properties/p:endpoints/p:* + /p:properties/p:attributes/p:* + } def self::implementation(opts) opts[:instances] ||= File.expand_path(File.join(__dir__,'..','..','server','instances')) opts[:global_handlerwrappers] ||= File.expand_path(File.join(__dir__,'..','..','server','handlerwrappers')) opts[:handlerwrappers] ||= '' opts[:topics] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','topics.xml')) opts[:properties_init] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','properties.init')) - opts[:properties_schema_active] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','properties.schema.active')) - opts[:properties_schema_finished] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','properties.schema.finished')) - opts[:properties_schema_inactive] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','properties.schema.inactive')) + opts[:properties_empty] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','properties.empty')) opts[:transformation_dslx] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','transformation_dslx.xsl')) opts[:transformation_service] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','transformation.xml')) opts[:empty_dslx] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','empty_dslx.xml')) opts[:notifications_init] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','notifications')) + opts[:states] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','states.xml')) + opts[:backend_run] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','backend','run')) + opts[:backend_template] ||= File.expand_path(File.join(__dir__,'..','..','server','resources','backend','instance.template')) + opts[:backend_opts] ||= 'opts.yaml' + opts[:watchdog_frequency] ||= 7 + opts[:watchdog_start_off] ||= false + opts[:backend_instance] ||= 'instance.rb' opts[:infinite_loop_stop] ||= 10000 + opts[:redis_path] ||= '/tmp/redis.sock' + opts[:redis_db] ||= 3 + opts[:redis] = Redis.new(path: opts[:redis_path], db: opts[:redis_db]) + opts[:statemachine] = CPEE::StateMachine.new opts[:states], %w{running simulating replaying finishing stopping abandoned finished} do |id| + opts[:redis].get("instance:#{id}/state") + end + opts[:runtime_cmds] << [ "startclean", "Delete instances before starting.", Proc.new { |status| Dir.glob(File.expand_path(File.join(opts[:instances],'*'))).each do |d| FileUtils.rm_r(d) if File.basename(d) =~ /^\d+$/ end } ] Proc.new do - Dir[opts[:global_handlerwrappers] + "/*.rb"].each do |h| - require h - end unless opts[:global_handlerwrappers].strip == '' - Dir[opts[:handlerwrappers] + "/*.rb"].each do |h| - require h - end unless opts[:handlerwrappers].strip == '' + parallel do + CPEE::watch_services(@riddl_opts[:watchdog_start_off]) + EM.add_periodic_timer(@riddl_opts[:watchdog_frequency]) do + CPEE::watch_services(@riddl_opts[:watchdog_start_off]) + end + end + cleanup do + CPEE::cleanup_services(@riddl_opts[:watchdog_start_off]) + end - controller = {} - Dir[File.join(opts[:instances],'*','properties.xml')].each do |e| - id = ::File::basename(::File::dirname(e)) - (controller[id.to_i] = (Controller.new(id,opts)) rescue nil) + interface 'main' do + run CPEE::Instances, opts if get '*' + run CPEE::NewInstance, opts if post 'instance-new' + on resource '\d+' do |r| + run CPEE::Info, opts if get + run CPEE::DeleteInstance, opts if delete + end end interface 'properties' do |r| id = r[:h]['RIDDL_DECLARATION_PATH'].split('/')[1].to_i - use Riddl::Utils::Properties::implementation(controller[id].properties, PropertiesHandler.new(controller[id]), opts[:mode]) if controller[id] + use CPEE::Properties::implementation(id.to_i, opts) end - interface 'main' do - run CPEE::Instances, controller if get '*' - run CPEE::NewInstance, controller, opts if post 'instance-new' - run CPEE::NewXMLInstance, controller, opts if post 'instance-new-xml' - on resource do |r| - run CPEE::Info, controller if get - run CPEE::DeleteInstance, controller, opts if delete - on resource 'console' do - run CPEE::ConsoleUI, controller if get - run CPEE::Console, controller if get 'cmdin' - end - on resource 'callbacks' do - run CPEE::Callbacks, controller, opts if get - on resource do - run CPEE::ExCallback, controller if get || put || post || delete - end - end - end + interface 'notifications' do |r| + id = r[:h]['RIDDL_DECLARATION_PATH'].split('/')[1].to_i + use CPEE::Notifications::implementation(id.to_i, opts) end - interface 'notifications' do |r| + interface 'callbacks' do |r| id = r[:h]['RIDDL_DECLARATION_PATH'].split('/')[1].to_i - use Riddl::Utils::Notifications::Producer::implementation(controller[id].notifications, NotificationsHandler.new(controller[id]), opts[:mode]) if controller[id] + use CPEE::Callbacks::implementation(id.to_i, opts) end end end - class ExCallback < Riddl::Implementation #{{{ - def response - controller = @a[0] - id = @r[0].to_i - callback = @r[2] - controller[id].mutex.synchronize do - if controller[id].callbacks.has_key?(callback) - controller[id].callbacks[callback].callback(@p,@h) - else - @status = 503 - end - end + def self::watch_services(watchdog_start_off) + return if watchdog_start_off + EM.defer do + Dir[File.join(__dir__,'..','..','server','routing','*.rb')].each do |s| + s = s.sub(/\.rb$/,'') + pid = (File.read(s + '.pid').to_i rescue nil) + if (pid.nil? || !(Process.kill(0, pid) rescue false)) && !File.exist?(s + '.lock') + system "#{s}.rb restart 1>/dev/null 2>&1" + puts "➡ Service #{File.basename(s,'.rb')} started ..." + end + end end - end #}}} - - class Callbacks < Riddl::Implementation #{{{ - def response - controller = @a[0] - opts = @a[1] - id = @r[0].to_i - unless controller[id] - @status = 400 - return + end + def self::cleanup_services(watchdog_start_off) + return if watchdog_start_off + Dir[File.join(__dir__,'..','..','server','routing','*.rb')].each do |s| + s = s.sub(/\.rb$/,'') + pid = (File.read(s + '.pid').to_i rescue nil) + if !pid.nil? || (Process.kill(0, pid) rescue false) + system "#{s}.rb stop 1>/dev/null 2>&1" + puts "➡ Service #{File.basename(s,'.rb')} stopped ..." end - Riddl::Parameter::Complex.new("info","text/xml") do - cb = XML::Smart::string("<callbacks details='#{opts[:mode]}'/>") - if opts[:mode] == :debug - controller[id].callbacks.each do |k,v| - cb.root.add("callback",{"id" => k},"[#{v.protocol.to_s}] #{v.info}") - end - end - cb.to_s - end end - end #}}} + end class Instances < Riddl::Implementation #{{{ def response - controller = @a[0] + redis = @a[0][:redis] Riddl::Parameter::Complex.new("wis","text/xml") do ins = XML::Smart::string('<instances/>') - controller.sort{|a,b| b[0] <=> a[0] }.each do |k,v| - ins.root.add('instance', v.info, 'uuid' => v.uuid, 'id' => k, 'state' => v.state, 'state_changed' => v.state_changed ) unless v.nil? + redis.zrevrange('instances',0,-1).each do |instance| + statekey = "instance:#{instance}/state" + attributes = "instance:#{instance}/attributes/" + info = redis.get(attributes + 'info') + uuid = redis.get(attributes + 'uuid') + state = redis.get(statekey) + state_changed = redis.get(File.join(statekey,'@changed')) + ins.root.add('instance', info, 'uuid' => uuid, 'id' => instance, 'state' => state, 'state_changed' => state_changed ) end ins.to_s end end end #}}} class NewInstance < Riddl::Implementation #{{{ - def response - controller = @a[0] - opts = @a[1] - name = @p[0].value - id = controller.keys.sort.last.to_i - - while true - id += 1 - unless Dir.exists? opts[:instances] + "/#{id}" - Dir.mkdir(opts[:instances] + "/#{id}") rescue nil - break - end + def path(e) + ret = [] + until e.qname.name == 'properties' + ret << (e.class == XML::Smart::Dom::Attribute ? '@' : '') + e.qname.name + e = e.parent end - - controller[id] = Controller.new(id,opts) - controller[id].info = name - controller[id].state_change! - - @headers << Riddl::Header.new("CPEE-INSTANCE", controller[id].instance) - @headers << Riddl::Header.new("CPEE-INSTANCE-URL", controller[id].instance_url) - @headers << Riddl::Header.new("CPEE-INSTANCE-UUID", controller[id].uuid) - - Riddl::Parameter::Simple.new("id", id) + File.join(*ret.reverse) end - end #}}} - class NewXMLInstance < Riddl::Implementation #{{{ - def response - controller = @a[0] - opts = @a[1] - xml = @p[0].value.read - id = controller.keys.sort.last.to_i - - while true - id += 1 - unless Dir.exists? opts[:instances] + "/#{id}" - Dir.mkdir(opts[:instances] + "/#{id}") rescue nil - break + def response + opts = @a[0] + redis = opts[:redis] + doc = XML::Smart::open_unprotected(opts[:properties_init]) + doc.register_namespace 'p', 'http://cpee.org/ns/properties/2.0' + name = @p[0].value + id = redis.zcount('instances','-inf','+inf').to_i + 1 + uuid = SecureRandom.uuid + instance = 'instance:' + id.to_s + redis.multi do |multi| + multi.zadd('instances',id,id) + doc.root.find(PROPERTIES_PATHS_FULL.join(' | ')).each do |e| + if e.class == XML::Smart::Dom::Element && e.element_only? + val = e.find('*').map { |f| f.dump }.join + multi.set(File.join(instance, path(e)), val) + else + multi.set(File.join(instance, path(e)), e.text) + end end + doc.root.find(PROPERTIES_PATHS_INDEX_UNORDERED.join(' | ')).each do |e| + p = path(e) + multi.sadd(File.join(instance, File.dirname(p)), File.basename(p)) + end + doc.root.find(PROPERTIES_PATHS_INDEX_ORDERED.join(' | ')).each_with_index do |e,i| + p = path(e) + multi.zadd(File.join(instance, File.dirname(p)), i, File.basename(p)) + end + multi.set(File.join(instance, 'attributes', 'uuid'), SecureRandom.uuid) + multi.zadd(File.join(instance, 'attributes'), -2, 'uuid') + multi.set(File.join(instance, 'attributes', 'info'), name) + multi.zadd(File.join(instance, 'attributes'), -1, 'info') + multi.set(File.join(instance, 'state', '@changed'), Time.now.xmlschema(3)) end - File.write(File.join(opts[:instances].to_s,id.to_s,'properties.xml'),xml) - controller[id] = Controller.new(id,opts) - controller[id].state_change! + @headers << Riddl::Header.new("CPEE-INSTANCE", id.to_s) + @headers << Riddl::Header.new("CPEE-INSTANCE-URL", File.join(opts[:url].to_s,id.to_s)) + @headers << Riddl::Header.new("CPEE-INSTANCE-UUID", uuid) - @headers << Riddl::Header.new("CPEE-INSTANCE", controller[id].instance) - @headers << Riddl::Header.new("CPEE-INSTANCE-URL", controller[id].instance_url) - @headers << Riddl::Header.new("CPEE-INSTANCE-UUID", controller[id].uuid) - - Riddl::Parameter::Simple.new("id", id) + Riddl::Parameter::Simple.new("id", id.to_s) end end #}}} class Info < Riddl::Implementation #{{{ def response - controller = @a[0] + opts = @a[0] id = @r[0].to_i - unless controller[id] - @status = 400 + unless opts[:redis].exists?("instance:#{id}/state") + @status = 404 return end Riddl::Parameter::Complex.new("info","text/xml") do i = XML::Smart::string <<-END - <info instance='#{@r[0]}'> + <info instance='#{id}'> <notifications/> <properties/> <callbacks/> </info> END i.to_s end end end #}}} - class ConsoleUI < Riddl::Implementation #{{{ - def response - controller = @a[0] - id = @r[0].to_i - unless controller[id] - @status = 400 - return - end - Riddl::Parameter::Complex.new("res","text/html") do - <<-END - <!DOCTYPE html> - <html> - <head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> - <title>Instance Web Console</title> - <style type="text/css"> - [contenteditable] { display: inline; } - [contenteditable]:focus { outline: 0px solid transparent; } - body{ font-family: Courier,Courier New,Monospace} - </style> - <script type="text/javascript" src="//#{controller[id].host}/js_libs/jquery.min.js"></script> - <script type="text/javascript" src="//#{controller[id].host}/js_libs/ansi_up.js"></script> - <script type="text/javascript" src="//#{controller[id].host}/js_libs/console.js"></script> - </head> - <body> - <p>Instance Web Console. Type "help" to get started.</p> - <div class="console-line" id="console-template" style="display: none"> - <strong>console$&nbsp;</strong><div class='edit' contenteditable="true" ></div> - </div> - <div class="console-line"> - <strong>console$&nbsp;</strong><div class='edit' contenteditable="true"></div> - </div> - </body> - </html> - END - end - end - end #}}} - class Console < Riddl::Implementation #{{{ - def response - controller = @a[0] - id = @r[0].to_i - unless controller[id] - @status = 400 - return - end - Riddl::Parameter::Complex.new("res","text/plain") do - begin - controller[id].console(@p[0].value) - rescue => e - e.message - end - end - end - end #}}} - class DeleteInstance < Riddl::Implementation #{{{ def response - controller = @a[0] - opts = @a[1] + opts = @a[0] + redis = opts[:redis] id = @r[0].to_i - unless controller[id] - @status = 400 + unless redis.exists("instance:#{id}/state") + @status = 404 return end - controller.delete(id) - FileUtils.rm_r(opts[:instances] + "/#{@r[0]}") + redis.multi do |multi| + multi.del redis.keys("instance:#{id}/*").to_a + multi.zrem 'instances', id + end end end #}}} end