diff --git a/app/assets/javascripts/dashboard.js b/app/assets/javascripts/dashboard.js index e528e5de..5f570d9e 100644 --- a/app/assets/javascripts/dashboard.js +++ b/app/assets/javascripts/dashboard.js @@ -2,6 +2,8 @@ $(function() { var CHART_START = window.vis ? vis.moment().add(-1, 'minute') : undefined; var DEFAULT_REFRESH_INTERVAL = 5000; + var refreshInterval; + var dataset; var graph; var groups; @@ -46,17 +48,21 @@ $(function() { }; var refreshData = function(callback) { - var jqxhr = $.ajax({ - dataType: 'json', - method: 'GET' - }); - jqxhr.done(function(response) { - (callback || _.noop)(response); - setGroupVisibility(response); - updateChartData(response); - updateTable(response); - requestAnimationFrame(refreshChart); - }); + if (! $.isController('dashboard')) { + clearInterval(refreshInterval); + } else { + var jqxhr = $.ajax({ + dataType: 'json', + method: 'GET' + }); + jqxhr.done(function(response) { + (callback || _.noop)(response); + setGroupVisibility(response); + updateChartData(response); + updateTable(response); + requestAnimationFrame(refreshChart); + }); + } }; var setGroupVisibility = function(response) { @@ -101,6 +107,7 @@ $(function() { initializeChart(); refreshData(); var refresh_interval = location.search.match(/interval=(\d+)/) ? parseInt(RegExp.$1) : DEFAULT_REFRESH_INTERVAL; - setInterval(refreshData, refresh_interval); + refreshInterval = setInterval(refreshData, refresh_interval); } + }); diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 62301102..3aa3adf4 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -25,6 +25,11 @@ class SubmissionsController < ApplicationController create_and_respond(object: @submission) end + def command_substitutions(filename) + {class_name: File.basename(filename, File.extname(filename)).camelize, filename: filename, module_name: File.basename(filename, File.extname(filename)).underscore} + end + private :command_substitutions + def copy_comments # copy each annotation and set the target_file.id unless(params[:annotations_arr].nil?) @@ -88,6 +93,11 @@ class SubmissionsController < ApplicationController # end hijack do |tubesock| + # probably add: + # ensure + # #guarantee that the thread is releasing the DB connection after it is done + # ActiveRecord::Base.connectionpool.releaseconnection + # end Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive? @@ -103,7 +113,7 @@ class SubmissionsController < ApplicationController socket = result[:socket] socket.on :message do |event| - Rails.logger.info("Docker sending: " + event.data) + Rails.logger.info( Time.now.getutc.to_s + ": Docker sending: " + event.data) handle_message(event.data, tubesock) end @@ -112,7 +122,7 @@ class SubmissionsController < ApplicationController end tubesock.onmessage do |data| - Rails.logger.debug("Client sending: " + data) + Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data) # Check wether the client send a JSON command and kill container # if the command is 'exit', send it to docker otherwise. begin @@ -122,9 +132,11 @@ class SubmissionsController < ApplicationController @docker_client.exit_container(result[:container]) else socket.send data + Rails.logger.debug('Sent the received client data to docker:' + data) end rescue JSON::ParserError socket.send data + Rails.logger.debug('Rescued parsing error, sent the received client data to docker:' + data) end end else @@ -145,8 +157,8 @@ class SubmissionsController < ApplicationController kill_socket(tubesock) else # Filter out information about run_command, test_command, user or working directory - run_command = @submission.execution_environment.run_command - test_command = @submission.execution_environment.test_command + run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename]) + test_command = @submission.execution_environment.test_command % command_substitutions(params[:filename]) if !(/root|workspace|#{run_command}|#{test_command}/.match(message)) parse_message(message, 'stdout', tubesock) end @@ -157,6 +169,7 @@ class SubmissionsController < ApplicationController begin parsed = JSON.parse(message) socket.send_data message + Rails.logger.info('parse_message sent: ' + message) rescue JSON::ParserError => e # Check wether the message contains multiple lines, if true try to parse each line if ((recursive == true) && (message.include? "\n")) @@ -166,6 +179,7 @@ class SubmissionsController < ApplicationController else parsed = {'cmd'=>'write','stream'=>output_stream,'data'=>message} socket.send_data JSON.dump(parsed) + Rails.logger.info('parse_message sent: ' + JSON.dump(parsed)) end end end diff --git a/app/views/execution_environments/index.html.slim b/app/views/execution_environments/index.html.slim index 0ccb880c..dc30898f 100644 --- a/app/views/execution_environments/index.html.slim +++ b/app/views/execution_environments/index.html.slim @@ -6,6 +6,7 @@ h1 = ExecutionEnvironment.model_name.human(count: 2) tr th = t('activerecord.attributes.execution_environment.name') th = t('activerecord.attributes.execution_environment.user') + th = t('activerecord.attributes.execution_environment.pool_size') th = t('activerecord.attributes.execution_environment.memory_limit') th = t('activerecord.attributes.execution_environment.network_enabled') th = t('activerecord.attributes.execution_environment.permitted_execution_time') @@ -16,6 +17,7 @@ h1 = ExecutionEnvironment.model_name.human(count: 2) tr td = execution_environment.name td = link_to(execution_environment.author, execution_environment.author) + td = execution_environment.pool_size td = execution_environment.memory_limit td = symbol_for(execution_environment.network_enabled) td = execution_environment.permitted_execution_time diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index d01792d1..a9ce3a54 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -4,7 +4,7 @@ span.badge.pull-right.score - p.lead = @exercise.description + p.lead = render_markdown(@exercise.description) #alert.alert.alert-danger role='alert' h4 = t('.alert.title') diff --git a/app/views/request_for_comments/show.html.erb b/app/views/request_for_comments/show.html.erb index 7894d66f..c27e5478 100644 --- a/app/views/request_for_comments/show.html.erb +++ b/app/views/request_for_comments/show.html.erb @@ -4,6 +4,14 @@ <% user = @request_for_comment.user + submission_id = self.class.connection.execute("select id from submissions + where exercise_id = + #{@request_for_comment.exercise_id} AND + user_id = #{@request_for_comment.user_id} AND + #{@request_for_comment.user_id} > created_at + order by created_at desc + limit 1").first['id'].to_i + submission = Submission.find(submission_id) %> <%= user %> | <%= @request_for_comment.requested_at %> diff --git a/config/locales/de.yml b/config/locales/de.yml index eb44cab3..bbd0e5de 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -300,7 +300,7 @@ de: failure: Fehlerhafte E-Mail oder Passwort. success: Sie haben sich erfolgreich angemeldet. create_through_lti: - session_with_outcome: 'Nachdem Sie die Aufgabe bearbeitet haben, wird Ihre Bewertung an %{consumer} übermittelt.' + session_with_outcome: 'Bitte beachten Sie, dass zur Gutschrift der Punkte Ihr Code nach der Bearbeitung durch Klicken auf den Button "Code zur Bewertung abgeben" eingetragen werden muss.' session_without_outcome: 'Dies ist eine Übungs-Session. Ihre Bewertung wird nicht an %{consumer} übermittelt.' destroy: link: Abmelden diff --git a/config/locales/en.yml b/config/locales/en.yml index 5a36aefc..fc08792a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -300,7 +300,7 @@ en: failure: Invalid email or password. success: Successfully signed in. create_through_lti: - session_with_outcome: 'After you have finished the exercise, your grade will be transmitted to %{consumer}.' + session_with_outcome: 'Please click "Submit Code for Assessment" after scoring to send your score %{consumer}.' session_without_outcome: 'This is a practice session. Your grade will not be transmitted to %{consumer}.' destroy: link: Sign out diff --git a/lib/docker_client.rb b/lib/docker_client.rb index 4a353394..654c6504 100644 --- a/lib/docker_client.rb +++ b/lib/docker_client.rb @@ -162,6 +162,7 @@ class DockerClient @socket ||= create_socket(@container) # Newline required to flush @socket.send command + "\n" + Rails.logger.info('Sent command: ' + command.to_s) {status: :container_running, socket: @socket, container: @container} else {status: :container_depleted} @@ -173,18 +174,23 @@ class DockerClient We need to start a second thread to kill the websocket connection, as it is impossible to determine whether further input is requested. """ - @thread = Thread.new do - timeout = @execution_environment.permitted_execution_time.to_i # seconds - sleep(timeout) - if container.status != :returned - Rails.logger.info('Killing container after timeout of ' + timeout.to_s + ' seconds.') - # send timeout to the tubesock socket - if(@tubesock) - @tubesock.send_data JSON.dump({'cmd' => 'timeout'}) - end - kill_container(container) + #begin + @thread = Thread.new do + timeout = @execution_environment.permitted_execution_time.to_i # seconds + sleep(timeout) + if container.status != :returned + Rails.logger.info('Killing container after timeout of ' + timeout.to_s + ' seconds.') + # send timeout to the tubesock socket + if(@tubesock) + @tubesock.send_data JSON.dump({'cmd' => 'timeout'}) + end + kill_container(container) + end end - end + #ensure + # guarantee that the thread is releasing the DB connection after it is done + # ActiveRecord::Base.connectionpool.releaseconnection + #end end def exit_container(container) @@ -233,6 +239,7 @@ class DockerClient end def self.find_image_by_tag(tag) + # todo: cache this. Docker::Image.all.detect { |image| image.info['RepoTags'].flatten.include?(tag) } end @@ -246,8 +253,10 @@ class DockerClient def initialize(options = {}) @execution_environment = options[:execution_environment] - @image = self.class.find_image_by_tag(@execution_environment.docker_image) - fail(Error, "Cannot find image #{@execution_environment.docker_image}!") unless @image + # todo: eventually re-enable this if it is cached. But in the end, we do not need this. + # docker daemon got much too much load. all not 100% necessary calls to the daemon were removed. + #@image = self.class.find_image_by_tag(@execution_environment.docker_image) + #fail(Error, "Cannot find image #{@execution_environment.docker_image}!") unless @image end def self.initialize_environment @@ -255,7 +264,9 @@ class DockerClient fail(Error, 'Docker configuration missing!') end Docker.url = config[:host] if config[:host] - check_availability! + # todo: availability check disabled for performance reasons. Reconsider if this is necessary. + # docker daemon got much too much load. all not 100% necessary calls to the daemon were removed. + # check_availability! FileUtils.mkdir_p(LOCAL_WORKSPACE_ROOT) end @@ -298,7 +309,7 @@ class DockerClient output = container.exec(['bash', '-c', command]) Rails.logger.info "output from container.exec" Rails.logger.info output - result = {status: output[2] == 0 ? :ok : :failed, stdout: output[0].join, stderr: output[1].join} + result = {status: output[2] == 0 ? :ok : :failed, stdout: output[0].join.force_encoding('utf-8'), stderr: output[1].join.force_encoding('utf-8')} end # if we use pooling and recylce the containers, put it back. otherwise, destroy it. (DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container)