Skip to content
Draft
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
3 changes: 3 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@
"node.killBehavior.description": "Configures how debug processes are killed when stopping the session. Can be:\n\n- forceful (default): forcefully tears down the process tree. Sends SIGKILL on posix, or `taskkill.exe /F` on Windows.\n- polite: gracefully tears down the process tree. It's possible that misbehaving processes continue to run after shutdown in this way. Sends SIGTERM on posix, or `taskkill.exe` with no `/F` (force) flag on Windows.\n- none: no termination will happen.",
"browser.killBehavior.description": "Configures how browser processes are killed when stopping the session with `cleanUp: wholeBrowser`. Can be:\n\n- forceful (default): forcefully tears down the process tree. Sends SIGKILL on posix, or `taskkill.exe /F` on Windows.\n- polite: gracefully tears down the process tree. It's possible that misbehaving processes continue to run after shutdown in this way. Sends SIGTERM on posix, or `taskkill.exe` with no `/F` (force) flag on Windows.\n- none: no termination will happen.",
"node.label": "Node.js",
"reactNative.label": "React Native",
"reactNative.snippet.attach.label": "React Native: Attach",
"reactNative.snippet.attach.description": "Attach to a running React Native app",
"node.launch.args.description": "Command line arguments passed to the program.\n\nCan be an array of strings or a single string. When the program is launched in a terminal, setting this property to a single string will result in the arguments not being escaped for the shell.",
"node.launch.autoAttachChildProcesses.description": "Attach debugger to new child processes automatically.",
"node.launch.config.name": "Launch",
Expand Down
7 changes: 7 additions & 0 deletions src/adapter/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,13 @@ export class Thread implements IVariableStoreLocationProvider {
// console.profile/console.endProfile. Otherwise, these just no-op.
this._cdp.Profiler.enable({});

// For React Native targets, enable the React Native application
// domain so the app knows a debugger is attached. This method is not part
// of the standard CDP protocol, so it's sent through the raw session.
if (this.launchConfig.type === DebugType.ReactNative) {
this._cdp.session.send('ReactNativeApplication.enable', {});
}

this._ensureDebuggerEnabledAndRefreshDebuggerId();

if (this.launchConfig.noDebug) {
Expand Down
37 changes: 37 additions & 0 deletions src/build/generate-contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
INodeAttachConfiguration,
INodeBaseConfiguration,
INodeLaunchConfiguration,
IReactNativeAttachConfiguration,
ITerminalLaunchConfiguration,
KillBehavior,
nodeAttachConfigDefaults,
Expand Down Expand Up @@ -500,6 +501,41 @@ const nodeAttachConfig: IDebugger<INodeAttachConfiguration> = {
defaults: nodeAttachConfigDefaults,
};

// `websocketAddress` is resolved internally from the dev server target picker
// and must not be set by the user, so it's omitted from the React Native schema.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { websocketAddress: _rnWebsocketAddress, ...reactNativeAttachAttributes } =
nodeAttachConfig.configurationAttributes;

/**
* React Native attach configuration. Behaves like a Node attach, but the
* distinct `react-native` type drives React Native-specific behavior: the
* target is discovered from the dev server (via the target picker) and
* `ReactNativeApplication.enable` is sent on attach. Reuses the Node attach
* attributes, minus `websocketAddress`.
*/
const reactNativeAttachConfig: IDebugger<IReactNativeAttachConfiguration> = {
type: DebugType.ReactNative,
request: 'attach',
label: refString('reactNative.label'),
languages: commonLanguages,
configurationSnippets: [
{
label: refString('reactNative.snippet.attach.label'),
description: refString('reactNative.snippet.attach.description'),
body: {
type: DebugType.ReactNative,
request: 'attach',
name: '${1:Attach to React Native}',
port: 8081,
skipFiles: [`${nodeInternalsToken}/**`],
},
},
],
configurationAttributes: reactNativeAttachAttributes,
defaults: { ...nodeAttachConfigDefaults, type: DebugType.ReactNative },
};

/**
* Node attach configuration.
*/
Expand Down Expand Up @@ -1162,6 +1198,7 @@ const editorBrowserAttachConfig: IDebugger<IEditorBrowserAttachConfiguration> =

export const debuggers = [
nodeAttachConfig,
reactNativeAttachConfig,
nodeLaunchConfig,
nodeTerminalConfiguration,
extensionHostConfig,
Expand Down
2 changes: 2 additions & 0 deletions src/common/contributionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const enum DebugType {
ExtensionHost = 'pwa-extensionHost',
Terminal = 'node-terminal',
Node = 'pwa-node',
ReactNative = 'react-native',
Chrome = 'pwa-chrome',
Edge = 'pwa-msedge',
EditorBrowser = 'pwa-editor-browser',
Expand All @@ -101,6 +102,7 @@ const debugTypes: { [K in DebugType]: null } = {
[DebugType.ExtensionHost]: null,
[DebugType.Terminal]: null,
[DebugType.Node]: null,
[DebugType.ReactNative]: null,
[DebugType.Chrome]: null,
[DebugType.Edge]: null,
[DebugType.EditorBrowser]: null,
Expand Down
17 changes: 17 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,16 @@ export interface INodeAttachConfiguration extends INodeBaseConfiguration {
attachExistingChildren: boolean;
}

/**
* Configuration for a React Native attach. Structurally identical to a Node
* attach, but with a distinct `type` discriminant that drives React
* Native-specific behavior: the target is discovered from the dev server and
* `ReactNativeApplication.enable` is sent on attach.
*/
export type IReactNativeAttachConfiguration =
& Omit<INodeAttachConfiguration, 'type'>
& { type: DebugType.ReactNative };

export interface IChromiumLaunchConfiguration extends IChromiumBaseConfiguration {
request: 'launch';

Expand Down Expand Up @@ -787,6 +797,7 @@ export interface ITerminalDelegateConfiguration extends INodeBaseConfiguration {

export type AnyNodeConfiguration =
| INodeAttachConfiguration
| IReactNativeAttachConfiguration
| INodeLaunchConfiguration
| ITerminalLaunchConfiguration
| IExtensionHostLaunchConfiguration
Expand Down Expand Up @@ -817,6 +828,9 @@ export type ResolvingExtensionHostConfiguration = ResolvingConfiguration<
IExtensionHostLaunchConfiguration
>;
export type ResolvingNodeAttachConfiguration = ResolvingConfiguration<INodeAttachConfiguration>;
export type ResolvingReactNativeAttachConfiguration = ResolvingConfiguration<
IReactNativeAttachConfiguration
>;
export type ResolvingNodeLaunchConfiguration = ResolvingConfiguration<INodeLaunchConfiguration>;
export type ResolvingTerminalDelegateConfiguration = ResolvingConfiguration<
ITerminalDelegateConfiguration
Expand All @@ -829,6 +843,7 @@ export type ResolvingTerminalConfiguration =
| ResolvingTerminalLaunchConfiguration;
export type ResolvingNodeConfiguration =
| ResolvingNodeAttachConfiguration
| ResolvingReactNativeAttachConfiguration
| ResolvingNodeLaunchConfiguration;
export type ResolvingChromeConfiguration = ResolvingConfiguration<AnyChromeConfiguration>;
export type ResolvingEdgeConfiguration = ResolvingConfiguration<AnyEdgeConfiguration>;
Expand All @@ -839,6 +854,7 @@ export type AnyResolvingConfiguration =
| ResolvingExtensionHostConfiguration
| ResolvingChromeConfiguration
| ResolvingNodeAttachConfiguration
| ResolvingReactNativeAttachConfiguration
| ResolvingNodeLaunchConfiguration
| ResolvingTerminalConfiguration
| ResolvingEdgeConfiguration
Expand Down Expand Up @@ -1136,6 +1152,7 @@ export function applyDefaults(
const defaultBrowserLocation = location === 'remote' ? ('ui' as const) : ('workspace' as const);
switch (config.type) {
case DebugType.Node:
case DebugType.ReactNative:
configWithDefaults = applyNodeDefaults(config);
break;
case DebugType.Edge:
Expand Down
30 changes: 22 additions & 8 deletions src/targets/node/nodeAttacher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import { DebugType } from '../../common/contributionUtils';
import { ILogger, LogTag } from '../../common/logging';
import { delay } from '../../common/promiseUtil';
import { isLoopback } from '../../common/urlUtils';
import { AnyLaunchConfiguration, INodeAttachConfiguration } from '../../configuration';
import {
AnyLaunchConfiguration,
INodeAttachConfiguration,
IReactNativeAttachConfiguration,
} from '../../configuration';
import { retryGetNodeEndpoint } from '../browser/spawn/endpoints';
import { ISourcePathResolverFactory } from '../sourcePathResolverFactory';
import { IStopMetadata } from '../targets';
Expand All @@ -25,6 +29,13 @@ import { IProgram, StubProgram, WatchDogProgram } from './program';
import { IRestartPolicy, RestartPolicyFactory } from './restartPolicy';
import { WatchDog } from './watchdogSpawn';

/**
* Configurations handled by the Node attacher. React Native attach is
* structurally identical to a Node attach and reuses this launcher; only the
* `type` discriminant differs.
*/
type NodeAttachConfiguration = INodeAttachConfiguration | IReactNativeAttachConfiguration;

/**
* Attaches to ongoing Node processes. This works pretty similar to the
* existing Node launcher, except with how we attach to the entry point:
Expand All @@ -33,7 +44,7 @@ import { WatchDog } from './watchdogSpawn';
* child processes operate just like those we boot with the NodeLauncher.
*/
@injectable()
export class NodeAttacher extends NodeAttacherBase<INodeAttachConfiguration> {
export class NodeAttacher extends NodeAttacherBase<NodeAttachConfiguration> {
private telemetry?: IProcessTelemetry;

constructor(
Expand All @@ -49,14 +60,17 @@ export class NodeAttacher extends NodeAttacherBase<INodeAttachConfiguration> {
/**
* @inheritdoc
*/
protected resolveParams(params: AnyLaunchConfiguration): INodeAttachConfiguration | undefined {
return params.type === DebugType.Node && params.request === 'attach' ? params : undefined;
protected resolveParams(params: AnyLaunchConfiguration): NodeAttachConfiguration | undefined {
return (params.type === DebugType.Node || params.type === DebugType.ReactNative)
&& params.request === 'attach'
? params
: undefined;
}

/**
* @inheritdoc
*/
protected async launchProgram(runData: IRunData<INodeAttachConfiguration>): Promise<void> {
protected async launchProgram(runData: IRunData<NodeAttachConfiguration>): Promise<void> {
const doLaunch = async (
restartPolicy: IRestartPolicy,
restarting?: IProgram,
Expand Down Expand Up @@ -153,7 +167,7 @@ export class NodeAttacher extends NodeAttacherBase<INodeAttachConfiguration> {
*/
protected override createLifecycle(
cdp: Cdp.Api,
run: IRunData<INodeAttachConfiguration>,
run: IRunData<NodeAttachConfiguration>,
target: Cdp.Target.TargetInfo,
) {
if (target.openerId) {
Expand All @@ -179,7 +193,7 @@ export class NodeAttacher extends NodeAttacherBase<INodeAttachConfiguration> {

protected async onFirstInitialize(
cdp: Cdp.Api,
run: IRunData<INodeAttachConfiguration>,
run: IRunData<NodeAttachConfiguration>,
parentInfo: Cdp.Target.TargetInfo,
) {
// We use a lease file to indicate to the process that the debugger is
Expand Down Expand Up @@ -222,7 +236,7 @@ export class NodeAttacher extends NodeAttacherBase<INodeAttachConfiguration> {

private async setEnvironmentVariables(
cdp: Cdp.Api,
run: IRunData<INodeAttachConfiguration>,
run: IRunData<NodeAttachConfiguration>,
leasePath: string,
openerId: string,
binary: NodeBinary,
Expand Down
2 changes: 2 additions & 0 deletions src/targets/sourcePathResolverFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class NodeOnlyPathResolverFactory implements ISourcePathResolverFactory {
public create(c: AnyLaunchConfiguration, logger: ILogger) {
if (
c.type === DebugType.Node
|| c.type === DebugType.ReactNative
|| c.type === DebugType.Terminal
|| c.type === DebugType.ExtensionHost
) {
Expand Down Expand Up @@ -68,6 +69,7 @@ export class SourcePathResolverFactory implements ISourcePathResolverFactory {
public create(c: AnyLaunchConfiguration, logger: ILogger) {
if (
c.type === DebugType.Node
|| c.type === DebugType.ReactNative
|| c.type === DebugType.Terminal
|| c.type === DebugType.ExtensionHost
) {
Expand Down
2 changes: 2 additions & 0 deletions src/ui/configuration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
NodeInitialDebugConfigurationProvider,
} from './nodeDebugConfigurationProvider';
import { NodeConfigurationResolver } from './nodeDebugConfigurationResolver';
import { ReactNativeConfigurationResolver } from './reactNativeConfigurationResolver';
import { TerminalDebugConfigurationResolver } from './terminalDebugConfigurationResolver';

export const allConfigurationResolvers = [
Expand All @@ -29,6 +30,7 @@ export const allConfigurationResolvers = [
EditorBrowserDebugConfigurationResolver,
ExtensionHostConfigurationResolver,
NodeConfigurationResolver,
ReactNativeConfigurationResolver,
TerminalDebugConfigurationResolver,
];

Expand Down
88 changes: 88 additions & 0 deletions src/ui/configuration/reactNativeConfigurationResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import * as l10n from '@vscode/l10n';
import { inject, injectable } from 'inversify';
import * as vscode from 'vscode';
import { CancellationToken } from 'vscode';
import { IResourceProvider } from '../../adapter/resourceProvider';
import { DebugType } from '../../common/contributionUtils';
import {
applyNodeDefaults,
IReactNativeAttachConfiguration,
ResolvingConfiguration,
} from '../../configuration';
import { ExtensionContext } from '../../ioc-extras';
import { pickReactNativeProcess } from '../reactNativeProcessPicker';
import { BaseConfigurationResolver } from './baseConfigurationResolver';

/**
* Configuration resolver for React Native attach. It reuses the Node
* attach machinery, but resolves the target by querying the dev server's
* `/json/list` endpoint and prompting the user to pick one. The chosen target's
* inspector WebSocket URL is written to `websocketAddress`, which the Node
* attacher connects to directly.
*/
@injectable()
export class ReactNativeConfigurationResolver
extends BaseConfigurationResolver<IReactNativeAttachConfiguration>
{
constructor(
@inject(ExtensionContext) context: vscode.ExtensionContext,
@inject(IResourceProvider) private readonly resourceProvider: IResourceProvider,
) {
super(context);
}

/**
* @override
*/
protected async resolveDebugConfigurationAsync(
_folder: vscode.WorkspaceFolder | undefined,
config: ResolvingConfiguration<IReactNativeAttachConfiguration>,
cancellationToken?: CancellationToken,
): Promise<IReactNativeAttachConfiguration | undefined> {
// React Native is attach-only: js-debug connects to an already-running app
// via the dev server, it never launches the app itself. (`request` is typed
// as `attach`, but the raw config from the user can be anything.)
const request: string = config.request;
if (request !== 'attach') {
throw new Error(
l10n.t('`{0}` configurations only support the `attach` request.', DebugType.ReactNative),
);
}

// The target's WebSocket URL is discovered from the dev server, so users
// must never provide it themselves.
if (config.websocketAddress) {
throw new Error(
l10n.t(
'`websocketAddress` is not supported for `{0}` configurations.',
DebugType.ReactNative,
),
);
}

// The dev server port comes from the launch config; default to Metro's 8081.
const port = typeof config.port === 'number' ? config.port : 8081;
const websocketAddress = await pickReactNativeProcess(
this.resourceProvider,
port,
cancellationToken,
);
if (!websocketAddress) {
return undefined; // cancelled
}

config.websocketAddress = websocketAddress;
return applyNodeDefaults(config) as IReactNativeAttachConfiguration;
}

/**
* @override
*/
protected getType() {
return DebugType.ReactNative as const;
}
}
Loading