flor 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/CREDITS.md +21 -0
  4. data/LICENSE.txt +4 -1
  5. data/Makefile +4 -0
  6. data/README.md +8 -0
  7. data/flor.gemspec +10 -10
  8. data/lib/flor.rb +2 -2
  9. data/lib/flor/changes.rb +3 -3
  10. data/lib/flor/colours.rb +14 -8
  11. data/lib/flor/conf.rb +63 -58
  12. data/lib/flor/core.rb +4 -4
  13. data/lib/flor/core/executor.rb +65 -29
  14. data/lib/flor/core/node.rb +37 -20
  15. data/lib/flor/core/procedure.rb +182 -40
  16. data/lib/flor/core/texecutor.rb +125 -52
  17. data/lib/flor/djan.rb +111 -82
  18. data/lib/flor/dollar.rb +31 -30
  19. data/lib/flor/flor.rb +314 -237
  20. data/lib/flor/id.rb +7 -2
  21. data/lib/flor/log.rb +250 -245
  22. data/lib/flor/parser.rb +72 -38
  23. data/lib/flor/pcore/_arr.rb +10 -10
  24. data/lib/flor/pcore/_att.rb +49 -14
  25. data/lib/flor/pcore/_coll.rb +18 -0
  26. data/lib/flor/pcore/_obj.rb +23 -7
  27. data/lib/flor/pcore/_pat_.rb +1 -1
  28. data/lib/flor/pcore/_pat_guard.rb +8 -0
  29. data/lib/flor/pcore/_pat_obj.rb +3 -3
  30. data/lib/flor/pcore/_pat_or.rb +4 -0
  31. data/lib/flor/pcore/_pat_regex.rb +24 -0
  32. data/lib/flor/pcore/_skip.rb +4 -0
  33. data/lib/flor/pcore/_val.rb +0 -1
  34. data/lib/flor/pcore/all.rb +111 -0
  35. data/lib/flor/pcore/any.rb +83 -0
  36. data/lib/flor/pcore/arith.rb +35 -6
  37. data/lib/flor/pcore/break.rb +39 -1
  38. data/lib/flor/pcore/case.rb +82 -4
  39. data/lib/flor/pcore/cmp.rb +7 -7
  40. data/lib/flor/pcore/collect.rb +50 -0
  41. data/lib/flor/pcore/cond.rb +17 -3
  42. data/lib/flor/pcore/cursor.rb +8 -2
  43. data/lib/flor/pcore/detect.rb +45 -0
  44. data/lib/flor/pcore/each.rb +52 -0
  45. data/lib/flor/pcore/empty.rb +60 -0
  46. data/lib/flor/pcore/filter.rb +94 -0
  47. data/lib/flor/pcore/find.rb +67 -0
  48. data/lib/flor/pcore/for_each.rb +65 -0
  49. data/lib/flor/pcore/includes.rb +32 -0
  50. data/lib/flor/pcore/inject.rb +55 -0
  51. data/lib/flor/pcore/iterator.rb +151 -0
  52. data/lib/flor/pcore/keys.rb +60 -0
  53. data/lib/flor/pcore/length.rb +34 -7
  54. data/lib/flor/pcore/logo.rb +18 -0
  55. data/lib/flor/pcore/loop.rb +4 -0
  56. data/lib/flor/pcore/map.rb +77 -46
  57. data/lib/flor/pcore/match.rb +8 -2
  58. data/lib/flor/pcore/matchr.rb +4 -5
  59. data/lib/flor/pcore/move.rb +3 -3
  60. data/lib/flor/pcore/noeval.rb +13 -0
  61. data/lib/flor/pcore/not.rb +16 -0
  62. data/lib/flor/pcore/on.rb +172 -0
  63. data/lib/flor/pcore/on_cancel.rb +54 -0
  64. data/lib/flor/pcore/on_error.rb +68 -0
  65. data/lib/flor/pcore/rand.rb +2 -2
  66. data/lib/flor/pcore/range.rb +2 -1
  67. data/lib/flor/pcore/reduce.rb +124 -0
  68. data/lib/flor/pcore/reverse.rb +46 -0
  69. data/lib/flor/pcore/select.rb +72 -0
  70. data/lib/flor/pcore/set.rb +8 -0
  71. data/lib/flor/pcore/stall.rb +10 -0
  72. data/lib/flor/pcore/to_array.rb +61 -0
  73. data/lib/flor/pcore/until.rb +34 -0
  74. data/lib/flor/punit/cancel.rb +30 -5
  75. data/lib/flor/punit/ccollect.rb +11 -0
  76. data/lib/flor/punit/cmap.rb +10 -5
  77. data/lib/flor/punit/concurrence.rb +42 -51
  78. data/lib/flor/punit/cron.rb +33 -0
  79. data/lib/flor/punit/do_trap.rb +42 -0
  80. data/lib/flor/punit/every.rb +48 -13
  81. data/lib/flor/punit/graft.rb +3 -3
  82. data/lib/flor/punit/on_timeout.rb +38 -0
  83. data/lib/flor/punit/schedule.rb +69 -6
  84. data/lib/flor/punit/signal.rb +54 -0
  85. data/lib/flor/punit/sleep.rb +1 -1
  86. data/lib/flor/punit/task.rb +4 -1
  87. data/lib/flor/punit/trap.rb +188 -13
  88. data/lib/flor/tools/shell.rb +408 -62
  89. data/lib/flor/tools/shell_out.rb +31 -0
  90. data/lib/flor/unit.rb +1 -1
  91. data/lib/flor/unit/caller.rb +177 -0
  92. data/lib/flor/unit/executor.rb +1 -0
  93. data/lib/flor/unit/ganger.rb +15 -21
  94. data/lib/flor/unit/hook.rb +1 -1
  95. data/lib/flor/unit/hooker.rb +22 -10
  96. data/lib/flor/unit/loader.rb +22 -22
  97. data/lib/flor/unit/logger.rb +63 -36
  98. data/lib/flor/unit/models.rb +6 -1
  99. data/lib/flor/unit/models/execution.rb +12 -1
  100. data/lib/flor/unit/models/message.rb +7 -0
  101. data/lib/flor/unit/models/trap.rb +31 -17
  102. data/lib/flor/unit/scheduler.rb +18 -10
  103. data/lib/flor/unit/storage.rb +83 -23
  104. data/lib/flor/unit/waiter.rb +1 -2
  105. metadata +96 -52
  106. data/lib/flor/deep.rb +0 -144
  107. data/lib/flor/punit/on.rb +0 -57
  108. data/lib/flor/unit/runner.rb +0 -84
  109. data/match.md +0 -22
