From 265ae264d2f8adbd8289ad94a6972f1f63fe3f57 Mon Sep 17 00:00:00 2001 From: Jake Goulding Date: Sun, 10 Dec 2023 22:05:56 -0500 Subject: [PATCH] Nudge the user to kill programs using excessive CPU --- .../spec/features/excessive_execution_spec.rb | 64 +++++++++++++++++++ tests/spec/spec_helper.rb | 4 ++ ui/frontend/.eslintrc.js | 1 + ui/frontend/.prettierignore | 1 + ui/frontend/Notifications.module.css | 7 ++ ui/frontend/Notifications.tsx | 27 +++++++- ui/frontend/configureStore.ts | 4 +- ui/frontend/observer.ts | 54 ++++++++++++++++ ui/frontend/reducers/globalConfiguration.ts | 4 ++ ui/frontend/reducers/output/execute.ts | 45 +++++++++++-- ui/frontend/selectors/index.ts | 44 +++++++++++++ ui/frontend/websocketMiddleware.ts | 2 + ui/src/server_axum/websocket.rs | 29 +++++++++ 13 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 tests/spec/features/excessive_execution_spec.rb create mode 100644 ui/frontend/observer.ts 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. +
+ + +
+
+ ) : null; +}; + interface NotificationProps { children: React.ReactNode; onClose: () => void; } const Notification: React.FC = ({ onClose, children }) => ( -
+
{children}