command_proposal 1.0.14 → 1.0.17

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49fcf371fa04144d6c3a47f4cf8170bd30c1571c61f44d636df41591227f4d66
4
- data.tar.gz: 990a726d0441c95757ec17679e495b1300f4de2e5ac284df842bf82180c6912f
3
+ metadata.gz: db4149d1e53cb021ee11594b4f8cf74f8a10ae4d0cba115fc0996649a27a70b5
4
+ data.tar.gz: bdbe876429d3fb79aff2e46058bfd42d674315a90e01c89f812df43022f7895f
5
5
  SHA512:
6
- metadata.gz: 3d9d03fbe537ce4da08d6229829148ef8dbdbe1433ae56db6b08f97313a6329d41b2f63c8f433a197f34559521e37a652bad931f9be3a2eb0f4b12265184f1bf
7
- data.tar.gz: 24ba8d9ad1147fa9d6ee21298d547e774fd444ee0370bc79e629c1ad63fefb9d022aa95d9d78e56b1f58db7cf8d2e191731dd4970b88fd60484395c9ec376ccb
6
+ metadata.gz: 57566b279852c47f8756db74b1693214885017fd5eab3f7874fc9146b7b62a5d6f386c2ce9e3349f18fece131f5de32addfb915354c312103b88aca41638e6d6
7
+ data.tar.gz: 8475af7df8402fbc2e49e5f727280cb29a8ab7aab24e2572ec49ce820f1799217b06082632d114a13c326cf8d3d24f6477094d28efa8b7c45362d7dd90d56cef
@@ -142,14 +142,16 @@ cmdDocReady(function() {
142
142
 
143
143
  console_input.textContent = ""
144
144
 
145
- var result = document.createElement("div")
146
- result.classList.add("result")
145
+ if (!(/^[\s\n]*$/.test(line.textContent))) {
146
+ var result = document.createElement("div")
147
+ result.classList.add("result")
147
148
 
148
- var spinner = document.createElement("i")
149
- spinner.className = "fa fa-circle-o-notch fa-spin cmd-icon-grey"
150
- result.append(spinner)
149
+ var spinner = document.createElement("i")
150
+ spinner.className = "fa fa-circle-o-notch fa-spin cmd-icon-grey"
151
+ result.append(spinner)
151
152
 
152
- line.appendChild(result)
153
+ line.appendChild(result)
154
+ }
153
155
 
154
156
  lines.appendChild(line)
155
157
  stored_entry = undefined
@@ -176,41 +178,77 @@ cmdDocReady(function() {
176
178
  body: JSON.stringify(params),
177
179
  done: function(res, status, req) {
178
180
  if (status == 200) {
179
- var json = JSON.parse(res)
180
- line.querySelector(".result").remove()
181
-
182
- var result = document.createElement("div")
183
- result.classList.add("result")
184
-
185
- if (json.error) {
186
- result.classList.add("cmd-error")
187
- result.textContent = json.error
188
- } else {
189
- var truncate = 2000
190
- if (json.result.length > truncate-3) {
191
- result.textContent = json.result.slice(0, truncate-3) + "..."
192
- var encoded = encodeURIComponent(json.result)
193
-
194
- var download = document.createElement("a")
195
- download.classList.add("cmd-truncated-download")
196
- download.setAttribute("href", "data:application/txt," + encoded)
197
- download.setAttribute("download", "result.txt")
198
- download.textContent = "Output truncated. Click here to download full result."
199
-
200
- line.insertAdjacentElement("afterend", download)
201
- } else {
202
- result.textContent = json.result
203
- }
204
- }
205
-
206
- line.appendChild(result)
181
+ handleSuccessfulCommand(evt, line, JSON.parse(res))
207
182
  } else {
208
183
  console.log("Error: ", res, req);
209
184
  }
210
- evt.finish()
211
185
  }
212
186
  })
213
187
  })
214
188
  }
