haplo 2.1.0-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,28 @@
1
+
2
+ module PluginTool
3
+
4
+ module LocalConfig
5
+
6
+ LOCAL_CONFIG_FILENAME = "server.config.json"
7
+
8
+ def self.load
9
+ if File.exist? LOCAL_CONFIG_FILENAME
10
+ @@local_config = JSON.parse(File.open(LOCAL_CONFIG_FILENAME) { |f| f.read })
11
+ else
12
+ @@local_config = {}
13
+ end
14
+ end
15
+
16
+ def self.get_list(list_name)
17
+ lookup = @@local_config[list_name]
18
+ return [] unless lookup
19
+ list = []
20
+ server_name = PluginTool.get_server_hostname
21
+ list.concat(lookup['*']) if lookup.has_key?('*')
22
+ list.concat(lookup[server_name]) if server_name && lookup.has_key?(server_name)
23
+ list
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,44 @@
1
+
2
+ module PluginTool
3
+
4
+ # NOTE: Also update packing.rb if this changes
5
+ ALLOWED_PLUGIN_DIRS = ['js', 'static', 'template', 'test', 'data']
6
+
7
+ def self.generate_manifest(directory)
8
+ manifest = Hash.new
9
+ Dir.glob("#{directory}/**/*").sort.each do |pathname|
10
+ # Ignore directories
11
+ next unless File.file? pathname
12
+ # Ignore Emacs backup files
13
+ next if pathname =~ /\~\z/
14
+ # Check file
15
+ filename = pathname.slice(directory.length + 1, pathname.length)
16
+ raise "Bad filename for #{filename}" unless filename =~ /\A([a-zA-Z0-9_\/\-]+\/)?([a-z0-9_-]+\.[a-z0-9]+)\z/
17
+ dir = $1
18
+ name = $2
19
+ if dir != nil
20
+ dir = dir.gsub(/\/\z/,'')
21
+ raise "Bad directory #{dir}" unless dir =~ /\A([a-zA-Z0-9_\-]+)[a-zA-Z0-9_\/\-]*\z/
22
+ raise "Bad root directory #{$1}" unless ALLOWED_PLUGIN_DIRS.include?($1)
23
+ end
24
+ # Get hash of file
25
+ digest = File.open(pathname) { |f| Digest::SHA256.hexdigest(f.read) }
26
+ # And add to manifest
27
+ manifest[filename] = digest
28
+ end
29
+ manifest
30
+ end
31
+
32
+ def self.determine_manifest_changes(from, to)
33
+ changes = []
34
+ from.each_key do |name|
35
+ changes << [name, :delete] unless to.has_key?(name)
36
+ end
37
+ to.each do |name,hash|
38
+ changes << [name, hash] unless from[name] == hash
39
+ end
40
+ changes
41
+ end
42
+
43
+ end
44
+
@@ -0,0 +1,68 @@
1
+
2
+ module PluginTool
3
+
4
+ class Minimiser
5
+ Context = Java::OrgMozillaJavascript::Context
6
+
7
+ def initialize
8
+ # Load UglifyJS into a JavaScript interpreter
9
+ raise "Another JS Context is active" unless nil == Context.getCurrentContext()
10
+ @cx = Context.enter();
11
+ @cx.setLanguageVersion(Context::VERSION_1_7)
12
+ @javascript_scope = @cx.initStandardObjects()
13
+ ['js_min.js','uglifyjs/parse-js.js','uglifyjs/process.js','uglifyjs/squeeze-more.js'].each do |filename|
14
+ js = File.open("#{File.dirname(__FILE__)}/#{filename}") { |f| f.read }
15
+ @cx.evaluateString(@javascript_scope, js, "<#{filename}>", 1, nil);
16
+ end
17
+ @js_min = @javascript_scope.get("js_min", @javascript_scope);
18
+ end
19
+
20
+ def process(data, filename)
21
+ if filename =~ /\.js\z/
22
+ # JavaScript - use UglifyJS loaded into the JavaScript interpreter
23
+ @js_min.call(@cx, @javascript_scope, @javascript_scope, [data])
24
+
25
+ elsif filename =~ /\.html\z/
26
+ # Simple processing of HTML
27
+ # Remove HTML comments
28
+ html = data.gsub(/\<\!\-\-.+?\-\-\>/m,'')
29
+ # Remove indents
30
+ html.gsub!(/^\s+/,'')
31
+ # Remove any unnecessary line breaks (fairly conservative)
32
+ html.gsub!(/\>[\r\n]+\</m,'><')
33
+ html.gsub!(/([\>}])[\r\n]+([\<{])/m,'\1\2')
34
+ html
35
+
36
+ elsif filename =~ /\.css\z/
37
+ # Simple processing of CSS
38
+ css = data.gsub(/(^|\s)\/\*.+?\*\/($|\s)/m,'') # remove C style comments
39
+ out = []
40
+ css.split(/[\r\n]+/).each do |line|
41
+ line.chomp!; line.gsub!(/^\s+/,''); line.gsub!(/\s+$/,'')
42
+ line.gsub!(/\s+/,' ') # contract spaces
43
+ line.gsub!(/\s*:\s*/,':') # remove unnecessary spaces
44
+ if line =~ /\S/
45
+ out << line
46
+ end
47
+ end
48
+ css = out.join("\n")
49
+ # Remove unnecessary line endings
50
+ css.gsub!(/[\r\n]*(\{[^\}]+\})/m) do |m|
51
+ $1.gsub(/[\r\n]/m,'')
52
+ end
53
+ css
54
+
55
+ else
56
+ # No processing
57
+ data
58
+ end
59
+ end
60
+
61
+ def finish
62
+ Context.exit()
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+
@@ -0,0 +1,8 @@
1
+
2
+ module PluginTool
3
+
4
+ def self.beep
5
+ $stdout.write("\07")
6
+ end
7
+
8
+ end
@@ -0,0 +1,88 @@
1
+
2
+ module PluginTool
3
+
4
+ def self.make_new_plugin(plugin_name)
5
+ unless plugin_name =~ /\A[a-z0-9_]+\z/ && plugin_name.length > 8
6
+ end_on_error "Bad plugin name - must use a-z0-9_ only, and more than 8 characters."
7
+ end
8
+ if File.exist?(plugin_name)
9
+ end_on_error "File or directory #{plugin_name} already exists"
10
+ end
11
+ FileUtils.mkdir(plugin_name)
12
+ ['js', 'static', 'template', 'test'].each do |dir|
13
+ FileUtils.mkdir("#{plugin_name}/#{dir}")
14
+ end
15
+ random = java.security.SecureRandom.new()
16
+ rbytes = Java::byte[20].new
17
+ random.nextBytes(rbytes)
18
+ install_secret = String.from_java_bytes(rbytes).unpack('H*').join
19
+ plugin_url_fragment = plugin_name.gsub('_','-')
20
+ File.open("#{plugin_name}/plugin.json",'w') do |file|
21
+ file.write(<<__E)
22
+ {
23
+ "pluginName": "#{plugin_name}",
24
+ "pluginAuthor": "TODO Your Company",
25
+ "pluginVersion": 1,
26
+ "displayName": "#{plugin_name.split('_').map {|e| e.capitalize} .join(' ')}",
27
+ "displayDescription": "TODO Longer description of plugin",
28
+ "installSecret": "#{install_secret}",
29
+ "apiVersion": 4,
30
+ "load": [
31
+ "js/#{plugin_name}.js"
32
+ ],
33
+ "respond": ["/do/#{plugin_url_fragment}"]
34
+ }
35
+ __E
36
+ end
37
+ File.open("#{plugin_name}/js/#{plugin_name}.js",'w') do |file|
38
+ file.write(<<__E)
39
+
40
+ P.respond("GET", "/do/#{plugin_url_fragment}/example", [
41
+ ], function(E) {
42
+ E.render({
43
+ pageTitle: "Example page"
44
+ });
45
+ });
46
+
47
+ __E
48
+ end
49
+ File.open("#{plugin_name}/template/example.html",'w') do |file|
50
+ file.write(<<__E)
51
+ <p>This is an example template.</p>
52
+ __E
53
+ end
54
+ File.open("#{plugin_name}/test/#{plugin_name}_test1.js",'w') do |file|
55
+ file.write(<<__E)
56
+
57
+ t.test(function() {
58
+
59
+ // For documentation, see
60
+ // http://docs.haplo.org/dev/plugin/tests
61
+
62
+ t.assert(true);
63
+
64
+ });
65
+
66
+ __E
67
+ end
68
+ File.open("#{plugin_name}/requirements.schema",'w') do |file|
69
+ file.write("\n\n\n")
70
+ end
71
+ puts <<__E
72
+
73
+ Plugin #{plugin_name} has been created. Run
74
+
75
+ haplo-plugin -p #{plugin_name}
76
+
77
+ to upload it to the server, then visit
78
+
79
+ https://<HOSTNAME>/do/#{plugin_url_fragment}/example
80
+
81
+ to see a sample page.
82
+
83
+ See http://docs.haplo.org/dev/plugin for more information.
84
+
85
+ __E
86
+ end
87
+
88
+ end
@@ -0,0 +1,89 @@
1
+
2
+ module PluginTool
3
+
4
+ @@notification_options = nil
5
+ @@notification_have_suppressed_first_system_audit_entry = false
6
+ @@notification_announce_reconnect = false
7
+
8
+ def self.start_notifications(options)
9
+ @@notification_options = options
10
+ @@notification_queue_name = nil
11
+ Thread.new do
12
+ while true
13
+ begin
14
+ self.do_notifications
15
+ rescue => e
16
+ puts "NOTICE: Lost notification connection, will attempt to reconnect soon."
17
+ @@notification_announce_reconnect = true
18
+ sleep(5) # throttle errors
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def self.do_notifications
25
+ http = make_http_connection
26
+ puts "NOTICE: Notification connection established." if @@notification_announce_reconnect
27
+ while true
28
+ sleep(0.25) # small throttle of requests
29
+ path = '/api/development-plugin-loader/get-notifications'
30
+ path << "?queue=#{@@notification_queue_name}" if @@notification_queue_name
31
+ request = Net::HTTP::Get.new(path)
32
+ setup_request(request)
33
+ # Server uses long-polling, and will respond after a long timeout, or when a notification
34
+ # has been queued for sending to this process.
35
+ response = http.request(request)
36
+ if response.kind_of?(Net::HTTPOK)
37
+ @@notification_queue_name = response['X-Queue-Name']
38
+ begin
39
+ decode_and_handle_notifications response.body
40
+ rescue
41
+ puts "NOTICE: Error handling notification from server."
42
+ end
43
+ else
44
+ raise "Bad response"
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.decode_and_handle_notifications(encoded)
50
+ size = encoded.length
51
+ pos = 0
52
+ while pos < (size - 12)
53
+ type = encoded[pos, 4]
54
+ data_size = encoded[pos + 4, 8].to_i(16)
55
+ data = encoded[pos + 12, data_size]
56
+ pos += 12 + data_size
57
+ handle_notification(type, data)
58
+ end
59
+ end
60
+
61
+ def self.handle_notification(type, data)
62
+ case type
63
+ when 'log '
64
+ # Output from console.log()
65
+ puts "LOG:#{data}"
66
+ when 'audt'
67
+ decoded = JSON.parse(data)
68
+ kind = decoded.find { |name,value| name == 'auditEntryType' }.last
69
+ if kind =~ /\A[A-Z\-]+\z/
70
+ # System audit entry - suppressed by default
71
+ unless @@notification_options.show_system_audit
72
+ unless @@notification_have_suppressed_first_system_audit_entry
73
+ @@notification_have_suppressed_first_system_audit_entry = true
74
+ puts "NOTICE: System audit trail entries are not being shown. Run with --show-system-audit to display."
75
+ end
76
+ return
77
+ end
78
+ end
79
+ puts "AUDIT -------------------------------------------"
80
+ decoded.each do |key, value|
81
+ puts sprintf("%22s: %s", key, value.to_s)
82
+ end
83
+ else
84
+ puts "WARNING: Unknown notification received from server. Upgrade the plugin tool using 'jgem update haplo'."
85
+ sleep(5) # throttle problematic responses
86
+ end
87
+ end
88
+
89
+ end
@@ -0,0 +1,62 @@
1
+
2
+ module PluginTool
3
+
4
+ PACKING_ACCEPTABLE_FILENAME = /\A(js|static|template|test|data)\/([a-z0-9_-]+\/)*[a-z0-9_-]+\.[a-z0-9]+\z/
5
+ PACKING_ACCEPTABLE_EXCEPTIONS = ['plugin.json', 'requirements.schema', 'global.js', 'certificates-temp-http-api.pem']
6
+ PACKING_EXCLUDE = ['developer.json']
7
+
8
+ def self.pack_plugin(plugin_name, output_directory)
9
+ # Get filenames and sort
10
+ files = Dir.glob("#{plugin_name}/**/*").map do |filename|
11
+ if File.file? filename
12
+ filename[plugin_name.length+1, filename.length]
13
+ else
14
+ nil
15
+ end
16
+ end .select { |f| !PACKING_EXCLUDE.include?(f) }.compact.sort
17
+ # Check each filename is acceptable
18
+ files.each do |filename|
19
+ unless filename =~ PACKING_ACCEPTABLE_FILENAME || PACKING_ACCEPTABLE_EXCEPTIONS.include?(filename)
20
+ puts "File '#{filename}' has an unacceptable filename"
21
+ exit 1
22
+ end
23
+ end
24
+ # Clean output directory
25
+ output_plugin_dir = "#{output_directory}/#{plugin_name}"
26
+ puts "Output directory: #{output_plugin_dir}"
27
+ if File.exist? output_plugin_dir
28
+ puts "Removing old output directory #{output_plugin_dir}"
29
+ FileUtils.rm_r(output_plugin_dir)
30
+ end
31
+ # Make file structure
32
+ FileUtils.mkdir(output_plugin_dir)
33
+ # Process each file, building a manifest
34
+ puts "Processing files:"
35
+ manifest = ''
36
+ minimiser = PluginTool::Minimiser.new
37
+ files.each do |filename|
38
+ puts " #{filename}"
39
+ data = File.open("#{plugin_name}/#{filename}") { |f| f.read }
40
+ # Minimise file?
41
+ unless filename =~ /\Ajs\//
42
+ data = minimiser.process(data, filename)
43
+ end
44
+ hash = Digest::SHA256.hexdigest(data)
45
+ # Make sure output directory exists, write file
46
+ output_pathname = "#{output_plugin_dir}/#{filename}"
47
+ output_directory = File.dirname(output_pathname)
48
+ FileUtils.mkdir_p(output_directory) unless File.directory?(output_directory)
49
+ File.open(output_pathname, "w") { |f| f.write data }
50
+ # Filename entry in Manifest
51
+ manifest << "F #{hash} #{filename}\n"
52
+ end
53
+ minimiser.finish
54
+ # Write manifest and version
55
+ File.open("#{output_plugin_dir}/manifest", "w") { |f| f.write manifest }
56
+ version = Digest::SHA256.hexdigest(manifest)
57
+ File.open("#{output_plugin_dir}/version", "w") { |f| f.write "#{version}\n" }
58
+ # All done
59
+ puts "Version: #{version}\nPlugin packed."
60
+ end
61
+
62
+ end
@@ -0,0 +1,227 @@
1
+
2
+ module PluginTool
3
+
4
+ class Plugin
5
+ DEFAULT_PLUGIN_LOAD_PRIORITY = 9999999
6
+
7
+ def initialize(name, options)
8
+ @name = name
9
+ @options = options
10
+ end
11
+ attr_accessor :name
12
+ attr_accessor :plugin_dir
13
+ attr_accessor :loaded_plugin_id
14
+
15
+ # ---------------------------------------------------------------------------------------------------------
16
+
17
+ @@pending_apply = []
18
+
19
+ # ---------------------------------------------------------------------------------------------------------
20
+
21
+ def start
22
+ # Check to see if the plugin is valid
23
+ unless File.file?("#{@name}/plugin.json")
24
+ end_on_error "Plugin #{@name} does not exist (no plugin.json file)"
25
+ end
26
+ # Setup for using the plugin
27
+ @plugin_dir = @name
28
+ end_on_error "logic error" unless @plugin_dir =~ /\A[a-zA-Z0-9_-]+\z/
29
+ @loaded_plugin_id = nil
30
+ end
31
+
32
+ def plugin_load_priority
33
+ pj = File.open("#{@plugin_dir}/plugin.json") { |f| JSON.parse(f.read) }
34
+ pj['loadPriority'] || DEFAULT_PLUGIN_LOAD_PRIORITY
35
+ end
36
+
37
+ def print_banner
38
+ puts "Plugin: #{@plugin_dir}"
39
+ end
40
+
41
+ def setup_for_server
42
+ # Make the first empty manifest (may be replaced from server)
43
+ @current_manifest = {}
44
+
45
+ # If minimisation is active, clear the current manifest so all files are uploaded again.
46
+ if @options.minimiser != nil
47
+ @current_manifest = {}
48
+ end
49
+
50
+ # See if the plugin has already been registered with the server
51
+ s_found_info = PluginTool.post_with_json_response("/api/development-plugin-loader/find-registration", {:name => @name})
52
+ if s_found_info["found"]
53
+ # Store info returned by the server
54
+ @loaded_plugin_id = s_found_info["plugin_id"]
55
+ @current_manifest = s_found_info["manifest"]
56
+ end
57
+
58
+ # If there isn't an existing plugin registered, create a new one
59
+ if @loaded_plugin_id == nil
60
+ s_create_info = PluginTool.post_with_json_response("/api/development-plugin-loader/create")
61
+ end_on_error "Couldn't communicate successfully with server." if s_create_info["protocol_error"]
62
+ end_on_error "Failed to create plugin on server" unless s_create_info["plugin_id"] != nil
63
+ @loaded_plugin_id = s_create_info["plugin_id"]
64
+ end
65
+ end
66
+
67
+ # ---------------------------------------------------------------------------------------------------------
68
+
69
+ def exclude_files_from_syntax_check
70
+ @exclude_files_from_syntax_check ||= begin
71
+ # developer.json file might contain some files which should not be syntax checked
72
+ exclude_files_from_check = []
73
+ developer_json_pathname = "#{@plugin_dir}/developer.json"
74
+ if File.exist? developer_json_pathname
75
+ developer_json = JSON.parse(File.read(developer_json_pathname))
76
+ if developer_json['excludeFromSyntaxCheck'].kind_of?(Array)
77
+ exclude_files_from_check = developer_json['excludeFromSyntaxCheck']
78
+ end
79
+ end
80
+ exclude_files_from_check
81
+ end
82
+ end
83
+
84
+ # ---------------------------------------------------------------------------------------------------------
85
+
86
+ def command(cmd)
87
+ case cmd
88
+ when 'license-key'
89
+ application_id = @options.args.first
90
+ if application_id == nil || application_id !~ /\A\d+\z/
91
+ end_on_error "Numeric application ID must be specified"
92
+ end
93
+ generate_license_key(application_id)
94
+
95
+ when 'pack'
96
+ PluginTool.pack_plugin(@name, @options.output)
97
+
98
+ when 'reset-db'
99
+ puts "Resetting database on server for #{@name}..."
100
+ reset_result = PluginTool.post_with_json_response("/api/development-plugin-loader/resetdb/#{@loaded_plugin_id}")
101
+ end_on_error "Couldn't remove old database tables" unless reset_result["result"] == 'success'
102
+ apply_result = PluginTool.post_with_json_response("/api/development-plugin-loader/apply", :plugins => @loaded_plugin_id)
103
+ end_on_error "Couldn't apply changes" unless apply_result["result"] == 'success'
104
+ puts "Done."
105
+
106
+ when 'uninstall'
107
+ puts "Uninstalling plugin #{@name} from server..."
108
+ reset_result = PluginTool.post_with_json_response("/api/development-plugin-loader/uninstall/#{@loaded_plugin_id}")
109
+ end_on_error "Couldn't uninstall plugin" unless reset_result["result"] == 'success'
110
+ puts "Done."
111
+
112
+ when 'test'
113
+ puts "Running tests..."
114
+ params = {}
115
+ params["test"] = @options.args.first unless @options.args.empty?
116
+ test_result = PluginTool.post_with_json_response("/api/development-plugin-loader/run-tests/#{@loaded_plugin_id}", params)
117
+ end_on_error "Couldn't run tests" unless test_result["result"] == 'success'
118
+ puts
119
+ puts test_result["output"] || ''
120
+ puts test_result["summary"] || "(unknown results)"
121
+
122
+ when 'develop'
123
+ # do nothing here
124
+
125
+ else
126
+ end_on_error "Unknown command '#{cmd}'"
127
+
128
+ end
129
+ end
130
+
131
+ # ---------------------------------------------------------------------------------------------------------
132
+
133
+ def develop_setup
134
+ end
135
+
136
+ def develop_scan_and_upload(first_run)
137
+ should_apply = first_run
138
+ next_manifest = PluginTool.generate_manifest(@plugin_dir)
139
+ if !(next_manifest.has_key?("plugin.json"))
140
+ # If the plugin.json file is deleted, just uninstall the plugin from the server
141
+ command('uninstall')
142
+ @is_uninstalled = true
143
+ return
144
+ elsif @is_uninstalled
145
+ should_apply = true
146
+ end
147
+ changes = PluginTool.determine_manifest_changes(@current_manifest, next_manifest)
148
+ upload_failed = false
149
+ changes.each do |filename, action|
150
+ filename =~ /\A(.*?\/)?([^\/]+)\z/
151
+ params = {:filename => $2}
152
+ params[:directory] = $1.gsub(/\/\z/,'') if $1
153
+ if action == :delete
154
+ puts " #{@name}: Deleting #{filename}"
155
+ PluginTool.post_with_json_response("/api/development-plugin-loader/delete-file/#{@loaded_plugin_id}", params)
156
+ else
157
+ puts " #{@name}: Uploading #{filename}"
158
+ data = File.open("#{@plugin_dir}/#{filename}") { |f| f.read }
159
+ hash = action
160
+ # Minimise file before uploading?
161
+ if @options.minimiser != nil && filename =~ /\A(static|template)\//
162
+ size_before = data.length
163
+ data = @options.minimiser.process(data, filename)
164
+ size_after = data.length
165
+ hash = Digest::SHA256.hexdigest(data)
166
+ puts " minimisation: #{size_before} -> #{size_after} (#{(size_after * 100) / size_before}%)"
167
+ end
168
+ r = PluginTool.post_with_json_response("/api/development-plugin-loader/put-file/#{@loaded_plugin_id}", params, {:file => [filename, data]})
169
+ if r["result"] == 'success'
170
+ # If the file was uploaded successfully, but the hash didn't match, abort now
171
+ end_on_error "#{@name}: Disagreed with server about uploaded file hash: local=#{hash}, remote=#{r["hash"]}" unless hash == r["hash"]
172
+ else
173
+ # Otherwise mark as a failed upload to stop an apply operation which will fail
174
+ upload_failed = true
175
+ end
176
+ PluginTool.syntax_check(self, filename) if filename =~ /\.(js|hsvt)\z/i
177
+ end
178
+ end
179
+ if upload_failed
180
+ puts "\n#{@name}: Not applying changes due to failure\n\n"
181
+ else
182
+ if !(changes.empty?) || should_apply
183
+ @@pending_apply.push(self) unless @@pending_apply.include?(self)
184
+ end
185
+ end
186
+ @current_manifest = next_manifest
187
+ end
188
+
189
+ # ---------------------------------------------------------------------------------------------------------
190
+
191
+ def self.do_apply
192
+ return if @@pending_apply.empty?
193
+ puts "Applying changes on server: #{@@pending_apply.map { |p| p.name } .join(', ')}"
194
+ r = PluginTool.post_with_json_response("/api/development-plugin-loader/apply", {
195
+ :plugins => @@pending_apply.map { |p| p.loaded_plugin_id }.join(' ')
196
+ })
197
+ if r["result"] == 'success'
198
+ @@pending_apply = []
199
+ else
200
+ puts "\n\nDidn't apply changes on server\n\n"
201
+ PluginTool.beep
202
+ end
203
+ end
204
+
205
+ # ---------------------------------------------------------------------------------------------------------
206
+
207
+ def generate_license_key(application_id)
208
+ info = File.open("#{@plugin_dir}/plugin.json") { |f| JSON.parse(f.read) }
209
+ if info["installSecret"] == nil
210
+ end_on_error "#{@name}: No installSecret specified in plugin.json"
211
+ end
212
+ license_key = HMAC::SHA1.sign(info["installSecret"], "application:#{application_id}")
213
+ puts <<__E
214
+
215
+ Plugin: #{@name}
216
+ Application: #{application_id}
217
+ License key: #{license_key}
218
+ __E
219
+ end
220
+
221
+ def end_on_error(err)
222
+ puts err
223
+ exit 1
224
+ end
225
+ end
226
+
227
+ end