zillabyte-cli 0.0.21 → 0.0.22

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,1158 +0,0 @@
1
- require "zillabyte/cli/base"
2
- require "zillabyte/cli/config"
3
- require "zillabyte/common"
4
- require "pty"
5
- require 'indentation'
6
- require 'open3'
7
- require 'securerandom'
8
- require 'colorize'
9
- require 'time_difference'
10
-
11
- # manage custom apps
12
- #
13
- class Zillabyte::Command::Apps < Zillabyte::Command::Base
14
-
15
- MAX_POLL_SECONDS = 60 * 5
16
- POLL_SLEEP = 0.5
17
-
18
- # apps
19
- #
20
- # list custom apps
21
- # --type TYPE # specify an output type i.e. json
22
- #
23
- def index
24
- self.list
25
- end
26
-
27
- # apps
28
- #
29
- # list custom apps
30
- # --type TYPE # specify an output type i.e. json
31
- def list
32
- type = options[:type]
33
-
34
- headings = ["id", "name", "state", "cycles"]
35
- rows = api.app.list.map do |row|
36
- if headings.size == 0
37
- headings = row.keys
38
- headings.delete("rel_dir")
39
- end
40
- v = row.values_at *headings
41
- v
42
- end
43
-
44
- display "apps:\n" if type.nil?
45
- display TableOutputBuilder.build_table(headings, rows, type)
46
- display "Total number of apps: "+rows.length.to_s if type.nil?
47
- end
48
-
49
-
50
- # apps:push [DIR]
51
- #
52
- # uploads an app
53
- #
54
- # --config CONFIG_FILE # use the given config file
55
- # --type TYPE # specify an output type i.e. json
56
- # --directory DIR # app directory
57
- #
58
- #Examples:
59
- #
60
- # $ zillabyte apps:push .
61
- #
62
- def push
63
-
64
- since = Time.now.utc.to_s
65
- dir = options[:directory] || shift_argument
66
- if dir.nil?
67
- dir = Dir.pwd
68
- else
69
- dir = File.expand_path(dir)
70
- end
71
- options[:directory] = dir
72
- type = options[:type]
73
-
74
- res = api.apps.push_directory dir, session, options
75
-
76
- if res['error']
77
- error("error: #{res['error_message']}", type)
78
- else
79
- display "app ##{res['id']} #{res['action']}" if type.nil?
80
- end
81
-
82
- display "Starting up your app...please wait..." if type.nil?
83
- sleep(2) # wait for kill command
84
-
85
- lf = LogFormatter::Startup.new
86
- api.logs.get_startup(res['id'], "_ALL_", {:push => true}) do |hash|
87
-
88
- # Error?
89
- error(hash['error_message'], type) if hash['error']
90
-
91
- # Print it
92
- lf.print_log_line(hash) if type.nil?
93
-
94
- # Exit when we get the 'done' message
95
- # exit(0) if (hash['line'] || '').downcase.include?("app deployed")
96
-
97
- end
98
- display "{}" if type == "json"
99
-
100
- end
101
- alias_command "push", "apps:push"
102
-
103
-
104
-
105
- # apps:pull ID DIR
106
- #
107
- # pulls an app source to a directory.
108
- #
109
- # --force # pulls even if the directory exists
110
- # --type TYPE # specify an output type i.e. json
111
- # --directory DIR # Directory of the app
112
- #
113
- #Examples:
114
- #
115
- # $ zillabyte apps:pull .
116
- #
117
- def pull
118
-
119
- app_id = options[:id] || shift_argument
120
-
121
- if !(app_id =~ /^\d*$/)
122
- options[:is_name] = true
123
- end
124
-
125
- dir = options[:directory] || shift_argument
126
- error("no directory given", type) if dir.nil?
127
- dir = File.expand_path(dir)
128
-
129
- type = options[:type]
130
-
131
- error("no id given", type) if app_id.nil?
132
-
133
- # Create if not exists..
134
- if File.exists?(dir)
135
- if Dir.entries(dir).size != 2 and options[:force].nil?
136
- error("target directory not empty. use --force to override", type)
137
- end
138
- else
139
- FileUtils.mkdir_p(dir)
140
- end
141
-
142
- res = api.apps.pull_to_directory app_id, dir, session, options
143
-
144
- if res['error']
145
- error("error: #{res['error_message']}", type)
146
- else
147
- if type == "json"
148
- display "{}"
149
- else
150
- display "app ##{res['id']} pulled to #{dir}"
151
- end
152
- end
153
-
154
- end
155
- alias_command "pull", "apps:pull"
156
-
157
-
158
- # apps:delete ID
159
- #
160
- # deletes an app. if the app is running, this command will kill it.
161
- #
162
- # -f, --force # don't ask for confirmation
163
- # --type TYPE # specify an output type i.e. json
164
- #
165
- def delete
166
- app_id = options[:id] || shift_argument
167
-
168
- if app_id.nil?
169
- app_id = read_name_from_conf(options)
170
- options[:is_name] = true
171
- elsif !(app_id =~ /^\d*$/)
172
- options[:is_name] = true
173
- end
174
- forced = options[:force]
175
- type = options[:type]
176
-
177
- if not forced
178
-
179
- if !type.nil?
180
- error("specify -f, --force to confirm deletion", type)
181
- end
182
-
183
- while true
184
-
185
- display "This operation cannot be undone. Are you sure you want to delete this app? (yes/no):", false
186
- confirm = ask
187
- break if confirm == "yes" || confirm == "no"
188
- display "Please enter 'yes' to delete the app or 'no' to exit"
189
- end
190
- end
191
-
192
- confirmed = forced || confirm == "yes"
193
-
194
- if confirmed
195
- response = api.apps.delete(app_id, options)
196
- if type == "json"
197
- display "{}"
198
- else
199
- display response["body"]
200
- end
201
- end
202
-
203
- end
204
-
205
-
206
- # apps:prep [DIR]
207
- #
208
- # --directory DIR # app directory
209
- # --type TYPE # specify an output type i.e. json
210
- #
211
- def prep
212
-
213
- type = options[:type]
214
- dir = options[:directory] || shift_argument
215
- if dir.nil?
216
- dir = Dir.pwd
217
- else
218
- dir = File.expand_path(dir)
219
- end
220
- options[:directory] = dir
221
- meta = Zillabyte::CLI::Config.get_config_info(dir)
222
-
223
- if meta.nil?
224
- error("The specified directory (#{dir}) does not appear to contain a valid Zillabyte configuration file.", type)
225
- end
226
-
227
- case meta["language"]
228
- when "ruby"
229
-
230
- # Execute in the bundler context
231
- full_script = File.join(dir, meta["script"])
232
- cmd = "cd \"#{meta['home_dir']}\"; unset BUNDLE_GEMFILE; unset RUBYOPT; bundle install"
233
- exec(cmd)
234
-
235
- when "python"
236
- vDir = "#{meta['home_dir']}/vEnv"
237
- lock_file = meta['home_dir']+"/zillabyte_thread_lock_file"
238
- if File.exists?(lock_file)
239
- sleep(1) while File.exists?(lock_file)
240
- else
241
- begin
242
- cmd = "touch #{lock_file}; virtualenv --clear --system-site-packages #{vDir}; PYTHONPATH=~/zb1/multilang/python/Zillabyte #{vDir}/bin/pip install -r #{meta['home_dir']}/requirements.txt"
243
- system cmd, :out => :out
244
- ensure
245
- File.delete(lock_file)
246
- end
247
- end
248
-
249
- when "js"
250
- end
251
-
252
- end
253
- alias_command "prep", "apps:prep"
254
-
255
-
256
-
257
-
258
- # apps:init [LANG] [DIR]
259
- #
260
- # initializes a new executable in DIR
261
- # [LANG] defaults to ruby, and [DIR] to the current directory
262
- #
263
- # --type TYPE # specify an output type i.e. json
264
- # --directory DIR # Directory of the app
265
- #
266
- #Examples:
267
- #
268
- # $ zillabyte apps:init python contact_extractor
269
- #
270
- def init
271
-
272
- lang = options[:lang] || shift_argument || "ruby"
273
- dir = options[:directory] || shift_argument
274
- if dir.nil?
275
- dir = Dir.pwd
276
- else
277
- dir = File.expand_path(dir)
278
- end
279
- type = options[:type]
280
-
281
- languages = ["ruby","python", "js"]
282
-
283
- error("Unsupported language #{lang}. We only support #{languages.join(', ')}.", type) if not languages.include? lang
284
-
285
- display "initializing empty #{lang} app in #{dir}" if type.nil?
286
- FileUtils.cp_r( File.expand_path("../templates/#{lang}", __FILE__) + "/." , dir )
287
-
288
-
289
- end
290
-
291
-
292
-
293
- # apps:logs ID [OPERATION_NAME]
294
- #
295
- # streams logs from the distributed workers
296
- #
297
- # --type TYPE # specify an output type i.e. json
298
- # -v, --verbose LEVEL # sets the verbosity (error, info, debug) (default: info)
299
- #
300
- def logs
301
-
302
- app_id = options[:id] || shift_argument
303
- if app_id.nil?
304
- app_id = read_name_from_conf(options)
305
- options[:is_name] = true
306
- elsif !(app_id =~ /^\d*$/)
307
- options[:is_name] = true
308
- end
309
-
310
- operation_id = options[:operation] || shift_argument || '_ALL_'
311
- category = options[:verbose] || '_ALL_'
312
- type = options[:type]
313
-
314
- carry_settings = {
315
- :category => category
316
- }
317
-
318
- display "Retrieving logs for app ##{app_id}...please wait..." if type.nil?
319
- lf = LogFormatter::Operation.new
320
- self.api.logs.get(app_id, operation_id, options) do |line|
321
-
322
- error(line['error_message'], type) if line['error']
323
- lf.print_log_line(line) if type.nil?
324
-
325
- end
326
-
327
- end
328
- alias_command "logs", "apps:logs"
329
-
330
-
331
-
332
- # apps:errors ID
333
- #
334
- # Show recent errors generated by the app
335
- # --type TYPE # specify an output type i.e. json
336
- #
337
- def errors
338
-
339
- # Init
340
- app_id = options[:id] || shift_argument
341
-
342
- # No name?
343
- if app_id.nil?
344
- app_id = read_name_from_conf(options)
345
- options[:is_name] = true
346
- elsif !(app_id =~ /^\d*$/)
347
- options[:is_name] = true
348
- end
349
-
350
- type = options[:type]
351
-
352
- # Make the request
353
- res = api.request(
354
- :expects => 200,
355
- :method => :get,
356
- :body => options.to_json,
357
- :path => "/flows/#{CGI.escape(app_id)}/errors"
358
- )
359
-
360
- # Render
361
- display "Recent errors:" if type.nil?
362
- headings = ["operation", "date", "error"]
363
- rows = (res.body["recent_errors"] || []).map do |row|
364
- if row['date']
365
- d = Time.at(row['date']/1000)
366
- else
367
- d = nil
368
- end
369
- [row['name'], d, row['message']]
370
- end
371
- rows.sort! do |a,b|
372
- a[1] <=> b[1]
373
- end
374
- color_map = {}
375
- colors = LogFormatter::COLORS.clone
376
- rows.each do |row|
377
- name = row[0]
378
- time = row[1]
379
- message = row[2].strip
380
- color_map[name] ||= colors.shift
381
- if time
382
- display "#{"* #{name} - #{time_ago_in_words(time)} ago".colorize(color_map[name])}:" if type.nil?
383
- else
384
- display "#{"* #{name}".colorize(color_map[name])}:" if type.nil?
385
- end
386
- message.split('\n').each do |sub_line|
387
- display " #{sub_line}" if type.nil?
388
- end
389
- end
390
-
391
- end
392
-
393
- # apps:cycles ID [OPTIONS]
394
- #
395
- # operations on the app's cycles (batches).
396
- # with no options, the command lists the apps cycles
397
- # -n, --next # request the app to move to the next cycle
398
- # -f, --forever # don't wait on cycles any more
399
- # --type TYPE # specify an output type i.e. json
400
- #
401
- def cycles
402
- app_id = options[:id] || shift_argument
403
- type = options[:type]
404
-
405
- trigger_next = options[:next] || false
406
- trigger_forever = options[:forever] || false
407
-
408
- if app_id.nil?
409
- app_id = read_name_from_conf(options)
410
- options[:is_name] = true
411
- elsif !(app_id =~ /^\d*$/)
412
- options[:is_name] = true
413
- end
414
-
415
- if trigger_next
416
- # Trigger the next app
417
- response = api.apps.create_cycle(app_id, options)
418
- elsif trigger_forever
419
- response = api.apps.run_forever(app_id, options)
420
- else
421
- # List the apps
422
- response = api.apps.list_cycles(app_id, options)
423
- # TODO List the sequence number for this app.
424
- display "Most recent cyles of the app:" if type.nil?
425
- headings = ["Cycle_id", "State", "Start", "End"]
426
- rows = response["cycles"]
427
- rows = rows.map do |row|
428
- start_time = DateTime.parse(row["start"]).strftime("%m/%d/%Y %I:%M%p")
429
- end_time = row["end"].nil? ? "---" : DateTime.parse(row["end"]).strftime("%m/%d/%Y %I:%M%p")
430
- [row["cycle_id"], row["state"], start_time, end_time ] #TODO Pretty print time
431
- end
432
-
433
- display TableOutputBuilder.build_table(headings, rows, type)
434
- display "Total number of cycles executed: #{response['total']}" if type.nil?
435
- return
436
- end
437
-
438
- if response["job_id"]
439
- options[:job_id] = response["job_id"]
440
- app_id = response["flow_id"]
441
- options.delete :is_name
442
-
443
- start = Time.now.utc
444
- display "Next cycle request sent. If your app was RETIRED this may take slightly longer." if type.nil?
445
-
446
- while(Time.now.utc < start + MAX_POLL_SECONDS) do
447
-
448
- # Poll
449
- res = self.api.apps.cycles_poll(app_id, options)
450
-
451
- case res['status']
452
- when 'completed'
453
- if res['return']
454
- if type == "json"
455
- return "{}"
456
- else
457
- display res['return']
458
- end
459
- else
460
- throw "something is wrong: #{res}"
461
- end
462
- # success! continue below
463
- break
464
- when 'running'
465
- sleep(POLL_SLEEP)
466
- # display ".", false
467
- else
468
- throw "unknown status: #{res}"
469
- end
470
-
471
- end
472
- elsif response["error"]
473
- error(response["error"], type)
474
- else
475
- error("remote server error (a380)", type)
476
- end
477
- end
478
-
479
-
480
- # apps:test [DIR]
481
- #
482
- # tests a local app with sample data
483
- #
484
- # --config CONFIG_FILE # use the given config file
485
- # --output OUTPUT_FILE # writes sink output to a file
486
- # --wait MAX # max time to spend on each operation (default 10 seconds)
487
- # --batches BATCHES # number of batches to emit (default 1)
488
- # --directory DIR # app directory
489
- #
490
- def test
491
-
492
- output = options[:output]
493
- otype = options[:type] #type is used below for something else
494
-
495
- max_seconds = (options[:wait] || "30").to_i
496
- batches = (options[:batches] || "1").to_i
497
-
498
- def read_message(read_stream, color)
499
- msg = nil
500
- read_stream.each do |line|
501
- line.strip!
502
- if(line == "end")
503
- return msg
504
- end
505
- begin
506
- hash = JSON.parse(line)
507
- if(hash["command"] == "done")
508
- msg = "done"
509
- else
510
- msg = hash.to_json
511
- end
512
- rescue
513
- next
514
- end
515
- end
516
- msg
517
- end
518
-
519
- def write_message(write_stream, msg)
520
- write_stream.write msg.strip + "\n"
521
- write_stream.write "end\n"
522
- write_stream.flush
523
- end
524
-
525
- def truncate_message(msg)
526
- return msg if(!msg.instance_of?(String))
527
- t_length = 50 # truncates entries to this length
528
- m_length = msg.length
529
- msg_out = m_length > t_length ? msg[0..t_length-3]+"..." : msg
530
- msg_out
531
- end
532
-
533
- def handshake(write_stream, read_stream, node, color)
534
- begin
535
- write_message write_stream, "{\"pidDir\": \"/tmp\"}\n"
536
- read_message read_stream, color # Read to "end\n"
537
- rescue Exception => e
538
- puts "Error handshaking node: #{node}"
539
- raise e
540
- end
541
- end
542
-
543
-
544
-
545
- # INIT
546
- dir = options[:directory] || shift_argument
547
- if dir.nil?
548
- dir = Dir.pwd
549
- else
550
- dir = File.expand_path(dir)
551
- end
552
- options[:directory] = dir
553
-
554
- meta = Zillabyte::API::Apps.get_rich_meta_info_from_script(dir, self, {:test => true})
555
- if meta.nil?
556
- error "this is not a valid zillabyte app directory"
557
- exit
558
- end
559
-
560
- # Show the user what we know about their app...
561
- display "inferring your app details..."
562
- colors = {}
563
- describe_app(meta, colors)
564
-
565
-
566
- # Extract the app's information..
567
- nodes = meta["nodes"]
568
- write_to_next_each = []
569
- write_queue = []
570
- stream_messages = {}
571
- default_stream = "_default"
572
-
573
- # Iterate all nodes sequentially and invoke them in separate processes...
574
- nodes.each do |node|
575
-
576
- # Init
577
- type = node["type"]
578
- name = node["name"]
579
- consumes = node["consumes"]
580
- emits = node["emits"]
581
-
582
- color = colors[name] || :default
583
-
584
- op_display = lambda do |msg, override_color = nil|
585
- display "#{name} - #{msg}".colorize(override_color || color)
586
- end
587
-
588
- # A Source?
589
- if type == "source"
590
-
591
- # A source from relation?
592
- if node['matches'] or node["relation"]
593
- matches = node['matches'] || (node["relation"]["query"])
594
- emits = emits.first #For spouting from a relation, there should only be one emits
595
- op_display.call "Grabbing remote data"
596
-
597
- res = api.query.agnostic(matches)
598
- rows = res["rows"]
599
- column_aliases = res["column_aliases"]
600
-
601
-
602
- if(rows.nil? or rows.length == 0)
603
- raise NameError, "Could not find data that matches your 'matches' clause"
604
- end
605
- rows.each do |tuple|
606
- values = {}
607
- meta = {}
608
- tuple.each do |k, v|
609
- if(k == "id")
610
- next
611
- elsif(k == "confidence" or k == "since" or k == "source")
612
- meta[k] = v
613
- else
614
- values[k] = v
615
- end
616
- end
617
- read_msg = {"tuple" => values, "meta" => meta, "column_aliases" => column_aliases}.to_json
618
- values = Hash[values.map{|k, v| [truncate_message(k), truncate_message(v)]}]
619
- op_display.call "emitted: #{values} #{meta} to #{emits}"
620
- stream_messages[emits] ||= []
621
- stream_messages[emits] << read_msg
622
- end
623
-
624
- # Done processing...
625
- next
626
-
627
- else
628
-
629
- # A regular source..
630
- stream_messages[default_stream] ||= []
631
- stream_messages[default_stream] << "{\"command\": \"begin_cycle\"}\n"
632
- emits.each {|ss| stream_messages[ss] = []} #initialize streams
633
- stream_size_at_last_call_to_next_tuple = Hash[emits.map {|ss| [ss, 0]}] #initialize initial size of streams (all 0)
634
- # the above initializations are used to deal with the case where end_cycle_policy == "null_emit"
635
- n_batches_emitted = 1
636
- end_cycle_received = false
637
- last_call_next_tuple = false
638
-
639
- end
640
-
641
- # An Aggregate?
642
- elsif type == "aggregate"
643
- if node['consumes']
644
- input_stream = node['consumes']
645
- else
646
- input_stream = stream_messages.keys.first
647
- end
648
- messages = stream_messages[input_stream] || []
649
- stream_messages[input_stream] = []
650
-
651
- group_by = node['group_by']
652
- group_tuples = {}
653
- messages.each do |msg|
654
- msg = JSON.parse(msg)
655
- tuple = msg["tuple"].to_json
656
- meta = msg["meta"].to_json
657
- column_aliases = msg["column_aliases"] || {}
658
- aliases = Hash[column_aliases.map{|h| [h["alias"],h["concrete_name"]]}]
659
- gt = {}
660
- group_by.each do |field|
661
- field_name = aliases[field] || field
662
- gt[field] = msg["tuple"][field_name]
663
- end
664
-
665
- msg_no_brackets = "\"tuple\": #{tuple}, \"meta\": #{meta}, \"column_aliases\": #{column_aliases.to_json}"
666
- if group_tuples[gt]
667
- group_tuples[gt] << msg_no_brackets
668
- else
669
- group_tuples[gt] = [msg_no_brackets]
670
- end
671
- end
672
-
673
- group_tuples.each do |group_tuple, tuples|
674
- stream_messages[input_stream] << "{\"command\": \"begin_group\", \"tuple\": #{group_tuple.to_json}, \"meta\":{}}\n"
675
- tuples.each do |t|
676
- stream_messages[input_stream] << "{\"command\": \"aggregate\", #{t}}\n"
677
- end
678
- stream_messages[input_stream] << "{\"command\": \"end_group\"}\n"
679
- end
680
-
681
- # A Sink?
682
- elsif type == "sink"
683
-
684
- if consumes.nil?
685
- error "The node #{name} must declare which stream it 'consumes'"
686
- end
687
- messages = stream_messages[consumes] || []
688
-
689
- table = Terminal::Table.new :title => name
690
- csv_str = CSV.generate do |csv|
691
- header_written = false;
692
- messages.each do |msg|
693
- obj = JSON.parse(msg)
694
- if obj['tuple']
695
- if header_written == false
696
- keys = [obj['tuple'].keys, obj['meta'].keys].flatten
697
- csv << keys
698
- table << keys
699
- table << :separator
700
- header_written = true
701
- end
702
- vals = [obj['tuple'].values, obj['meta'].values].flatten
703
- csv << vals
704
- table << vals
705
- end
706
- end
707
- end
708
-
709
- display table.to_s.colorize(color)
710
-
711
- if output
712
- filename = "#{output}.csv"
713
- f = File.open(filename, "w")
714
- f.write(csv_str)
715
- f.close()
716
- op_display.call "output written to #{filename}"
717
- end
718
-
719
- next
720
- end
721
-
722
-
723
- cmd = command("--execute_live --name #{name}", otype, dir)
724
- begin
725
-
726
- # Start the operation...
727
- op_display.call "beginning #{type} #{name}"
728
- Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thread|
729
- begin
730
-
731
- # Init
732
- handshake stdin, stdout, node, color
733
- write_queue = []
734
- read_queue = []
735
-
736
- if consumes.nil?
737
- # Assume default stream (this should only happen for the source)
738
- stream_name = default_stream
739
- else
740
- stream_name = consumes
741
- end
742
- write_queue = stream_messages[stream_name].clone
743
- stream_messages.delete(stream_name)
744
-
745
- # Start writing the messages...
746
- stuff_to_read = false
747
- writing_thread = Thread.start do
748
-
749
- while(true)
750
-
751
- case type
752
- when 'source'
753
- break if n_batches_emitted > batches
754
- if write_queue.empty?
755
- sleep 0.5
756
- next
757
- end
758
- else
759
- break if write_queue.empty?
760
- end
761
-
762
- # Make sure we're not reading anything...
763
- while(stuff_to_read) # TODO: semaphores
764
- sleep 0.5 # spin wait
765
- end
766
-
767
- # Get next mesage
768
- write_msg = write_queue.shift
769
-
770
- # Make it human-understable
771
- write_json = JSON.parse(write_msg)
772
- if write_json['tuple']
773
- display_hash = Hash[write_json['tuple'].map{|k, v| [truncate_message(k), truncate_message(v)]}]
774
- op_display.call "receiving: #{display_hash}"
775
- elsif write_json['command'] == 'next'
776
- last_call_next_tuple = true
777
- op_display.call "getting next set of tuples in the batch"
778
- else
779
- puts write_json
780
- end
781
-
782
- # Actually send it to the process
783
- begin
784
- write_message stdin, write_msg
785
- stuff_to_read = true
786
- sleep 0.1
787
- rescue Exception => e
788
- puts "Error running #{cmd}: #{e}"
789
- raise e
790
- end
791
- end
792
- end
793
-
794
- # Start reading messages...
795
- reading_thread = Thread.start do
796
- while(true)
797
-
798
- # If the end cycle command is received, we either trigger the next cycle if the number of emitted
799
- # cycles is less than what the user requested, or we break
800
- if type == "source" and end_cycle_received
801
- write_queue << "{\"command\": \"begin_cycle\"}\n"
802
- n_batches_emitted += 1
803
- end_cycle_received = false
804
- last_call_next_tuple = false
805
- stuff_to_read = false
806
- break if n_batches_emitted > batches
807
- sleep 0.5
808
- next
809
- end
810
-
811
- # Get next message
812
- read_msg = read_message(stdout, color)
813
- if read_msg == "done" || read_msg.nil?
814
- stuff_to_read = false
815
-
816
- # For sources, if we receive a "done", check to see if any of the streams emitted by the source has
817
- # increased in size since the last call to next_tuple. If so, the cycle isn't over, otherwise, the
818
- # current call to next_tuple emitted nothing and if the end_cycle_policy is set to null_emit, this
819
- # should end the current cycle.
820
- if type == "source"
821
- if last_call_next_tuple and node["end_cycle_policy"] == "null_emit"
822
- end_cycle_received = true
823
- emits.each do |ss|
824
- end_cycle_received = false if stream_messages[ss].size > stream_size_at_last_call_to_next_tuple[ss]
825
- break
826
- end
827
- next if end_cycle_received
828
- end
829
-
830
- # If the policy isn't "null_emit", then just request next_tuple again
831
- write_queue << "{\"command\": \"next\"}\n"
832
- end
833
-
834
- # For other operations, if the queue is empty then we're done
835
- if write_queue.empty?
836
- break # exit while loop
837
- else
838
- sleep 0.5 # spin wait
839
- next
840
- end
841
- end
842
- stuff_to_read = true
843
-
844
- # Process message
845
- obj = JSON.parse(read_msg)
846
-
847
- # process the received tuple or other commands
848
- if obj['tuple']
849
-
850
- # if
851
- tt = obj['tuple']
852
- tt.each do |kk, vv|
853
- if tt[kk].nil? and type == "source" and node["end_cycle_policy"] == "null_emit"
854
- end_cycle_received = true
855
- # read rest of stuff in stdout buffer until "done"
856
- mm = nil
857
- while(mm != "done")
858
- mm = read_message(stdout, color)
859
- end
860
- break
861
- end
862
- end
863
- next if end_cycle_received
864
-
865
- # Convert to a incoming tuple for the next operation
866
- next_msg = {
867
- :tuple => obj['tuple'],
868
- :meta => obj['meta']
869
- }
870
- emit_stream = obj['stream']
871
- stream_messages[emit_stream] ||= []
872
- stream_messages[emit_stream] << next_msg.to_json
873
-
874
- display_hash = Hash[obj['tuple'].map{|k, v| [truncate_message(k), truncate_message(v)]}]
875
- op_display.call "emitted: #{display_hash} to #{emit_stream}"
876
-
877
- # track stream message size to end cycles when necessary
878
- stream_size_at_last_call_to_next_tuple[emit_stream] = stream_messages[emit_stream].size if type == "source"
879
- elsif obj['command'] == 'end_cycle'
880
- end_cycle_received = true
881
- # command:end_cycle should always be followed by done, read it (below) so
882
- # that it doesn't interfere with next
883
- read_message(stdout, color)
884
- elsif obj['command'] == 'log'
885
- op_display.call "log: #{obj['msg']}"
886
- elsif obj['command'] == 'fail'
887
- op_display.call "error: #{obj['msg']}", :red
888
- exit(1)
889
- else
890
- error "unknown message: #{read_msg}"
891
- end
892
-
893
- end
894
- end
895
-
896
- # stderr thread
897
- stderr_thread = Thread.start do
898
- stderr.each do |line|
899
- op_display.call("stderr: #{line}", :red)
900
- end
901
- end
902
-
903
- begin
904
- killed = Timeout.timeout(max_seconds) do
905
- reading_thread.join()
906
- writing_thread.join()
907
- stderr_thread.kill()
908
- op_display.call "completed #{type} #{name}"
909
- end
910
- rescue Timeout::Error
911
- op_display.call "max time reached. preempting #{type} #{name}. set --wait to increase", :red
912
- reading_thread.kill() if reading_thread.alive?
913
- writing_thread.kill() if writing_thread.alive?
914
- stderr_thread.kill() if stderr_thread.alive?
915
- end
916
-
917
- rescue Errno::EIO
918
- puts "Errno:EIO error, but this probably just means " +
919
- "that the process has finished giving output"
920
- end
921
- end
922
- rescue PTY::ChildExited
923
- puts "The child process exited!"
924
- end
925
- end
926
-
927
- end
928
- alias_command "test", "apps:test"
929
-
930
-
931
- # apps:kill ID
932
- #
933
- # kills the given app
934
- #
935
- # --config CONFIG_FILE # use the given config file
936
- # --type TYPE # specify an output type i.e. json
937
- #
938
- def kill
939
-
940
- id = options[:id] || shift_argument
941
- type = options[:type]
942
-
943
- if id.nil?
944
- id = read_name_from_conf(options)
945
- options[:is_name] = true
946
- elsif !(id =~ /^\d*$/)
947
- options[:is_name] = true
948
- end
949
-
950
- display "Killing app ##{id}...please wait..." if type.nil?
951
- api.apps.kill(id, options)
952
-
953
- if type == "json"
954
- display "{}"
955
- else
956
- display "App ##{id} killed"
957
- end
958
-
959
- end
960
-
961
-
962
- # apps:live_run [OPERATION_NAME] [PIPE_NAME] [DIR]
963
- #
964
- # runs a local app with live data
965
- #
966
- # --config CONFIG_FILE # use the given config file
967
- # --type TYPE # specify an output type i.e. json
968
- # --directory DIR # Directory of the app
969
- #
970
- # HIDDEN:
971
- def live_run
972
-
973
- name = options[:name] || shift_argument
974
- type = options[:type]
975
-
976
- thread_id = options[:thread] || shift_argument || ""
977
- dir = options[:directory] || shift_argument
978
- if dir.nil?
979
- dir = Dir.pwd
980
- else
981
- dir = File.expand_path(dir)
982
- end
983
- options[:directory] = dir
984
-
985
- meta = Zillabyte::CLI::Config.get_config_info(dir, options)
986
-
987
- if meta.nil?
988
- error("could not find meta information for: #{dir}", type)
989
- end
990
-
991
- if(thread_id == "")
992
- exec(command("--execute_live --name #{name.to_s}",type, dir))
993
- else
994
- exec(command("--execute_live --name #{name.to_s} --pipe #{thread_id}",type, dir))
995
- end
996
- end
997
- alias_command "live_run", "apps:live_run"
998
-
999
-
1000
- # apps:info [DIR]
1001
- #
1002
- # outputs the info for the app in the dir.
1003
- #
1004
- # --pretty # Pretty prints the info output
1005
- # --type TYPE # specify an output type i.e. json
1006
- # --directory DIR # Directory of the app
1007
- #
1008
- def info
1009
-
1010
- dir = options[:directory] || shift_argument
1011
- if dir.nil?
1012
- dir = Dir.pwd
1013
- else
1014
- dir = File.expand_path(dir)
1015
- end
1016
- options[:directory] = dir
1017
-
1018
- info_file = "#{dir}/#{SecureRandom.uuid}"
1019
- type = options[:type]
1020
-
1021
- cmd = command("--info --file #{info_file}", type, dir)
1022
- <<<<<<< HEAD
1023
- app_info = Zillabyte::Command::Apps.get_info(cmd, info_file, dir)
1024
- =======
1025
- app_info = Zillabyte::Command::Apps.get_info(cmd, info_file, options)
1026
- >>>>>>> b7501a641428a0952068d1837b20293619aba0cd
1027
-
1028
- if type == "json"
1029
- puts app_info
1030
- else
1031
- if options[:pretty]
1032
- puts JSON.pretty_generate(JSON.parse(app_info))
1033
- else
1034
- puts app_info
1035
- end
1036
- end
1037
-
1038
- exit
1039
- end
1040
- alias_command "info", "apps:info"
1041
-
1042
-
1043
- #
1044
- # --type TYPE # specify an output type i.e. json
1045
- #
1046
- <<<<<<< HEAD
1047
- def self.get_info(cmd, info_file, dir, options = {})
1048
- =======
1049
- def self.get_info(cmd, info_file, options = {})
1050
-
1051
- >>>>>>> b7501a641428a0952068d1837b20293619aba0cd
1052
- type = options[:type]
1053
-
1054
- response = `#{cmd}`
1055
- if($?.exitstatus == 1)
1056
-
1057
- File.delete("#{info_file}") if File.exists?(info_file)
1058
-
1059
- if options[:type].nil?
1060
- exit(1)
1061
- else
1062
- Zillabyte::Helpers.error("error: #{response}", type)
1063
- end
1064
- end
1065
-
1066
- app_info = {}
1067
- File.open("#{info_file}", "r+").each do |line|
1068
- line = JSON.parse(line)
1069
- if(line["type"])
1070
- app_info["nodes"] << line
1071
- else
1072
- app_info = line
1073
- app_info["nodes"] = []
1074
- end
1075
- end
1076
- File.delete("#{info_file}")
1077
-
1078
- app_info = app_info.to_json
1079
- if(File.exists?("#{dir}/info_to_java.in"))
1080
- java_pipe = open("#{dir}/info_to_java.in","w+")
1081
- java_pipe.puts(app_info+"\n")
1082
- java_pipe.flush
1083
- java_pipe.close()
1084
- end
1085
-
1086
- app_info
1087
- end
1088
-
1089
-
1090
- private
1091
-
1092
- #
1093
- # --type TYPE # specify an output type i.e. json
1094
- #
1095
- def command(arg="--execute_live", type = nil, dir = Dir.pwd, ignore_stderr = false)
1096
- meta = Zillabyte::CLI::Config.get_config_info(dir, self, options)
1097
-
1098
- #meta = Zillabyte::API::Functions.get_rich_meta_info_from_script(dir, self)
1099
- error("could not extract meta information. missing zillabyte.conf.yml?", type) if meta.nil?
1100
- error(meta["error_message"], type) if meta['status'] == "error"
1101
- full_script = File.join(dir, meta["script"])
1102
- stderr_opt = "2> /dev/null" if ignore_stderr
1103
-
1104
- case meta["language"]
1105
- when "ruby"
1106
- # Execute in the bundler context
1107
- cmd = "cd \"#{dir}\"; unset BUNDLE_GEMFILE; ZILLABYTE_HARNESS=1 bundle exec ruby \"#{full_script}\" #{arg} #{stderr_opt}"
1108
-
1109
- when "python"#{
1110
- if(File.directory?("#{dir}/vEnv"))
1111
- cmd = "cd \"#{dir}\"; PYTHONPATH=~/zb1/multilang/python/Zillabyte #{dir}/vEnv/bin/python \"#{full_script}\" #{arg} #{stderr_opt}"
1112
- else
1113
- cmd = "cd \"#{dir}\"; PYTHONPATH=~/zb1/multilang/python/Zillabyte python \"#{full_script}\" #{arg} #{stderr_opt}"
1114
- end
1115
-
1116
- when "js"
1117
- # cmd = "#{Zillabyte::API::CASPERJS_BIN} #{Zillabyte::API::API_CLIENT_JS} \"#{full_script}\" #{arg}"
1118
- cmd = "cd \"#{dir}\"; NODE_PATH=~/zb1/multilang/js/src/lib #{Zillabyte::API::NODEJS_BIN} \"#{full_script}\" #{arg} #{stderr_opt}"
1119
-
1120
- else
1121
- error("no language specified", type)
1122
- end
1123
-
1124
- return cmd
1125
-
1126
- end
1127
-
1128
- #
1129
- #
1130
- #
1131
- def describe_app(meta, colors = {})
1132
- @colors ||= [:green, :yellow, :magenta, :cyan, :light_black, :light_green, :light_yellow, :light_blue, :light_magenta, :light_cyan]
1133
- rjust = 20
1134
- display "#{'app name'.rjust(rjust)}: #{meta['name']}"
1135
- display "#{'app language'.rjust(rjust)}: #{meta['language']}"
1136
-
1137
- meta['nodes'].each_with_index do |node, index|
1138
- colors[node['name']] ||= @colors.shift
1139
- color = colors[node['name']]
1140
- display (("="*rjust + " operation ##{index}").colorize(color))
1141
- display "#{"name".rjust(rjust)}: #{node['name'].to_s.colorize(color)}"
1142
- display "#{"type".rjust(rjust)}: #{node['type'].to_s.colorize(color)}"
1143
- display "#{"matches".rjust(rjust)}: #{JSON.pretty_generate(node['matches']).indent(rjust+2).lstrip.colorize(color)}" if node['matches']
1144
- display "#{"emits".rjust(rjust)}: #{JSON.pretty_generate(node['emits']).indent(rjust+2).lstrip.colorize(color)}" if node['emits']
1145
- end
1146
-
1147
- end
1148
-
1149
- #
1150
- #
1151
- def read_name_from_conf(options = {})
1152
- type = options[:type]
1153
- hash = Zillabyte::API::Apps.get_rich_meta_info_from_script Dir.pwd, options
1154
- error("No id given and current directory does not contain a valid Zillabyte configuration file. Please specify an app id or run command from the directory containing the app.",type) if hash["error"]
1155
- hash["name"]
1156
- end
1157
-
1158
- end