nodectl 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +24 -0
- data/Gemfile +4 -0
- data/README.md +3 -0
- data/Rakefile +1 -0
- data/TODO.md +7 -0
- data/assets/javascript/application.coffee +22 -0
- data/assets/javascript/bootstrap.js +2006 -0
- data/assets/javascript/controllers/application_controller.coffee +73 -0
- data/assets/javascript/controllers/service_controller.coffee +62 -0
- data/assets/javascript/event_handler.coffee +51 -0
- data/assets/javascript/models/action.coffee +28 -0
- data/assets/javascript/models/instance.coffee +7 -0
- data/assets/javascript/models/service.coffee +75 -0
- data/assets/javascript/models/version.coffee +3 -0
- data/assets/javascript/notifier.coffee +64 -0
- data/assets/javascript/router.coffee +3 -0
- data/assets/javascript/routes/application.coffee +6 -0
- data/assets/javascript/routes/log.coffee +4 -0
- data/assets/javascript/routes/service.coffee +3 -0
- data/assets/javascript/store.coffee +32 -0
- data/assets/javascript/templates/application.hbs +82 -0
- data/assets/javascript/templates/index.hbs +1 -0
- data/assets/javascript/templates/log.hbs +1 -0
- data/assets/javascript/templates/service.hbs +110 -0
- data/assets/javascript/templates/views/action.hbs +20 -0
- data/assets/javascript/templates/views/actions_list.hbs +26 -0
- data/assets/javascript/templates/views/environments_list.hbs +9 -0
- data/assets/javascript/templates/views/install_button.hbs +1 -0
- data/assets/javascript/templates/views/mass_run.hbs +30 -0
- data/assets/javascript/templates/views/notifier_control.hbs +7 -0
- data/assets/javascript/templates/views/param.hbs +6 -0
- data/assets/javascript/templates/views/terminal.hbs +8 -0
- data/assets/javascript/ui.coffee +24 -0
- data/assets/javascript/vendor/ansiparse.js +186 -0
- data/assets/javascript/vendor/audio.min.js +24 -0
- data/assets/javascript/vendor/audiojs.swf +0 -0
- data/assets/javascript/vendor/ember-data.js +10528 -0
- data/assets/javascript/vendor/ember.js +40583 -0
- data/assets/javascript/vendor/handlebars.js +2746 -0
- data/assets/javascript/vendor/jquery.js +8829 -0
- data/assets/javascript/vendor/ladda.js +157 -0
- data/assets/javascript/vendor/moment.min.js +6 -0
- data/assets/javascript/vendor/notify-osd.js +319 -0
- data/assets/javascript/vendor/player-graphics.gif +0 -0
- data/assets/javascript/vendor/spin.js +218 -0
- data/assets/javascript/views/action_view.coffee +18 -0
- data/assets/javascript/views/actions_list_view.coffee +2 -0
- data/assets/javascript/views/environments_list_view.coffee +2 -0
- data/assets/javascript/views/install_button_view.coffee +34 -0
- data/assets/javascript/views/mass_run_view.coffee +107 -0
- data/assets/javascript/views/notifier_control_view.coffee +29 -0
- data/assets/javascript/views/terminal_view.coffee +56 -0
- data/assets/stylesheets/application.css +137 -0
- data/assets/stylesheets/bootstrap-theme.css +347 -0
- data/assets/stylesheets/bootstrap.css +5785 -0
- data/assets/stylesheets/fonts/glyphicons-halflings-regular.eot +0 -0
- data/assets/stylesheets/fonts/glyphicons-halflings-regular.svg +229 -0
- data/assets/stylesheets/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/assets/stylesheets/fonts/glyphicons-halflings-regular.woff +0 -0
- data/assets/stylesheets/ladda-themeless.css +330 -0
- data/assets/stylesheets/ladda.css +392 -0
- data/assets/stylesheets/notify-osd.css +49 -0
- data/assets/stylesheets/terminal.css +12 -0
- data/bin/nodectl +4 -0
- data/lib/nodectl.rb +137 -0
- data/lib/nodectl/action.rb +80 -0
- data/lib/nodectl/binding.rb +13 -0
- data/lib/nodectl/cli.rb +123 -0
- data/lib/nodectl/context.rb +34 -0
- data/lib/nodectl/database.rb +55 -0
- data/lib/nodectl/generators/init.rb +22 -0
- data/lib/nodectl/generators/templates/init/.gitignore +5 -0
- data/lib/nodectl/generators/templates/init/config/manifest.yml +1 -0
- data/lib/nodectl/generators/templates/init/config/server.yml +7 -0
- data/lib/nodectl/instance.rb +100 -0
- data/lib/nodectl/log.rb +13 -0
- data/lib/nodectl/manager.rb +71 -0
- data/lib/nodectl/multi_io.rb +16 -0
- data/lib/nodectl/options.rb +47 -0
- data/lib/nodectl/process.rb +145 -0
- data/lib/nodectl/promised_file.rb +37 -0
- data/lib/nodectl/recipe.rb +197 -0
- data/lib/nodectl/repository.rb +80 -0
- data/lib/nodectl/server.rb +103 -0
- data/lib/nodectl/service.rb +126 -0
- data/lib/nodectl/stream/buffer.rb +44 -0
- data/lib/nodectl/stream/events_session.rb +39 -0
- data/lib/nodectl/stream/file.rb +69 -0
- data/lib/nodectl/stream/file_session.rb +35 -0
- data/lib/nodectl/stream/file_with_memory.rb +17 -0
- data/lib/nodectl/stream/websocket.rb +90 -0
- data/lib/nodectl/version.rb +3 -0
- data/lib/nodectl/watchdog.rb +17 -0
- data/lib/nodectl/webapp/actions.rb +21 -0
- data/lib/nodectl/webapp/base.rb +29 -0
- data/lib/nodectl/webapp/instances.rb +46 -0
- data/lib/nodectl/webapp/public/sounds/alert.mp3 +0 -0
- data/lib/nodectl/webapp/public/sounds/error.mp3 +0 -0
- data/lib/nodectl/webapp/public/sounds/question.mp3 +0 -0
- data/lib/nodectl/webapp/services.rb +39 -0
- data/lib/nodectl/webapp/settings.rb +7 -0
- data/lib/nodectl/webapp/ui.rb +19 -0
- data/lib/nodectl/webapp/versions.rb +12 -0
- data/lib/nodectl/webapp/views/ui.haml +20 -0
- data/nodectl.gemspec +35 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/stream/buffer_spec.rb +33 -0
- metadata +365 -0
data/bin/nodectl
ADDED
data/lib/nodectl.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
require "nodectl/version"
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "thin"
|
5
|
+
require "eventmachine"
|
6
|
+
require "em-websocket"
|
7
|
+
require 'sinatra/base'
|
8
|
+
require 'sinatra/json'
|
9
|
+
require "logger"
|
10
|
+
require 'stringio'
|
11
|
+
require "yaml"
|
12
|
+
require "time"
|
13
|
+
require "dbm"
|
14
|
+
require 'multi_json'
|
15
|
+
|
16
|
+
module Nodectl
|
17
|
+
autoload :Action, "nodectl/action.rb"
|
18
|
+
autoload :Binding, "nodectl/binding.rb"
|
19
|
+
autoload :CLI, "nodectl/cli.rb"
|
20
|
+
autoload :Context, "nodectl/context.rb"
|
21
|
+
autoload :Database, "nodectl/database.rb"
|
22
|
+
autoload :Log, "nodectl/log.rb"
|
23
|
+
autoload :Manager, "nodectl/manager.rb"
|
24
|
+
autoload :MultiIO, "nodectl/multi_io.rb"
|
25
|
+
autoload :Instance, "nodectl/instance.rb"
|
26
|
+
autoload :Options, "nodectl/options.rb"
|
27
|
+
autoload :PromisedFile, "nodectl/promised_file.rb"
|
28
|
+
autoload :Repository, "nodectl/repository.rb"
|
29
|
+
autoload :Recipe, "nodectl/recipe.rb"
|
30
|
+
autoload :Process, "nodectl/process.rb"
|
31
|
+
autoload :Server, "nodectl/server.rb"
|
32
|
+
autoload :Service, "nodectl/service.rb"
|
33
|
+
autoload :Watchdog, "nodectl/watchdog.rb"
|
34
|
+
|
35
|
+
module Generators
|
36
|
+
autoload :Init, "nodectl/generators/init.rb"
|
37
|
+
end
|
38
|
+
|
39
|
+
module Webapp
|
40
|
+
autoload :Actions, "nodectl/webapp/actions.rb"
|
41
|
+
autoload :Base, "nodectl/webapp/base.rb"
|
42
|
+
autoload :Instances, "nodectl/webapp/instances.rb"
|
43
|
+
autoload :Services, "nodectl/webapp/services.rb"
|
44
|
+
autoload :Settings, "nodectl/webapp/settings.rb"
|
45
|
+
autoload :UI, "nodectl/webapp/ui.rb"
|
46
|
+
autoload :Versions, "nodectl/webapp/versions.rb"
|
47
|
+
end
|
48
|
+
|
49
|
+
module Stream
|
50
|
+
autoload :Buffer, "nodectl/stream/buffer.rb"
|
51
|
+
autoload :EventsSession, "nodectl/stream/events_session.rb"
|
52
|
+
autoload :File, "nodectl/stream/file.rb"
|
53
|
+
autoload :FileSession, "nodectl/stream/file_session.rb"
|
54
|
+
autoload :FileWithMemory, "nodectl/stream/file_with_memory.rb"
|
55
|
+
autoload :Websocket, "nodectl/stream/websocket.rb"
|
56
|
+
end
|
57
|
+
|
58
|
+
NotFound = Class.new(Sinatra::NotFound)
|
59
|
+
|
60
|
+
class << self
|
61
|
+
attr_reader :options
|
62
|
+
|
63
|
+
# Node initialization: here default global options could be overriden
|
64
|
+
#
|
65
|
+
# @param options [Hash] overrides for default options
|
66
|
+
def boot(options = {})
|
67
|
+
@options = Options.new(options)
|
68
|
+
|
69
|
+
# Routing targets
|
70
|
+
Dir["#{@options[:recipe_dir]}/**/*.rb"].each do |path|
|
71
|
+
require path
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Key-value store for node
|
76
|
+
#
|
77
|
+
# @return [Nodectl::Database]
|
78
|
+
def database
|
79
|
+
@database ||= Nodectl::Database.new(options[:database])
|
80
|
+
end
|
81
|
+
|
82
|
+
def server
|
83
|
+
@server ||= Nodectl::Server.new(options)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Get node manifest: list of hashes with service definitions. It looks like:
|
87
|
+
#
|
88
|
+
# [
|
89
|
+
# {
|
90
|
+
# "name" => "hub",
|
91
|
+
# "repo" => "git://example.com:service.git",
|
92
|
+
# "path" => "hub"
|
93
|
+
# },
|
94
|
+
# ...
|
95
|
+
# ]
|
96
|
+
#
|
97
|
+
# @return [Array] node manifest
|
98
|
+
def manifest
|
99
|
+
@manifest ||= YAML.load_file(@options[:manifest]) || []
|
100
|
+
rescue SystemCallError => e
|
101
|
+
$stderr.puts "Manifest loading error: #{e.message}"
|
102
|
+
abort
|
103
|
+
end
|
104
|
+
|
105
|
+
# Get logger instance
|
106
|
+
#
|
107
|
+
# @return [Logger] logger instance
|
108
|
+
def logger
|
109
|
+
unless defined? @logger
|
110
|
+
file = File.open(options[:logger_dir].join("nodectl.log"), "a")
|
111
|
+
file.sync = true
|
112
|
+
io = MultiIO.new(STDOUT, file)
|
113
|
+
|
114
|
+
@logger ||= Logger.new(io)
|
115
|
+
@logger.level = Logger.const_get(options[:log_level])
|
116
|
+
end
|
117
|
+
|
118
|
+
@logger
|
119
|
+
end
|
120
|
+
|
121
|
+
# Redirect all standard streams to null device
|
122
|
+
def shut_up!
|
123
|
+
$stdout.reopen(File.open(File::NULL, "w"))
|
124
|
+
$stderr.reopen(File.open(File::NULL, "w"))
|
125
|
+
$stdin.reopen(File.open(File::NULL, "r"))
|
126
|
+
end
|
127
|
+
|
128
|
+
# Base path of assets folder
|
129
|
+
#
|
130
|
+
# @return [String]
|
131
|
+
def assets_path
|
132
|
+
File.absolute_path(File.join(__FILE__, "../../assets"))
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
include Nodectl::Recipe::ObjectMethods
|
@@ -0,0 +1,80 @@
|
|
1
|
+
class Nodectl::Action < Nodectl::Process
|
2
|
+
@manager = Nodectl::Manager.new(self)
|
3
|
+
|
4
|
+
attr_reader :id
|
5
|
+
attr_reader :service
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :output
|
8
|
+
|
9
|
+
def initialize(service, name, options = {}, &blk)
|
10
|
+
super(options, &blk)
|
11
|
+
|
12
|
+
@id = Nodectl::Action.next_id
|
13
|
+
@service = service
|
14
|
+
@name = name
|
15
|
+
|
16
|
+
onkill do
|
17
|
+
result = if @exit_status == 0
|
18
|
+
"succeed"
|
19
|
+
else
|
20
|
+
"failed"
|
21
|
+
end
|
22
|
+
|
23
|
+
self.class.onkill_call(self, reason: result)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def run
|
28
|
+
rout, @wout = IO.pipe
|
29
|
+
|
30
|
+
@output = Nodectl::Stream::FileWithMemory.new(rout)
|
31
|
+
|
32
|
+
super
|
33
|
+
|
34
|
+
@started_at = Time.now
|
35
|
+
|
36
|
+
Nodectl::Action[@id] = self
|
37
|
+
Nodectl.logger.info("action: [#{pid}] started: #{service.name}##{name}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def as_json
|
41
|
+
{
|
42
|
+
"id" => @id,
|
43
|
+
"pid" => @pid,
|
44
|
+
"name" => @name,
|
45
|
+
"service_id" => @service.name,
|
46
|
+
"ws" => "/actions/#{id}",
|
47
|
+
"exit_status" => @exit_status,
|
48
|
+
"started_at" => @started_at.iso8601
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def pre_fork_block
|
55
|
+
$stdout.reopen(@wout)
|
56
|
+
$stderr.reopen(@wout)
|
57
|
+
|
58
|
+
$stdout.sync = true
|
59
|
+
$stderr.sync = true
|
60
|
+
|
61
|
+
@wout.close
|
62
|
+
end
|
63
|
+
|
64
|
+
class << self
|
65
|
+
def onkill(&blk)
|
66
|
+
@onkill ||= []
|
67
|
+
@onkill << blk
|
68
|
+
end
|
69
|
+
|
70
|
+
def onkill_call(action, options = {})
|
71
|
+
@onkill.each { |b| b.call(action, options) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def run(service, name, *args, &blk)
|
75
|
+
action = new(service, name, *args, &blk)
|
76
|
+
action.run
|
77
|
+
action
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Nodectl::Binding
|
2
|
+
attr_reader :service
|
3
|
+
attr_reader :recipe
|
4
|
+
|
5
|
+
def initialize(service, recipe)
|
6
|
+
@service = service
|
7
|
+
@recipe = recipe
|
8
|
+
end
|
9
|
+
|
10
|
+
def method_missing(name, *args, &blk)
|
11
|
+
@recipe.public_send(name, @service, *args, &blk)
|
12
|
+
end
|
13
|
+
end
|
data/lib/nodectl/cli.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
class Nodectl::CLI < Thor
|
2
|
+
class_option :root, desc: "Root directory path"
|
3
|
+
class_option :config_dir, desc: "Config directory path"
|
4
|
+
class_option :source_dir, desc: "Source directory path"
|
5
|
+
class_option :logger_dir, desc: "Logger directory path"
|
6
|
+
class_option :manifest, desc: "Manifest file path"
|
7
|
+
class_option :database, desc: "Nodectl database path"
|
8
|
+
class_option :log_level, desc: "Log level", enum: ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"]
|
9
|
+
|
10
|
+
desc "init [HOST_PATH]", "Initialize new node"
|
11
|
+
def init(host_path = "./")
|
12
|
+
Nodectl::Generators::Init.start [host_path]
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "recipes", "Show available recipes list"
|
16
|
+
def recipes
|
17
|
+
boot
|
18
|
+
|
19
|
+
Nodectl::Recipe.all.each do |recipe|
|
20
|
+
puts recipe.name
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "recipe RECIPE_NAME", "Show recipe info"
|
25
|
+
def recipe(recipe_name)
|
26
|
+
boot
|
27
|
+
|
28
|
+
recipe = Nodectl::Recipe.find!(recipe_name)
|
29
|
+
|
30
|
+
print "Logs: "
|
31
|
+
puts recipe.logs.map(&:to_s).join(", ")
|
32
|
+
|
33
|
+
print "Actions: "
|
34
|
+
puts recipe.actions.map(&:to_s).join(", ")
|
35
|
+
|
36
|
+
print "Triggers: "
|
37
|
+
puts recipe.triggers.map(&:to_s).join(", ")
|
38
|
+
|
39
|
+
rescue Nodectl::NotFound
|
40
|
+
$stderr.puts "recipe '#{recipe_name}' not found"
|
41
|
+
abort
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "services", "Show services list"
|
45
|
+
def services
|
46
|
+
boot
|
47
|
+
|
48
|
+
Nodectl::Service.load_register
|
49
|
+
|
50
|
+
Nodectl::Service.all.each do |service|
|
51
|
+
puts "#{service.name} #{service.status}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "install SERVICE_NAME", "Install specified service"
|
56
|
+
def install(service_name)
|
57
|
+
boot
|
58
|
+
|
59
|
+
service = find_service!(service_name)
|
60
|
+
recipe = find_recipe!(service)
|
61
|
+
|
62
|
+
recipe.run_action :install
|
63
|
+
end
|
64
|
+
|
65
|
+
option :ui_disabled, desc: "Disable UI and only API", type: :boolean
|
66
|
+
option :daemonize, desc: "Daemonize server", type: :boolean, default: false
|
67
|
+
option :pidfile, desc: "File to save server PID"
|
68
|
+
|
69
|
+
option :http_host, desc: "Host to bind webserver"
|
70
|
+
option :http_port, desc: "Port to bind webserver"
|
71
|
+
|
72
|
+
option :websocket_host, desc: "Host to bind websocket server"
|
73
|
+
option :websocket_port, desc: "Port to bind websocket server"
|
74
|
+
|
75
|
+
# AMPQ support is not implemented yet
|
76
|
+
#
|
77
|
+
# option :amqp_host, desc: "Host of AMQP broker"
|
78
|
+
# option :amqp_port, desc: "Port of AMQP broker"
|
79
|
+
# option :amqp_username, desc: "AMQP username"
|
80
|
+
# option :amqp_password, desc: "AMQP password"
|
81
|
+
# option :amqp_vhost, desc: "AMQP vhost"
|
82
|
+
|
83
|
+
desc 'server', 'Run node'
|
84
|
+
def server
|
85
|
+
boot
|
86
|
+
|
87
|
+
Nodectl::server.run
|
88
|
+
end
|
89
|
+
|
90
|
+
desc 'version', 'Show nodectl version'
|
91
|
+
def version
|
92
|
+
puts "Nodectl #{Nodectl::VERSION}"
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def boot
|
98
|
+
Nodectl.boot(options)
|
99
|
+
end
|
100
|
+
|
101
|
+
def find_service!(service_name)
|
102
|
+
service = Nodectl::Service.find(service_name)
|
103
|
+
|
104
|
+
unless service
|
105
|
+
$stderr.puts "service '#{service_name}' not found"
|
106
|
+
abort
|
107
|
+
end
|
108
|
+
|
109
|
+
service
|
110
|
+
end
|
111
|
+
|
112
|
+
def find_recipe!(service)
|
113
|
+
recipe = service.recipe_binding
|
114
|
+
|
115
|
+
unless recipe
|
116
|
+
$stderr.puts "recipe '#{service.recipe_name}' for service '#{service.name}' not found"
|
117
|
+
abort
|
118
|
+
end
|
119
|
+
|
120
|
+
recipe
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Nodectl::Context
|
2
|
+
attr_reader :recipe
|
3
|
+
attr_reader :service
|
4
|
+
|
5
|
+
def initialize(recipe, service, methods = nil)
|
6
|
+
@recipe = recipe
|
7
|
+
@service = service
|
8
|
+
|
9
|
+
if methods
|
10
|
+
extend methods
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def exec(*args)
|
15
|
+
Process.exec(*args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def chdir(dir)
|
19
|
+
Dir.chdir(dir)
|
20
|
+
end
|
21
|
+
|
22
|
+
def run(command, options = {}, &blk)
|
23
|
+
instance = Nodectl::Instance.new(service, command, options) do
|
24
|
+
instance_eval &blk if blk
|
25
|
+
end
|
26
|
+
|
27
|
+
instance.run
|
28
|
+
instance
|
29
|
+
end
|
30
|
+
|
31
|
+
def run_action(action_name, options = {})
|
32
|
+
service.recipe_binding.run_action(action_name, options)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class Nodectl::Database
|
2
|
+
LoadError = Class.new(StandardError)
|
3
|
+
DumpError = Class.new(StandardError)
|
4
|
+
|
5
|
+
def initialize(file_path)
|
6
|
+
@db = DBM.open(file_path, 0666, DBM::WRCREAT)
|
7
|
+
end
|
8
|
+
|
9
|
+
def reset
|
10
|
+
@db.clear
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch(key, default = nil)
|
14
|
+
blob = @db[key]
|
15
|
+
|
16
|
+
if blob
|
17
|
+
load(blob)
|
18
|
+
elsif default
|
19
|
+
default
|
20
|
+
else
|
21
|
+
raise LoadError, "key not found: '#{key}'"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def [](key)
|
26
|
+
blob = @db[key]
|
27
|
+
|
28
|
+
if blob
|
29
|
+
load(blob)
|
30
|
+
else
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def []=(key, value)
|
36
|
+
@db[key] = Marshal.dump(value)
|
37
|
+
rescue TypeError => e
|
38
|
+
raise LoadError, "marshal dump error for key '#{key}' and value '#{value}': #{e.message}"
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def dump(value)
|
44
|
+
Marshal.dump(value)
|
45
|
+
rescue TypeError => e
|
46
|
+
raise LoadError, "marshal dump error for key '#{key}' and value '#{value}': #{e.message}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def load(value)
|
50
|
+
Marshal.load(value)
|
51
|
+
rescue TypeError => e
|
52
|
+
raise LoadError, "marshal load error for key '#{key}': #{e.message}"
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|