@@ -1,5 +1,59 @@
1
1
 
2
2
  class Flor::Pro::Signal < Flor::Procedure
3
+ #
4
+ # Used in conjuction with "on".
5
+ #
6
+ # An external (or internal) agent may send a signal to an execution and the
7
+ # execution may have a "on" handler for it.
8
+ #
9
+ # For example, imagine an execution with a sub part that checks every day
10
+ # at noon and closes cases that are over a certain date:
11
+ #
12
+ # ```
13
+ # on 'close'
14
+ # # close the case and then cancel main part...
15
+ # cancel ref: 'main'
16
+ #
17
+ # every 'day at noon'
18
+ # signal 'close' if f.date_to < today
19
+ #
20
+ # sequence tag: 'main'
21
+ # # main part ...
22
+ # ```
23
+ #
24
+ # The "every day at noon" sub part could be replaced by a signal emitted by
25
+ # a Ruby script triggered by a cron daemon, thus going from internal agent
26
+ # to external agent.
27
+ #
28
+ # The `Flor::Unit` class has a `#signal` method handy for that:
29
+ # ```ruby
30
+ # flor_unit.signal('close', exid: execution_id)
31
+ # ```
32
+ # It accepts `exid:` and `payload:` messages.
33
+ #
34
+ # ## signal payloads
35
+ #
36
+ # The payload at the point of signalling is transmitted over to the
37
+ # receiving "on" or "trap".
38
+ #
39
+ # ```
40
+ # set f.a 'A'
41
+ # signal 'close'
42
+ # set f.b 'B'
43
+ # [ 0 1 2 ]
44
+ # set f.c 'C'
45
+ # ```
46
+ # passes `{ 'ret' => [ 0, 1, 2 ], 'a' => 'A', 'b' => 'B' }` as payload
47
+ # to any intercepting "on" or "trap" (`c` is not passed).
48
+ #
49
+ # Externally, you can signal with a specific payload thanks to:
50
+ # ```ruby
51
+ # flor_unit.signal('close', exid: execution_id, payload: { 'f0' => 'zero' })
52
+ # ```
53
+ #
54
+ # ## see also
55
+ #
56
+ # On and trap.
3
57
 
4
58
  name 'signal'
5
59
 
@@ -19,7 +19,7 @@ class Flor::Pro::Sleep < Flor::Procedure
19
19
  def receive_last
20
20
 
21
21
  t = att('for', nil)
22
- fail ArgumentError.new("missing a sleep time duration") unless t
22
+ fail Flor::FlorError.new("missing a sleep time duration", self) unless t
23
23
 
24
24
  m = wrap('point' => 'receive').first
25
25
 
@@ -43,11 +43,11 @@ class Flor::Pro::Task < Flor::Procedure
43
43
  wrap(
44
44
  'point' => 'task',
45
45
  'exid' => exid, 'nid' => nid,
46
+ 'tags' => list_tags,
46
47
  'tasker' => tasker,
47
48
  'taskname' => taskname,
48
49
  'attl' => attl, 'attd' => attd,
49
50
  'payload' => determine_payload)
50
- #.tap { |x| pp x.first }
51
51
  end
52
52
 
53
53
  def cancel
@@ -59,6 +59,7 @@ class Flor::Pro::Task < Flor::Procedure
59
59
  wrap(
60
60
  'point' => 'detask',
61
61
  'exid' => exid, 'nid' => nid,
62
+ 'tags' => list_tags,
62
63
  'tasker' => att(nil),
63
64
  'attl' => attl, 'attd' => attd,
64
65
  'payload' => determine_payload)
@@ -66,6 +67,8 @@ class Flor::Pro::Task < Flor::Procedure
66
67
 
67
68
  protected
68
69
 
70
+ # Returns an array attribute list / attribute dictionary.
71
+ #
69
72
  def determine_atts
70
73
 
71
74
  attl, attd = [], {}
@@ -1,17 +1,186 @@
1
1
 
2
- # # trap
3
- #
4
- # ## range:/scope:
5
- # * subnid (default)
6
- # * execution/exe
7
- # * domain
8
- # * subdomain
9
- #
10
- # ## bind:
11
- # * parent (default)
12
- # * root
13
- #
2
+ # Would it be worth the pain implementing bind:?
3
+
14
4
  class Flor::Pro::Trap < Flor::Procedure