189
+
190
+ function handleSuccessfulCommand(evt, line, json) {
191
+ if (json.error) {
192
+ addLineResult(line, json.error, "cmd-error")
193
+ } else if (json.status != "started") {
194
+ addLineResult(line, json.result, json.status == "failed" ? "cmd-error" : "")
195
+ } else {
196
+ return setTimeout(function() { pollIteration(evt, line, json.results_endpoint) }, 2000)
197
+ }
198
+
199
+ evt.finish()
200
+ }
201
+
202
+ function addLineResult(line, text, result_class) {
203
+ line.querySelector(".result").remove()
204
+
205
+ if (!text || /^[\s\n]*$/.test(text)) { return }
206
+
207
+ var result = document.createElement("div")
208
+ result.classList.add("result")
209
+ if (result_class) { result.classList.add(result_class) }
210
+
211
+ var truncate = 2000
212
+ if (text.length > truncate-3) {
213
+ result.textContent = text.slice(0, truncate-3) + "..."
214
+ var encoded = encodeURIComponent(text)
215
+
216
+ var download = document.createElement("a")
217
+ download.classList.add("cmd-truncated-download")
218
+ download.setAttribute("href", "data:application/txt," + encoded)
219
+ download.setAttribute("download", "result.txt")
220
+ download.textContent = "Output truncated. Click here to download full result."
221
+
222
+ line.insertAdjacentElement("afterend", download)
223
+ } else {
224
+ result.textContent = text
225
+ }
226
+
227
+ line.appendChild(result)
228
+ }
229
+
230
+ function pollIteration(evt, line, endpoint) {
231
+ var client = new HttpClient()
232
+ client.get(endpoint, {
233
+ headers: {
234
+ "Content-Type": "application/json",
235
+ "X-CSRF-Token": $.rails.csrfToken()
236
+ },
237
+ done: function(res, status, req) {
238
+ if (status == 200) {
239
+ var json = JSON.parse(res)
240
+ if (json.status == "started") {
241
+ setTimeout(function() { pollIteration(evt, line, endpoint) }, 2000)
242
+ } else {
243
+ addLineResult(line, json.result, json.status == "failed" ? "cmd-error" : "")
244
+ evt.finish()
245
+ }
246
+ } else {
247
+ console.log("Error: ", res, req);
248
+ evt.finish()
249
+ }
250
+ }
251
+ })
252
+ }
215
253
  }
216
254
  })
@@ -75,4 +75,7 @@
75
75
  font-size: 12px;
76
76
  color: grey;
77
77
  }
78
+ &.cmd-past-iterations tbody tr:nth-child(even) {
79
+ background: whitesmoke;
80
+ }
78
81
  }
@@ -9,6 +9,16 @@ class ::CommandProposal::IterationsController < ::CommandProposal::EngineControl
9
9
 
10
10
  layout "application"
11
11
 
12
+ def show
13
+ @iteration = ::CommandProposal::Iteration.find(params[:id])
14
+
15
+ render json: {
16
+ results_endpoint: cmd_path(@iteration),
17
+ result: @iteration.result,
18
+ status: @iteration.status
19
+ }
20
+ end
21
+
12
22
  def create
13
23
  return error!("You do not have permission to run commands.") unless can_command?
14
24
 
@@ -17,32 +27,32 @@ class ::CommandProposal::IterationsController < ::CommandProposal::EngineControl
17
27
  return error!("Can only run commands on type: :console") unless @task.console?
18
28
  return error!("Session has not been approved.") unless has_approval?(@task)
19
29
 
20
- if @task.iterations.many?
21
- runner = ::CommandProposal.sessions["task:#{@task.id}"]
22
- elsif @task.iterations.one?
30
+ if @task.iterations.one?
23
31
  # Track console details in first iteration
24
32
  @task.first_iteration.update(started_at: Time.current, status: :started)
25
- runner = ::CommandProposal::Services::Runner.new
26
- ::CommandProposal.sessions["task:#{@task.id}"] = runner
27
33
  end
28
34
 
29
- return error!("Session has expired. Please start a new session.") if runner.nil?
30
-
31
35
  @task.user = command_user # Separate from update to ensure it's set first
32
36
  @task.update(code: params[:code]) # Creates a new iteration
33
37
  @iteration = @task.current_iteration
34
38
  @iteration.update(status: :approved) # Task was already approved, and this is line-by-line
35
39
 
36
40
  # async, but wait for the job to finish
37
- ::CommandProposal::CommandRunnerJob.perform_later(@iteration.id)
41
+ ::CommandProposal::CommandRunnerJob.perform_later(@iteration.id, "task:#{@task.id}")
42
+
43
+ max_wait_seconds = 3
38
44
  loop do
39
- sleep 0.2
45
+ break unless max_wait_seconds.positive?
46
+
47
+ max_wait_seconds -= sleep 0.2
40
48
 
41
49
  break if @iteration.reload.complete?
42
50
  end
43
51
 
44
52
  render json: {
45
- result: @iteration.result
53
+ results_endpoint: cmd_path(@iteration),
54
+ result: @iteration.result,
55
+ status: @iteration.status
46
56
  }
47
57
  end
48
58
 
@@ -53,12 +53,12 @@ class ::CommandProposal::TasksController < ::CommandProposal::EngineController
53
53
  def create
