command_proposal 1.0.13 → 1.0.16

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: d57823efe2c4175b2786f8346a67ea68e96804bc980f06fc4afb0feb30a8dc9a
4
- data.tar.gz: 212e7e868bf94fe559701ee696fc5487a9a17bcc3b549f35880843d8564f927d
3
+ metadata.gz: 43e714b8d84f486d67fabcf4d171cc38f6fe3e97c1e4cd776b558ef9feef2bb0
4
+ data.tar.gz: 5107629d432ed32fb26bd3fbfe56094287b1a9a0eb2feaf72b1e9f493b3d1dfd
5
5
  SHA512:
6
- metadata.gz: 96795b4fff0569ca5ed7c965cc1d27dcaa2fd753301a62cee0da1a0e7f684daa9489b5ca5f51f195bd58a8a86c3f9f77aea2b9e21017fc6c14c5ee61b73169e3
7
- data.tar.gz: 6e15d7c2aeadeb488bfc35784cdeb2626a29cfa18f6a98264af30d35e45ecc7b8df8d8356d70ef4e7b07ca2228c78149dfd3a0bd7bc77e6ad00a3d17df2d63b1
6
+ metadata.gz: 283e95ed280a541f6e607e2802068664244d532186448cafe42850763bb318159335b33b64e5411c729a65cbd427de94b6e672154efe0231487a71ba8023e659
7
+ data.tar.gz: bbece28324234cfee33481dbdeef5fdadd0b90ff982bb3eeabeaa76e6732c558298ed8dd7a0e313f9304b574dc7559e13187c4bdc3e7cec0a3ed7c0c85102e9c
@@ -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
@@ -23,7 +23,6 @@ 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
27
  has_many :comments
29
28
  belongs_to :task
@@ -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
@@ -1,18 +1,18 @@
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)
8
- elsif iteration.completed?
8
+ elsif iteration.complete?
9
9
  "Error: No response"
10
10
  else
11
11
  content_tag :i, nil, class: "fa fa-circle-o-notch fa-spin cmd-icon-grey"
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.13"
2
+ VERSION = "1.0.16"
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.13
4
+ version: 1.0.16
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-09 00:00:00.000000000 Z
11
+ date: 2022-03-11 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