5
+ #
6
+ # Watches the messages emitted in the execution and reacts when
7
+ # a message matches certain criteria.
8
+ #
9
+ # Once the trap is set (once the execution interprets its branch), it
10
+ # will trigger for any matching message, unless the `count:` attribute
11
+ # is set.
12
+ #
13
+ # When the execution terminates, the trap is removed as well.
14
+ #
15
+ # By default, the observation range is the execution, only messages
16
+ # in the execution where the trap was set are considered.
17
+ # The trap can be extended via the `range:` attribute.
18
+ #
19
+ # "trap" triggers a function, while "on" triggers a block.
20
+ #
21
+ # ## the point: criterion
22
+ #
23
+ # The simplest thing to trap is a 'point'. Here, the trap is set for
24
+ # any message whose point is 'terminated':
25
+ # ```
26
+ # sequence
27
+ # trap 'terminated'
28
+ # def msg \ trace "terminated(f:$(msg.from))"
29
+ # trace "here($(nid))"
30
+ # # OR
31
+ # #sequence
32
+ # # trap 'terminated'
33
+ # # def msg \ trace "terminated(f:$(msg.from))"
34
+ # # trace "here($(nid))"
35
+ # ```
36
+ #
37
+ # ## the heap: criterion
38
+ #
39
+ # Given a procedure like `sequence`, `concurrence` or `apply`, trigger
40
+ # a piece of code each time the procedure receives the "execute" or the
41
+ # "receive" message.
42
+ #
43
+ # ```
44
+ # trap heap: 'sequence'
45
+ # def msg
46
+ # trace "$(msg.point)-$(msg.tree.0)-$(msg.nid)<-$(msg.from)"
47
+ # sequence
48
+ # noret _
49
+ # #
50
+ # # will trace:
51
+ # # 0:execute-sequence-0_1<-0
52
+ # # 1:receive--0_1<-0_1_0
53
+ # # 2:receive--0<-0_1
54
+ # ```
55
+ #
56
+ # ## the heat: criterion
57
+ #
58
+ # While `heap:` filters on the actual flor procedure names, `heat:` is
59
+ # looser, it catches whatever stands "at the beginning of the flor line".
60
+ # It's useful to catch function calls.
61
+ #
62
+ # ```
63
+ # trap heat: 'fun0'
64
+ # def msg
65
+ # trace "t-$(msg.tree.0)-$(msg.nid)"
66
+ # define fun0
67
+ # trace "c-fun0-$(nid)"
68
+ # sequence
69
+ # fun0 _
70
+ # fun0 # not a call
71
+ #
72
+ # # will trace:
73
+ # # 0:t-fun0-0_2_0
74
+ # # 1:c-fun0-0_1_1_0_0-2
75
+ # # 2:t--0_2_0
76
+ # # 3:t--0_2_0
77
+ # # 4:t-fun0-0_2_1
78
+ # ```
79
+ #
80
+ # `heat:` accepts strings or regular expressions:
81
+ #
82
+ # ```
83
+ # trap heat: [ /^fun\d+$/ 'funx' ]
84
+ # def msg \ trace "t-$(msg.tree.0)-$(msg.nid)"
85
+ # ```
86
+ #
87
+ # ## the tag: criterion
88
+ #
89
+ # Traps one or multiple tags. Default to trapping on "entered". May trap
90
+ # leaving tags with `point: 'left'` or any with
91
+ # `point: [ 'entered', 'left' ]`.
92
+ #
93
+ # ```
94
+ # trap tag: 'x'
95
+ # def msg
96
+ # trace "$(msg.tags.-1)-$(msg.point)"
97
+ # # ...
98
+ # sequence tag: 'x'
99
+ # trace 'c'
100
+ #
101
+ # # will trace "x-entered" and "c"
102
+ # ```
103
+ #
104
+ # ## the signal: criterion
105
+ #
106
+ # `signal:` traps signals directed at the execution. Signals are
107
+ # directly
108
+ # ```
109
+ # trap signal: 'S0'
110
+ # def msg
111
+ # trace "S0"
112
+ # # ...
113
+ # signal 'S0'
114
+ # ```
115
+ #
116
+ # Note that it's OK to trap all signals, whatever their name, directed at
117
+ # the execution by using `point: 'signal'`, as in:
118
+ # ```
119
+ # trap point: 'signal'
120
+ # def msg
121
+ # trace "caught signal '$(sig)'"
122
+ # ```
123
+ #
124
+ # [on](on.md) is a macro that turns
125
+ # ```
126
+ # on 'rejected'
127
+ # trace "execution received signal $(sig)"
128
+ # ```
129
+ # into
130
+ # ```
131
+ # trap signal: 'rejected'
132
+ # def msg
133
+ # trace "execution received signal $(sig)"
134
+ # ```
135
+ # Please note how "on" accepts a block while "trap" accepts a function.
136
+ #
137
+ # ## the range: limit
138
+ #
139
+ # The range limit determines what is the range, or scope of the trap.
140
+ # By default the trap only care about nodes below its parent. In other words,
141
+ # the trap binds itself to its parent and observes the messages occuring
142
+ # in the execution in the branch of nodes whose root is the parent.
143
+ #
144
+ # The possible values for `range:` are:
145
+ # * 'subnid' (default)
146
+ # * 'execution'
147
+ # * 'domain'
148
+ # * 'subdomain'
149
+ #
150
+ # With 'execution', the trap observes all the messages emitted within the
151
+ # same execution.
152
+ #
153
+ # With 'domain', all the messages in all the execution of the same domain are
154
+ # observed. For example,
155
+ # ```
156
+ # trap point: 'signal', domain: 'net.acme'
157
+ # def msg \ trace "signal '$(sig)' caught"
158
+ # ```
159
+ # once set, will be triggered for each signal received by an execution in the
160
+ # 'net.acme' domain.
161
+ #
162
+ # Likewise,
163
+ # ```
164
+ # trap point: 'signal', subdomain: 'org.acme'
165
+ # def msg \ trace "signal '$(sig)' caught"
166
+ # ```
167
+ # Will trap all signals in the domain "org.acme" and its subdomains,
168
+ # (org.acme.accounting, org.acme.engineering, org.acme.whatever.x.y.z, ...)
169
+ #
170
+ #
171
+ # ## the count: limit
172
+ #
173
+ # ```
174
+ # trap tag: 'x' count: 2
175
+ # # ...
176
+ # ```
177
+ # will trigger when the execution enters the tag 'x', but will trigger only
178
+ # twice.
179
+ #
180
+ #
181
+ # ## see also
182
+ #
183
+ # On and signal.
15
184
 
16
185
  name 'trap'
17
186
 
@@ -40,9 +209,15 @@ class Flor::Pro::Trap < Flor::Procedure
40
209
  points = att_a(nil, nil) unless points || tags
