Skip to content

Commit

Permalink
chore: make trace server work over http (#23561)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Jun 7, 2023
1 parent 2e327c9 commit 0b30f20
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 63 deletions.
28 changes: 17 additions & 11 deletions packages/playwright-core/src/server/trace/viewer/traceViewer.ts
Expand Up @@ -94,9 +94,9 @@ async function startTraceViewerServer(traceUrls: string[], options?: Options): P
});

const params = traceUrls.map(t => `trace=${t}`);
const transport = options?.transport || (options?.isServer ? new StdinServer() : undefined);

if (options?.transport) {
const transport = options?.transport;
if (transport) {
const guid = createGuid();
params.push('ws=' + guid);
const wss = new wsServer({ server: server.server(), path: '/' + guid });
Expand Down Expand Up @@ -135,7 +135,6 @@ async function startTraceViewerServer(traceUrls: string[], options?: Options): P
}

export async function openTraceViewerApp(traceUrls: string[], browserName: string, options?: Options): Promise<Page> {
const stdinServer = options?.isServer ? new StdinServer() : undefined;
const { url } = await startTraceViewerServer(traceUrls, options);
const traceViewerPlaywright = createPlaywright({ sdkLanguage: 'javascript', isInternalPlaywright: true });
const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName;
Expand Down Expand Up @@ -176,7 +175,6 @@ export async function openTraceViewerApp(traceUrls: string[], browserName: strin
page.on('close', () => process.exit());

await page.mainFrame().goto(serverSideCallMetadata(), url);
stdinServer?.setPage(page);
return page;
}

Expand All @@ -187,7 +185,7 @@ async function openTraceInBrowser(traceUrls: string[], options?: Options) {
await open(url, { wait: true }).catch(() => {});
}

class StdinServer {
class StdinServer implements Transport {
private _pollTimer: NodeJS.Timeout | undefined;
private _traceUrl: string | undefined;
private _page: Page | undefined;
Expand All @@ -197,7 +195,6 @@ class StdinServer {
const url = data.toString().trim();
if (url === this._traceUrl)
return;
this._traceUrl = url;
if (url.endsWith('.json'))
this._pollLoadTrace(url);
else
Expand All @@ -206,15 +203,24 @@ class StdinServer {
process.stdin.on('close', () => this._selfDestruct());
}

setPage(page: Page) {
this._page = page;
if (this._traceUrl)
this._loadTrace(this._traceUrl);
async dispatch(method: string, params: any) {
if (method === 'ready') {
if (this._traceUrl)
this._loadTrace(this._traceUrl);
}
}

onclose() {
this._selfDestruct();
}

sendEvent?: (method: string, params: any) => void;
close?: () => void;

private _loadTrace(url: string) {
this._traceUrl = url;
clearTimeout(this._pollTimer);
this._page?.mainFrame().evaluateExpression(`window.setTraceURL(${JSON.stringify(url)})`).catch(() => {});
this.sendEvent?.('loadTrace', { url });
}

private _pollLoadTrace(url: string) {
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-test/src/runner/uiMode.ts
Expand Up @@ -87,6 +87,9 @@ class UIMode {

this._transport = {
dispatch: async (method, params) => {
if (method === 'ping')
return;

if (method === 'exit') {
exitPromise.resolve();
return;
Expand Down
47 changes: 8 additions & 39 deletions packages/trace-viewer/src/ui/uiModeView.tsx
Expand Up @@ -37,11 +37,14 @@ import { toggleTheme } from '@web/theme';
import { artifactsFolderName } from '@testIsomorphic/folders';
import { msToString, settings, useSetting } from '@web/uiUtils';
import type { ActionTraceEvent } from '@trace/trace';
import { connect } from './wsPort';

let updateRootSuite: (config: FullConfig, rootSuite: Suite, loadErrors: TestError[], progress: Progress | undefined) => void = () => {};
let runWatchedTests = (fileNames: string[]) => {};
let xtermSize = { cols: 80, rows: 24 };

let sendMessage: (method: string, params?: any) => Promise<any> = async () => {};

const xtermDataSource: XtermDataSource = {
pending: [],
clear: () => {},
Expand Down Expand Up @@ -96,7 +99,10 @@ export const UIModeView: React.FC<{}> = ({
React.useEffect(() => {
inputRef.current?.focus();
setIsLoading(true);
initWebSocket(() => setIsDisconnected(true)).then(() => reloadTests());
connect({ onEvent: dispatchEvent, onClose: () => setIsDisconnected(true) }).then(send => {
sendMessage = send;
reloadTests();
});
}, [reloadTests]);

updateRootSuite = React.useCallback((config: FullConfig, rootSuite: Suite, loadErrors: TestError[], newProgress: Progress | undefined) => {
Expand Down Expand Up @@ -639,43 +645,6 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => {
return sendMessage('list', {});
};

let lastId = 0;
let _ws: WebSocket;
const callbacks = new Map<number, { resolve: (arg: any) => void, reject: (arg: Error) => void }>();

const initWebSocket = async (onClose: () => void) => {
const guid = new URLSearchParams(window.location.search).get('ws');
const ws = new WebSocket(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.hostname}:${window.location.port}/${guid}`);
await new Promise(f => ws.addEventListener('open', f));
ws.addEventListener('close', onClose);
ws.addEventListener('message', event => {
const message = JSON.parse(event.data);
const { id, result, error, method, params } = message;
if (id) {
const callback = callbacks.get(id);
if (!callback)
return;
callbacks.delete(id);
if (error)
callback.reject(new Error(error));
else
callback.resolve(result);
} else {
dispatchMessage(method, params);
}
});
_ws = ws;
};

const sendMessage = async (method: string, params: any): Promise<any> => {
const id = ++lastId;
const message = { id, method, params };
_ws.send(JSON.stringify(message));
return new Promise((resolve, reject) => {
callbacks.set(id, { resolve, reject });
});
};

const sendMessageNoReply = (method: string, params?: any) => {
if ((window as any)._overrideProtocolForTest) {
(window as any)._overrideProtocolForTest({ method, params }).catch(() => {});
Expand All @@ -687,7 +656,7 @@ const sendMessageNoReply = (method: string, params?: any) => {
});
};

const dispatchMessage = (method: string, params?: any) => {
const dispatchEvent = (method: string, params?: any) => {
if (method === 'listChanged') {
refreshRootSuite(false).catch(() => {});
return;
Expand Down
27 changes: 14 additions & 13 deletions packages/trace-viewer/src/ui/workbenchLoader.tsx
Expand Up @@ -21,6 +21,7 @@ import { MultiTraceModel } from './modelUtil';
import './workbench.css';
import { toggleTheme } from '@web/theme';
import { Workbench } from './workbench';
import { connect } from './wsPort';

export const WorkbenchLoader: React.FunctionComponent<{
}> = () => {
Expand Down Expand Up @@ -82,13 +83,19 @@ export const WorkbenchLoader: React.FunctionComponent<{
}
}

(window as any).setTraceURL = (url: string) => {
setTraceURLs([url]);
setDragOver(false);
setProcessingErrorMessage(null);
};
if (earlyTraceURL) {
(window as any).setTraceURL(earlyTraceURL);
if (params.has('isServer')) {
connect({
onEvent(method: string, params?: any) {
if (method === 'loadTrace') {
setTraceURLs([params!.url]);
setDragOver(false);
setProcessingErrorMessage(null);
}
},
onClose() {}
}).then(sendMessage => {
sendMessage('ready');
});
} else if (!newTraceURLs.some(url => url.startsWith('blob:'))) {
// Don't re-use blob file URLs on page load (results in Fetch error)
setTraceURLs(newTraceURLs);
Expand Down Expand Up @@ -176,9 +183,3 @@ export const WorkbenchLoader: React.FunctionComponent<{
};

export const emptyModel = new MultiTraceModel([]);

let earlyTraceURL: string | undefined = undefined;

(window as any).setTraceURL = (url: string) => {
earlyTraceURL = url;
};
54 changes: 54 additions & 0 deletions packages/trace-viewer/src/ui/wsPort.ts
@@ -0,0 +1,54 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

let lastId = 0;
let _ws: WebSocket;
const callbacks = new Map<number, { resolve: (arg: any) => void, reject: (arg: Error) => void }>();

export async function connect(options: { onEvent: (method: string, params?: any) => void, onClose: () => void }): Promise<(method: string, params?: any) => Promise<any>> {
const guid = new URLSearchParams(window.location.search).get('ws');
const ws = new WebSocket(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.hostname}:${window.location.port}/${guid}`);
await new Promise(f => ws.addEventListener('open', f));
ws.addEventListener('close', options.onClose);
ws.addEventListener('message', event => {
const message = JSON.parse(event.data);
const { id, result, error, method, params } = message;
if (id) {
const callback = callbacks.get(id);
if (!callback)
return;
callbacks.delete(id);
if (error)
callback.reject(new Error(error));
else
callback.resolve(result);
} else {
options.onEvent(method, params);
}
});
_ws = ws;
setInterval(() => sendMessage('ping').catch(() => {}), 30000);
return sendMessage;
}

const sendMessage = async (method: string, params?: any): Promise<any> => {
const id = ++lastId;
const message = { id, method, params };
_ws.send(JSON.stringify(message));
return new Promise((resolve, reject) => {
callbacks.set(id, { resolve, reject });
});
};

0 comments on commit 0b30f20

Please sign in to comment.