Skip to content

Commit cd51357

Browse files
committed
improve "Run Terminal Command in all Student Projects"
- raise default timeout to 1 minute (from 30s) - show how long it took - allow timeout to be configured - explain what will happen
1 parent 837fead commit cd51357

File tree

4 files changed

+122
-70
lines changed

4 files changed

+122
-70
lines changed

src/packages/frontend/course/configuration/terminal-command.tsx

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
44
*/
55

6-
import { List as AntdList, Button, Card, Form, Input, Space } from "antd";
6+
import {
7+
List as AntdList,
8+
Button,
9+
Card,
10+
Form,
11+
Input,
12+
InputNumber,
13+
Space,
14+
} from "antd";
715
import { List, Map, fromJS } from "immutable";
816

917
import {
@@ -14,12 +22,13 @@ import {
1422
useActions,
1523
useRedux,
1624
} from "@cocalc/frontend/app-framework";
25+
import { useState } from "react";
1726
import { Gap, Icon } from "@cocalc/frontend/components";
1827
import { COLORS } from "@cocalc/util/theme";
1928
import { CourseActions } from "../actions";
29+
import { MAX_PARALLEL_TASKS } from "../student-projects/actions";
2030
import { CourseStore, TerminalCommand, TerminalCommandOutput } from "../store";
2131
import { Result } from "../student-projects/run-in-all-projects";
22-
2332
interface Props {
2433
name: string;
2534
}
@@ -30,8 +39,9 @@ export const TerminalCommandPanel: React.FC<Props> = React.memo(
3039
const actions = useActions<CourseActions>({ name });
3140
const terminal_command: TerminalCommand | undefined = useRedux(
3241
name,
33-
"terminal_command"
42+
"terminal_command",
3443
);
44+
const [timeout, setTimeout] = useState<number | null>(1);
3545

3646
function render_button(running: boolean): Rendered {
3747
return (
@@ -59,16 +69,30 @@ export const TerminalCommandPanel: React.FC<Props> = React.memo(
5969
run_terminal_command();
6070
}}
6171
>
62-
<Space.Compact style={{ display: "flex", whiteSpace: "nowrap" }}>
72+
<Space.Compact
73+
style={{
74+
display: "flex",
75+
whiteSpace: "nowrap",
76+
marginBottom: "5px",
77+
}}
78+
>
6379
<Input
6480
style={{ fontFamily: "monospace" }}
6581
placeholder="Terminal command..."
6682
onChange={(e) => {
6783
set_field("input", e.target.value);
6884
}}
85+
onPressEnter={() => run_terminal_command()}
6986
/>
7087
{render_button(running)}
7188
</Space.Compact>
89+
<InputNumber
90+
value={timeout}
91+
onChange={(t) => setTimeout(t ?? null)}
92+
min={0}
93+
max={30}
94+
addonAfter={"minute timeout"}
95+
/>
7296
</Form>
7397
);
7498
}
@@ -117,12 +141,12 @@ export const TerminalCommandPanel: React.FC<Props> = React.memo(
117141

118142
function set_field(
119143
field: "input" | "running" | "output",
120-
value: any
144+
value: any,
121145
): void {
122146
const store: CourseStore = get_store();
123147
let terminal_command: TerminalCommand = store.get(
124148
"terminal_command",
125-
Map() as TerminalCommand
149+
Map() as TerminalCommand,
126150
);
127151
if (value == null) {
128152
terminal_command = terminal_command.delete(field);
@@ -158,12 +182,11 @@ export const TerminalCommandPanel: React.FC<Props> = React.memo(
158182
if (!input) return;
159183
try {
160184
set_field("running", true);
161-
await actions.student_projects.run_in_all_student_projects(
162-
input,
163-
undefined,
164-
undefined,
165-
run_log
166-
);
185+
await actions.student_projects.run_in_all_student_projects({
186+
command: input,
187+
timeout: (timeout ? timeout : 1) * 60,
188+
log: run_log,
189+
});
167190
} finally {
168191
set_field("running", false);
169192
}
@@ -192,11 +215,13 @@ export const TerminalCommandPanel: React.FC<Props> = React.memo(
192215
{render_terminal()}
193216
<hr />
194217
<span style={{ color: "#666" }}>
195-
Run a terminal command in the home directory of all student projects.
218+
Run a bash terminal command in the home directory of all student
219+
projects. Up to {MAX_PARALLEL_TASKS} commands run in parallel, with a
220+
timeout of {timeout} minutes.
196221
</span>
197222
</Card>
198223
);
199-
}
224+
},
200225
);
201226

202227
const PROJECT_LINK_STYLE: CSS = {
@@ -236,6 +261,8 @@ const Output: React.FC<{ result: TerminalCommandOutput }> = React.memo(
236261
const stdout = result.get("stdout");
237262
const stderr = result.get("stderr");
238263
const noresult = !stdout && !stderr;
264+
const timeout = result.get("timeout");
265+
const total_time = result.get("total_time");
239266

240267
return (
241268
<div style={{ padding: 0, width: "100%" }}>
@@ -244,8 +271,16 @@ const Output: React.FC<{ result: TerminalCommandOutput }> = React.memo(
244271
</a>
245272
{stdout && <pre style={CODE_STYLE}>{stdout}</pre>}
246273
{stderr && <pre style={ERR_STYLE}>{stderr}</pre>}
247-
{noresult && <div>No result/possibly timeout</div>}
274+
{noresult && (
275+
<div>
276+
No output{" "}
277+
{total_time != null && timeout != null && total_time >= timeout - 5
278+
? "(possible timeout)"
279+
: ""}
280+
</div>
281+
)}
282+
{total_time != null && <>(Time: {total_time} seconds)</>}
248283
</div>
249284
);
250-
}
285+
},
251286
);

src/packages/frontend/course/store.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export type TerminalCommandOutput = TypedMap<{
4444
project_id: string;
4545
stdout?: string;
4646
stderr?: string;
47+
time_ms?: number;
4748
}>;
4849

4950
export type TerminalCommand = TypedMap<{
@@ -226,7 +227,7 @@ export class CourseStore extends Store<CourseState> {
226227
// that graded the given student, or undefined if no relevant assignment.
227228
public get_peers_that_graded_student(
228229
assignment_id: string,
229-
student_id: string
230+
student_id: string,
230231
): string[] {
231232
const peers: string[] = [];
232233
const assignment = this.get_assignment(assignment_id);
@@ -374,7 +375,7 @@ export class CourseStore extends Store<CourseState> {
374375
const name = users.get_name(student.get("account_id"));
375376
if (name != null) {
376377
extra = ` (You call them "${student.has("first_name")} ${student.has(
377-
"last_name"
378+
"last_name",
378379
)}", but they call themselves "${name}".)`;
379380
}
380381
}
@@ -392,7 +393,7 @@ export class CourseStore extends Store<CourseState> {
392393
}
393394
if (student.has("first_name") || student.has("last_name")) {
394395
return [student.get("last_name", ""), student.get("first_name", "")].join(
395-
" "
396+
" ",
396397
);
397398
}
398399
const account_id = student.get("account_id");
@@ -430,7 +431,7 @@ export class CourseStore extends Store<CourseState> {
430431
opts: {
431432
include_deleted?: boolean;
432433
deleted_only?: boolean;
433-
} = {}
434+
} = {},
434435
): string[] {
435436
// include_deleted = if true, also include deleted projects
436437
// deleted_only = if true, only include deleted projects
@@ -474,8 +475,8 @@ export class CourseStore extends Store<CourseState> {
474475
v.sort((a, b) =>
475476
cmp(
476477
this.get_student_sort_name(a.get("student_id")),
477-
this.get_student_sort_name(b.get("student_id"))
478-
)
478+
this.get_student_sort_name(b.get("student_id")),
479+
),
479480
);
480481
return v;
481482
}
@@ -489,14 +490,14 @@ export class CourseStore extends Store<CourseState> {
489490

490491
public get_nbgrader_scores(
491492
assignment_id: string,
492-
student_id: string
493+
student_id: string,
493494
): { [ipynb: string]: NotebookScores | string } | undefined {
494495
const { assignment } = this.resolve({ assignment_id });
495496
return assignment?.getIn(["nbgrader_scores", student_id])?.toJS();
496497
}
497498

498499
public get_nbgrader_score_ids(
499-
assignment_id: string
500+
assignment_id: string,
500501
): { [ipynb: string]: string[] } | undefined {
501502
const { assignment } = this.resolve({ assignment_id });
502503
const ids = assignment?.get("nbgrader_score_ids")?.toJS();
@@ -606,7 +607,7 @@ export class CourseStore extends Store<CourseState> {
606607
// get info about relation between a student and a given assignment
607608
public student_assignment_info(
608609
student_id: string,
609-
assignment_id: string
610+
assignment_id: string,
610611
): {
611612
last_assignment?: LastCopyInfo;
612613
last_collect?: LastCopyInfo;
@@ -665,7 +666,7 @@ export class CourseStore extends Store<CourseState> {
665666
step: AssignmentCopyStep,
666667
assignment_id: string,
667668
student_id: string,
668-
no_error?: boolean
669+
no_error?: boolean,
669670
): boolean {
670671
const x = this.getIn([
671672
"assignments",
@@ -688,7 +689,7 @@ export class CourseStore extends Store<CourseState> {
688689
}
689690

690691
public get_assignment_status(
691-
assignment_id: string
692+
assignment_id: string,
692693
): AssignmentStatus | undefined {
693694
//
694695
// Compute and return an object that has fields (deleted students are ignored)
@@ -791,7 +792,7 @@ export class CourseStore extends Store<CourseState> {
791792

792793
public student_handout_info(
793794
student_id: string,
794-
handout_id: string
795+
handout_id: string,
795796
): { status?: LastCopyInfo; handout_id: string; student_id: string } {
796797
// status -- important to be undefined if no info -- assumed in code
797798
const status = this.getIn(["handouts", handout_id, "status", student_id]);
@@ -819,7 +820,7 @@ export class CourseStore extends Store<CourseState> {
819820
}
820821

821822
public get_handout_status(
822-
handout_id: string
823+
handout_id: string,
823824
): undefined | { handout: number; not_handout: number } {
824825
//
825826
// Compute and return an object that has fields (deleted students are ignored)
@@ -881,13 +882,13 @@ export class CourseStore extends Store<CourseState> {
881882
student_project_ids: set(
882883
this.get_student_project_ids({
883884
include_deleted: true,
884-
})
885+
}),
885886
),
886887
deleted_project_ids: set(
887888
this.get_student_project_ids({
888889
include_deleted: true,
889890
deleted_only: true,
890-
})
891+
}),
891892
),
892893
upgrade_goal,
893894
});

0 commit comments

Comments
 (0)