41
210
  points = [ 'entered' ] if tags && ! points
42
211
 
212
+ att_a('sig', 'signal', 'signals', [])
213
+ .each { |sig| (points ||= []) << 'signal'; (names ||= []) << sig }
214
+
215
+ points = points.uniq if points
216
+ names = names.uniq if names
217
+
43
218
  msg =
44
219
  if fun
45
- apply(fun, [], tree[2], false).first
220
+ apply(fun, [], tree[2], anid: false).first
46
221
  else
47
222
  wrap_reply.first
48
223
  end
@@ -1,8 +1,10 @@
1
1
 
2
+ require 'io/console'
2
3
  require 'terminal-table'
3
4
 
4
5
  require 'flor'
5
6
  require 'flor/unit'
7
+ require 'flor/tools/shell_out'
6
8
 
7
9
 
8
10
  module Flor::Tools
@@ -25,6 +27,8 @@ module Flor::Tools
25
27
  prepare_hooks
26
28
 
27
29
  @hook = 'on'
30
+ @mute = false
31
+ @paging = true
28
32
 
29
33
  @unit.start
30
34
 
@@ -35,7 +39,9 @@ module Flor::Tools
35
39
 
36
40
  @c = Flor.colours({})
37
41
 
38
- if argv.any?
42
+ load_floshrc
43
+
44
+ if argv && argv.any?
39
45
  do_eval(argv.join(' '))
40
46
  else
41
47
  print_header
@@ -47,6 +53,11 @@ module Flor::Tools
47
53
 
48
54
  protected
49
55
 
56
+ def parse_single_json_value(s)
57
+
58
+ Flor::ConfExecutor.interpret_line(s) rescue nil
59
+ end
60
+
50
61
  def prepare_home
51
62
 
52
63
  home = File.join(@root, 'home')
@@ -102,15 +113,35 @@ module Flor::Tools
102
113
  end
103
114
  end
104
115
 
116
+ def load_floshrc
117
+
118
+ @mute = true
119
+
120
+ [
121
+ File.join(@root, 'etc/floshrc'),
122
+ File.join(@root, 'home/.floshrc'),
123
+ '.floshrc'
124
+ ].each { |f| (File.readlines(f) rescue []).each { |l| do_eval(l) } }
125
+
126
+ ensure
127
+
128
+ @mute = false
129
+ end
130
+
105
131
  def print_header
106
132
 
107
- puts "flosh - a flor #{Flor::VERSION} shell"
133
+ git = (' ' + `git log -1`.lines.first.split.last[0, 7]) rescue ''
134
+ git = "#{@c.yellow}#{git}#{@c.reset}"
135
+
136
+ puts "flosh - a flor #{Flor::VERSION}#{git} shell"
108
137
  end
109
138
 
110
139
  def prompt
111
140
 
112
141
  root = @c.dark_gray(@root + '/')
113
142
 
143
+ time = Time.now.strftime(' %H:%M:%S')
144
+
114
145
  ec = @unit.executions.where(status: 'active').count
115
146
  exes = ' ' + @c.yellow("ex#{ec}")
116
147
 
@@ -123,13 +154,13 @@ module Flor::Tools
123
154
  end
124
155
  tas = ta > 0 ? ' ' + @c.yellow("ta#{ta}") : ''
125
156
 
126
- "#{root}#{exes}#{tis}#{tas} > "
157
+ "#{root}#{time}#{exes}#{tis}#{tas} > "
127
158
  end
128
159
 
129
160
  def do_eval(line)
130
161
 