54
54
  @task = ::CommandProposal::Task.new(task_params.except(:code))
55
55
  @task.user = command_user
56
- @task.skip_approval = true unless approval_required?
56
+ @task.skip_approval = true unless approval_required?(@task.session_type)
57
57
 
58
58
  # Cannot create the iteration until the task is created, so save then update
59
59
  if @task.save && @task.update(task_params)
60
60
  if @task.console?
61
- @task.iterations.create(requester: command_user) # Blank iteration to track approval
61
+ @task.code = nil # Creates a blank iteration to track approval
62
62
  redirect_to cmd_path(@task)
63
63
  else
64
64
  redirect_to cmd_path(:edit, @task)
@@ -72,7 +72,7 @@ class ::CommandProposal::TasksController < ::CommandProposal::EngineController
72
72
  def update
73
73
  @task = ::CommandProposal::Task.find_by!(friendly_id: params[:id])
74
74
  @task.user = command_user
75
- @task.skip_approval = true unless approval_required?
75
+ @task.skip_approval = true unless approval_required?(@task.session_type)
76
76
 
77
77
  if @task.update(task_params)
78
78
  redirect_to cmd_path(@task)
@@ -9,7 +9,7 @@ module CommandProposal
9
9
 
10
10
  def can_approve?(iteration)
11
11
  return false unless permitted_to_use?
12
- return true unless approval_required?
12
+ return true unless approval_required?(iteration.task.session_type)
13
13
  return if iteration.nil?
14
14
 
15
15
  command_user.try("#{cmd_config.role_scope}?") && !current_is_author?(iteration)
@@ -17,13 +17,18 @@ module CommandProposal
17
17
 
18
18
  def has_approval?(task)
19
19
  return false unless permitted_to_use?
20
- return true unless approval_required?
20
+ return true unless approval_required?(task.session_type)
21
21
 
22
22
  task&.approved?
23
23
  end
24
24
 
25
- def approval_required?
26
- cmd_config.approval_required?
25
+ def approval_required?(task_type=nil)
26
+ return false unless cmd_config.approval_required?
27
+ return true if task_type.blank?
28
+
29
+ skips = cmd_config.skip_approval_for_types.presence || []
30
+
31
+ Array.wrap(skips).map(&:to_sym).exclude?(task_type.to_sym)
27
32
  end
28
33
 
29
34
  def current_is_author?(iteration)
@@ -2,10 +2,26 @@ module CommandProposal
2
2
  class CommandRunnerJob < ApplicationJob
3
3
  queue_as :default
4
4
 
5
- def perform(iteration_id)
5
+ def perform(iteration_id, runner_key=nil)
6
6
  iteration = ::CommandProposal::Iteration.find(iteration_id)
7
+ runner = ::CommandProposal.sessions[runner_key] if runner_key.present?
7
8
 
8
- ::CommandProposal::Services::Runner.new.execute(iteration)
9
+ if runner_key.present? && runner.blank?
10
+ if iteration.task.console? && iteration.task.iterations.count > 2 # 1 for init, and the 1 for current running code
11
+ return ::CommandProposal::Services::Runner.new.quick_fail(
12
+ iteration,
13
+ "Session has expired. Please start a new session."
14
+ )
15
+ else
16
+ runner = ::CommandProposal::Services::Runner.new
17
+ end
18
+
19
+ ::CommandProposal.sessions[runner_key] = runner if runner_key.present?
20
+ else
21
+ runner ||= ::CommandProposal::Services::Runner.new
22
+ end
23
+
24
+ runner.execute(iteration)
9
25
  end
10
26
  end
11
27
  end
@@ -9,6 +9,6 @@ class ::CommandProposal::Comment < ApplicationRecord
9
9
  self.table_name = :command_proposal_comments
10
10
  include ::CommandProposal::Service::ExternalBelong
11
11
 
12
- belongs_to :iteration, optional: true
12
+ belongs_to :iteration, optional: true, class_name: "CommandProposal::Iteration"
13
13
  external_belongs_to :author
14
14
  end
@@ -23,10 +23,9 @@ class ::CommandProposal::Iteration < ApplicationRecord
23
23
 
24
24
  TRUNCATE_COUNT = 2000
25
25
  # Also hardcoded in JS: app/assets/javascripts/command_proposal/console.js
26
- PAGINATION_PER = 2
27
26
 
28
- has_many :comments
29
- belongs_to :task
27
+ has_many :comments, class_name: "CommandProposal::Comment"
28
+ belongs_to :task, class_name: "CommandProposal::Task"
30
29
  external_belongs_to :requester
