Skip to content

Commit b891d7a

Browse files
authored
Merge pull request #4253 from cardstack/cs-10535-investigate-broken-code-formatting-in-staging
Fix AI bot messages rendered as user messages on staging
2 parents 5d0930b + 65fe8c2 commit b891d7a

9 files changed

Lines changed: 237 additions & 51 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@ jobs:
535535
"server-endpoints/search-test.ts",
536536
"server-endpoints/info-test.ts",
537537
"server-endpoints/search-prerendered-test.ts",
538+
"server-config-test.ts",
538539
"server-endpoints/stripe-session-test.ts",
539540
"server-endpoints/stripe-webhook-test.ts",
540541
"server-endpoints/user-and-catalog-test.ts",

packages/matrix/docker/synapse/index.ts

Lines changed: 125 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from 'path';
22
import * as os from 'os';
33
import * as crypto from 'crypto';
4+
import * as net from 'net';
45
import * as fse from 'fs-extra';
56
import { request } from '@playwright/test';
67
import {
@@ -40,14 +41,58 @@ export interface SynapseInstance extends SynapseConfig {
4041
}
4142

4243
const synapses = new Map<string, SynapseInstance>();
44+
const dynamicHostPortStartAttempts = 5;
45+
46+
function findAvailablePort(preferred?: number): Promise<number> {
47+
return new Promise((resolve, reject) => {
48+
let server = net.createServer();
49+
50+
server.on('error', (error: NodeJS.ErrnoException) => {
51+
server.close();
52+
if (preferred != null && error.code === 'EADDRINUSE') {
53+
findAvailablePort(undefined).then(resolve, reject);
54+
return;
55+
}
56+
reject(error);
57+
});
58+
59+
server.listen(preferred ?? 0, '127.0.0.1', () => {
60+
let address = server.address();
61+
if (!address || typeof address === 'string') {
62+
server.close(() =>
63+
reject(new Error('Could not determine available port')),
64+
);
65+
return;
66+
}
67+
let { port } = address;
68+
server.close((error) => {
69+
if (error) {
70+
reject(error);
71+
return;
72+
}
73+
resolve(port);
74+
});
75+
});
76+
});
77+
}
4378

4479
function randB64Bytes(numBytes: number): string {
4580
return crypto.randomBytes(numBytes).toString('base64').replace(/=*$/, '');
4681
}
4782

83+
function isPortBindError(error: unknown): boolean {
84+
let message = error instanceof Error ? error.message : String(error);
85+
return /address already in use|port is already allocated/i.test(message);
86+
}
87+
4888
export async function cfgDirFromTemplate(
4989
template: string,
5090
dataDir?: string,
91+
options?: {
92+
publicBaseUrl?: string;
93+
host?: string;
94+
port?: number;
95+
},
5196
): Promise<SynapseConfig> {
5297
const templateDir = path.join(__dirname, template);
5398

@@ -69,7 +114,9 @@ export async function cfgDirFromTemplate(
69114
const macaroonSecret = randB64Bytes(16);
70115
const formSecret = randB64Bytes(16);
71116

72-
const baseUrl = `http://${SYNAPSE_IP_ADDRESS}:${SYNAPSE_PORT}`;
117+
const host = options?.host ?? SYNAPSE_IP_ADDRESS;
118+
const port = options?.port ?? SYNAPSE_PORT;
119+
const baseUrl = options?.publicBaseUrl ?? `http://${host}:${port}`;
73120

74121
// now copy homeserver.yaml, applying substitutions
75122
console.log(`Gen ${path.join(templateDir, 'homeserver.yaml')}`);
@@ -95,8 +142,8 @@ export async function cfgDirFromTemplate(
95142
);
96143

97144
return {
98-
port: SYNAPSE_PORT,
99-
host: SYNAPSE_IP_ADDRESS,
145+
port,
146+
host,
100147
baseUrl,
101148
configDir,
102149
registrationSecret,
@@ -136,51 +183,80 @@ export async function synapseStart(
136183
}
137184
await Promise.allSettled(stopPromises);
138185
}
139-
const synCfg = await cfgDirFromTemplate(
140-
opts?.template ?? 'test',
141-
opts?.dataDir,
142-
);
143-
let containerName =
144-
opts?.containerName ||
145-
(isEnvironmentMode()
146-
? getSynapseContainerName()
147-
: path.basename(synCfg.configDir));
148-
console.log(
149-
`Starting synapse with config dir ${synCfg.configDir} in container ${containerName}...`,
186+
let useDynamicHostPort = Boolean(
187+
isEnvironmentMode() || opts?.dynamicHostPort,
150188
);
151189
await dockerCreateNetwork({ networkName: 'boxel' });
152190

153-
let dockerParams: string[] = [
154-
'--rm',
155-
'-v',
156-
`${synCfg.configDir}:/data`,
157-
'-v',
158-
`${path.join(__dirname, 'templates')}:/custom/templates/`,
159-
];
160-
if (isEnvironmentMode() || opts?.dynamicHostPort) {
161-
// Dynamic host port, with fixed container IP only when not running in branch mode
162-
if (!isEnvironmentMode()) {
163-
dockerParams.push(`--ip=${synCfg.host}`);
164-
}
165-
dockerParams.push('-p', '0:8008/tcp', '--network=boxel');
166-
} else {
167-
dockerParams.push(
168-
`--ip=${synCfg.host}`,
169-
'-p',
170-
`${synCfg.port}:8008/tcp`,
171-
'--network=boxel',
191+
let hostPort = SYNAPSE_PORT;
192+
let synCfg!: SynapseConfig;
193+
let containerName!: string;
194+
let synapseId!: string;
195+
let attempts = useDynamicHostPort ? dynamicHostPortStartAttempts : 1;
196+
197+
for (let attempt = 1; attempt <= attempts; attempt++) {
198+
hostPort = useDynamicHostPort ? await findAvailablePort() : SYNAPSE_PORT;
199+
synCfg = await cfgDirFromTemplate(opts?.template ?? 'test', opts?.dataDir, {
200+
host: useDynamicHostPort ? '127.0.0.1' : SYNAPSE_IP_ADDRESS,
201+
port: hostPort,
202+
publicBaseUrl: `http://localhost:${hostPort}`,
203+
});
204+
containerName =
205+
opts?.containerName ||
206+
(isEnvironmentMode()
207+
? getSynapseContainerName()
208+
: path.basename(synCfg.configDir));
209+
console.log(
210+
`Starting synapse with config dir ${synCfg.configDir} in container ${containerName}...`,
172211
);
173-
}
174212

175-
const synapseId = await dockerRun({
176-
image: 'matrixdotorg/synapse:v1.126.0',
177-
containerName,
178-
dockerParams,
179-
applicationParams: ['run'],
180-
runAsUser: true,
181-
});
213+
let dockerParams: string[] = [
214+
'--rm',
215+
'-v',
216+
`${synCfg.configDir}:/data`,
217+
'-v',
218+
`${path.join(__dirname, 'templates')}:/custom/templates/`,
219+
];
220+
if (useDynamicHostPort) {
221+
// In dynamic-host-port mode multiple harnesses may run concurrently, so
222+
// we must not claim the shared fixed Synapse container IP.
223+
dockerParams.push('-p', `${hostPort}:8008/tcp`, '--network=boxel');
224+
} else {
225+
dockerParams.push(
226+
`--ip=${synCfg.host}`,
227+
'-p',
228+
`${synCfg.port}:8008/tcp`,
229+
'--network=boxel',
230+
);
231+
}
182232

183-
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}`);
233+
try {
234+
synapseId = await dockerRun({
235+
image: 'matrixdotorg/synapse:v1.126.0',
236+
containerName,
237+
dockerParams,
238+
applicationParams: ['run'],
239+
runAsUser: true,
240+
});
241+
break;
242+
} catch (error) {
243+
if (
244+
!useDynamicHostPort ||
245+
!isPortBindError(error) ||
246+
attempt === attempts
247+
) {
248+
throw error;
249+
}
250+
console.warn(
251+
`Synapse host port ${hostPort} was claimed before Docker bound it; retrying (${attempt}/${attempts})...`,
252+
);
253+
if (!opts?.dataDir) {
254+
await fse.remove(synCfg.configDir);
255+
}
256+
}
257+
}
258+
259+
console.log(`Started synapse with id ${synapseId} on port ${hostPort}`);
184260

185261
// Await Synapse healthcheck
186262
await dockerExec({
@@ -199,9 +275,13 @@ export async function synapseStart(
199275
],
200276
});
201277

202-
let hostPort = synCfg.port;
203-
if (isEnvironmentMode() || opts?.dynamicHostPort) {
204-
hostPort = await resolveHostPort(synapseId);
278+
if (useDynamicHostPort) {
279+
let resolvedPort = await resolveHostPort(synapseId);
280+
if (resolvedPort !== hostPort) {
281+
throw new Error(
282+
`Synapse started on unexpected host port ${resolvedPort}; expected ${hostPort}`,
283+
);
284+
}
205285
console.log(`Synapse dynamic host port: ${hostPort}`);
206286
}
207287

packages/realm-server/scripts/start-production.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ EXTERNAL_CATALOG_REALM_URL="${RESOLVED_EXTERNAL_CATALOG_REALM_URL:-$DEFAULT_EXTE
2525
NODE_NO_WARNINGS=1 \
2626
LOW_CREDIT_THRESHOLD=2000 \
2727
MATRIX_URL=https://matrix.boxel.ai \
28+
MATRIX_SERVER_NAME=boxel.ai \
2829
BOXEL_HOST_URL=https://app.boxel.ai \
2930
REALM_SERVER_MATRIX_USERNAME=realm_server \
3031
PUBLISHED_REALM_BOXEL_SPACE_DOMAIN='boxel.space' \

packages/realm-server/scripts/start-staging.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ EXTERNAL_CATALOG_REALM_URL="${RESOLVED_EXTERNAL_CATALOG_REALM_URL:-$DEFAULT_EXTE
2525
NODE_NO_WARNINGS=1 \
2626
LOW_CREDIT_THRESHOLD=2000 \
2727
MATRIX_URL=https://matrix-staging.stack.cards \
28+
MATRIX_SERVER_NAME=stack.cards \
2829
BOXEL_HOST_URL=https://realms-staging.stack.cards \
2930
REALM_SERVER_MATRIX_USERNAME=realm_server \
3031
PUBLISHED_REALM_BOXEL_SPACE_DOMAIN='staging.boxel.dev' \

packages/realm-server/server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,9 @@ export class RealmServer {
707707
hostsOwnAssets: false,
708708
assetsURL: this.assetsURL.href,
709709
matrixURL: this.matrixClient.matrixURL.href.replace(/\/$/, ''),
710-
matrixServerName: this.matrixClient.matrixURL.hostname,
710+
matrixServerName:
711+
process.env.MATRIX_SERVER_NAME ||
712+
this.matrixClient.matrixURL.hostname,
711713
realmServerURL: this.serverURL.href,
712714
resolvedBaseRealmURL: rewriteRealmURL(config.resolvedBaseRealmURL),
713715
resolvedCatalogRealmURL: rewriteRealmURL(

packages/realm-server/tests/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ import './server-endpoints/queue-status-test';
158158
import './server-endpoints/realm-lifecycle-test';
159159
import './server-endpoints/search-test';
160160
import './server-endpoints/search-prerendered-test';
161+
import './server-config-test';
161162
import './server-endpoints/info-test';
162163
import './server-endpoints/stripe-session-test';
163164
import './server-endpoints/stripe-webhook-test';
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { module, test } from 'qunit';
2+
import { basename } from 'path';
3+
import { dirSync } from 'tmp';
4+
5+
import { RealmServer } from '../server';
6+
7+
module(basename(__filename), function () {
8+
test('prefers MATRIX_SERVER_NAME over matrix URL hostname in host config', async function (assert) {
9+
let originalMatrixServerName = process.env.MATRIX_SERVER_NAME;
10+
let tempDir = dirSync({ unsafeCleanup: true });
11+
12+
process.env.MATRIX_SERVER_NAME = 'stack.cards';
13+
14+
try {
15+
let server = new RealmServer({
16+
serverURL: new URL('http://127.0.0.1:4448'),
17+
realms: [],
18+
virtualNetwork: {} as any,
19+
matrixClient: {
20+
matrixURL: new URL('http://localhost:8008/'),
21+
} as any,
22+
realmServerSecretSeed: 'test-realm-server-secret',
23+
realmSecretSeed: 'test-realm-secret',
24+
grafanaSecret: 'test-grafana-secret',
25+
realmsRootPath: tempDir.name,
26+
dbAdapter: {} as any,
27+
queue: {} as any,
28+
definitionLookup: {} as any,
29+
assetsURL: new URL('http://example.com/notional-assets-host/'),
30+
matrixRegistrationSecret: 'test-matrix-registration-secret',
31+
getIndexHTML: async () =>
32+
`<html><head><meta name="@cardstack/host/config/environment" content="${encodeURIComponent(
33+
JSON.stringify({
34+
matrixURL: 'http://localhost:8008',
35+
matrixServerName: 'localhost',
36+
realmServerURL: 'http://localhost:4201/',
37+
publishedRealmBoxelSpaceDomain: 'localhost:4201',
38+
publishedRealmBoxelSiteDomain: 'localhost:4201',
39+
}),
40+
)}"></head><body></body></html>`,
41+
});
42+
43+
let html = await (server as any).retrieveIndexHTML();
44+
let match = html.match(
45+
/<meta name="@cardstack\/host\/config\/environment" content="([^"]+)">/,
46+
);
47+
48+
assert.ok(match, 'host config environment meta tag is present');
49+
50+
let config = JSON.parse(decodeURIComponent(match![1]));
51+
assert.strictEqual(
52+
config.matrixServerName,
53+
'stack.cards',
54+
'uses MATRIX_SERVER_NAME override in host config',
55+
);
56+
} finally {
57+
if (originalMatrixServerName == null) {
58+
delete process.env.MATRIX_SERVER_NAME;
59+
} else {
60+
process.env.MATRIX_SERVER_NAME = originalMatrixServerName;
61+
}
62+
tempDir.removeCallback();
63+
}
64+
});
65+
});

packages/software-factory/src/harness/isolated-realm-stack.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ export async function startIsolatedRealmStack({
341341
GRAFANA_SECRET,
342342
HOST_URL: context.hostURL,
343343
MATRIX_URL: context.matrixURL,
344+
MATRIX_SERVER_NAME: new URL(context.matrixURL).hostname,
344345
MATRIX_REGISTRATION_SHARED_SECRET: context.matrixRegistrationSecret,
345346
REALM_SERVER_MATRIX_USERNAME: DEFAULT_MATRIX_SERVER_USERNAME,
346347
REALM_SERVER_FULL_INDEX_ON_STARTUP: String(fullIndexOnStartup),

0 commit comments

Comments
 (0)