131
- line = line.strip
132
- return if line[0, 1] == '#'
162
+ line = line.match(/\A([^#]*)(#.+)?\n*\z/)[1]
163
+ return if line == ''
133
164
 
134
165
  md = line.split(/\s/).first
135
166
  cmd = "cmd_#{md}".to_sym
@@ -155,7 +186,6 @@ module Flor::Tools
155
186
  line = prompt_and_read
156
187
 
157
188
  break unless line
158
- next if line.strip == ''
159
189
 
160
190
  do_eval(line)
161
191
  end
@@ -166,15 +196,26 @@ module Flor::Tools
166
196
  #
167
197
  # command helpers
168
198
 
199
+ ALIASES = {}
200
+
169
201
  def self.make_alias(a, b)
170
202
 
171
203
  define_method("hlp_#{a}") { "alias to #{b.inspect}" }
172
204
  alias_method "man_#{a}", "man_#{b}" rescue nil
173
205
  alias_method "cmd_#{a}", "cmd_#{b}"
206
+
207
+ (ALIASES[b] ||= []) << a
174
208
  end
175
209
 
176
- def fname(line, index=1); line.split(/\s+/)[index]; end
177
- alias arg fname
210
+ def self.is_alias?(c)
211
+
212
+ !! ALIASES.values.find { |a| a.include?(c) }
213
+ end
214
+
215
+ def args(line); line.split(/\s+/); end
216
+ def argc(line); args(line).count; end
217
+ def arg(line, index=1); args(line)[index]; end
218
+ alias fname arg
178
219
 
179
220
  def choose_path(line)
180
221
 
@@ -186,9 +227,19 @@ module Flor::Tools
186
227
  when /\Ap/
187
228
  @payload_path
188
229
  when /\At/
230
+ fail ArgumentError.new("'frag' argument missing") unless b
189
231
  Dir[File.join(@root, 'var/tasks/**/*.json')].find { |pa| pa.index(b) }
190
232
  when /\Ar/
191
233
  @ra_flow_path
234
+ when /\Ae/
235
+ exe = lookup_execution(b)
236
+ #p exe
237
+ #pp exe.data
238
+ #puts JSON.pretty_generate(exe.data)
239
+ #puts JSON.dump(exe.data)
240
+ #puts JSON.generate(exe.data, indent: ' ', space: ' ', object_nl: "\n", array_nl: "\n")
241
+ puts Flor.to_d(exe.data, width: true, colours: true)
242
+ fail NotImplementedError
192
243
  else
193
244
  @flow_path
194
245
  end
@@ -232,19 +283,162 @@ module Flor::Tools
232
283
  .find { |e| e.data['nodes'][nid] }
233
284
 
234
285
  fail ArgumentError.new(
235
- "found #{exes.count} execution#{exes.count > 1 ? 's' : ''} " +
286
+ "found #{exes.count} execution#{exes.count != 1 ? 's' : ''} " +
236
287
  "matching \"%#{exid}%\", but none with a #{onid.inspect} node"
237
288
  ) unless exe
238
289
 
239
290
  [ exe.exid, nid ]
240
291
  end
241
292
 
293
+ def print(o)
294
+
295
+ ::Kernel.print(o) unless @mute
296
+ end
297
+
298
+ def puts(o)
299
+
300
+ ::Kernel.puts(o) unless @mute
301
+ end
302
+
303
+ def page_puts(s)
304
+ ::Kernel.puts s
305
+ end
306
+ def page_vi(s)
307
+ IO.popen(
308
+ @unit.conf['fls_vi'] || 'vi -',
309
+ mode: 'w'
310
+ ) { |io| io.write(Flor.decolour(s)) }
311
+ end
312
+ def page_more(s)
313
+ IO.popen(
314
+ @unit.conf['fls_more'] || 'less -R -N',
315
+ mode: 'w'
316
+ ) { |io| io.write(s) }
317
+ end
318
+
319
+ def page(o)
320
+
321
+ return if @mute
322
+
323
+ s = o.is_a?(String) ? o : o.string
324
+
325
+ if @paging == false
326
+ page_puts(s)
327
+ elsif @paging == :vi
328
+ page_vi(s)
329
+ elsif @paging == :more
330
+ page_more(s)
331
+ else
332
+ if s.lines.to_a.size > IO.console.winsize[0]
333
+ page_more(s)
334
+ else
335
+ page_puts(s)
336
+ end
337
+ end
338
+ end
339
+
242
340
  #
243
341
  # the commands
244
342
 
343
+ def hlp_paging
344
+ %{ sets the paging mode }
345
+ end
346
+ def man_paging
347
+ %{
348
+ * paging off
349
+ disable paging, command output will always be output to stdout
350
+ * paging on
351
+ enable paging, command output longer than terminal height will be paged
352
+ * paging vi|vim
353
+ always page into vim, whatever length, loses the colours
354
+ * paging more|less
355
+ always page into vim, whatever length, loses the colours
356
+ }
357
+ end
358
+ def cmd_paging(line)
359
+
360
+ @paging =
361
+ case arg(line)
362
+ when 'vi', 'vim' then :vi
363
+ when 'no', 'off', 'false' then false
364
+ when 'more', 'less' then :more
365
+ #when 'on', 'true' then true
366
+ else true
367
+ end
368
+ puts "paging set to #{@paging}"
369
+ end
370
+
371
+ def hlp_read
372
+ %{ reads (pages) a file }
373
+ end
374
+ def man_read
375
+ %{
376
+ * read filename
377
+ reads (pages) a file
378
+ }
379
+ end
380
+ def cmd_read(line)
381
+ page(File.read(arg(line)))
382
+ end
383
+
384
+ def hlp_page
385
+ %{ pages the output of the remainder of the command line }
386
+ end
387
+ def man_page
388
+ %{
389
+ * page filepath
390
+ reads the file and pages it
391
+ * page command arg0 arg1 ... argN
392
+ runs `command arg0 arg1 ... argN` and pages its output
393
+ }
394
+ end
395
+ def cmd_page(line, paging=:more)
396
+
397
+ current_paging = @paging
398
+ @paging = paging
399
+
400
+ line = line.strip.split(/\s+/, 2)[1]
401
+
402
+ if line.nil? || line.empty?
403
+ do_eval('man page')
404
+ elsif File.exist?(line) && ! File.directory?(line)
405
+ page(File.read(line))
406
+ else
407
+ do_eval(line)
408
+ end
409
+
410
+ ensure
411
+
412
+ @paging = current_paging
413
+ end
414
+
415
+ def hlp_vi
416
+ %{ runs the remainder of the command line and then edit output in vi }
417
+ end
418
+ def man_vi
419
+ %{
420
+ * vi filepath
421
+ reads the file and edits it
422
+ * vi command arg0 arg1 ... argN
423
+ runs `command arg0 arg1 ... argN` and opens its output with vi
424
+ }
425
+ end
426
+ def cmd_vi(line)
427
+ cmd_page(line, :vi)
428
+ end
429
+ make_alias('vim', 'vi')
430
+
245
431
  def hlp_launch
246
432
  %{ launches a new execution of #{@flow_path} }
247
433
  end
434
+ def man_launch
435
+ %{
436
+ * launch
437
+ launches an execution of the current flow
438
+ * launch k0: v0, k1: v1, ...
439
+ launches an execution with the given variables
440
+ }
441
+ end
248
442
  def cmd_launch(line)
249
443
 
250
444
  flow = File.read(@flow_path)
@@ -252,6 +446,9 @@ module Flor::Tools
252
446
  payload = Flor::ConfExecutor.interpret(@payload_path)
253
447
  domain = 'shell'
254
448
 
449
+ vars = Flor::ConfExecutor.interpret_line("\n" + line[6..-1]) rescue {}
450
+ variables.merge!(vars)
451
+
255
452
  exid = @unit.launch(
256
453
  flow, domain: domain, vars: variables, payload: payload)
257
454
 
@@ -271,13 +468,16 @@ module Flor::Tools
271
468
  end
272
469
  def cmd_help(line)
273
470
 
471
+ o = StringIO.new
472
+
274
473
  if cmd = arg(line)
474
+
275
475
  begin
276
476
  send("man_#{cmd}").split("\n").collect(&:strip).each do |l|
277
477
  if l[0, 1] == '*'
278
- puts " #{@c.dg}*#{@c.rs} #{l[1..-1].strip}"
478
+ o.puts " #{@c.dg}*#{@c.rs} #{l[1..-1].strip}"
279
479
  else
280
- puts " #{@c.dark_gray(l)}"
480
+ o.puts " #{@c.dark_gray(l)}"
281
481
  end
282
482
  end
283
483
  rescue => err
@@ -286,16 +486,20 @@ module Flor::Tools
286
486
 
287
487
  else
288
488
 
289
- puts
290
- puts "## available commands:"
291
- puts
489
+ o.puts
490
+ o.puts "## available commands:"
491
+ o.puts
292
492
  COMMANDS.each do |cmd|
293
- print "* #{@c.yellow(cmd)}"
294
- if hlp = (send("hlp_#{cmd}") rescue nil); print " - #{hlp.strip}"; end
295
- puts
493
+ o.print "* #{@c.yellow(cmd)}"
494
+ if hlp = (send("hlp_#{cmd}") rescue nil)
495
+ o.print " - #{hlp.strip}"
496
+ end
497
+ o.puts
296
498
  end
297
- puts
499
+ o.puts
298
500
  end
501
+
502
+ page(o)
299
503
  end
300
504
  make_alias('h', 'help')
301
505
  make_alias('man', 'help')
@@ -324,7 +528,7 @@ module Flor::Tools
324
528
  def cmd_parse(line)
325
529
 
326
530
  source = File.read(@flow_path)
327
- tree = Flor::Lang.parse(source, nil, {})
531
+ tree = Flor.parse(source, nil, {})
328
532
 
329
533
  case arg(line)
330
534
  when 'd', 'raw'
@@ -336,7 +540,7 @@ module Flor::Tools
336
540
  when 'p'
337
541
  p tree
338
542
  else
339
- Flor.print_tree(tree, '0', headers: false)
543
+ puts Flor.tree_to_s(tree, '0', headers: false)
340
544
  end
341
545
  end
342
546
 
@@ -365,20 +569,62 @@ module Flor::Tools
365
569
  puts "not found"
366
570
  end
367
571
  end
572
+ make_alias('e', 'edit')
573
+
574
+ def hlp_cat
575
+ %{ prints the current flow }
576
+ end
577
+ def man_cat
578
+ %{
579
+ * cat
580
+ prints the current flow
581
+ }
582
+ end
583
+ def cmd_cat(line)
584
+
585
+ puts " # #{@flow_path}\n"
586
+ File.readlines(@flow_path).each { |line| puts " #{line}" }
587
+ end
368
588
 
369
589
  def hlp_conf
370
- %{ prints current unit configuration }
590
+ %{ prints or sets in current unit configuration }
591
+ end
592
+ def man_conf
593
+ %{
594
+ * conf
595
+ prints current unit configuration
596
+ * conf key
597
+ prints value for a single key
598
+ * conf key value
599
+ sets value for key
600
+ }
371
601
  end
372
602
  def cmd_conf(line)
373
- puts Flor.to_d(@unit.conf, colour: true, indent: 1, width: true)
603
+
604
+ key, value = arg(line), (args(line)[2..-1] || []).join(' ')
605
+
606
+ if key && @unit.conf.has_key?(key)
607
+ puts Flor.to_d(
608
+ { key: @unit.conf[key] }, colour: true, indent: 1, width: true)
609
+ end
610
+
611
+ if key && argc(line) > 2
612
+ @unit.conf[key] = parse_single_json_value(value)
613
+ puts " #{@c.dg}# ==>#{@c.reset}"
614
+ puts Flor.to_d(
615
+ { key: @unit.conf[key] }, colour: true, indent: 1, width: true)
616
+ elsif key
617
+ # alreay done
618
+ else
619
+ page(Flor.to_d(@unit.conf, colour: true, indent: 1, width: true))
620
+ end
374
621
  end
375
622
 
376
623
  def hlp_t
377
624
  %{ prints the file hierarchy for #{@root} }
378
625
  end
379
626
  def cmd_t(line)
380
- puts
381
- system("tree -C #{@root}")
627
+ page(`tree -C #{@root}`)
382
628
  end
383
629
 
384
630
  def hlp_tasks
@@ -394,6 +640,8 @@ module Flor::Tools
394
640
  end
395
641
  def cmd_tasks(line)
396
642
 
643
+ o = StringIO.new
644
+
397
645
  frag = arg(line)
398
646
 
399
647
  table = Terminal::Table.new(
@@ -425,33 +673,54 @@ module Flor::Tools
425
673
  table.add_row([
426
674
  aright(i), tasker, nid, @c.yellow(exid), pl, mt ]) }
427
675
 
428
- puts table
429
- puts "#{tas.count} task#{tas.count > 1 ? 's' : ''}.\n"
676
+ o.puts table
677
+ o.puts "#{tas.count} task#{tas.count != 1 ? 's' : ''}.\n"
678
+
679
+ page(o)
430
680
  end
431
681
  make_alias('tas', 'tasks')
432
682
 
433
683
  def hlp_executions
434
684
  %{ lists the executions currently active }
435
685
  end
686
+ def man_executions
687
+ %{
688
+ * executions
689
+ lists all the executions currently active
690
+ * executions all
691
+ lists all the executions
692
+ }
693
+ end
436
694
  def cmd_executions(line)
437
695
 
696
+ o = StringIO.new
697
+
438
698
  exes = @unit.executions
439
699
  exes = exes.where(status: 'active') unless arg(line) == 'all'
440
700
 
441
701
  table = Terminal::Table.new(
442
702
  #title: 'executions',
443
- headings: %w[ id exid started ],
703
+ headings: %w[ id exid name started tis status ],
444
704
  style: table_style)
445
705
  #table.align_column(0, :right)
446
706
 
447
707
  exes
448
708
  .each { |e|
709
+ vs = e.zero_variables
449
710
  table.add_row([
450
- aright(e.id), @c.yl(e.exid), e.ctime[0, 19]
711
+ aright(e.id),
712
+ @c.yl(e.exid),
713
+ %w[ execution_name process_name flow_name name ]
714
+ .collect { |k| vs[k] }.compact.first,
715
+ e.ctime[0, 19],
716
+ aright(@unit.timers.where(exid: e.exid, status: 'active').count),
717
+ e.failed? ? 'failed' : 'running'
451
718
  ]) }
452
719
 
453
- puts table
454
- puts "#{exes.count} execution#{exes.count > 1 ? 's' : ''}.\n"
720
+ o.puts table
721
+ o.puts "#{exes.count} execution#{exes.count != 1 ? 's' : ''}.\n"
722
+
723
+ page(o)
455
724
  end
456
725
  make_alias('exes', 'executions')
457
726
 
@@ -460,6 +729,8 @@ module Flor::Tools
460
729
  end
461
730
  def cmd_timers(line)
462
731
 
732
+ o = StringIO.new
733
+
463
734
  tis = @unit.timers
464
735
  tis = tis.where(status: 'active') unless arg(line) == 'all'
465
736
 
@@ -477,8 +748,10 @@ module Flor::Tools
477
748
  aright(t.schedule), t.ntime[0, 19]
478
749
  ]) }
479
750
 
480
- puts table
481
- puts "#{tis.count} timer#{tis.count > 1 ? 's' : ''}.\n"
751
+ o.puts table
752
+ o.puts "#{tis.count} timer#{tis.count != 1 ? 's' : ''}.\n"
753
+
754
+ page(o)
482
755
  end
483
756
  make_alias('tis', 'timers')
484
757
 
@@ -508,6 +781,8 @@ module Flor::Tools
508
781
  "couldn't find a task matching #{t.inspect}"
509
782
  ) unless path
510
783
 
784
+ puts "found task at #{path}"
785
+
511
786
  m = JSON.parse(File.read(path))
512
787
  @unit.return(m)
513
788
  FileUtils.rm(path)
@@ -530,10 +805,13 @@ module Flor::Tools
530
805
  @unit.conf.select! { |k, v| ! k.match(/\Alog_/) }
531
806
 
532
807
  rest = line.match(/\A[a-z]+(\s+.+)?/)[1]
533
- rest = nil if rest && rest.strip == 'off'
534
- rest = 'stdout,dbg' if rest && rest.strip == 'on'
808
+ rest = rest.strip if rest
809
+ rest = nil if rest && rest == 'off'
810
+ rest = 'stdout,dbg' if rest && %w[ on 1 ].include?(rest)
811
+
812
+ @unit.conf.merge!(Flor::Conf.interpret_flor_debug(debug: rest)) if rest
535
813
 
536
- @unit.conf.merge!(Flor::Conf.interpret_flor_debug(rest)) if rest
814
+ cmd_conf('') # display conf
537
815
  end
538
816
 
539
817
  def hlp_hook
@@ -584,39 +862,49 @@ module Flor::Tools
584
862
  table
585
863
  end
586
864
 
865
+ def lookup_execution(id)
866
+
867
+ if id.match(/\A\d+\z/)
868
+ @unit.executions[id.to_i] ||
869
+ fail(ArgumentError.new("execution #{id} not found"))
870
+ else
871
+ @unit.executions.first(Sequel.like(:exid, "%#{id}%")) ||
872
+ fail(ArgumentError.new("execution matching \"%#{id}%\" not found"))
873
+ end
874
+ end
875
+
587
876
  def detail_execution(id)
588
877
 
589
- exe =
590
- if id.match(/\A\d+\z/)
591
- @unit.executions[id.to_i] ||
592
- fail(ArgumentError.new("execution #{id} not found"))
593
- else
594
- @unit.executions.first(Sequel.like(:exid, "%#{id}%")) ||
595
- fail(ArgumentError.new("execution matching \"%#{id}%\" not found"))
596
- end
878
+ o = StringIO.new
879
+
880
+ exe = lookup_execution(id)
597
881
 
598
882
  eid = { id: exe.id, exid: exe.exid }.inspect
599
883
  con = Flor::Storage.from_blob(exe.values.delete(:content))
600
884
  exe.values[:content] = '...'
601
885
 
602
- puts @c.dg("--- exe #{eid} :")
603
- puts h_to_table(exe.values)
604
- puts @c.dg(" exe #{eid} content:")
886
+ o.puts @c.dg("--- exe #{eid} :")
887
+ o.puts h_to_table(exe.values)
888
+ o.puts @c.dg(" exe #{eid} content:")
605
889
  nodes = con.delete('nodes')
606
890
  con['nodes'] = '...'
607
- puts indent(' ', h_to_table(con))
608
- puts @c.dg(" exe #{eid} content/nodes:")
891
+ o.puts indent(' ', h_to_table(con))
892
+ o.puts @c.dg(" exe #{eid} content/nodes:")
609
893
  table = Terminal::Table.new(
610
894
  headings: %w[ nid data ],
611
895
  style: table_style.merge(all_separators: true))
612
896
  nodes.each do |k, v|
613
897
  table.add_row([ k, Flor.to_d(v, colour: true, indent: 0, width: 70) ])
614
898
  end
615
- puts indent(' ', table)
899
+ o.puts indent(' ', table)
900
+
901
+ page(o)
616
902
  end
617
903
 
618
904
  def detail_timer(id)
619
905
 
906
+ o = StringIO.new
907
+
620
908
  timer = @unit.timers[id.to_i]
621
909
 
622
910
  fail ArgumentError.new("timer #{id} not found") unless timer
@@ -626,15 +914,33 @@ module Flor::Tools
626
914
  con = Flor::Storage.from_blob(timer.delete(:content))
627
915
  timer[:content] = '...'
628
916
 
629
- puts @c.dg("--- timer #{tid} :")
630
- puts Flor.to_d(timer, colour: true, indent: 1, width: true)
631
- puts @c.dg("--- timer #{tid} content:")
632
- puts Flor.to_d(con, colour: true, indent: 1, width: true)
917
+ o.puts @c.dg("--- timer #{tid} :")
918
+ o.puts Flor.to_d(timer, colour: true, indent: 1, width: true)
919
+ o.puts @c.dg("--- timer #{tid} content:")
920
+ o.puts Flor.to_d(con, colour: true, indent: 1, width: true)
921
+
922
+ page(o)
633
923
  end
634
924
 
635
925
  def detail_task(id)
636
926
 
637
- fail NotImplementedError
927
+ pa = Dir["#{@root}/var/tasks/**/*#{id}*.json"].first
928
+
929
+ fail ArgumentError.new("found no task matching #{id}") unless pa
930
+
931
+ ta = JSON.load(File.read(pa))
932
+ tconf = ta['tconf']; ta['tconf'] = '...'
933
+ payload = ta['payload']; ta['payload'] = '...'
934
+
935
+ o = StringIO.new
936
+ o.puts "#{@c.dg} #\n # path: #{pa}\n ##{@c.reset}"
937
+ o.puts Flor.to_d(ta, colour: true, indent: 1, width: true)
938
+ o.puts "#{@c.dg} # tconf:#{@c.reset}"
939
+ o.puts Flor.to_d(tconf, colour: true, indent: 3, width: true)
940
+ o.puts "#{@c.dg} # payload:#{@c.reset}"
941
+ o.puts Flor.to_d(payload, colour: true, indent: 3, width: true)
942
+
943
+ page(o)
638
944
  end
639
945
 
640
946
  def hlp_detail
@@ -646,7 +952,7 @@ fail NotImplementedError
646
952
  displays full execution information
647
953
  * detail ti|timer id
648
954
  displays full timer information
649
- * detail task frag
955
+ * detail t|task frag
650
956
  displays task
651
957
  }