31
30
  external_belongs_to :approver
32
31
 
@@ -66,6 +65,10 @@ class ::CommandProposal::Iteration < ApplicationRecord
66
65
  task.primary_iteration == self
67
66
  end
68
67
 
68
+ def approved?
69
+ super || (session_type == "function" && approved_at?)
70
+ end
71
+
69
72
  def complete?
70
73
  success? || failed? || cancelled? || terminated?
71
74
  end
@@ -13,6 +13,7 @@ module CommandProposal
13
13
  delegate :description, to: :iteration
14
14
  delegate :args, to: :iteration
15
15
  delegate :code, to: :iteration
16
+ delegate :result, to: :iteration
16
17
  delegate :status, to: :iteration
17
18
  delegate :approved_at, to: :iteration
18
19
  delegate :started_at, to: :iteration
@@ -9,7 +9,7 @@ class ::CommandProposal::Task < ApplicationRecord
9
9
  self.table_name = :command_proposal_tasks
10
10
  attr_accessor :user, :skip_approval
11
11
 
12
- has_many :iterations
12
+ has_many :iterations, class_name: "CommandProposal::Iteration"
13
13
  has_many :ordered_iterations, -> { order(created_at: :desc) }, class_name: "CommandProposal::Iteration"
14
14
 
15
15
  scope :search, ->(text) {
@@ -1,7 +1,7 @@
1
1
  <% if lines.none? && !(skip_empty ||= false) -%><div class="line"></div><% end
2
2
  -%><% lines.each do |iteration| -%>
3
3
  <div class="line"><%= iteration.code
4
- -%><div class="result"><%=
4
+ -%><div class="result <%= 'cmd-error' if iteration.failed? %>"><%=
5
5
  truncate = ::CommandProposal::Iteration::TRUNCATE_COUNT
6
6
  if iteration.result.present?
7
7
  iteration.result.truncate(truncate)
@@ -12,7 +12,7 @@
12
12
  end
13
13
  %></div
14
14
  ></div><%=
15
- if iteration.result.length > truncate
15
+ if iteration.result&.length.to_i > truncate
16
16
  link_to("Output truncated. Click here to download full result.", "data:application/txt,#{ERB::Util.url_encode(iteration.result)}", class: "cmd-truncated-download", download: "result.txt")
17
17
  end
18
18
  %><% end -%>
@@ -1,5 +1,5 @@
1
1
  <% if lines.blank? && !(skip_empty ||= false) -%><div class="line"></div><% end
2
- -%><% lines&.split("\n").each do |line|
2
+ -%><% lines&.split("\n")&.each do |line|
3
3
  truncate = ::CommandProposal::Iteration::TRUNCATE_COUNT
4
4
  -%><div class="line"><%= line.truncate(truncate) -%></div><%=
5
5
  if line.length > truncate
@@ -1,8 +1,12 @@
1
- <% if @task.iterations.many? %>
2
- <table class="cmd-table">
1
+ <% if @task.iterations.any? %>
2
+ <table class="cmd-table cmd-past-iterations">
3
3
  <thead>
4
4
  <th>Timestamp / Link</th>
5
5
  <th>Status</th>
6
+ <% if @iteration&.params.present? %>
7
+ <th>Params</th>
8
+ <% end %>
9
+ <th>Result</th>
6
10
  <!-- <th># Comments</th> -->
7
11
  <!-- <th>Diff</th> -->
8
12
  </thead>
@@ -15,6 +19,14 @@
15
19
  <%= link_to iteration.created_at.strftime("%b %-d, %Y at %H:%M"), cmd_path(@task, iteration: iteration.id) %>
16
20
  </td>
17
21
  <td><%= iteration.status.capitalize %></td>
22
+ <% if @iteration&.params.present? %>
23
+ <td>
24
+ <% iteration&.args.each do |arg_k, arg_v| %>
25
+ <span class=cmd-arg""><%= arg_k %>=<%= arg_v.to_s.truncate(50) %></span>
26
+ <% end %>
27
+ </td>
28
+ <% end %>
29
+ <td><%= iteration.result.to_s.truncate(200) %></td>
18
30
  <!-- <td><%#= iteration.comments.count %></td> -->
19
31
  <!-- <td><%#= link_to "Diff", cmd_path(@task, iteration: @iteration.id, diff: iteration.id) %></td> -->
20
32
  </tr>
@@ -13,6 +13,7 @@ module CommandProposal
13
13
  :approval_callback,
14
14
  :success_callback,
15
15
  :failed_callback,
16
+ :skip_approval_for_types,
16
17
  )
