Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

check for appReadyMessage before previewing app #5265

Merged
merged 4 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions extensions/positron-python/src/client/positron-run-app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export interface RunAppOptions {
* The optional URL path at which to preview the application.
*/
urlPath?: string;

/**
* The optional app ready message to wait for in the terminal before previewing the application.
*/
appReadyMessage?: string;
}

/**
Expand Down Expand Up @@ -78,9 +83,12 @@ export interface DebugAppOptions {
* The optional URL path at which to preview the application.
*/
urlPath?: string;
}

export interface DebugConfiguration {}
/**
* The optional app ready message to wait for in the terminal before previewing the application.
*/
appReadyMessage?: string;
}

/**
* The public API of the Positron Run App extension.
Expand Down
31 changes: 25 additions & 6 deletions extensions/positron-python/src/client/positron/webAppCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,46 @@ export function activateWebAppCommands(serviceContainer: IServiceContainer, disp
'FastAPI',
(runtime, document, _urlPrefix) => getFastAPIDebugConfig(serviceContainer, runtime, document),
'/docs',
'Application startup complete',
),
registerExecCommand(Commands.Exec_Flask_In_Terminal, 'Flask', (_runtime, document, _urlPrefix) =>
getFlaskDebugConfig(document),
),
registerExecCommand(Commands.Exec_Gradio_In_Terminal, 'Gradio', (_runtime, document, urlPrefix) =>
getGradioDebugConfig(document, urlPrefix),
),
registerExecCommand(Commands.Exec_Shiny_In_Terminal, 'Shiny', (_runtime, document, _urlPrefix) =>
getShinyDebugConfig(document),
registerExecCommand(
Commands.Exec_Shiny_In_Terminal,
'Shiny',
(_runtime, document, _urlPrefix) => getShinyDebugConfig(document),
undefined,
'Application startup complete',
),
registerExecCommand(Commands.Exec_Streamlit_In_Terminal, 'Streamlit', (_runtime, document, _urlPrefix) =>
getStreamlitDebugConfig(document),
),
registerDebugCommand(Commands.Debug_Dash_In_Terminal, 'Dash', (_runtime, document, urlPrefix) =>
getDashDebugConfig(document, urlPrefix),
),
registerDebugCommand(Commands.Debug_FastAPI_In_Terminal, 'FastAPI', (runtime, document, _urlPrefix) =>
getFastAPIDebugConfig(serviceContainer, runtime, document),
registerDebugCommand(
Commands.Debug_FastAPI_In_Terminal,
'FastAPI',
(runtime, document, _urlPrefix) => getFastAPIDebugConfig(serviceContainer, runtime, document),
'/docs',
'Application startup complete',
),
registerDebugCommand(Commands.Debug_Flask_In_Terminal, 'Flask', (_runtime, document, _urlPrefix) =>
getFlaskDebugConfig(document),
),
registerDebugCommand(Commands.Debug_Gradio_In_Terminal, 'Gradio', (_runtime, document, urlPrefix) =>
getGradioDebugConfig(document, urlPrefix),
),
registerDebugCommand(Commands.Debug_Shiny_In_Terminal, 'Shiny', (_runtime, document, _urlPrefix) =>
getShinyDebugConfig(document),
registerDebugCommand(
Commands.Debug_Shiny_In_Terminal,
'Shiny',
(_runtime, document, _urlPrefix) => getShinyDebugConfig(document),
undefined,
'Application startup complete',
),
registerDebugCommand(Commands.Debug_Streamlit_In_Terminal, 'Streamlit', (_runtime, document, _urlPrefix) =>
getStreamlitDebugConfig(document),
Expand All @@ -67,6 +80,7 @@ function registerExecCommand(
urlPrefix?: string,
) => DebugConfiguration | undefined | Promise<DebugConfiguration | undefined>,
urlPath?: string,
appReadyMessage?: string,
): vscode.Disposable {
return vscode.commands.registerCommand(command, async () => {
const runAppApi = await getPositronRunAppApi();
Expand Down Expand Up @@ -98,6 +112,7 @@ function registerExecCommand(
return terminalOptions;
},
urlPath,
appReadyMessage,
});
});
}
Expand All @@ -110,6 +125,8 @@ function registerDebugCommand(
document: vscode.TextDocument,
urlPrefix?: string,
) => DebugConfiguration | undefined | Promise<DebugConfiguration | undefined>,
urlPath?: string,
appReadyMessage?: string,
): vscode.Disposable {
return vscode.commands.registerCommand(command, async () => {
const runAppApi = await getPositronRunAppApi();
Expand All @@ -129,6 +146,8 @@ function registerDebugCommand(
stopOnEntry: false,
};
},
urlPath,
appReadyMessage,
});
});
}
Expand Down
89 changes: 73 additions & 16 deletions extensions/positron-run-app/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ type PositronProxyInfo = {
finishProxySetup: (targetOrigin: string) => Promise<void>;
};

type AppPreviewOptions = {
proxyInfo?: PositronProxyInfo;
urlPath?: string;
appReadyMessage?: string;
};

export class PositronRunAppApiImpl implements PositronRunApp, vscode.Disposable {
private readonly _debugApplicationSequencerByName = new SequencerByKey<string>();
private readonly _debugApplicationDisposableByName = new Map<string, vscode.Disposable>();
Expand Down Expand Up @@ -162,7 +168,12 @@ export class PositronRunAppApiImpl implements PositronRunApp, vscode.Disposable
await this.setShellIntegrationSupported(true);

if (e.terminal === terminal) {
const didPreviewUrl = await previewUrlInExecutionOutput(e.execution, proxyInfo, options.urlPath);
const previewOptions: AppPreviewOptions = {
proxyInfo,
urlPath: options.urlPath,
appReadyMessage: options.appReadyMessage,
};
const didPreviewUrl = await previewUrlInExecutionOutput(e.execution, previewOptions);
if (didPreviewUrl) {
resolve(didPreviewUrl);
}
Expand Down Expand Up @@ -279,7 +290,12 @@ export class PositronRunAppApiImpl implements PositronRunApp, vscode.Disposable
await this.setShellIntegrationSupported(true);

if (await e.terminal.processId === processId) {
const didPreviewUrl = await previewUrlInExecutionOutput(e.execution, proxyInfo, options.urlPath);
const previewOptions: AppPreviewOptions = {
proxyInfo,
urlPath: options.urlPath,
appReadyMessage: options.appReadyMessage,
};
const didPreviewUrl = await previewUrlInExecutionOutput(e.execution, previewOptions);
if (didPreviewUrl) {
resolve(didPreviewUrl);
}
Expand Down Expand Up @@ -344,50 +360,91 @@ export class PositronRunAppApiImpl implements PositronRunApp, vscode.Disposable
}
}

async function previewUrlInExecutionOutput(execution: vscode.TerminalShellExecution, proxyInfo?: PositronProxyInfo, urlPath?: string) {
async function previewUrlInExecutionOutput(execution: vscode.TerminalShellExecution, options: AppPreviewOptions) {
// Wait for the server URL to appear in the terminal output, or a timeout.
const stream = execution.read();
const appReadyMessage = options.appReadyMessage?.trim();
const url = await raceTimeout(
(async () => {
// If an appReadyMessage is not provided, we'll consider the app ready as soon as the URL is found.
let appReady = !appReadyMessage;
let appUrl = undefined;
for await (const data of stream) {
log.warn('Execution:', execution.commandLine.value, data);
log.trace('Execution:', execution.commandLine.value, data);

// Ansi escape codes seem to mess up the regex match on Windows, so remove them first.
const dataCleaned = removeAnsiEscapeCodes(data);
const match = dataCleaned.match(localUrlRegex)?.[0];
if (match) {
return new URL(match.trim());

// Check if the app is ready, if it's not already ready and an appReadyMessage is provided.
if (!appReady && appReadyMessage) {
appReady = dataCleaned.includes(appReadyMessage);
if (appReady) {
log.debug(`App is ready - found appReadyMessage: '${appReadyMessage}'`);
// If the app URL was already found, we're done!
if (appUrl) {
return appUrl;
}
}
}

// Check if the app url is found in the terminal output.
if (!appUrl) {
const match = dataCleaned.match(localUrlRegex)?.[0];
if (match) {
appUrl = new URL(match.trim());
log.debug(`Found app URL in terminal output: ${appUrl.toString()}`);
// If the app is ready, we're done!
if (appReady) {
return appUrl;
}
}
}
}
log.warn('URL not found in terminal output');
return undefined;

// If we're here, we've reached the end of the stream without finding the app URL and/or
// the appReadyMessage.
if (!appReady) {
// It's possible that the app is ready, but the appReadyMessage was not found, for
// example, if the message has changed or was missed somehow. Log a warning.
log.warn(`Expected app ready message '${appReadyMessage}' not found in terminal`);
}
if (!appUrl) {
log.error('App URL not found in terminal output');
}
return appUrl;
})(),
15_000,
() => log.error('Timed out waiting for server output in terminal'),
);

if (url === undefined) {
log.warn('Timed out waiting for server URL in terminal output');
if (!url) {
log.error('Cannot preview URL. App is not ready or URL not found in terminal output.');
return false;
}

// Example: http://localhost:8500
const localBaseUri = vscode.Uri.parse(url.toString());

// Example: http://localhost:8500/url/path or http://localhost:8500
const localUri = urlPath ?
vscode.Uri.joinPath(localBaseUri, urlPath) : localBaseUri;
const localUri = options.urlPath ?
vscode.Uri.joinPath(localBaseUri, options.urlPath) : localBaseUri;

// Example: http://localhost:8080/proxy/5678/url/path or http://localhost:8080/proxy/5678
let previewUri = undefined;
if (proxyInfo) {
if (options.proxyInfo) {
// On Web (specifically Positron Server Web and not PWB), we need to set up the proxy with
// the urlPath appended to avoid issues where the app does not set the base url of the app
// or the base url of referenced assets correctly.
const applyWebPatch = isPositronWeb && !isRunningOnPwb;
const targetOrigin = applyWebPatch ? localUri.toString(true) : localBaseUri.toString();

log.debug(`Finishing proxy setup for app at ${targetOrigin}`);

// Finish the Positron proxy setup so that proxy middleware is hooked up.
await proxyInfo.finishProxySetup(targetOrigin);
previewUri = !applyWebPatch && urlPath ? vscode.Uri.joinPath(proxyInfo.externalUri, urlPath) : proxyInfo.externalUri;
await options.proxyInfo.finishProxySetup(targetOrigin);
previewUri = !applyWebPatch && options.urlPath
? vscode.Uri.joinPath(options.proxyInfo.externalUri, options.urlPath)
: options.proxyInfo.externalUri;
} else {
previewUri = await vscode.env.asExternalUri(localUri);
}
Expand Down
11 changes: 9 additions & 2 deletions extensions/positron-run-app/src/positron-run-app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export interface RunAppOptions {
* The optional URL path at which to preview the application.
*/
urlPath?: string;

/**
* The optional app ready message to wait for in the terminal before previewing the application.
*/
appReadyMessage?: string;
}

/**
Expand Down Expand Up @@ -78,9 +83,11 @@ export interface DebugAppOptions {
* The optional URL path at which to preview the application.
*/
urlPath?: string;
}

export interface DebugConfiguration {
/**
* The optional app ready message to wait for in the terminal before previewing the application.
*/
appReadyMessage?: string;
}

/**
Expand Down
4 changes: 1 addition & 3 deletions test/smoke/src/areas/positron/apps/python-apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ describe('Python Applications #pr #win', () => {
await expect(viewer.getViewerFrame().getByText('Hello World')).toBeVisible({ timeout: 30000 });
});

// https://github.com/posit-dev/positron/issues/4949
// FastAPI is not working as expected on Ubuntu
it.skip('Python - Verify Basic FastAPI App [C903306]', async function () {
it('Python - Verify Basic FastAPI App [C903306]', async function () {
const app = this.app as Application;
const viewer = app.workbench.positronViewer;

Expand Down
Loading