652
958
  end
@@ -688,7 +994,7 @@ fail NotImplementedError
688
994
  end
689
995
  make_alias('can', 'cancel')
690
996
 
691
- def render_node(t, nid)
997
+ def render_node(o, t, nid)
692
998
 
693
999
  ni = nid
694
1000
  head = t[0]
@@ -703,12 +1009,12 @@ fail NotImplementedError
703
1009
  rest = @c.green(" <--")
704
1010
  end
705
1011
 
706
- puts " #{ni} #{head}#{rest}"
1012
+ o.puts " #{ni} #{head}#{rest}"
707
1013
 
708
1014
  return unless t[1].is_a?(Array)
709
1015
 
710
1016
  t[1].each_with_index do |ct, i|
711
- render_node(t[1][i], "#{nid}_#{i}")
1017
+ render_node(o, t[1][i], "#{nid}_#{i}")
712
1018
  end
713
1019
  end
714
1020
 
@@ -724,12 +1030,18 @@ fail NotImplementedError
724
1030
  end
725
1031
  def cmd_render(line)
726
1032
 
1033
+ o = StringIO.new
1034
+
727
1035
  frag = arg(line)
728
1036
 
729
1037
  exe =
730
1038
  @unit.executions.first(Sequel.like(:exid, "%#{frag}%")) ||
