Adding support for 'ionic serve' in browser and 'ionic run --livereload' for Android
This commit is contained in:
Родитель
c15aa47789
Коммит
b70f23a262
|
@ -9,7 +9,7 @@
|
|||
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"outDir": "out/src",
|
||||
"outDir": "${workspaceRoot}/out/src",
|
||||
"preLaunchTask": "build"
|
||||
},
|
||||
{
|
||||
|
@ -20,19 +20,20 @@
|
|||
"args": ["test/testProject", "--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ],
|
||||
"stopOnEntry": false,
|
||||
"sourceMaps": true,
|
||||
"outDir": "out/test",
|
||||
"outDir": "${workspaceRoot}/out/src",
|
||||
"preLaunchTask": "prepare-integration-tests"
|
||||
},
|
||||
{
|
||||
"name": "Launch debugger as server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"program": "./out/webkit/debugCordova.js",
|
||||
"program": "${workspaceRoot}/out/src/debugger/debugCordova.js",
|
||||
"runtimeArgs": ["--harmony"],
|
||||
"stopOnEntry": false,
|
||||
"args": [ "--server=4712" ],
|
||||
"sourceMaps": true,
|
||||
"outDir": "out"
|
||||
"outDir": "${workspaceRoot}/out",
|
||||
"preLaunchTask": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
20
package.json
20
package.json
|
@ -161,6 +161,13 @@
|
|||
"port": 9220,
|
||||
"sourceMaps": true,
|
||||
"cwd": "${workspaceRoot}"
|
||||
},
|
||||
{
|
||||
"name": "Run in the browser",
|
||||
"type": "cordova",
|
||||
"request": "launch",
|
||||
"platform": "browser",
|
||||
"cwd": "${workspaceRoot}"
|
||||
}
|
||||
],
|
||||
"configurationAttributes": {
|
||||
|
@ -222,6 +229,19 @@
|
|||
"type": "number",
|
||||
"description": "Timeout for individual steps when launching iOS app",
|
||||
"default": 5000
|
||||
},
|
||||
"noLivereload": {
|
||||
"type": "boolean",
|
||||
"description": "Set to true to disable Ionic live reload (changes to the sources will not be pushed to the running app)",
|
||||
"default": false
|
||||
},
|
||||
"devServerPort": {
|
||||
"type": "number",
|
||||
"description": "The port on which Ionic's live reload server should listen on"
|
||||
},
|
||||
"devServerAddress": {
|
||||
"type": "string",
|
||||
"description": "The IP address that Ionic should use for the live reload server"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -5,6 +5,15 @@
|
|||
interface ICordovaLaunchRequestArgs extends DebugProtocol.LaunchRequestArguments, ICordovaAttachRequestArgs {
|
||||
iosDebugProxyPort?: number;
|
||||
appStepLaunchTimeout?: number;
|
||||
|
||||
// Ionic livereload properties
|
||||
noLivereload?: boolean;
|
||||
devServerPort?: number;
|
||||
devServerAddress?: string;
|
||||
|
||||
// Chrome debug properties
|
||||
url?: string;
|
||||
userDataDir?: string;
|
||||
}
|
||||
|
||||
interface ICordovaAttachRequestArgs extends DebugProtocol.AttachRequestArguments, IAttachRequestArgs {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for details.
|
||||
|
||||
|
||||
import * as child_process from 'child_process';
|
||||
import * as elementtree from 'elementtree';
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
|
@ -10,20 +11,20 @@ import * as path from 'path';
|
|||
import * as Q from 'q';
|
||||
|
||||
import {CordovaIosDeviceLauncher} from './cordovaIosDeviceLauncher';
|
||||
|
||||
import {cordovaRunCommand, execCommand} from './extension';
|
||||
|
||||
import {cordovaRunCommand, cordovaStartCommand, execCommand, killChildProcess} from './extension';
|
||||
import {WebKitDebugAdapter} from '../../debugger/webkit/webKitDebugAdapter';
|
||||
|
||||
import {CordovaProjectHelper} from '../utils/cordovaProjectHelper';
|
||||
|
||||
import {TelemetryHelper} from '../utils/telemetryHelper';
|
||||
import {IProjectType, TelemetryHelper} from '../utils/telemetryHelper';
|
||||
import {settingsHome} from '../utils/settingsHelper'
|
||||
|
||||
export class CordovaDebugAdapter extends WebKitDebugAdapter {
|
||||
private outputLogger: (message: string, error?: boolean) => void;
|
||||
private adbPortForwardingInfo: {targetDevice: string, port: number};
|
||||
private static CHROME_DATA_DIR = 'chrome_sandbox_dir'; // The directory to use for the sandboxed Chrome instance that gets launched to debug the app
|
||||
|
||||
public constructor(outputLogger: (message: string, error?: boolean) => void) {
|
||||
private outputLogger: (message: string, error?: boolean) => void;
|
||||
private adbPortForwardingInfo: { targetDevice: string, port: number };
|
||||
private ionicLivereloadProcess: child_process.ChildProcess;
|
||||
|
||||
public constructor(outputLogger: (message: string, error?: boolean) => void) {
|
||||
super();
|
||||
this.outputLogger = outputLogger;
|
||||
}
|
||||
|
@ -37,23 +38,30 @@ export class CordovaDebugAdapter extends WebKitDebugAdapter {
|
|||
let platform = launchArgs.platform && launchArgs.platform.toLowerCase();
|
||||
|
||||
return TelemetryHelper.determineProjectTypes(launchArgs.cwd)
|
||||
.then((projectType) => generator.add('projectType', projectType, false))
|
||||
.then(() => {
|
||||
this.outputLogger(`Launching for ${platform} (This may take a while)...`);
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
generator.add('platform', platform, false);
|
||||
return this.launchAndroid(launchArgs);
|
||||
case 'ios':
|
||||
generator.add('platform', platform, false);
|
||||
return this.launchIos(launchArgs);
|
||||
default:
|
||||
generator.add('unknownPlatform', platform, true);
|
||||
throw new Error(`Unknown Platform: ${platform}`);
|
||||
}
|
||||
}).then(() => {
|
||||
return this.attach(launchArgs);
|
||||
});
|
||||
.then((projectType) => {
|
||||
generator.add('projectType', projectType, false);
|
||||
this.outputLogger(`Launching for ${platform} (This may take a while)...`);
|
||||
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
generator.add('platform', platform, false);
|
||||
return this.launchAndroid(launchArgs, projectType);
|
||||
case 'ios':
|
||||
generator.add('platform', platform, false);
|
||||
return this.launchIos(launchArgs, projectType);
|
||||
case 'browser':
|
||||
generator.add('platform', platform, false);
|
||||
return this.launchBrowser(launchArgs, projectType);
|
||||
default:
|
||||
generator.add('unknownPlatform', platform, true);
|
||||
throw new Error(`Unknown Platform: ${platform}`);
|
||||
}
|
||||
}).then(() => {
|
||||
// For the browser platform, we call super.launch(), which already attaches. For other platforms, attach here
|
||||
if (platform !== 'browser') {
|
||||
return this.attach(launchArgs);
|
||||
}
|
||||
});
|
||||
}).done(resolve, reject));
|
||||
}
|
||||
|
||||
|
@ -65,51 +73,81 @@ export class CordovaDebugAdapter extends WebKitDebugAdapter {
|
|||
let platform = attachArgs.platform && attachArgs.platform.toLowerCase();
|
||||
|
||||
return TelemetryHelper.determineProjectTypes(attachArgs.cwd)
|
||||
.then((projectType) => generator.add('projectType', projectType, false))
|
||||
.then(() => {
|
||||
this.outputLogger(`Attaching to ${platform}`);
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
generator.add('platform', platform, false);
|
||||
return this.attachAndroid(attachArgs);
|
||||
case 'ios':
|
||||
generator.add('platform', platform, false);
|
||||
return this.attachIos(attachArgs);
|
||||
default:
|
||||
generator.add('unknownPlatform', platform, true);
|
||||
throw new Error(`Unknown Platform: ${platform}`);
|
||||
}
|
||||
}).then((processedAttachArgs: IAttachRequestArgs & {url?: string}) => {
|
||||
this.outputLogger('Attaching to app.');
|
||||
this.outputLogger('', true); // Send blank message on stderr to include a divider between prelude and app starting
|
||||
return super.attach(processedAttachArgs, processedAttachArgs.url);
|
||||
});
|
||||
.then((projectType) => generator.add('projectType', projectType, false))
|
||||
.then(() => {
|
||||
this.outputLogger(`Attaching to ${platform}`);
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
generator.add('platform', platform, false);
|
||||
return this.attachAndroid(attachArgs);
|
||||
case 'ios':
|
||||
generator.add('platform', platform, false);
|
||||
return this.attachIos(attachArgs);
|
||||
default:
|
||||
generator.add('unknownPlatform', platform, true);
|
||||
throw new Error(`Unknown Platform: ${platform}`);
|
||||
}
|
||||
}).then((processedAttachArgs: IAttachRequestArgs & { url?: string }) => {
|
||||
this.outputLogger('Attaching to app.');
|
||||
this.outputLogger('', true); // Send blank message on stderr to include a divider between prelude and app starting
|
||||
return super.attach(processedAttachArgs, processedAttachArgs.url);
|
||||
});
|
||||
}).done(resolve, reject));
|
||||
}
|
||||
|
||||
public disconnect(): Promise<void> {
|
||||
return super.disconnect().then(() => {
|
||||
const errorLogger = (message) => this.outputLogger(message, true);
|
||||
|
||||
// Stop ADB port forwarding if necessary
|
||||
let adbPortPromise: Q.Promise<void>;
|
||||
|
||||
if (this.adbPortForwardingInfo) {
|
||||
const adbForwardStopArgs =
|
||||
['-s', this.adbPortForwardingInfo.targetDevice,
|
||||
'forward',
|
||||
'--remove', `tcp:${this.adbPortForwardingInfo.port}`];
|
||||
const errorLogger = (message) => this.outputLogger(message, true);
|
||||
return execCommand('adb', adbForwardStopArgs, errorLogger)
|
||||
.then(() => {});
|
||||
'forward',
|
||||
'--remove', `tcp:${this.adbPortForwardingInfo.port}`];
|
||||
adbPortPromise = execCommand('adb', adbForwardStopArgs, errorLogger)
|
||||
.then(() => void 0);
|
||||
} else {
|
||||
adbPortPromise = Q<void>(void 0);
|
||||
}
|
||||
|
||||
// Kill the Ionic dev server if necessary
|
||||
let killServePromise: Q.Promise<void>;
|
||||
|
||||
if (this.ionicLivereloadProcess) {
|
||||
killServePromise = killChildProcess(this.ionicLivereloadProcess, errorLogger).then(() => {
|
||||
this.ionicLivereloadProcess = null;
|
||||
});
|
||||
} else {
|
||||
killServePromise = Q<void>(void 0);
|
||||
}
|
||||
|
||||
// Wait on all the cleanups
|
||||
return Q.allSettled([adbPortPromise, killServePromise]).then(() => void 0);
|
||||
});
|
||||
}
|
||||
|
||||
private launchAndroid(launchArgs: ICordovaLaunchRequestArgs): Q.Promise<void> {
|
||||
private launchAndroid(launchArgs: ICordovaLaunchRequestArgs, projectType: IProjectType): Q.Promise<void> {
|
||||
let workingDirectory = launchArgs.cwd;
|
||||
let errorLogger = (message) => this.outputLogger(message, true);
|
||||
// Launch the app
|
||||
|
||||
// Prepare the command line args
|
||||
let isDevice = launchArgs.target.toLowerCase() === 'device';
|
||||
let args = ['run', 'android', isDevice ? '--device' : '--emulator', '--verbose'];
|
||||
let args = ['run', 'android', isDevice ? '--device' : '--emulator', '--verbose'];
|
||||
if (['device', 'emulator'].indexOf(launchArgs.target.toLowerCase()) === -1) {
|
||||
args.push(`--target=${launchArgs.target}`);
|
||||
}
|
||||
|
||||
// If we are using Ionic livereload, we let Ionic do the launch
|
||||
if (projectType.ionic && !launchArgs.noLivereload) {
|
||||
args.push('--livereload');
|
||||
|
||||
return this.startIonicDevServer(launchArgs, args).then(() => void 0);
|
||||
}
|
||||
|
||||
// Launch the app manually if not livereloading
|
||||
let cordovaResult = cordovaRunCommand(args, errorLogger, workingDirectory).then((output) => {
|
||||
let runOutput = output[0];
|
||||
let stderr = output[1];
|
||||
|
@ -130,56 +168,56 @@ export class CordovaDebugAdapter extends WebKitDebugAdapter {
|
|||
// Determine which device/emulator we are targeting
|
||||
|
||||
let adbDevicesResult = execCommand('adb', ['devices'], errorLogger)
|
||||
.then((devicesOutput) => {
|
||||
if (attachArgs.target.toLowerCase() === 'device') {
|
||||
let deviceMatch = /\n([^\t]+)\tdevice($|\n)/m.exec(devicesOutput.replace(/\r/g, ''));
|
||||
if (!deviceMatch) {
|
||||
errorLogger(devicesOutput);
|
||||
throw new Error('Unable to find device');
|
||||
.then((devicesOutput) => {
|
||||
if (attachArgs.target.toLowerCase() === 'device') {
|
||||
let deviceMatch = /\n([^\t]+)\tdevice($|\n)/m.exec(devicesOutput.replace(/\r/g, ''));
|
||||
if (!deviceMatch) {
|
||||
errorLogger(devicesOutput);
|
||||
throw new Error('Unable to find device');
|
||||
}
|
||||
return deviceMatch[1];
|
||||
} else {
|
||||
let emulatorMatch = /\n(emulator[^\t]+)\tdevice($|\n)/m.exec(devicesOutput.replace(/\r/g, ''));
|
||||
if (!emulatorMatch) {
|
||||
errorLogger(devicesOutput);
|
||||
throw new Error('Unable to find emulator');
|
||||
}
|
||||
return emulatorMatch[1];
|
||||
}
|
||||
return deviceMatch[1];
|
||||
} else {
|
||||
let emulatorMatch = /\n(emulator[^\t]+)\tdevice($|\n)/m.exec(devicesOutput.replace(/\r/g, ''));
|
||||
if (!emulatorMatch) {
|
||||
errorLogger(devicesOutput);
|
||||
throw new Error('Unable to find emulator');
|
||||
}, (err: Error): any => {
|
||||
let errorCode: string = (<any>err).code;
|
||||
if (errorCode && errorCode === 'ENOENT') {
|
||||
throw new Error('Unable to find adb. Please ensure it is in your PATH and re-open Visual Studio Code');
|
||||
}
|
||||
return emulatorMatch[1];
|
||||
}
|
||||
}, (err: Error): any => {
|
||||
let errorCode: string = (<any>err).code;
|
||||
if (errorCode && errorCode === 'ENOENT') {
|
||||
throw new Error('Unable to find adb. Please ensure it is in your PATH and re-open Visual Studio Code');
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
throw err;
|
||||
});
|
||||
let packagePromise = Q.nfcall(fs.readFile, path.join(attachArgs.cwd, 'platforms', 'android', 'AndroidManifest.xml'))
|
||||
.then((manifestContents) => {
|
||||
let parsedFile = elementtree.XML(manifestContents.toString());
|
||||
let packageKey = 'package';
|
||||
return parsedFile.attrib[packageKey];
|
||||
});
|
||||
.then((manifestContents) => {
|
||||
let parsedFile = elementtree.XML(manifestContents.toString());
|
||||
let packageKey = 'package';
|
||||
return parsedFile.attrib[packageKey];
|
||||
});
|
||||
return Q.all([packagePromise, adbDevicesResult]).spread((appPackageName, targetDevice) => {
|
||||
let getPidCommandArguments = ['-s', targetDevice, 'shell', `ps | grep ${appPackageName}`];
|
||||
|
||||
let findPidFunction = () => execCommand('adb', getPidCommandArguments, errorLogger).then((pidLine: string) => /^[^ ]+ +([^ ]+) /m.exec(pidLine));
|
||||
|
||||
return CordovaDebugAdapter.retryAsync(findPidFunction, (match) => !!match, 5, 1, 5000, 'Unable to find pid of cordova app').then((match: RegExpExecArray) => match[1])
|
||||
.then((pid) => {
|
||||
// Configure port forwarding to the app
|
||||
let forwardSocketCommandArguments = ['-s', targetDevice, 'forward', `tcp:${attachArgs.port}`, `localabstract:webview_devtools_remote_${pid}`];
|
||||
this.outputLogger('Forwarding debug port');
|
||||
return execCommand('adb', forwardSocketCommandArguments, errorLogger).then(() => {
|
||||
this.adbPortForwardingInfo = {targetDevice, port: attachArgs.port};
|
||||
return CordovaDebugAdapter.retryAsync(findPidFunction, (match) => !!match, 5, 1, 5000, 'Unable to find pid of cordova app').then((match: RegExpExecArray) => match[1])
|
||||
.then((pid) => {
|
||||
// Configure port forwarding to the app
|
||||
let forwardSocketCommandArguments = ['-s', targetDevice, 'forward', `tcp:${attachArgs.port}`, `localabstract:webview_devtools_remote_${pid}`];
|
||||
this.outputLogger('Forwarding debug port');
|
||||
return execCommand('adb', forwardSocketCommandArguments, errorLogger).then(() => {
|
||||
this.adbPortForwardingInfo = { targetDevice, port: attachArgs.port };
|
||||
});
|
||||
});
|
||||
});
|
||||
}).then(() => {
|
||||
let args: IAttachRequestArgs = {port: attachArgs.port, webRoot: attachArgs.cwd, cwd: attachArgs.cwd };
|
||||
let args: IAttachRequestArgs = { port: attachArgs.port, webRoot: attachArgs.cwd, cwd: attachArgs.cwd };
|
||||
return args;
|
||||
});
|
||||
}
|
||||
|
||||
private launchIos(launchArgs: ICordovaLaunchRequestArgs): Q.Promise<void> {
|
||||
private launchIos(launchArgs: ICordovaLaunchRequestArgs, projectType: IProjectType): Q.Promise<void> {
|
||||
if (os.platform() !== 'darwin') {
|
||||
return Q.reject<void>('Unable to launch iOS on non-mac machines');
|
||||
}
|
||||
|
@ -193,7 +231,7 @@ export class CordovaDebugAdapter extends WebKitDebugAdapter {
|
|||
if (launchArgs.target.toLowerCase() === 'device') {
|
||||
// cordova run ios does not terminate, so we do not know when to try and attach.
|
||||
// Instead, we try to launch manually using homebrew.
|
||||
return cordovaRunCommand(['build', 'ios', '--device'], errorLogger, workingDirectory).then(() => {
|
||||
return cordovaRunCommand(['build', 'ios', '--device'], errorLogger, workingDirectory).then(() => {
|
||||
let buildFolder = path.join(workingDirectory, 'platforms', 'ios', 'build', 'device');
|
||||
|
||||
this.outputLogger('Installing app on device');
|
||||
|
@ -221,7 +259,7 @@ export class CordovaDebugAdapter extends WebKitDebugAdapter {
|
|||
return CordovaIosDeviceLauncher.startDebugProxy(iosDebugProxyPort).then(() => {
|
||||
return CordovaIosDeviceLauncher.startApp(appBundleId, iosDebugProxyPort, appStepLaunchTimeout);
|
||||
});
|
||||
}).then(() => void(0));
|
||||
}).then(() => void (0));
|
||||
} else {
|
||||
let target = launchArgs.target.toLowerCase() === 'emulator' ? null : launchArgs.target;
|
||||
if (target) {
|
||||
|
@ -232,9 +270,9 @@ export class CordovaDebugAdapter extends WebKitDebugAdapter {
|
|||
errorLogger(output[0].replace(/\*+[^*]+\*+/g, '')); // Print out list of targets, without ** RUN SUCCEEDED **
|
||||
throw err;
|
||||
});
|
||||
}).then(() => void(0));
|
||||
}).then(() => void (0));
|
||||
} else {
|
||||
return cordovaRunCommand(['emulate', 'ios'], errorLogger, workingDirectory).then(() => void(0));
|
||||
return cordovaRunCommand(['emulate', 'ios'], errorLogger, workingDirectory).then(() => void (0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -253,12 +291,12 @@ export class CordovaDebugAdapter extends WebKitDebugAdapter {
|
|||
.then(path.basename);
|
||||
} else {
|
||||
return Q.nfcall(fs.readdir, path.join(attachArgs.cwd, 'platforms', 'ios', 'build', 'emulator')).then((entries: string[]) => {
|
||||
let filtered = entries.filter((entry) => /\.app$/.test(entry));
|
||||
if (filtered.length > 0) {
|
||||
return filtered[0];
|
||||
} else {
|
||||
throw new Error('Unable to find .app file');
|
||||
}
|
||||
let filtered = entries.filter((entry) => /\.app$/.test(entry));
|
||||
if (filtered.length > 0) {
|
||||
return filtered[0];
|
||||
} else {
|
||||
throw new Error('Unable to find .app file');
|
||||
}
|
||||
});
|
||||
}
|
||||
}).then((packagePath) => {
|
||||
|
@ -267,7 +305,7 @@ export class CordovaDebugAdapter extends WebKitDebugAdapter {
|
|||
let endpointsList = JSON.parse(response);
|
||||
let devices = endpointsList.filter((entry) =>
|
||||
attachArgs.target.toLowerCase() === 'device' ? entry.deviceId !== 'SIMULATOR'
|
||||
: entry.deviceId === 'SIMULATOR'
|
||||
: entry.deviceId === 'SIMULATOR'
|
||||
);
|
||||
let device = devices[0];
|
||||
// device.url is of the form 'localhost:port'
|
||||
|
@ -278,23 +316,176 @@ export class CordovaDebugAdapter extends WebKitDebugAdapter {
|
|||
}).then((targetPort) => {
|
||||
let findWebviewFunc = () => {
|
||||
return this.promiseGet(`http://localhost:${targetPort}/json`, 'Unable to communicate with target')
|
||||
.then((response: string) => {
|
||||
try {
|
||||
let webviewsList = JSON.parse(response);
|
||||
return webviewsList.filter((entry) => entry.url.indexOf(encodeURIComponent(packagePath)) !== -1);
|
||||
} catch (e) {
|
||||
throw new Error('Unable to find target app');
|
||||
}
|
||||
});
|
||||
.then((response: string) => {
|
||||
try {
|
||||
let webviewsList = JSON.parse(response);
|
||||
return webviewsList.filter((entry) => entry.url.indexOf(encodeURIComponent(packagePath)) !== -1);
|
||||
} catch (e) {
|
||||
throw new Error('Unable to find target app');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return CordovaDebugAdapter.retryAsync(findWebviewFunc, (webviewList) => webviewList.length > 0, attachArgs.attachAttempts, 1, attachArgs.attachDelay, 'Unable to find webview')
|
||||
.then((relevantViews) => {
|
||||
return {port: targetPort, url: relevantViews[0].url};
|
||||
});
|
||||
.then((relevantViews) => {
|
||||
return { port: targetPort, url: relevantViews[0].url };
|
||||
});
|
||||
});
|
||||
}).then(({port, url}) => {
|
||||
return {port: port, webRoot: attachArgs.cwd, cwd: attachArgs.cwd, url: url};
|
||||
return { port: port, webRoot: attachArgs.cwd, cwd: attachArgs.cwd, url: url };
|
||||
});
|
||||
}
|
||||
|
||||
private launchBrowser(launchArgs: ICordovaLaunchRequestArgs, projectType: IProjectType): Q.Promise<void> {
|
||||
let workingDirectory = launchArgs.cwd;
|
||||
let errorLogger = (message) => this.outputLogger(message, true);
|
||||
|
||||
// Currently, browser is only supported for Ionic projects
|
||||
if (!projectType.ionic) {
|
||||
let errorMessage = 'Browser is currently only supported for Ionic projects';
|
||||
|
||||
errorLogger(errorMessage);
|
||||
|
||||
return Q.reject<void>(new Error(errorMessage));
|
||||
}
|
||||
|
||||
// Set up "ionic serve" args
|
||||
let ionicServeArgs: string[] = [
|
||||
"serve",
|
||||
'--nobrowser'
|
||||
];
|
||||
|
||||
if (launchArgs.noLivereload) {
|
||||
ionicServeArgs.push('--nolivereload');
|
||||
}
|
||||
|
||||
// Deploy app to browser
|
||||
return Q(void 0).then(() => {
|
||||
return this.startIonicDevServer(launchArgs, ionicServeArgs);
|
||||
}).then((devServerUrl: string) => {
|
||||
// Prepare Chrome launch args
|
||||
launchArgs.url = devServerUrl;
|
||||
launchArgs.userDataDir = path.join(settingsHome(), CordovaDebugAdapter.CHROME_DATA_DIR);
|
||||
|
||||
// Launch Chrome and attach
|
||||
this.outputLogger('Attaching to app');
|
||||
|
||||
return super.launch(launchArgs);
|
||||
}).catch((err) => {
|
||||
errorLogger(err.message);
|
||||
|
||||
// Kill the Ionic dev server if necessary
|
||||
let killPromise: Q.Promise<void>;
|
||||
|
||||
if (this.ionicLivereloadProcess) {
|
||||
killPromise = killChildProcess(this.ionicLivereloadProcess, errorLogger);
|
||||
} else {
|
||||
killPromise = Q<void>(void 0);
|
||||
}
|
||||
|
||||
// Propagate the error
|
||||
return killPromise.finally(() => {
|
||||
this.ionicLivereloadProcess = null;
|
||||
Q.reject<void>(err)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an Ionic livereload server ("serve or "run / emulate --livereload"). Returns a promise fulfulled with the full URL to the server.
|
||||
*/
|
||||
private startIonicDevServer(launchArgs: ICordovaLaunchRequestArgs, cliArgs: string[]): Q.Promise<string> {
|
||||
if (launchArgs.devServerAddress) {
|
||||
cliArgs.concat(['--address', launchArgs.devServerAddress]);
|
||||
}
|
||||
|
||||
if (launchArgs.devServerPort) {
|
||||
cliArgs.concat(['--port', launchArgs.devServerPort.toString()]);
|
||||
}
|
||||
|
||||
let isServe: boolean = cliArgs[0] === 'serve';
|
||||
let serverReady: boolean = false;
|
||||
let serverReadyTimeout: number = 10000;
|
||||
let appReadyTimeout: number = 120000; // If we're not serving, the app needs to build and deploy (and potentially start the emulator), which can be very long
|
||||
let isLivereloading: boolean = isServe && cliArgs.indexOf('--nolivereload') === -1 || cliArgs.indexOf('--livereload') !== -1;
|
||||
let serverDeferred = Q.defer<void>();
|
||||
let appDeferred = Q.defer<void>();
|
||||
let serverOutput: string = '';
|
||||
|
||||
this.ionicLivereloadProcess = cordovaStartCommand(cliArgs, launchArgs.cwd);
|
||||
this.ionicLivereloadProcess.on('error', (err) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
serverDeferred.reject(new Error('Ionic not found, please run \'npm install –g ionic\' to install it globally'));
|
||||
} else {
|
||||
serverDeferred.reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
let serverOutputHandler = (data: Buffer) => {
|
||||
serverOutput += data.toString();
|
||||
|
||||
// Listen for the server to be ready. We check for the "Ionic server commands" string to decide that.
|
||||
//
|
||||
// Exemple output of Ionic dev server:
|
||||
//
|
||||
// Running live reload server: undefined
|
||||
// Watching: 0=www/**/*, 1=!www/lib/**/*
|
||||
// Running dev server: http://localhost:8100
|
||||
// Ionic server commands, enter:
|
||||
// restart or r to restart the client app from the root
|
||||
// goto or g and a url to have the app navigate to the given url
|
||||
// consolelogs or c to enable/disable console log output
|
||||
// serverlogs or s to enable/disable server log output
|
||||
// quit or q to shutdown the server and exit
|
||||
//
|
||||
// ionic $
|
||||
if (!serverReady && /Ionic server commands/.test(serverOutput)) {
|
||||
serverReady = true;
|
||||
serverDeferred.resolve(void 0);
|
||||
}
|
||||
|
||||
if (serverReady) {
|
||||
// Now that the server is ready, listen for the app to be ready as well. For "serve", this is always true, because no build and deploy is involved. For "run" and "emulate", we need to
|
||||
// wait until we encounter the "Ionic server commands" string a second time. The reason for that is that the output of the server will be the same as above, plus the build output, and
|
||||
// finally the "Ionic server commands" part will be printed again at the very end.
|
||||
if (isServe || /Ionic server commands(?:.*\r?\n)*.*Ionic server commands/.test(serverOutput)) {
|
||||
this.ionicLivereloadProcess.stdout.removeListener('data', serverOutputHandler);
|
||||
appDeferred.resolve(void 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (/Address Selection:/.test(serverOutput)) {
|
||||
// Ionic does not know which address to use for the dev server, and requires human interaction; error out and let the user know
|
||||
let errorMessage: string = 'Multiple addresses available for the Ionic dev server, please specify the \'devServerAddress\' property in the launch config.';
|
||||
let addresses: string[] = /(\d+\) .*)/gm.exec(serverOutput);
|
||||
|
||||
if (addresses) {
|
||||
// Give the user the list of addresses that Ionic found
|
||||
addresses.shift();
|
||||
errorMessage += [' Available addresses:'].concat(addresses).join(os.EOL + ' ');
|
||||
}
|
||||
|
||||
serverDeferred.reject(new Error(errorMessage));
|
||||
}
|
||||
};
|
||||
|
||||
this.ionicLivereloadProcess.stdout.on('data', serverOutputHandler);
|
||||
this.outputLogger(`Starting Ionic dev server (live reload: ${isLivereloading})`);
|
||||
|
||||
return serverDeferred.promise.timeout(serverReadyTimeout, `Starting the Ionic dev server timed out (${serverReadyTimeout} ms)`).then(() => {
|
||||
this.outputLogger('Building and deploying app');
|
||||
|
||||
return appDeferred.promise.timeout(appReadyTimeout, `Building and deploying the app timed out (${appReadyTimeout} ms)`);
|
||||
}).then(() => {
|
||||
// Find the dev server full URL
|
||||
let match: string[] = /Running dev server:.*(http:\/\/.*)/gm.exec(serverOutput);
|
||||
|
||||
if (!match || match.length < 2) {
|
||||
return Q.reject<string>(new Error('Unable to determine the Ionic dev server address, please try re-launching the debugger'));
|
||||
}
|
||||
|
||||
// The dev server address is the captured group at index 1 of the match
|
||||
return Q(match[1]);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -314,7 +505,7 @@ export class CordovaDebugAdapter extends WebKitDebugAdapter {
|
|||
|
||||
private promiseGet(url: string, reqErrMessage: string): Q.Promise<string> {
|
||||
let deferred = Q.defer<string>();
|
||||
let req = http.get(url, function (res) {
|
||||
let req = http.get(url, function(res) {
|
||||
let responseString = '';
|
||||
res.on('data', (data: Buffer) => {
|
||||
responseString += data.toString();
|
||||
|
|
|
@ -17,6 +17,7 @@ interface IPendingBreakpoint {
|
|||
export class CordovaPathTransformer implements IDebugTransformer {
|
||||
private _cordovaRoot: string;
|
||||
private _platform: string;
|
||||
private _webRoot: string;
|
||||
private _clientPathToWebkitUrl = new Map<string, string>();
|
||||
private _webkitUrlToClientPath = new Map<string, string>();
|
||||
private _shadowedClientPaths = new Map<string, string>();
|
||||
|
@ -34,6 +35,7 @@ export class CordovaPathTransformer implements IDebugTransformer {
|
|||
public attach(args: ICordovaAttachRequestArgs): void {
|
||||
this._cordovaRoot = args.cwd;
|
||||
this._platform = args.platform.toLowerCase();
|
||||
this._webRoot = args.webRoot || this._cordovaRoot;
|
||||
}
|
||||
|
||||
public setBreakpoints(args: ISetBreakpointsArgs): Promise<void> {
|
||||
|
@ -82,9 +84,9 @@ export class CordovaPathTransformer implements IDebugTransformer {
|
|||
const clientPath = this.getClientPath(webkitUrl);
|
||||
|
||||
if (!clientPath) {
|
||||
utils.Logger.log(`Paths.scriptParsed: could not resolve ${webkitUrl} to a file in the workspace. webRoot: ${this._cordovaRoot}`);
|
||||
utils.Logger.log(`Paths.scriptParsed: could not resolve ${webkitUrl} to a file in the workspace. webRoot: ${this._webRoot}`);
|
||||
} else {
|
||||
utils.Logger.log(`Paths.scriptParsed: resolved ${webkitUrl} to ${clientPath}. webRoot: ${this._cordovaRoot}`);
|
||||
utils.Logger.log(`Paths.scriptParsed: resolved ${webkitUrl} to ${clientPath}. webRoot: ${this._webRoot}`);
|
||||
this._clientPathToWebkitUrl.set(clientPath, webkitUrl);
|
||||
this._webkitUrlToClientPath.set(webkitUrl, clientPath);
|
||||
|
||||
|
@ -119,13 +121,27 @@ export class CordovaPathTransformer implements IDebugTransformer {
|
|||
}
|
||||
|
||||
public getClientPath(sourceUrl: string): string {
|
||||
let wwwRoot = path.join(this._cordovaRoot, 'www');
|
||||
|
||||
// Given an absolute file:/// (such as from the iOS simulator) vscode-chrome-debug's
|
||||
// default behavior is to use that exact file, if it exists. We don't want that,
|
||||
// since we know that those files are copies of files in the local folder structure.
|
||||
// A simple workaround for this is to convert file:// paths to bogus http:// paths
|
||||
sourceUrl = sourceUrl.replace('file:///', 'http://localhost/');
|
||||
let defaultPath = utils.webkitUrlToClientPath(this._cordovaRoot, sourceUrl);
|
||||
let wwwRoot = path.join(this._cordovaRoot, 'www');
|
||||
|
||||
// Find the mapped local file. Try looking first in the user-specified webRoot, then in the project root, and then in the www folder
|
||||
let defaultPath = '';
|
||||
[this._webRoot, this._cordovaRoot, wwwRoot].find((searchFolder) => {
|
||||
let mappedPath = utils.webkitUrlToClientPath(searchFolder, sourceUrl);
|
||||
|
||||
if (mappedPath) {
|
||||
defaultPath = mappedPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (defaultPath.toLowerCase().indexOf(wwwRoot.toLowerCase()) === 0) {
|
||||
// If the path appears to be in www, check to see if it exists in /merges/<platform>/<relative path>
|
||||
let relativePath = path.relative(wwwRoot, defaultPath);
|
||||
|
@ -144,4 +160,3 @@ export class CordovaPathTransformer implements IDebugTransformer {
|
|||
return defaultPath;
|
||||
}
|
||||
}
|
||||
;
|
|
@ -2,12 +2,14 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for details.
|
||||
|
||||
import * as child_process from 'child_process';
|
||||
import {CordovaProjectHelper} from '../utils/cordovaProjectHelper';
|
||||
import * as os from 'os';
|
||||
import * as Q from 'q';
|
||||
import * as util from 'util';
|
||||
|
||||
export function execCommand(command: string, args: string[], errorLogger: (message: string) => void): Q.Promise<string> {
|
||||
let deferred = Q.defer<string>();
|
||||
let proc = child_process.spawn(command, args, {stdio: 'pipe'});
|
||||
let proc = child_process.spawn(command, args, { stdio: 'pipe' });
|
||||
let stderr = '';
|
||||
let stdout = '';
|
||||
proc.stderr.on('data', (data: Buffer) => {
|
||||
|
@ -33,11 +35,11 @@ export function execCommand(command: string, args: string[], errorLogger: (messa
|
|||
|
||||
export function cordovaRunCommand(args: string[], errorLogger: (message: string) => void, cordovaRootPath: string): Q.Promise<string[]> {
|
||||
let defer = Q.defer<string[]>();
|
||||
|
||||
let cliName = CordovaProjectHelper.isIonicProject(cordovaRootPath) ? 'ionic' : 'cordova';
|
||||
let output = '';
|
||||
let stderr = '';
|
||||
let cordovaCommand = `cordova${os.platform() === 'win32' ? '.cmd' : ''}`;
|
||||
let process = child_process.spawn(cordovaCommand, args, {cwd: cordovaRootPath});
|
||||
let process = cordovaStartCommand(args, cordovaRootPath);
|
||||
|
||||
process.stderr.on('data', data => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
@ -48,7 +50,7 @@ export function cordovaRunCommand(args: string[], errorLogger: (message: string)
|
|||
if (exitCode) {
|
||||
errorLogger(stderr);
|
||||
errorLogger(output);
|
||||
defer.reject(new Error(`'cordova ${args.join(' ')}' failed with exit code ${exitCode}.`));
|
||||
defer.reject(new Error(util.format("'%s %s' failed with exit code %d", cliName, args.join(' '), exitCode)));
|
||||
} else {
|
||||
defer.resolve([output, stderr]);
|
||||
}
|
||||
|
@ -58,4 +60,30 @@ export function cordovaRunCommand(args: string[], errorLogger: (message: string)
|
|||
});
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
export function cordovaStartCommand(args: string[], cordovaRootPath: string): child_process.ChildProcess {
|
||||
let cliName = CordovaProjectHelper.isIonicProject(cordovaRootPath) ? 'ionic' : 'cordova';
|
||||
let commandExtension = os.platform() === 'win32' ? '.cmd' : '';
|
||||
let command = cliName + commandExtension;
|
||||
return child_process.spawn(command, args, { cwd: cordovaRootPath });
|
||||
}
|
||||
|
||||
export function killChildProcess(childProcess: child_process.ChildProcess, errorLogger: (message: string) => void): Q.Promise<void> {
|
||||
if (process.platform === "win32") {
|
||||
// Use taskkill to reliably kill the child process on all versions of Windows
|
||||
let command: string = "taskkill";
|
||||
let args: string[] = [
|
||||
"/pid",
|
||||
childProcess.pid.toString(),
|
||||
"/T",
|
||||
"/F"
|
||||
];
|
||||
|
||||
return execCommand(command, args, errorLogger).then(() => void 0);
|
||||
} else {
|
||||
childProcess.kill();
|
||||
|
||||
return Q<void>(void 0);
|
||||
}
|
||||
}
|
|
@ -63,4 +63,4 @@ export class CordovaCommandHelper {
|
|||
.then(() => deferred.promise);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
*******************************************************
|
||||
* *
|
||||
* Copyright (C) Microsoft. All rights reserved. *
|
||||
* *
|
||||
*******************************************************
|
||||
*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
export function settingsHome(): string {
|
||||
switch (os.platform()) {
|
||||
case 'win32':
|
||||
return path.join(process.env['APPDATA'], 'vscode-cordova');
|
||||
case 'darwin':
|
||||
case 'linux':
|
||||
return path.join(process.env['HOME'], '.vscode-cordova');
|
||||
default:
|
||||
throw new Error('UnexpectedPlatform');
|
||||
};
|
||||
}
|
|
@ -19,6 +19,8 @@ import * as Q from 'q';
|
|||
import * as readline from 'readline';
|
||||
import * as winreg from 'winreg';
|
||||
|
||||
import {settingsHome} from './settingsHelper'
|
||||
|
||||
// for poking around at internal applicationinsights options
|
||||
var sender = require ('applicationinsights/Library/Sender');
|
||||
var telemetryLogger = require ('applicationinsights/Library/Logging');
|
||||
|
@ -162,20 +164,8 @@ export module Telemetry {
|
|||
private static INTERNAL_DOMAIN_SUFFIX: string = 'microsoft.com';
|
||||
private static INTERNAL_USER_ENV_VAR: string = 'TACOINTERNAL';
|
||||
|
||||
private static get settingsHome(): string {
|
||||
switch (os.platform()) {
|
||||
case 'win32':
|
||||
return path.join(process.env['APPDATA'], 'vscode-cordova');
|
||||
case 'darwin':
|
||||
case 'linux':
|
||||
return path.join(process.env['HOME'], '.vscode-cordova');
|
||||
default:
|
||||
throw new Error('UnexpectedPlatform');
|
||||
};
|
||||
}
|
||||
|
||||
private static get telemetrySettingsFile(): string {
|
||||
return path.join(TelemetryUtils.settingsHome, TelemetryUtils.TELEMETRY_SETTINGS_FILENAME);
|
||||
return path.join(settingsHome(), TelemetryUtils.TELEMETRY_SETTINGS_FILENAME);
|
||||
}
|
||||
|
||||
public static init(appVersion: string, isOptedInValue: boolean): Q.Promise<any> {
|
||||
|
@ -325,8 +315,8 @@ export module Telemetry {
|
|||
* Save settings data in settingsHome/TelemetrySettings.json
|
||||
*/
|
||||
private static saveSettings(): void {
|
||||
if (!fs.existsSync(TelemetryUtils.settingsHome)) {
|
||||
fs.mkdirSync(TelemetryUtils.settingsHome);
|
||||
if (!fs.existsSync(settingsHome())) {
|
||||
fs.mkdirSync(settingsHome());
|
||||
}
|
||||
|
||||
fs.writeFileSync(TelemetryUtils.telemetrySettingsFile, JSON.stringify(TelemetryUtils.telemetrySettings));
|
||||
|
|
Загрузка…
Ссылка в новой задаче