17
18
 
18
19
  def initialize
@@ -30,6 +31,7 @@ module CommandProposal
30
31
  @approval_callback = nil
31
32
  @success_callback = nil
32
33
  @failed_callback = nil
34
+ @skip_approval_for_types = nil
33
35
  end
34
36
 
35
37
  def user_class
@@ -51,8 +51,21 @@ module CommandProposal
51
51
  proposal
52
52
  end
53
53
 
54
+ def quick_fail(iteration, msg)
55
+ @iteration = iteration
56
+ prepare
57
+
58
+ @iteration.status = :failed
59
+ @iteration.result = msg
60
+
61
+ complete
62
+ proposal = ::CommandProposal::Service::ProposalPresenter.new(@iteration)
63
+ @iteration = nil
64
+ proposal
65
+ end
66
+
54
67
  def quick_run(friendly_id)
55
- task = ::CommandProposal::Task.module.find_by!(friendly_id: friendly_id)
68
+ task = ::CommandProposal::Task.find_by!(friendly_id: friendly_id)
56
69
  iteration = task&.primary_iteration
57
70
 
58
71
  raise CommandProposal::Error, ":#{friendly_id} does not have approval to run." unless iteration&.approved?
@@ -85,12 +98,12 @@ module CommandProposal
85
98
  return @iteration.result = results_from_exception(e)
86
99
  end
87
100
 
88
- stored_stdout = $stdout
89
- $stdout = StringIO.new
101
+ stored_stdout = $stdout # quiet
102
+ $stdout = StringIO.new # quiet
90
103
  result = nil # Init var for scope
91
104
  status = nil
92
105
 
93
- running_thread = Thread.new do
106
+ runner_proc = proc {
94
107
  begin
95
108
  # Run `bring` functions in here so we can capture any string outputs
96
109
  # OR! Run the full runner and instead of saving to an iteration, return the string for prepending here
@@ -102,28 +115,36 @@ module CommandProposal
102
115
 
103
116
  result = results_from_exception(e)
104
117
  end
105
- end
106
-
107
- while running_thread.status.present?
108
- @iteration.reload
118
+ }
109
119
 
110
- if $stdout.try(:string) != @iteration.result
111
- @iteration.update(result: $stdout.try(:string).dup)
120
+ if Rails.application.config.active_job&.queue_adapter == :inline
121
+ runner_proc.call
122
+ else
123
+ running_thread = Thread.new do
124
+ runner_proc.call
112
125
  end
113
126
 
114
- if @iteration.cancelling?
115
- running_thread.exit
116
- status = :cancelled
117
- end
127
+ while running_thread.status.present?
128
+ @iteration.reload
118
129
 
119
- sleep 0.4
130
+ if $stdout.try(:string) != @iteration.result
131
+ @iteration.update(result: $stdout.try(:string).dup)
132
+ end
133
+
134
+ if @iteration.cancelling?
135
+ running_thread.exit
136
+ status = :cancelled
137
+ end
138
+
139
+ sleep 0.4
140
+ end
120
141
  end
121
142
 
122
143
  output = $stdout.try(:string)
123
144
  output = nil if output == ""
124
145
  # Not using presence because we want to maintain other empty objects such as [] and {}
125
146
 
126
- $stdout = stored_stdout
147
+ $stdout = stored_stdout # quiet
127
148
  @iteration.status = status
128
149
  @iteration.result = [output, result].compact.join("\n")
129
150
  end
@@ -13,7 +13,7 @@ module CommandProposal
13
13
  def terminate(iteration)
14
14
  return unless iteration.running?
15
15
 
16
- terminated_result = iteration.result + "\n\n~~~~~ TERMINATED ~~~~~"
16
+ terminated_result = "#{iteration&.result}\n\n~~~~~ TERMINATED ~~~~~"
17
17
  iteration.update(
18
18
  status: :terminated,
19
19
  result: terminated_result,
@@ -1,3 +1,3 @@
1
1
  module CommandProposal
2
- VERSION = "1.0.14"
2
+ VERSION = "1.0.17"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: command_proposal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.14
4
+ version: 1.0.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rocco Nicholls
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-17 00:00:00.000000000 Z
11
+ date: 2022-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -162,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
162
  - !ruby/object:Gem::Version
163
163
  version: '0'
164
164
  requirements: []
165
- rubygems_version: 3.2.22
165
+ rubygems_version: 3.2.3
166
166
  signing_key:
167
167
  specification_version: 4
168
168
  summary: Gives the ability to run approved commands through a UI in your browser