factor 0.5.15 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/commands/base.rb +123 -0
- data/lib/commands/registry.rb +186 -0
- data/lib/commands/workflows.rb +112 -0
- data/lib/factor.rb +54 -0
- data/lib/factor/version.rb +6 -0
- data/lib/listener.rb +26 -0
- data/lib/runtime.rb +232 -0
- data/lib/websocket_manager.rb +94 -0
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3df70093d41d893436a8b43a6d08f4ffe87249fb
|
4
|
+
data.tar.gz: 8b163aa8f9b1ebe4c9ddaf8c24f67d28917c2e10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e1d4fa2b2775bc2d34bc43acc8bed86f518d0b1865a56647679536454e1e0c52f0ee20db18d5cae137b256e4fafd8cf8f85b8c4c282eef88117482638c519cb
|
7
|
+
data.tar.gz: 29e9b70bf5ccf70e7cf1292c7ec4e8f11377c4a6efb1a96dc84c8b4939372a53151d486d73f9ee5464ee60b9a503f290b54ea56500101a240143f036d7ee6f87
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'colored'
|
4
|
+
require 'configatron'
|
5
|
+
require 'yaml'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
module Factor
|
9
|
+
module Commands
|
10
|
+
# Base command with common methods used by all commands
|
11
|
+
class Command
|
12
|
+
DEFAULT_FILENAME = {
|
13
|
+
connectors: File.expand_path('./connectors.yml'),
|
14
|
+
credentials: File.expand_path('./credentials.yml')
|
15
|
+
}
|
16
|
+
|
17
|
+
attr_accessor :destination_stream
|
18
|
+
|
19
|
+
def info(options = {})
|
20
|
+
log_line :info, options
|
21
|
+
end
|
22
|
+
|
23
|
+
def error(options = {})
|
24
|
+
log_line :error, options
|
25
|
+
end
|
26
|
+
|
27
|
+
def warn(options = {})
|
28
|
+
log_line :warn, options
|
29
|
+
end
|
30
|
+
|
31
|
+
def success(options = {})
|
32
|
+
log_line :success, options
|
33
|
+
end
|
34
|
+
|
35
|
+
def exception(message, exception)
|
36
|
+
error 'message' => message
|
37
|
+
error 'message' => " #{exception.message}"
|
38
|
+
exception.backtrace.each do |line|
|
39
|
+
error 'message' => " #{line}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def load_config(options = {})
|
44
|
+
load_config_data :credentials, options
|
45
|
+
load_config_data :connectors, options
|
46
|
+
end
|
47
|
+
|
48
|
+
def save_config(options={})
|
49
|
+
credentials_relative_path = options[:credentials] || DEFAULT_FILENAME[:credentials]
|
50
|
+
credentials_absolute_path = File.expand_path(credentials_relative_path)
|
51
|
+
connectors_relative_path = options[:connectors] || DEFAULT_FILENAME[:connectors]
|
52
|
+
connectors_absolute_path = File.expand_path(connectors_relative_path)
|
53
|
+
|
54
|
+
connectors = Hash[stringify(configatron.connectors.to_h).sort]
|
55
|
+
credentials = Hash[stringify(configatron.credentials.to_h).sort]
|
56
|
+
|
57
|
+
File.write(connectors_absolute_path,YAML.dump(connectors))
|
58
|
+
File.write(credentials_absolute_path,YAML.dump(credentials))
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def stringify(hash)
|
64
|
+
hash.inject({}) do |options, (key, value)|
|
65
|
+
options[key.to_s] = value.is_a?(Hash) ? stringify(value) : value
|
66
|
+
options
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def load_config_data(config_type, options = {})
|
71
|
+
relative_path = options[config_type] || DEFAULT_FILENAME[config_type]
|
72
|
+
absolute_path = File.expand_path(relative_path)
|
73
|
+
begin
|
74
|
+
data = YAML.load(File.read(absolute_path))
|
75
|
+
rescue
|
76
|
+
data = {}
|
77
|
+
end
|
78
|
+
configatron[config_type].configure_from_hash(data)
|
79
|
+
rescue => ex
|
80
|
+
exception "Couldn't load #{config_type} from #{absolute_path}", ex
|
81
|
+
end
|
82
|
+
|
83
|
+
def log_line(section, options = {})
|
84
|
+
options = { message: options } if options.is_a?(String)
|
85
|
+
tag = tag(options)
|
86
|
+
message = options['message'] || options[:message]
|
87
|
+
section_text = format_section(section)
|
88
|
+
write "[ #{section_text} ] [#{time}]#{tag} #{message}" if message
|
89
|
+
end
|
90
|
+
|
91
|
+
def format_section(section)
|
92
|
+
formated_section = section.to_s.upcase.center(10)
|
93
|
+
case section
|
94
|
+
when :error then formated_section.red
|
95
|
+
when :info then formated_section.bold
|
96
|
+
when :warn then formated_section.yellow
|
97
|
+
when :success then formated_section.green
|
98
|
+
else formated_section
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def tag(options)
|
103
|
+
primary = options['service_id'] || options['instance_id']
|
104
|
+
secondary = if options['service_id'] && options['instance_id']
|
105
|
+
":#{options['instane_id']}"
|
106
|
+
else
|
107
|
+
''
|
108
|
+
end
|
109
|
+
primary ? "[#{primary}#{secondary}]" : ''
|
110
|
+
end
|
111
|
+
|
112
|
+
def time
|
113
|
+
Time.now.localtime.strftime('%m/%d/%y %T.%L')
|
114
|
+
end
|
115
|
+
|
116
|
+
def write(message)
|
117
|
+
stream = @destination_stream || $stdout
|
118
|
+
stream.puts(message)
|
119
|
+
stream.flush
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'rest-client'
|
5
|
+
require 'erubis'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
require 'commands/base'
|
9
|
+
|
10
|
+
module Factor
|
11
|
+
module Commands
|
12
|
+
# Workflow is a Command to start the factor runtime from the CLI
|
13
|
+
class Registry < Factor::Commands::Command
|
14
|
+
|
15
|
+
def workflows(args, options)
|
16
|
+
list = get_yaml_data 'https://raw.githubusercontent.com/factor-io/index/master/workflows.yml'
|
17
|
+
|
18
|
+
list.each do |id, values|
|
19
|
+
puts "#{values['name'].bold} (#{id}): #{values['description']}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def connectors(args, options)
|
24
|
+
list = get_yaml_data 'https://raw.githubusercontent.com/factor-io/index/master/connectors.yml'
|
25
|
+
|
26
|
+
list.each do |id, values|
|
27
|
+
puts "#{values['name'].bold} (#{id})"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_connector(args, options)
|
32
|
+
puts "Workflow ID is required (factor registry connector add --help)".red unless args[0]
|
33
|
+
|
34
|
+
setup_connector args[0].to_s, options if args[0]
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_workflow(args, options)
|
38
|
+
begin
|
39
|
+
list = get_yaml_data 'https://raw.githubusercontent.com/factor-io/index/master/workflows.yml'
|
40
|
+
rescue
|
41
|
+
puts "Couldn't connect to the server to get connector information".red
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
|
45
|
+
unless args[0]
|
46
|
+
puts "Workflow ID is required".red
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
|
50
|
+
begin
|
51
|
+
workflow_id = args[0].to_s
|
52
|
+
workflow_info = list[workflow_id]
|
53
|
+
config_url = workflow_info['config']
|
54
|
+
workflow_name = workflow_info['name']
|
55
|
+
rescue
|
56
|
+
puts "Couldn't find information for #{workflow_id}".red
|
57
|
+
exit
|
58
|
+
end
|
59
|
+
|
60
|
+
load_config(credentials:options.credentials, connectors:options.connectors)
|
61
|
+
|
62
|
+
begin
|
63
|
+
config = get_json_data(config_url)
|
64
|
+
rescue
|
65
|
+
puts "Couldn't pull up configuration information from #{config_url}".red
|
66
|
+
exit
|
67
|
+
end
|
68
|
+
|
69
|
+
if !config['required-connectors'] || !config['required-connectors'].is_a?(Array) || !config['variables'] || !config['variables'].is_a?(Hash)
|
70
|
+
puts "Configuration information for the workflow is missing information"
|
71
|
+
exit
|
72
|
+
end
|
73
|
+
|
74
|
+
config['required-connectors'].each do |connector_id|
|
75
|
+
if configatron.credentials.to_hash[connector_id.to_sym]
|
76
|
+
puts "#{connector_id} already configured".green
|
77
|
+
else
|
78
|
+
setup_connector(connector_id,options)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
variables = {}
|
83
|
+
config['variables'].each do |var_id,var_info|
|
84
|
+
puts var_info['description'] if var_info['description']
|
85
|
+
values = begin
|
86
|
+
JSON.parse(options.values)
|
87
|
+
rescue
|
88
|
+
{}
|
89
|
+
end
|
90
|
+
variables[var_id] = values[var_id]
|
91
|
+
variables[var_id] ||= ask("#{var_info['name'].bold}#{' (optional)' if var_info['optional']}: ").to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
begin
|
95
|
+
template = RestClient.get(config['template'])
|
96
|
+
rescue
|
97
|
+
puts "Couldn't find a template at #{config['template']}".red
|
98
|
+
exit
|
99
|
+
end
|
100
|
+
|
101
|
+
begin
|
102
|
+
eruby = Erubis::Eruby.new(template)
|
103
|
+
workflow_content = eruby.result(variables)
|
104
|
+
rescue
|
105
|
+
puts "Failed to generate template".red
|
106
|
+
exit
|
107
|
+
end
|
108
|
+
|
109
|
+
workflow_filename = "workflow-#{workflow_id}.rb"
|
110
|
+
begin
|
111
|
+
File.write(workflow_filename, workflow_content)
|
112
|
+
rescue
|
113
|
+
puts "Failed to write the file to disk. Check permissions.".red
|
114
|
+
exit
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
puts "Created #{workflow_name} successfully".green
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def setup_connector(connector_id, options)
|
124
|
+
begin
|
125
|
+
list = get_yaml_data 'https://raw.githubusercontent.com/factor-io/index/master/connectors.yml'
|
126
|
+
rescue
|
127
|
+
puts "Couldn't connect to the server to get connector information".red
|
128
|
+
exit
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
begin
|
133
|
+
connector_info = list[connector_id]
|
134
|
+
connector_name = connector_info['name']
|
135
|
+
new_connectors = connector_info['connectors']
|
136
|
+
required_credentials = connector_info['credentials']
|
137
|
+
rescue
|
138
|
+
puts "Couldn't find information for '#{connector_id}'".red
|
139
|
+
exit
|
140
|
+
end
|
141
|
+
|
142
|
+
unless connector_name && new_connectors && required_credentials
|
143
|
+
puts "Couldn't find information for '#{connector_id}'".red
|
144
|
+
exit
|
145
|
+
end
|
146
|
+
|
147
|
+
load_config(credentials:options.credentials, connectors:options.connectors)
|
148
|
+
connectors = configatron.connectors.to_hash
|
149
|
+
credentials = configatron.credentials.to_hash
|
150
|
+
|
151
|
+
connectors[connector_id] = new_connectors
|
152
|
+
|
153
|
+
required_credentials.each do |credential_id, credential_info|
|
154
|
+
puts credential_info['description'] if credential_info['description']
|
155
|
+
credentials[connector_id] ||= {}
|
156
|
+
values = begin
|
157
|
+
JSON.parse(options.values)
|
158
|
+
rescue
|
159
|
+
{}
|
160
|
+
end
|
161
|
+
credentials[connector_id][credential_id.to_s] = values[credential_id.to_s]
|
162
|
+
credentials[connector_id][credential_id.to_s] ||= ask("#{connector_name.bold} #{credential_info['name'].bold}#{' (optional)' if credential_info['optional']}: ").to_s
|
163
|
+
end
|
164
|
+
|
165
|
+
configatron[:credentials].configure_from_hash(credentials)
|
166
|
+
configatron[:connectors].configure_from_hash(connectors)
|
167
|
+
|
168
|
+
save_config(credentials:options.credentials, connectors:options.connectors)
|
169
|
+
|
170
|
+
puts "Setup #{connector_name} successfully".green
|
171
|
+
end
|
172
|
+
|
173
|
+
def get_yaml_data(url)
|
174
|
+
raw_content = RestClient.get(url)
|
175
|
+
list = YAML.parse(raw_content).to_ruby
|
176
|
+
list
|
177
|
+
end
|
178
|
+
|
179
|
+
def get_json_data(url)
|
180
|
+
raw_content = RestClient.get(url)
|
181
|
+
data = JSON.parse(raw_content)
|
182
|
+
data
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'configatron'
|
4
|
+
|
5
|
+
require 'commands/base'
|
6
|
+
require 'runtime'
|
7
|
+
|
8
|
+
module Factor
|
9
|
+
module Commands
|
10
|
+
# Workflow is a Command to start the factor runtime from the CLI
|
11
|
+
class Workflow < Factor::Commands::Command
|
12
|
+
def initialize
|
13
|
+
@workflows = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def server(_args, options)
|
17
|
+
config_settings = {}
|
18
|
+
config_settings[:credentials] = options.credentials
|
19
|
+
config_settings[:connectors] = options.connectors
|
20
|
+
workflow_filename = File.expand_path(options.path || '.')
|
21
|
+
@destination_stream = File.new(options.log, 'w+') if options.log
|
22
|
+
|
23
|
+
load_config(config_settings)
|
24
|
+
load_all_workflows(workflow_filename)
|
25
|
+
block_until_interupt
|
26
|
+
log_message 'status' => 'info', 'message' => 'Good bye!'
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def load_all_workflows(workflow_filename)
|
32
|
+
glob_ending = workflow_filename[-1] == '/' ? '' : '/'
|
33
|
+
glob = "#{workflow_filename}#{glob_ending}*.rb"
|
34
|
+
file_list = Dir.glob(glob)
|
35
|
+
if !file_list.all? { |file| File.file?(file) }
|
36
|
+
error "#{workflow_filename} is neither a file or directory"
|
37
|
+
elsif file_list.count == 0
|
38
|
+
error 'No workflows in this directory to run'
|
39
|
+
else
|
40
|
+
file_list.each { |filename| load_workflow(filename) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def block_until_interupt
|
45
|
+
log_message 'status' => 'info', 'message' => 'Ctrl-c to exit'
|
46
|
+
begin
|
47
|
+
loop do
|
48
|
+
sleep 1
|
49
|
+
end
|
50
|
+
rescue Interrupt
|
51
|
+
log_message 'status' => 'info', 'message' => 'Exiting app...'
|
52
|
+
ensure
|
53
|
+
@workflows.keys.each { |filename| unload_workflow(filename) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_workflow(filename)
|
58
|
+
workflow_filename = File.expand_path(filename)
|
59
|
+
log_message(
|
60
|
+
'status' => 'info',
|
61
|
+
'message' => "Loading workflow from #{workflow_filename}")
|
62
|
+
begin
|
63
|
+
workflow_definition = File.read(workflow_filename)
|
64
|
+
rescue => ex
|
65
|
+
exception "Couldn't read file #{workflow_filename}", ex
|
66
|
+
return
|
67
|
+
end
|
68
|
+
|
69
|
+
log_message(
|
70
|
+
'status' => 'info',
|
71
|
+
'message' => 'Setting up workflow processor')
|
72
|
+
begin
|
73
|
+
connector_settings = configatron.connectors.to_hash
|
74
|
+
credential_settings = configatron.credentials.to_hash
|
75
|
+
runtime = Runtime.new(connector_settings, credential_settings)
|
76
|
+
runtime.logger = method(:log_message)
|
77
|
+
rescue => ex
|
78
|
+
message = "Couldn't setup workflow process for #{workflow_filename}"
|
79
|
+
exception message, ex
|
80
|
+
end
|
81
|
+
|
82
|
+
@workflows[workflow_filename] = fork do
|
83
|
+
begin
|
84
|
+
log_message(
|
85
|
+
'status' => 'info',
|
86
|
+
'message' => "Starting #{workflow_filename}")
|
87
|
+
runtime.load(workflow_definition)
|
88
|
+
rescue => ex
|
89
|
+
exception "Couldn't load #{workflow_filename}", ex
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def unload_workflow(filename)
|
95
|
+
workflow_filename = File.expand_path(filename)
|
96
|
+
log_message(
|
97
|
+
'status' => 'info',
|
98
|
+
'message' => "Stopping #{workflow_filename}")
|
99
|
+
Process.kill('SIGINT', @workflows[workflow_filename])
|
100
|
+
end
|
101
|
+
|
102
|
+
def log_message(message_info)
|
103
|
+
case message_info['status']
|
104
|
+
when 'info' then info message_info
|
105
|
+
when 'success' then success message_info
|
106
|
+
when 'warn' then warn message_info
|
107
|
+
else error message_info
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/factor.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'commander/import'
|
4
|
+
|
5
|
+
require 'factor/version'
|
6
|
+
require 'commands/workflows'
|
7
|
+
require 'commands/registry'
|
8
|
+
|
9
|
+
program :name, 'Factor.io Server'
|
10
|
+
program :version, Factor::VERSION
|
11
|
+
program :description, 'Factor.io Server to run workflows'
|
12
|
+
|
13
|
+
command 'server' do |c|
|
14
|
+
c.syntax = 'factor server [options]'
|
15
|
+
c.description = 'Start the Factor.io Server in the current local directory'
|
16
|
+
c.option '--log FILE', String, 'Log file path. Default is stdout.'
|
17
|
+
c.option '--credentials FILE', String, 'credentials.yml file path.'
|
18
|
+
c.option '--connectors FILE', String, 'connectors.yml file path'
|
19
|
+
c.option '--path FILE', String, 'Path to workflows'
|
20
|
+
c.when_called Factor::Commands::Workflow, :server
|
21
|
+
end
|
22
|
+
|
23
|
+
command 'registry workflows' do |c|
|
24
|
+
c.syntax = 'factor registry workflows'
|
25
|
+
c.description = 'Get list of available workflow jumpstarts'
|
26
|
+
c.when_called Factor::Commands::Registry, :workflows
|
27
|
+
end
|
28
|
+
|
29
|
+
command 'registry workflows add' do |c|
|
30
|
+
c.syntax = 'factor registry workflow add <id>'
|
31
|
+
c.description = 'Get list of available workflows'
|
32
|
+
c.option '--credentials FILE', String, 'credentials.yml file path.'
|
33
|
+
c.option '--connectors FILE', String, 'connectors.yml file path'
|
34
|
+
c.option '--values \'{"api_key":"foo"}\'', String, "{}"
|
35
|
+
c.when_called Factor::Commands::Registry, :add_workflow
|
36
|
+
end
|
37
|
+
|
38
|
+
command 'registry connectors' do |c|
|
39
|
+
c.syntax = 'factor registry connectors'
|
40
|
+
c.description = 'Get list of available connectors'
|
41
|
+
c.when_called Factor::Commands::Registry, :connectors
|
42
|
+
end
|
43
|
+
|
44
|
+
command 'registry connector add' do |c|
|
45
|
+
c.syntax = 'factor registry connector add <id>'
|
46
|
+
c.description = 'Get list of available connectors'
|
47
|
+
c.option '--credentials FILE', String, 'credentials.yml file path.'
|
48
|
+
c.option '--connectors FILE', String, 'connectors.yml file path'
|
49
|
+
c.option '--values \'{"api_key":"foo"}\'', String, "{}"
|
50
|
+
c.when_called Factor::Commands::Registry, :add_connector
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_command 's', 'server'
|
54
|
+
alias_command 'r', 'registry'
|
data/lib/listener.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'websocket_manager'
|
4
|
+
|
5
|
+
module Factor
|
6
|
+
# Class Listener for integrating with connector service
|
7
|
+
class Listener
|
8
|
+
def initialize(url)
|
9
|
+
@url = url
|
10
|
+
end
|
11
|
+
|
12
|
+
def listener(listener_id)
|
13
|
+
listen("#{@url}/listeners/#{listener_id}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def action(action_id)
|
17
|
+
listen("#{@url}/actions/#{action_id}")
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def listen(uri_path)
|
23
|
+
WebSocketManager.new(uri_path)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/runtime.rb
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'yaml'
|
6
|
+
require 'eventmachine'
|
7
|
+
require 'uri'
|
8
|
+
require 'faye/websocket'
|
9
|
+
require 'ostruct'
|
10
|
+
|
11
|
+
require 'listener'
|
12
|
+
require 'commands/base'
|
13
|
+
|
14
|
+
module Factor
|
15
|
+
# Runtime class is the magic of the server
|
16
|
+
class Runtime
|
17
|
+
attr_accessor :logger, :name, :description, :id, :instance_id, :connectors, :credentials
|
18
|
+
|
19
|
+
def initialize(connectors, credentials)
|
20
|
+
@workflow_spec = {}
|
21
|
+
@sockets = []
|
22
|
+
@instance_id = SecureRandom.hex(3)
|
23
|
+
@reconnect = true
|
24
|
+
|
25
|
+
trap 'SIGINT' do
|
26
|
+
info "Exiting '#{@instance_id}'"
|
27
|
+
@reconnect = false
|
28
|
+
@sockets.each { |s| s.close }
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
|
32
|
+
@connectors = {}
|
33
|
+
flat_hash(connectors).each do |key, connector_url|
|
34
|
+
@connectors[key] = Listener.new(connector_url)
|
35
|
+
end
|
36
|
+
|
37
|
+
@credentials = {}
|
38
|
+
credentials.each do |connector_id, credential_settings|
|
39
|
+
@credentials[connector_id] = credential_settings
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def load(workflow_definition)
|
44
|
+
EM.run do
|
45
|
+
instance_eval(workflow_definition)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def listen(service_ref, params = {}, &block)
|
50
|
+
service_map = service_ref.split('::')
|
51
|
+
service_id = service_map.first
|
52
|
+
listener_id = service_map.last
|
53
|
+
service_key = service_map[0..-2].map{|k| k.to_sym}
|
54
|
+
|
55
|
+
ws = @connectors[service_key].listener(listener_id)
|
56
|
+
|
57
|
+
handle_on_open(service_ref, 'Listener', ws, params)
|
58
|
+
|
59
|
+
ws.on :close do
|
60
|
+
error 'Listener disconnected'
|
61
|
+
if @reconnect
|
62
|
+
warn 'Reconnecting...'
|
63
|
+
sleep 3
|
64
|
+
ws.open
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
ws.on :message do |event|
|
69
|
+
listener_response = JSON.parse(event.data)
|
70
|
+
case listener_response['type']
|
71
|
+
when'start_workflow'
|
72
|
+
success "Workflow '#{service_id}::#{listener_id}' triggered"
|
73
|
+
error_handle_call(listener_response, &block)
|
74
|
+
when 'return'
|
75
|
+
success "Workflow '#{service_ref}' started"
|
76
|
+
when 'fail'
|
77
|
+
error "Workflow '#{service_ref}' failed to start"
|
78
|
+
when 'log'
|
79
|
+
listener_response['message'] = " #{listener_response['message']}"
|
80
|
+
log_message(listener_response)
|
81
|
+
else
|
82
|
+
error "Unknown listener response: #{listener_response}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
ws.on :retry do |event|
|
87
|
+
warn event[:message]
|
88
|
+
end
|
89
|
+
|
90
|
+
ws.on :error do |event|
|
91
|
+
err = 'Error during WebSocket handshake: Unexpected response code: 401'
|
92
|
+
if event.message == err
|
93
|
+
error "Sorry but you don't have access to this listener,
|
94
|
+
| either because your token is invalid or your plan doesn't
|
95
|
+
| support this listener"
|
96
|
+
else
|
97
|
+
error 'Failure in WebSocket connection to connector service'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
ws.open
|
102
|
+
|
103
|
+
@sockets << ws
|
104
|
+
end
|
105
|
+
|
106
|
+
def run(service_ref, params = {}, &block)
|
107
|
+
service_map = service_ref.split('::')
|
108
|
+
service_id = service_map.first
|
109
|
+
action_id = service_map.last
|
110
|
+
service_key = service_map[0..-2].map{|k| k.to_sym}
|
111
|
+
|
112
|
+
ws = @connectors[service_key].action(action_id)
|
113
|
+
|
114
|
+
handle_on_open(service_ref, 'Action', ws, params)
|
115
|
+
|
116
|
+
ws.on :error do
|
117
|
+
error 'Connection dropped while calling action'
|
118
|
+
end
|
119
|
+
|
120
|
+
ws.on :message do |event|
|
121
|
+
action_response = JSON.parse(event.data)
|
122
|
+
case action_response['type']
|
123
|
+
when 'return'
|
124
|
+
ws.close
|
125
|
+
success "Action '#{service_ref}' responded"
|
126
|
+
error_handle_call(action_response, &block)
|
127
|
+
when 'fail'
|
128
|
+
ws.close
|
129
|
+
error " #{action_response['message']}"
|
130
|
+
error "Action '#{service_ref}' failed"
|
131
|
+
when 'log'
|
132
|
+
action_response['message'] = " #{action_response['message']}"
|
133
|
+
log_message(action_response)
|
134
|
+
else
|
135
|
+
error "Unknown action response: #{action_response}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
ws.open
|
140
|
+
|
141
|
+
@sockets << ws
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
class DeepStruct < OpenStruct
|
147
|
+
def initialize(hash=nil)
|
148
|
+
@table = {}
|
149
|
+
@hash_table = {}
|
150
|
+
|
151
|
+
if hash
|
152
|
+
hash.each do |k,v|
|
153
|
+
@table[k.to_sym] = (v.is_a?(Hash) ? self.class.new(v) : v)
|
154
|
+
@hash_table[k.to_sym] = v
|
155
|
+
|
156
|
+
new_ostruct_member(k)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def to_h
|
162
|
+
@hash_table
|
163
|
+
end
|
164
|
+
|
165
|
+
def [](idx)
|
166
|
+
hash = marshal_dump
|
167
|
+
hash[idx.to_sym]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def simple_object_convert(item)
|
172
|
+
if item.is_a?(Hash)
|
173
|
+
DeepStruct.new(item)
|
174
|
+
elsif item.is_a?(Array)
|
175
|
+
item.map do |i|
|
176
|
+
simple_object_convert(i)
|
177
|
+
end
|
178
|
+
else
|
179
|
+
item
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def flat_hash(h,f=[],g={})
|
184
|
+
return g.update({ f=>h }) unless h.is_a? Hash
|
185
|
+
h.each { |k,r| flat_hash(r,f+[k],g) }
|
186
|
+
g
|
187
|
+
end
|
188
|
+
|
189
|
+
def handle_on_open(service_ref, dsl_type, ws, params)
|
190
|
+
service_map = service_ref.split('::')
|
191
|
+
service_id = service_map.first
|
192
|
+
|
193
|
+
ws.on :open do
|
194
|
+
params.merge!(@credentials[service_id.to_sym] || {})
|
195
|
+
success "#{dsl_type.capitalize} '#{service_ref}' called"
|
196
|
+
ws.send(params.to_json)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def error_handle_call(listener_response, &block)
|
201
|
+
content = simple_object_convert(listener_response['payload'])
|
202
|
+
block.call(content) if block
|
203
|
+
rescue => ex
|
204
|
+
error "Error in workflow definition: #{ex.message}"
|
205
|
+
ex.backtrace.each do |line|
|
206
|
+
error " #{line}"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def success(msg)
|
211
|
+
log_message('type' => 'log', 'status' => 'success', 'message' => msg)
|
212
|
+
end
|
213
|
+
|
214
|
+
def warn(msg)
|
215
|
+
log_message('type' => 'log', 'status' => 'warn', 'message' => msg)
|
216
|
+
end
|
217
|
+
|
218
|
+
def error(msg)
|
219
|
+
log_message('type' => 'log', 'status' => 'error', 'message' => msg)
|
220
|
+
end
|
221
|
+
|
222
|
+
def info(msg)
|
223
|
+
log_message('type' => 'log', 'status' => 'info', 'message' => msg)
|
224
|
+
end
|
225
|
+
|
226
|
+
def log_message(message_info)
|
227
|
+
message_info['instance_id'] = @instance_id
|
228
|
+
message_info['workflow_id'] = @id
|
229
|
+
@logger.call(message_info) if @logger
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'faye/websocket'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Factor
|
7
|
+
# class for managing the web socket connections
|
8
|
+
class WebSocketManager
|
9
|
+
attr_accessor :keep_open, :events, :state
|
10
|
+
|
11
|
+
def initialize(uri, headers = {})
|
12
|
+
u = URI(uri)
|
13
|
+
@uri = u.to_s
|
14
|
+
@settings = { ping: 10, retry: 5 }
|
15
|
+
@settings[:headers] = headers if headers && headers != {}
|
16
|
+
@state = :closed
|
17
|
+
@events = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def open
|
21
|
+
if closed?
|
22
|
+
@state = :opening
|
23
|
+
connect
|
24
|
+
end
|
25
|
+
@state
|
26
|
+
end
|
27
|
+
|
28
|
+
def close
|
29
|
+
if open?
|
30
|
+
@state = :closing
|
31
|
+
@ws.close
|
32
|
+
end
|
33
|
+
@state
|
34
|
+
end
|
35
|
+
|
36
|
+
def on(event, &block)
|
37
|
+
@events[event] = block
|
38
|
+
end
|
39
|
+
|
40
|
+
def open?
|
41
|
+
@state == :open
|
42
|
+
end
|
43
|
+
|
44
|
+
def opening?
|
45
|
+
@state == :opening
|
46
|
+
end
|
47
|
+
|
48
|
+
def closed?
|
49
|
+
@state == :closed
|
50
|
+
end
|
51
|
+
|
52
|
+
def closing?
|
53
|
+
@state == :closing
|
54
|
+
end
|
55
|
+
|
56
|
+
def send(msg)
|
57
|
+
@ws.send(msg)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def call_event(event, data)
|
63
|
+
@events[event].call(data) if @events[event]
|
64
|
+
end
|
65
|
+
|
66
|
+
def connect
|
67
|
+
EM.run do
|
68
|
+
begin
|
69
|
+
@ws = Faye::WebSocket::Client.new(@uri, nil, @settings)
|
70
|
+
|
71
|
+
@ws.on :close do |event|
|
72
|
+
@state = :closed
|
73
|
+
call_event :close, event
|
74
|
+
end
|
75
|
+
|
76
|
+
@ws.on :message do |msg|
|
77
|
+
call_event :message, msg
|
78
|
+
end
|
79
|
+
|
80
|
+
@ws.on :open do |event|
|
81
|
+
@state = :open
|
82
|
+
call_event :open, event
|
83
|
+
end
|
84
|
+
|
85
|
+
@ws.on :error do |event|
|
86
|
+
call_event :error, event
|
87
|
+
end
|
88
|
+
rescue => ex
|
89
|
+
call_event :fail, ex.message
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: factor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maciej Skierkowski
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-10-
|
11
|
+
date: 2014-10-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: commander
|
@@ -150,6 +150,14 @@ files:
|
|
150
150
|
- "./spec/spec_helper.rb"
|
151
151
|
- "./spec/workflow_spec.rb"
|
152
152
|
- bin/factor
|
153
|
+
- lib/commands/base.rb
|
154
|
+
- lib/commands/registry.rb
|
155
|
+
- lib/commands/workflows.rb
|
156
|
+
- lib/factor.rb
|
157
|
+
- lib/factor/version.rb
|
158
|
+
- lib/listener.rb
|
159
|
+
- lib/runtime.rb
|
160
|
+
- lib/websocket_manager.rb
|
153
161
|
homepage: https://factor.io
|
154
162
|
licenses: []
|
155
163
|
metadata: {}
|