Skip to content

Commit a897a87

Browse files
committed
Merge branch 'master' into fs2
2 parents e146dd1 + 0bf7e80 commit a897a87

File tree

9 files changed

+136
-64
lines changed

9 files changed

+136
-64
lines changed

src/packages/conat/core/client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,10 @@ export class Client extends EventEmitter {
510510
reconnection: true,
511511
});
512512

513-
this.conn.on("info", (info) => {
513+
this.conn.on("info", (info, ack) => {
514+
if (typeof ack == "function") {
515+
ack();
516+
}
514517
const firstTime = this.info == null;
515518
this.info = info;
516519
this.emit("info", info);

src/packages/conat/core/server.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import {
5353
import { Patterns } from "./patterns";
5454
import { is_array } from "@cocalc/util/misc";
5555
import { UsageMonitor } from "@cocalc/conat/monitor/usage";
56-
import { once } from "@cocalc/util/async-utils";
56+
import { once, until } from "@cocalc/util/async-utils";
5757
import {
5858
clusterLink,
5959
type ClusterLink,
@@ -940,7 +940,7 @@ export class ConatServer extends EventEmitter {
940940
this.subscriptions[id] = new Set<string>();
941941
}
942942

943-
socket.emit("info", { ...this.info(), user });
943+
this.sendInfo(socket, user);
944944

945945
socket.on("stats", ({ recv0 }) => {
946946
const s = this.stats[socket.id];
@@ -1086,6 +1086,38 @@ export class ConatServer extends EventEmitter {
10861086
});
10871087
};
10881088

1089+
sendInfo = async (socket, user) => {
1090+
// we send info with an ack because I think sometimes the initial "info"
1091+
// message gets dropped, leaving a broken hanging connection that never
1092+
// does anything (until the user explicitly refreshes their browser).
1093+
// I did see what is probably this in production frequently.
1094+
try {
1095+
await until(
1096+
async () => {
1097+
if (!socket.conn?.readyState.startsWith("o")) {
1098+
// logger.debug(`failed to send "info" message to ${socket.id}`);
1099+
// readyState not defined or not opened or opening, so connection must
1100+
// have been closed before success.
1101+
return true;
1102+
}
1103+
try {
1104+
await socket
1105+
.timeout(7500)
1106+
.emitWithAck("info", { ...this.info(), user });
1107+
return true;
1108+
} catch (err) {
1109+
// logger.debug(`error sending "info" message to ${socket.id}`, err);
1110+
return false;
1111+
}
1112+
},
1113+
{ min: 5000, max: 30000, timeout: 120_000 },
1114+
);
1115+
} catch {
1116+
// never ack'd "info" after a few minutes -- could just be an old client,
1117+
// so don't do anything at this point.
1118+
}
1119+
};
1120+
10891121
address = () => getServerAddress(this.options);
10901122

10911123
// create new client in the same process connected to this server.
@@ -1349,8 +1381,8 @@ export class ConatServer extends EventEmitter {
13491381
const usage = async () => {
13501382
return { [this.id]: this.usage.stats() };
13511383
};
1352-
// user has to explicitly refresh there browser after
1353-
// being disconnected this way
1384+
// user has to explicitly refresh their browser after
1385+
// being disconnected this way:
13541386
const disconnect = async (ids: string | string[]) => {
13551387
if (typeof ids == "string") {
13561388
ids = [ids];

src/packages/conat/service/service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,12 @@ export class ConatService extends EventEmitter {
210210
await mesg.respond(resp);
211211
} catch (err) {
212212
const data = { error: `${err}` };
213-
await mesg.respond(data);
213+
try {
214+
await mesg.respond(data);
215+
} catch (err2) {
216+
// do not crash on sending an error report:
217+
logger.debug("WARNING: unable to send error", this.name, err, err2);
218+
}
214219
}
215220
}
216221
};

src/packages/conat/socket/util.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type Role = "client" | "server";
1313
// socketio and use those to manage things. This ping
1414
// is entirely a "just in case" backup if some event
1515
// were missed (e.g., a kill -9'd process...)
16-
export const PING_PONG_INTERVAL = 60000;
16+
export const PING_PONG_INTERVAL = 90000;
1717

1818
// We queue up unsent writes, but only up to a point (to not have a huge memory issue).
1919
// Any write beyond this size result in an exception.
@@ -24,8 +24,8 @@ export const PING_PONG_INTERVAL = 60000;
2424
export const DEFAULT_MAX_QUEUE_SIZE = 1000;
2525

2626
export let DEFAULT_COMMAND_TIMEOUT = 10_000;
27-
export let DEFAULT_KEEP_ALIVE = 90_000;
28-
export let DEFAULT_KEEP_ALIVE_TIMEOUT = 15_000;
27+
export let DEFAULT_KEEP_ALIVE = 25_000;
28+
export let DEFAULT_KEEP_ALIVE_TIMEOUT = 10_000;
2929

3030
export function setDefaultSocketTimeouts({
3131
command = DEFAULT_COMMAND_TIMEOUT,

src/packages/frontend/client/idle.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class IdleClient {
8484

8585
if (NEVER_TIMEOUT_VISIBLE) {
8686
// If the document is visible right now, then we
87-
// reset the idle timeout., just as if the mouse moved. This means
87+
// reset the idle timeout, just as if the mouse moved. This means
8888
// that users never get the standby timeout if their current browser
8989
// tab is considered visible according to the Page Visibility API
9090
// https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
@@ -110,6 +110,7 @@ export class IdleClient {
110110
// so that if the user sees the idle banner and immediately
111111
// dismisses it, then the experience is less disruptive.
112112
this.delayed_disconnect = setTimeout(() => {
113+
this.delayed_disconnect = undefined;
113114
console.log("Entering standby mode");
114115
this.standbyMode = true;
115116
// console.log("idle timeout: disconnect!");
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { readFile as readProjectFile } from "@cocalc/conat/files/read";
2+
import { once } from "events";
3+
import { path_split } from "@cocalc/util/misc";
4+
import mime from "mime-types";
5+
import getLogger from "../logger";
6+
7+
const DANGEROUS_CONTENT_TYPE = new Set(["image/svg+xml" /*, "text/html"*/]);
8+
9+
const logger = getLogger("hub:proxy:file-download");
10+
11+
// assumes request has already been authenticated!
12+
13+
export async function handleFileDownload(req, res, url, project_id) {
14+
logger.debug("handling the request via conat file streaming", url);
15+
const i = url.indexOf("files/");
16+
const compute_server_id = req.query.id ?? 0;
17+
let j = url.lastIndexOf("?");
18+
if (j == -1) {
19+
j = url.length;
20+
}
21+
const path = decodeURIComponent(url.slice(i + "files/".length, j));
22+
logger.debug("conat: get file", { project_id, path, compute_server_id, url });
23+
const fileName = path_split(path).tail;
24+
const contentType = mime.lookup(fileName);
25+
if (req.query.download != null || DANGEROUS_CONTENT_TYPE.has(contentType)) {
26+
const fileNameEncoded = encodeURIComponent(fileName)
27+
.replace(/['()]/g, escape)
28+
.replace(/\*/g, "%2A");
29+
res.setHeader(
30+
"Content-disposition",
31+
`attachment; filename*=UTF-8''${fileNameEncoded}`,
32+
);
33+
}
34+
res.setHeader("Content-type", contentType);
35+
36+
let headersSent = false;
37+
res.on("finish", () => {
38+
headersSent = true;
39+
});
40+
try {
41+
for await (const chunk of await readProjectFile({
42+
project_id,
43+
compute_server_id,
44+
path,
45+
// allow a long download time (1 hour), since files can be large and
46+
// networks can be slow.
47+
maxWait: 1000 * 60 * 60,
48+
})) {
49+
if (res.writableEnded || res.destroyed) {
50+
break;
51+
}
52+
if (!res.write(chunk)) {
53+
await once(res, "drain");
54+
}
55+
}
56+
res.end();
57+
} catch (err) {
58+
logger.debug(
59+
"ERROR streaming file",
60+
{ project_id, compute_server_id, path },
61+
err,
62+
);
63+
if (!headersSent) {
64+
res.statusCode = 500;
65+
res.end("Error reading file.");
66+
} else {
67+
// Data sent, forcibly kill the connection
68+
res.destroy(err);
69+
}
70+
}
71+
}

src/packages/hub/proxy/handle-request.ts

Lines changed: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,8 @@ import { stripBasePath } from "./util";
1010
import { ProjectControlFunction } from "@cocalc/server/projects/control";
1111
import siteUrl from "@cocalc/database/settings/site-url";
1212
import { parseReq } from "./parse";
13-
import { readFile as readProjectFile } from "@cocalc/conat/files/read";
14-
import { path_split } from "@cocalc/util/misc";
15-
import { once } from "@cocalc/util/async-utils";
1613
import hasAccess from "./check-for-access-to-project";
17-
import mime from "mime-types";
18-
19-
const DANGEROUS_CONTENT_TYPE = new Set(["image/svg+xml" /*, "text/html"*/]);
14+
import { handleFileDownload } from "./file-download";
2015

2116
const logger = getLogger("proxy:handle-request");
2217

@@ -84,7 +79,6 @@ export default function init({ projectControl, isPersonal }: Options) {
8479
// TODO: parseReq is called again in getTarget so need to refactor...
8580
const { type, project_id } = parsed;
8681
if (type == "files") {
87-
dbg("handling the request via conat file streaming");
8882
if (
8983
!(await hasAccess({
9084
project_id,
@@ -96,43 +90,7 @@ export default function init({ projectControl, isPersonal }: Options) {
9690
) {
9791
throw Error(`user does not have read access to project`);
9892
}
99-
const i = url.indexOf("files/");
100-
const compute_server_id = req.query.id ?? 0;
101-
let j = url.lastIndexOf("?");
102-
if (j == -1) {
103-
j = url.length;
104-
}
105-
const path = decodeURIComponent(url.slice(i + "files/".length, j));
106-
dbg("conat: get file", { project_id, path, compute_server_id, url });
107-
const fileName = path_split(path).tail;
108-
const contentType = mime.lookup(fileName);
109-
if (
110-
req.query.download != null ||
111-
DANGEROUS_CONTENT_TYPE.has(contentType)
112-
) {
113-
const fileNameEncoded = encodeURIComponent(fileName)
114-
.replace(/['()]/g, escape)
115-
.replace(/\*/g, "%2A");
116-
res.setHeader(
117-
"Content-disposition",
118-
`attachment; filename*=UTF-8''${fileNameEncoded}`,
119-
);
120-
}
121-
res.setHeader("Content-type", contentType);
122-
for await (const chunk of await readProjectFile({
123-
project_id,
124-
compute_server_id,
125-
path,
126-
// allow a long download time (1 hour), since files can be large and
127-
// networks can be slow.
128-
maxWait: 1000 * 60 * 60,
129-
})) {
130-
if (!res.write(chunk)) {
131-
// backpressure -- wait for it to resolve
132-
await once(res, "drain");
133-
}
134-
}
135-
res.end();
93+
await handleFileDownload(req, res, url, project_id);
13694
return;
13795
}
13896

@@ -182,8 +140,14 @@ export default function init({ projectControl, isPersonal }: Options) {
182140
await handleProxyRequest(req, res);
183141
} catch (err) {
184142
const msg = `WARNING: error proxying request ${req.url} -- ${err}`;
185-
res.writeHead(426, { "Content-Type": "text/html" });
186-
res.end(msg);
143+
try {
144+
// this will fail if handleProxyRequest already wrote a header, so we
145+
// try/catch it.
146+
res.writeHead(500, { "Content-Type": "text/html" });
147+
} catch {}
148+
try {
149+
res.end(msg);
150+
} catch {}
187151
// Not something to log as an error -- just debug; it's normal for it to happen, e.g., when
188152
// a project isn't running.
189153
logger.debug(msg);

src/packages/hub/servers/express-app.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
The main hub express app.
33
*/
44

5-
import compression from "compression";
65
import cookieParser from "cookie-parser";
76
import express from "express";
87
import ms from "ms";
@@ -33,6 +32,9 @@ import basePath from "@cocalc/backend/base-path";
3332
import { initConatServer } from "@cocalc/server/conat/socketio";
3433
import { conatSocketioCount } from "@cocalc/backend/data";
3534

35+
// NOTE: we are not using compression because that interferes with streaming file download,
36+
// and could be generally confusing.
37+
3638
// Used for longterm caching of files. This should be in units of seconds.
3739
const MAX_AGE = Math.round(ms("10 days") / 1000);
3840
const SHORT_AGE = Math.round(ms("10 seconds") / 1000);
@@ -80,12 +82,6 @@ export default async function init(opts: Options): Promise<{
8082
app.use(vhostShare());
8183
}
8284

83-
// Enable compression, as suggested by
84-
// http://expressjs.com/en/advanced/best-practice-performance.html#use-gzip-compression
85-
// NOTE "Express runs everything in order" --
86-
// https://github.com/expressjs/compression/issues/35#issuecomment-77076170
87-
app.use(compression());
88-
8985
app.use(cookieParser());
9086

9187
// Install custom middleware to track response time metrics via prometheus

src/packages/util/smc-version.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
/* autogenerated by the update_version script */
2-
exports.version=1752527312;
2+
exports.version=1752795879;

0 commit comments

Comments
 (0)