Skip to content

Commit 10a363a

Browse files
committed
Merge remote-tracking branch 'origin/master' into api-exec-await
2 parents 7abd8f4 + 652e524 commit 10a363a

38 files changed

+1485
-1224
lines changed

src/compute/compute/lib/listings.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export async function initListings({
5252
project_id,
5353
compute_server_id,
5454
getListing,
55-
createWatcher: (path: string, debounceMs: number) =>
56-
new Watcher(path, debounceMs),
55+
createWatcher: (path: string, debounce: number) =>
56+
new Watcher(path, { debounce }),
5757
onDeletePath: (path) => {
5858
logger.debug("onDeletePath -- TODO:", { path });
5959
},

src/packages/backend/path-watcher.ts

+30-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
*/
55

66
/*
7+
Watch A DIRECTORY for changes. Use ./watcher.ts for a single file.
8+
9+
710
Slightly generalized fs.watch that works even when the directory doesn't exist,
811
but also doesn't provide any information about what changed.
912
@@ -76,11 +79,18 @@ export class Watcher extends EventEmitter {
7679
private exists: boolean;
7780
private watchContents?: FSWatcher;
7881
private watchExistence?: FSWatcher;
82+
private interval_ms: number;
7983
private debounce_ms: number;
8084
private debouncedChange: any;
8185
private log: Function;
8286

83-
constructor(path: string, debounce_ms: number) {
87+
constructor(
88+
path: string,
89+
{
90+
debounce: debounce_ms = 0,
91+
interval: interval_ms,
92+
}: { debounce?: number; interval?: number } = {},
93+
) {
8494
super();
8595
this.log = logger.extend(path).debug;
8696
this.log(`initializing: poll=${POLLING}`);
@@ -89,10 +99,13 @@ export class Watcher extends EventEmitter {
8999
}
90100
this.path = path.startsWith("/") ? path : join(process.env.HOME, path);
91101
this.debounce_ms = debounce_ms;
92-
this.debouncedChange = debounce(this.change.bind(this), this.debounce_ms, {
93-
leading: true,
94-
trailing: true,
95-
}).bind(this);
102+
this.interval_ms = interval_ms ?? DEFAULT_POLL_MS;
103+
this.debouncedChange = this.debounce_ms
104+
? debounce(this.change, this.debounce_ms, {
105+
leading: true,
106+
trailing: true,
107+
}).bind(this)
108+
: this.change;
96109
this.init();
97110
}
98111

@@ -109,8 +122,16 @@ export class Watcher extends EventEmitter {
109122
}
110123
}
111124

125+
private chokidarOptions = () => {
126+
return {
127+
...ChokidarOpts,
128+
interval: this.interval_ms,
129+
binaryInterval: this.interval_ms,
130+
};
131+
};
132+
112133
private initWatchContents(): void {
113-
this.watchContents = watch(this.path, ChokidarOpts);
134+
this.watchContents = watch(this.path, this.chokidarOptions());
114135
this.watchContents.on("all", this.debouncedChange);
115136
this.watchContents.on("error", (err) => {
116137
this.log(`error watching listings -- ${err}`);
@@ -119,7 +140,7 @@ export class Watcher extends EventEmitter {
119140

120141
private async initWatchExistence(): Promise<void> {
121142
const containing_path = path_split(this.path).head;
122-
this.watchExistence = watch(containing_path, ChokidarOpts);
143+
this.watchExistence = watch(containing_path, this.chokidarOptions());
123144
this.watchExistence.on("all", this.watchExistenceChange(containing_path));
124145
this.watchExistence.on("error", (err) => {
125146
this.log(`error watching for existence of ${this.path} -- ${err}`);
@@ -147,9 +168,9 @@ export class Watcher extends EventEmitter {
147168
}
148169
};
149170

150-
private change(): void {
171+
private change = (): void => {
151172
this.emit("change");
152-
}
173+
};
153174

154175
public close(): void {
155176
this.watchExistence?.close();

src/packages/backend/watcher.ts

+50-50
Original file line numberDiff line numberDiff line change
@@ -4,78 +4,78 @@
44
*/
55

66
/*
7-
Watch a file for changes
7+
Watch one SINGLE FILE for changes. Use ./path-watcher.ts for a directory.
88
9-
Watch for changes to the given file. Returns obj, which
9+
Watch for changes to the given file, which means the mtime changes or the
10+
mode changes (e.g., readonly versus readwrite). Returns obj, which
1011
is an event emitter with events:
1112
12-
- 'change', ctime - when file changes or is created
13+
- 'change', ctime, stats - when file changes or is created
1314
- 'delete' - when file is deleted
1415
1516
and a method .close().
1617
1718
Only fires after the file definitely has not had its
18-
ctime changed for at least debounce ms (this is the atomic
19-
option to chokidar). Does NOT fire when the file first
20-
has ctime changed.
19+
ctime changed for at least debounce ms. Does NOT
20+
fire when the file first has ctime changed.
21+
22+
NOTE: for directories we use chokidar in path-watcher. However,
23+
for a single file using polling, chokidar is horribly buggy and
24+
lacking in functionality (e.g., https://github.com/paulmillr/chokidar/issues/1132),
25+
and declared all bugs fixed, so we steer clear. It had a lot of issues
26+
with just noticing actual file changes.
27+
28+
We *always* use polling to fully support networked filesystems.
2129
*/
2230

2331
import { EventEmitter } from "node:events";
24-
import { watch, FSWatcher } from "chokidar";
32+
import { unwatchFile, watchFile } from "node:fs";
2533
import { getLogger } from "./logger";
2634
import { debounce as lodashDebounce } from "lodash";
2735

28-
const L = getLogger("watcher");
36+
const logger = getLogger("backend:watcher");
2937

3038
export class Watcher extends EventEmitter {
3139
private path: string;
32-
private interval: number;
33-
private watcher: FSWatcher;
3440

35-
constructor(path: string, interval: number = 300, debounce: number = 0) {
41+
constructor(
42+
path: string,
43+
{ debounce, interval = 300 }: { debounce?: number; interval?: number } = {},
44+
) {
3645
super();
3746
this.path = path;
38-
this.interval = interval;
39-
40-
L.debug(`${path}: interval=${interval}, debounce=${debounce}`);
41-
this.watcher = watch(this.path, {
42-
interval: this.interval,
43-
// polling is critical for network mounted file systems,
44-
// and given architecture of cocalc there is no easy way around this.
45-
// E.g., on compute servers, everything breaks involving sync or cloudfs,
46-
// and in shared project s3/gcsfuse/sshfs would all break. So we
47-
// use polling.
48-
usePolling: true,
49-
persistent: false,
50-
alwaysStat: true,
51-
atomic: true,
52-
});
53-
this.watcher.on("unlink", () => {
54-
this.emit("delete");
55-
});
56-
this.watcher.on("unlinkDir", () => {
57-
this.emit("delete");
58-
});
59-
60-
const emitChange = lodashDebounce(
61-
(ctime) => this.emit("change", ctime),
62-
debounce,
63-
);
64-
this.watcher.on("error", (err) => {
65-
L.debug("WATCHER error -- ", err);
66-
});
67-
68-
this.watcher.on("change", (_, stats) => {
69-
if (stats == null) {
70-
L.debug("WATCHER change with no stats (shouldn't happen)", { path });
71-
return;
72-
}
73-
emitChange(stats.ctime);
74-
});
47+
48+
logger.debug("watchFile", { path, debounce, interval });
49+
watchFile(this.path, { persistent: false, interval }, this.handleChange);
50+
51+
if (debounce) {
52+
this.emitChange = lodashDebounce(this.emitChange, debounce);
53+
}
7554
}
7655

77-
close = async () => {
56+
private emitChange = (stats) => {
57+
this.emit("change", stats.ctime, stats);
58+
};
59+
60+
private handleChange = (curr, prev) => {
61+
const path = this.path;
62+
if (!curr.dev) {
63+
logger.debug("handleChange: delete", { path });
64+
this.emit("delete");
65+
return;
66+
}
67+
if (curr.mtimeMs == prev.mtimeMs && curr.mode == prev.mode) {
68+
logger.debug("handleChange: access but no change", { path });
69+
// just *accessing* triggers watchFile (really StatWatcher), of course.
70+
return;
71+
}
72+
logger.debug("handleChange: change", { path });
73+
this.emitChange(curr);
74+
};
75+
76+
close = () => {
77+
logger.debug("close", this.path);
7878
this.removeAllListeners();
79-
await this.watcher.close();
79+
unwatchFile(this.path, this.handleChange);
8080
};
8181
}

src/packages/frontend/chat/chatroom.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export const ChatRoom: React.FC<Props> = ({ project_id, path }) => {
197197
}
198198
placement="left"
199199
>
200-
<Icon name="arrow-down" /> <VisibleMDLG>New</VisibleMDLG>
200+
<Icon name="arrow-down" /> <VisibleMDLG>Newest</VisibleMDLG>
201201
</Tip>
202202
</Button>
203203
);

src/packages/frontend/chat/input.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ export default function ChatInput({
9292
const lastSavedRef = useRef<string | null>(null);
9393
const saveChat = useDebouncedCallback(
9494
(input) => {
95-
if (!isMountedRef.current || syncdb == null) return;
95+
if (!isMountedRef.current || syncdb == null || !saveOnUnmountRef.current)
96+
return;
9697
onChange(input);
9798
lastSavedRef.current = input;
9899
// also save to syncdb, so we have undo, etc.
@@ -129,8 +130,12 @@ export default function ChatInput({
129130
},
130131
);
131132

133+
const saveOnUnmountRef = useRef<boolean>(true);
132134
useEffect(() => {
133135
return () => {
136+
if (!saveOnUnmountRef.current) {
137+
return;
138+
}
134139
// save before unmounting. This is very important since if a new reply comes in,
135140
// then the input component gets unmounted, then remounted BELOW the reply.
136141
// Note: it is still slightly annoying, due to loss of focus... however, data
@@ -148,7 +153,6 @@ export default function ChatInput({
148153
) {
149154
return;
150155
}
151-
152156
syncdb.set({
153157
event: "draft",
154158
sender_id,
@@ -206,6 +210,9 @@ export default function ChatInput({
206210
saveChat(input);
207211
}}
208212
onShiftEnter={(input) => {
213+
// no need to save on unmount, since we are saving
214+
// the correct state below.
215+
saveOnUnmountRef.current = false;
209216
setInput(input);
210217
saveChat(input);
211218
saveChat.cancel();

src/packages/frontend/chat/message.tsx

+4-7
Original file line numberDiff line numberDiff line change
@@ -610,22 +610,19 @@ export default function Message(props: Readonly<Props>) {
610610
edited_message_ref.current = value;
611611
}}
612612
/>
613-
<div style={{ marginTop: "10px" }}>
613+
<div style={{ marginTop: "10px", display: "flex" }}>
614614
<Button
615-
type="primary"
616615
style={{ marginRight: "5px" }}
617-
onClick={saveEditedMessage}
618-
>
619-
<Icon name="save" /> Save Edited Message
620-
</Button>
621-
<Button
622616
onClick={() => {
623617
props.actions?.set_editing(message, false);
624618
props.actions?.delete_draft(date);
625619
}}
626620
>
627621
Cancel
628622
</Button>
623+
<Button type="primary" onClick={saveEditedMessage}>
624+
<Icon name="save" /> Save Edited Message
625+
</Button>
629626
</div>
630627
</div>
631628
);

src/packages/frontend/editors/task-editor/history-viewer.tsx

+6-18
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,18 @@ History viewer for Tasks notebooks --- very similar to same file in jupyter/ di
1010
import { Checkbox } from "antd";
1111
import { fromJS, Map } from "immutable";
1212
import TaskList from "./list";
13-
import { CSS, React, useState } from "../../app-framework";
13+
import { useState } from "../../app-framework";
1414
import { cmp } from "@cocalc/util/misc";
15-
import { SyncDB } from "@cocalc/sync/editor/db";
1615
import { Tasks } from "./types";
1716

18-
const SHOW_DONE_STYLE: CSS = {
17+
const SHOW_DONE_STYLE = {
1918
fontSize: "12pt",
2019
color: "#666",
2120
padding: "5px 15px",
2221
borderBottom: "1px solid lightgrey",
2322
} as const;
2423

25-
interface Props {
26-
syncdb: SyncDB;
27-
version: Date;
28-
font_size: number;
29-
}
30-
31-
export const TasksHistoryViewer: React.FC<Props> = ({
32-
syncdb,
33-
version,
34-
font_size,
35-
}) => {
24+
export function TasksHistoryViewer({ doc, project_id, path, font_size }) {
3625
const [show_done, set_show_done] = useState(false);
3726

3827
function render_task_list(doc) {
@@ -50,8 +39,8 @@ export const TasksHistoryViewer: React.FC<Props> = ({
5039

5140
return (
5241
<TaskList
53-
path={syncdb.get_path()}
54-
project_id={syncdb.get_project_id()}
42+
path={path}
43+
project_id={project_id}
5544
tasks={tasks}
5645
visible={visible}
5746
read_only={true}
@@ -60,7 +49,6 @@ export const TasksHistoryViewer: React.FC<Props> = ({
6049
);
6150
}
6251

63-
const doc = syncdb.version(version);
6452
return (
6553
<div
6654
style={{
@@ -81,4 +69,4 @@ export const TasksHistoryViewer: React.FC<Props> = ({
8169
{doc == null ? <span>Unknown version</span> : render_task_list(doc)}
8270
</div>
8371
);
84-
};
72+
}

src/packages/frontend/file-associations.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import videoExtensions from "video-extensions";
2323
import audioExtensions from "audio-extensions";
2424
import { filename_extension } from "@cocalc/util/misc";
2525

26-
export function filenameMode(path: string): string {
27-
return file_associations[filename_extension(path)]?.opts?.mode ?? "text";
26+
export function filenameMode(path: string, fallback = "text"): string {
27+
return file_associations[filename_extension(path)]?.opts?.mode ?? fallback;
2828
}
2929

3030
const codemirror_associations: { [ext: string]: string } = {

0 commit comments

Comments
 (0)