diff --git a/tests/spec/features/excessive_execution_spec.rb b/tests/spec/features/excessive_execution_spec.rb
new file mode 100644
index 000000000..d9814e1b9
--- /dev/null
+++ b/tests/spec/features/excessive_execution_spec.rb
@@ -0,0 +1,64 @@
+require 'json'
+
+require 'spec_helper'
+require 'support/editor'
+require 'support/playground_actions'
+
+RSpec.feature "Excessive executions", type: :feature, js: true do
+ include PlaygroundActions
+
+ before do
+ visit "/?#{config_overrides}"
+ editor.set(code)
+ end
+
+ scenario "a notification is shown" do
+ within(:header) { click_on("Run") }
+ within(:notification, text: 'will be automatically killed') do
+ expect(page).to have_button 'Kill the process now'
+ expect(page).to have_button 'Allow the process to continue'
+ end
+ end
+
+ scenario "the process is automatically killed if nothing is done" do
+ within(:header) { click_on("Run") }
+ expect(page).to have_selector(:notification, text: 'will be automatically killed', wait: 2)
+ expect(page).to_not have_selector(:notification, text: 'will be automatically killed', wait: 4)
+ expect(page).to have_content("Exited with signal 9")
+ end
+
+ scenario "the process can continue running" do
+ within(:header) { click_on("Run") }
+ within(:notification, text: 'will be automatically killed') do
+ click_on 'Allow the process to continue'
+ end
+ within(:output, :stdout) do
+ expect(page).to have_content("Exited normally")
+ end
+ end
+
+ def editor
+ Editor.new(page)
+ end
+
+ def code
+ <<~EOF
+ use std::time::{Duration, Instant};
+
+ fn main() {
+ let start = Instant::now();
+ while start.elapsed() < Duration::from_secs(5) {}
+ println!("Exited normally");
+ }
+ EOF
+ end
+
+ def config_overrides
+ config = {
+ killGracePeriodS: 3.0,
+ excessiveExecutionTimeS: 0.5,
+ }
+
+ "whte_rbt.obj=#{config.to_json}"
+ end
+end
diff --git a/tests/spec/spec_helper.rb b/tests/spec/spec_helper.rb
index 099e13f15..7f3350d2e 100644
--- a/tests/spec/spec_helper.rb
+++ b/tests/spec/spec_helper.rb
@@ -83,6 +83,10 @@
css { '[data-test-id = "stdin"]' }
end
+Capybara.add_selector(:notification) do
+ css { '[data-test-id = "notification"]' }
+end
+
RSpec.configure do |config|
config.after(:example, :js) do
page.execute_script <<~JS
diff --git a/ui/frontend/.eslintrc.js b/ui/frontend/.eslintrc.js
index 0a2c020d0..51002b9d7 100644
--- a/ui/frontend/.eslintrc.js
+++ b/ui/frontend/.eslintrc.js
@@ -86,6 +86,7 @@ module.exports = {
'editor/AceEditor.tsx',
'editor/SimpleEditor.tsx',
'hooks.ts',
+ 'observer.ts',
'reducers/browser.ts',
'reducers/client.ts',
'reducers/code.ts',
diff --git a/ui/frontend/.prettierignore b/ui/frontend/.prettierignore
index e3cfa3fa0..441f7d4a5 100644
--- a/ui/frontend/.prettierignore
+++ b/ui/frontend/.prettierignore
@@ -26,6 +26,7 @@ node_modules
!editor/AceEditor.tsx
!editor/SimpleEditor.tsx
!hooks.ts
+!observer.ts
!reducers/browser.ts
!reducers/client.ts
!reducers/code.ts
diff --git a/ui/frontend/Notifications.module.css b/ui/frontend/Notifications.module.css
index 00e27e9df..4f897b8f2 100644
--- a/ui/frontend/Notifications.module.css
+++ b/ui/frontend/Notifications.module.css
@@ -28,3 +28,10 @@ $space: 0.25em;
background: #e1e1db;
padding: $space;
}
+
+.action {
+ display: flex;
+ justify-content: center;
+ padding-top: 0.5em;
+ gap: 0.5em;
+}
diff --git a/ui/frontend/Notifications.tsx b/ui/frontend/Notifications.tsx
index ce63fe1c6..d9addcff3 100644
--- a/ui/frontend/Notifications.tsx
+++ b/ui/frontend/Notifications.tsx
@@ -4,6 +4,7 @@ import { Portal } from 'react-portal';
import { Close } from './Icon';
import { useAppDispatch, useAppSelector } from './hooks';
import { seenRustSurvey2022 } from './reducers/notifications';
+import { allowLongRun, wsExecuteKillCurrent } from './reducers/output/execute';
import * as selectors from './selectors';
import styles from './Notifications.module.css';
@@ -15,6 +16,7 @@ const Notifications: React.FC = () => {
+
);
@@ -36,13 +38,36 @@ const RustSurvey2022Notification: React.FC = () => {
) : null;
};
+const ExcessiveExecutionNotification: React.FC = () => {
+ const showExcessiveExecution = useAppSelector(selectors.excessiveExecutionSelector);
+ const time = useAppSelector(selectors.excessiveExecutionTimeSelector);
+ const gracePeriod = useAppSelector(selectors.killGracePeriodTimeSelector);
+
+ const dispatch = useAppDispatch();
+ const allow = useCallback(() => dispatch(allowLongRun()), [dispatch]);
+ const kill = useCallback(() => dispatch(wsExecuteKillCurrent()), [dispatch]);
+
+ return showExcessiveExecution ? (
+
+ The running process has used more than {time} of CPU time. This is often caused by an error in
+ the code. As the playground is a shared resource, the process will be automatically killed in{' '}
+ {gracePeriod}. You can always kill the process manually via the menu at the bottom of the
+ screen.
+