731
- fail(ArgumentError.new("execution matching \"%#{id}%\" not found"))
732
- #p exe
1039
+ fail(ArgumentError.new("execution matching \"%#{frag}%\" not found"))
1040
+ o.puts
1041
+ o.puts " exid: #{@c.yellow(exe.exid)}"
1042
+ o.puts " status: #{@c.yellow(exe.status)}"
1043
+ o.puts
1044
+
733
1045
  tree = exe.full_tree
734
1046
  nodes = exe.nodes
735
1047
 
@@ -752,7 +1064,9 @@ fail NotImplementedError
752
1064
  end
753
1065
  end
754
1066
 
755
- render_node(tree, '0')
1067
+ render_node(o, tree, '0')
1068
+
1069
+ page(o)
756
1070
  end
757
1071
  make_alias('r', 'render')
758
1072
 
@@ -776,12 +1090,44 @@ fail NotImplementedError
776
1090
  puts @c.yellow("re-apply message queued for #{exid} #{nid}")
777
1091
  end
778
1092
 
1093
+ def hlp_flosh
1094
+ %{ displays flosh explanation }
1095
+ end
1096
+ def man_flosh
1097
+ %{
1098
+ * flosh
1099
+ displays flosh explanation
1100
+ }
1101
+ end
1102
+ def cmd_flosh(line)
1103
+ page %{
1104
+ #{@c.yellow}# flosh#{@c.reset}
1105
+
1106
+ Flosh is a flor shell.
1107
+
1108
+ It's meant for demonstration purposes, to show how flor works.
1109
+
1110
+ Opening a flor shell, starts a flor scheduler pointing to #{@root}
1111
+
1112
+ #{@c.yellow}# flosh provided taskers#{@c.reset}
1113
+
1114
+ The following names can be used as "nato" taskers: alpha, bravo, charly, delta, fox, foxtrott, golf, echo, and hotel.
1115
+
1116
+ Nato taskers simply put their tasks under #{@root}/var/tasks/{tasker-name}/ as JSON files. Those file can be edited with the `edit task {frag}`.
1117
+
1118
+ Once edited (or not), a nato tasker task can be returned to flor (to the scheduler) with `return {frag}`.
1119
+ }
1120
+ end
1121
+
779
1122
  #
780
- # use Readline if possible
1123
+ # enumerate commands (for cmd_help)
781
1124
 
782
1125
  COMMANDS = self.allocate.methods \
783
1126
  .select { |m| m.to_s.match(/^cmd_/) }.collect { |m| m[4..-1] }.sort
784
1127
 
1128
+ #
1129
+ # use Readline if possible
1130
+
785
1131
  begin
786
1132
  require 'readline'
787
1133
  def prompt_and_read