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$ </strong><div class='edit' contenteditable="true" ></div>
- </div>
- <div class="console-line">
- <strong>console$ </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