Disallow launching unknown apps via browser client.

CVE-2018-1000006
This commit is contained in:
Aleš Pergl 2018-01-22 16:49:30 -06:00 committed by Charles Kerr
parent 32a1395bcf
commit c49cb29ddf
17 changed files with 1553 additions and 101 deletions

View file

@ -10,7 +10,7 @@
#if defined(OS_MACOSX) #if defined(OS_MACOSX)
extern "C" { extern "C" {
__attribute__((visibility("default"))) __attribute__((visibility("default")))
int AtomMain(int argc, const char* argv[]); int AtomMain(int argc, char* argv[]);
__attribute__((visibility("default"))) __attribute__((visibility("default")))
int AtomInitializeICUandStartNode(int argc, char *argv[]); int AtomInitializeICUandStartNode(int argc, char *argv[]);

View file

@ -15,11 +15,11 @@
#include "content/public/app/content_main.h" #include "content/public/app/content_main.h"
#if defined(OS_MACOSX) #if defined(OS_MACOSX)
int AtomMain(int argc, const char* argv[]) { int AtomMain(int argc, char* argv[]) {
atom::AtomMainDelegate delegate; atom::AtomMainDelegate delegate;
content::ContentMainParams params(&delegate); content::ContentMainParams params(&delegate);
params.argc = argc; params.argc = argc;
params.argv = argv; params.argv = const_cast<const char**>(argv);
atom::AtomCommandLine::Init(argc, argv); atom::AtomCommandLine::Init(argc, argv);
return content::ContentMain(params); return content::ContentMain(params);
} }

View file

@ -4,7 +4,8 @@
#include "atom/app/atom_main.h" #include "atom/app/atom_main.h"
#include <stdlib.h> #include <cstdlib>
#include <vector>
#if defined(OS_WIN) #if defined(OS_WIN)
#include <windows.h> // windows.h must be included first #include <windows.h> // windows.h must be included first
@ -15,9 +16,11 @@
#include <tchar.h> #include <tchar.h>
#include "atom/app/atom_main_delegate.h" #include "atom/app/atom_main_delegate.h"
#include "atom/app/command_line_args.h"
#include "atom/common/crash_reporter/win/crash_service_main.h" #include "atom/common/crash_reporter/win/crash_service_main.h"
#include "base/environment.h" #include "base/environment.h"
#include "base/process/launch.h" #include "base/process/launch.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/windows_version.h" #include "base/win/windows_version.h"
#include "content/public/app/sandbox_helper_win.h" #include "content/public/app/sandbox_helper_win.h"
#include "sandbox/win/src/sandbox_types.h" #include "sandbox/win/src/sandbox_types.h"
@ -52,18 +55,23 @@ bool IsEnvSet(const char* name) {
#if defined(OS_WIN) #if defined(OS_WIN)
int APIENTRY wWinMain(HINSTANCE instance, HINSTANCE, wchar_t* cmd, int) { int APIENTRY wWinMain(HINSTANCE instance, HINSTANCE, wchar_t* cmd, int) {
int argc = 0; struct Arguments {
wchar_t** wargv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); int argc = 0;
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
bool run_as_node = IsEnvSet(kRunAsNode); ~Arguments() { LocalFree(argv); }
} arguments;
if (!arguments.argv)
return -1;
#ifdef _DEBUG #ifdef _DEBUG
// Don't display assert dialog boxes in CI test runs // Don't display assert dialog boxes in CI test runs
static const auto kCI = "ELECTRON_CI"; static const auto kCI = "ELECTRON_CI";
bool is_ci = IsEnvSet(kCI); bool is_ci = IsEnvSet(kCI);
if (!is_ci) { if (!is_ci) {
for (int i = 0; i < argc; ++i) { for (int i = 0; i < arguments.argc; ++i) {
if (!_wcsicmp(wargv[i], L"--ci")) { if (!_wcsicmp(arguments.argv[i], L"--ci")) {
is_ci = true; is_ci = true;
_putenv_s(kCI, "1"); // set flag for child processes _putenv_s(kCI, "1"); // set flag for child processes
break; break;
@ -81,44 +89,12 @@ int APIENTRY wWinMain(HINSTANCE instance, HINSTANCE, wchar_t* cmd, int) {
} }
#endif #endif
bool run_as_node = IsEnvSet(kRunAsNode);
// Make sure the output is printed to console. // Make sure the output is printed to console.
if (run_as_node || !IsEnvSet("ELECTRON_NO_ATTACH_CONSOLE")) if (run_as_node || !IsEnvSet("ELECTRON_NO_ATTACH_CONSOLE"))
base::RouteStdioToConsole(false); base::RouteStdioToConsole(false);
// Convert argv to to UTF8
char** argv = new char*[argc];
for (int i = 0; i < argc; i++) {
// Compute the size of the required buffer
DWORD size = WideCharToMultiByte(CP_UTF8,
0,
wargv[i],
-1,
NULL,
0,
NULL,
NULL);
if (size == 0) {
// This should never happen.
fprintf(stderr, "Could not convert arguments to utf8.");
exit(1);
}
// Do the actual conversion
argv[i] = new char[size];
DWORD result = WideCharToMultiByte(CP_UTF8,
0,
wargv[i],
-1,
argv[i],
size,
NULL,
NULL);
if (result == 0) {
// This should never happen.
fprintf(stderr, "Could not convert arguments to utf8.");
exit(1);
}
}
#ifndef DEBUG #ifndef DEBUG
// Chromium has its own TLS subsystem which supports automatic destruction // Chromium has its own TLS subsystem which supports automatic destruction
// of thread-local data, and also depends on memory allocation routines // of thread-local data, and also depends on memory allocation routines
@ -139,14 +115,23 @@ int APIENTRY wWinMain(HINSTANCE instance, HINSTANCE, wchar_t* cmd, int) {
#endif #endif
if (run_as_node) { if (run_as_node) {
// Now that argv conversion is done, we can finally start. std::vector<char*> argv(arguments.argc);
std::transform(
arguments.argv, arguments.argv + arguments.argc, argv.begin(),
[](auto& a) { return _strdup(base::WideToUTF8(a).c_str()); });
base::AtExitManager atexit_manager; base::AtExitManager atexit_manager;
base::i18n::InitializeICU(); base::i18n::InitializeICU();
return atom::NodeMain(argc, argv); auto ret = atom::NodeMain(argv.size(), argv.data());
std::for_each(argv.begin(), argv.end(), free);
return ret;
} else if (IsEnvSet("ELECTRON_INTERNAL_CRASH_SERVICE")) { } else if (IsEnvSet("ELECTRON_INTERNAL_CRASH_SERVICE")) {
return crash_service::Main(cmd); return crash_service::Main(cmd);
} }
if (!atom::CheckCommandLineArguments(arguments.argc, arguments.argv))
return -1;
sandbox::SandboxInterfaceInfo sandbox_info = {0}; sandbox::SandboxInterfaceInfo sandbox_info = {0};
content::InitializeSandboxInfo(&sandbox_info); content::InitializeSandboxInfo(&sandbox_info);
atom::AtomMainDelegate delegate; atom::AtomMainDelegate delegate;
@ -154,33 +139,32 @@ int APIENTRY wWinMain(HINSTANCE instance, HINSTANCE, wchar_t* cmd, int) {
content::ContentMainParams params(&delegate); content::ContentMainParams params(&delegate);
params.instance = instance; params.instance = instance;
params.sandbox_info = &sandbox_info; params.sandbox_info = &sandbox_info;
atom::AtomCommandLine::Init(argc, argv); atom::AtomCommandLine::Init(arguments.argc, arguments.argv);
atom::AtomCommandLine::InitW(argc, wargv);
return content::ContentMain(params); return content::ContentMain(params);
} }
#elif defined(OS_LINUX) // defined(OS_WIN) #elif defined(OS_LINUX) // defined(OS_WIN)
int main(int argc, const char* argv[]) { int main(int argc, char* argv[]) {
if (IsEnvSet(kRunAsNode)) { if (IsEnvSet(kRunAsNode)) {
base::i18n::InitializeICU(); base::i18n::InitializeICU();
base::AtExitManager atexit_manager; base::AtExitManager atexit_manager;
return atom::NodeMain(argc, const_cast<char**>(argv)); return atom::NodeMain(argc, argv);
} }
atom::AtomMainDelegate delegate; atom::AtomMainDelegate delegate;
content::ContentMainParams params(&delegate); content::ContentMainParams params(&delegate);
params.argc = argc; params.argc = argc;
params.argv = argv; params.argv = const_cast<const char**>(argv);
atom::AtomCommandLine::Init(argc, argv); atom::AtomCommandLine::Init(argc, argv);
return content::ContentMain(params); return content::ContentMain(params);
} }
#else // defined(OS_LINUX) #else // defined(OS_LINUX)
int main(int argc, const char* argv[]) { int main(int argc, char* argv[]) {
if (IsEnvSet(kRunAsNode)) { if (IsEnvSet(kRunAsNode)) {
return AtomInitializeICUandStartNode(argc, const_cast<char**>(argv)); return AtomInitializeICUandStartNode(argc, argv);
} }
return AtomMain(argc, argv); return AtomMain(argc, argv);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
// Copyright (c) 2018 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ATOM_APP_COMMAND_LINE_ARGS_H_
#define ATOM_APP_COMMAND_LINE_ARGS_H_
#include "base/command_line.h"
namespace atom {
bool CheckCommandLineArguments(int argc, base::CommandLine::CharType** argv);
} // namespace atom
#endif // ATOM_APP_COMMAND_LINE_ARGS_H_

View file

@ -897,11 +897,7 @@ bool App::Relaunch(mate::Arguments* js_args) {
} }
if (!override_argv) { if (!override_argv) {
#if defined(OS_WIN)
const relauncher::StringVector& argv = atom::AtomCommandLine::wargv();
#else
const relauncher::StringVector& argv = atom::AtomCommandLine::argv(); const relauncher::StringVector& argv = atom::AtomCommandLine::argv();
#endif
return relauncher::RelaunchApp(argv); return relauncher::RelaunchApp(argv);
} }

View file

@ -38,6 +38,7 @@
#include "content/public/browser/resource_dispatcher_host.h" #include "content/public/browser/resource_dispatcher_host.h"
#include "content/public/browser/site_instance.h" #include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.h" #include "content/public/common/content_switches.h"
#include "content/public/common/resource_request_body.h" #include "content/public/common/resource_request_body.h"
#include "content/public/common/url_constants.h" #include "content/public/common/url_constants.h"
@ -237,6 +238,11 @@ void AtomBrowserClient::OverrideSiteInstanceForNavigation(
void AtomBrowserClient::AppendExtraCommandLineSwitches( void AtomBrowserClient::AppendExtraCommandLineSwitches(
base::CommandLine* command_line, base::CommandLine* command_line,
int process_id) { int process_id) {
// Make sure we're about to launch a known executable
base::FilePath child_path;
PathService::Get(content::CHILD_PROCESS_EXE, &child_path);
CHECK(base::MakeAbsoluteFilePath(command_line->GetProgram()) == child_path);
std::string process_type = std::string process_type =
command_line->GetSwitchValueASCII(::switches::kProcessType); command_line->GetSwitchValueASCII(::switches::kProcessType);
if (process_type != ::switches::kRendererProcess) if (process_type != ::switches::kRendererProcess)

View file

@ -140,11 +140,7 @@ bool RelaunchAppWithHelper(const base::FilePath& helper,
} }
int RelauncherMain(const content::MainFunctionParams& main_parameters) { int RelauncherMain(const content::MainFunctionParams& main_parameters) {
#if defined(OS_WIN)
const StringVector& argv = atom::AtomCommandLine::wargv();
#else
const StringVector& argv = atom::AtomCommandLine::argv(); const StringVector& argv = atom::AtomCommandLine::argv();
#endif
if (argv.size() < 4 || argv[1] != internal::kRelauncherTypeArg) { if (argv.size() < 4 || argv[1] != internal::kRelauncherTypeArg) {
LOG(ERROR) << "relauncher process invoked with unexpected arguments"; LOG(ERROR) << "relauncher process invoked with unexpected arguments";

View file

@ -10,31 +10,22 @@
namespace atom { namespace atom {
// static // static
std::vector<std::string> AtomCommandLine::argv_; base::CommandLine::StringVector AtomCommandLine::argv_;
#if defined(OS_WIN)
// static
std::vector<std::wstring> AtomCommandLine::wargv_;
#endif
// static // static
void AtomCommandLine::Init(int argc, const char* const* argv) { void AtomCommandLine::Init(int argc, base::CommandLine::CharType** argv) {
DCHECK(argv_.empty());
// NOTE: uv_setup_args does nothing on Windows, so we don't need to call it.
// Otherwise we'd have to convert the arguments from UTF16.
#if !defined(OS_WIN)
// Hack around with the argv pointer. Used for process.title = "blah" // Hack around with the argv pointer. Used for process.title = "blah"
char** new_argv = uv_setup_args(argc, const_cast<char**>(argv)); argv = uv_setup_args(argc, argv);
for (int i = 0; i < argc; ++i) {
argv_.push_back(new_argv[i]);
}
}
#if defined(OS_WIN)
// static
void AtomCommandLine::InitW(int argc, const wchar_t* const* argv) {
for (int i = 0; i < argc; ++i) {
wargv_.push_back(argv[i]);
}
}
#endif #endif
argv_.assign(argv, argv + argc);
}
#if defined(OS_LINUX) #if defined(OS_LINUX)
// static // static
void AtomCommandLine::InitializeFromCommandLine() { void AtomCommandLine::InitializeFromCommandLine() {

View file

@ -8,6 +8,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "base/command_line.h"
#include "base/macros.h" #include "base/macros.h"
#include "build/build_config.h" #include "build/build_config.h"
@ -16,13 +17,9 @@ namespace atom {
// Singleton to remember the original "argc" and "argv". // Singleton to remember the original "argc" and "argv".
class AtomCommandLine { class AtomCommandLine {
public: public:
static void Init(int argc, const char* const* argv); static const base::CommandLine::StringVector& argv() { return argv_; }
static std::vector<std::string> argv() { return argv_; }
#if defined(OS_WIN) static void Init(int argc, base::CommandLine::CharType** argv);
static void InitW(int argc, const wchar_t* const* argv);
static std::vector<std::wstring> wargv() { return wargv_; }
#endif
#if defined(OS_LINUX) #if defined(OS_LINUX)
// On Linux the command line has to be read from base::CommandLine since // On Linux the command line has to be read from base::CommandLine since
@ -31,11 +28,7 @@ class AtomCommandLine {
#endif #endif
private: private:
static std::vector<std::string> argv_; static base::CommandLine::StringVector argv_;
#if defined(OS_WIN)
static std::vector<std::wstring> wargv_;
#endif
DISALLOW_IMPLICIT_CONSTRUCTORS(AtomCommandLine); DISALLOW_IMPLICIT_CONSTRUCTORS(AtomCommandLine);
}; };

View file

@ -4,6 +4,7 @@
#include "atom/common/node_bindings.h" #include "atom/common/node_bindings.h"
#include <algorithm>
#include <string> #include <string>
#include <vector> #include <vector>
@ -17,6 +18,7 @@
#include "base/files/file_path.h" #include "base/files/file_path.h"
#include "base/path_service.h" #include "base/path_service.h"
#include "base/run_loop.h" #include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h" #include "base/threading/thread_task_runner_handle.h"
#include "base/trace_event/trace_event.h" #include "base/trace_event/trace_event.h"
#include "content/public/browser/browser_thread.h" #include "content/public/browser/browser_thread.h"
@ -179,7 +181,14 @@ void NodeBindings::Initialize() {
node::Environment* NodeBindings::CreateEnvironment( node::Environment* NodeBindings::CreateEnvironment(
v8::Handle<v8::Context> context) { v8::Handle<v8::Context> context) {
#if defined(OS_WIN)
auto& atom_args = AtomCommandLine::argv();
std::vector<std::string> args(atom_args.size());
std::transform(atom_args.cbegin(), atom_args.cend(), args.begin(),
[](auto& a) { return base::WideToUTF8(a); });
#else
auto args = AtomCommandLine::argv(); auto args = AtomCommandLine::argv();
#endif
// Feed node the path to initialization script. // Feed node the path to initialization script.
base::FilePath::StringType process_type; base::FilePath::StringType process_type;
@ -199,8 +208,7 @@ node::Environment* NodeBindings::CreateEnvironment(
resources_path.Append(FILE_PATH_LITERAL("electron.asar")) resources_path.Append(FILE_PATH_LITERAL("electron.asar"))
.Append(process_type) .Append(process_type)
.Append(FILE_PATH_LITERAL("init.js")); .Append(FILE_PATH_LITERAL("init.js"));
std::string script_path_str = script_path.AsUTF8Unsafe(); args.insert(args.begin() + 1, script_path.AsUTF8Unsafe());
args.insert(args.begin() + 1, script_path_str.c_str());
std::unique_ptr<const char*[]> c_argv = StringVectorToArgArray(args); std::unique_ptr<const char*[]> c_argv = StringVectorToArgArray(args);
node::Environment* env = node::CreateEnvironment( node::Environment* env = node::CreateEnvironment(

View file

@ -32,7 +32,7 @@ the hostname and the port number 'hostname:port'.
* `redirect` String (optional) - The redirect mode for this request. Should be * `redirect` String (optional) - The redirect mode for this request. Should be
one of `follow`, `error` or `manual`. Defaults to `follow`. When mode is `error`, one of `follow`, `error` or `manual`. Defaults to `follow`. When mode is `error`,
any redirection will be aborted. When mode is `manual` the redirection will be any redirection will be aborted. When mode is `manual` the redirection will be
deferred until [`request.followRedirect`](#requestfollowRedirect) is invoked. Listen for the [`redirect`](#event-redirect) event in deferred until [`request.followRedirect`](#requestfollowredirect) is invoked. Listen for the [`redirect`](#event-redirect) event in
this mode to get more details about the redirect request. this mode to get more details about the redirect request.
`options` properties such as `protocol`, `host`, `hostname`, `port` and `path` `options` properties such as `protocol`, `host`, `hostname`, `port` and `path`
@ -137,7 +137,7 @@ Returns:
* `responseHeaders` Object * `responseHeaders` Object
Emitted when there is redirection and the mode is `manual`. Calling Emitted when there is redirection and the mode is `manual`. Calling
[`request.followRedirect`](#requestfollowRedirect) will continue with the redirection. [`request.followRedirect`](#requestfollowredirect) will continue with the redirection.
### Instance Properties ### Instance Properties

View file

@ -291,7 +291,7 @@ Calling `event.preventDefault` will prevent the page `keydown`/`keyup` events
and the menu shortcuts. and the menu shortcuts.
To only prevent the menu shortcuts, use To only prevent the menu shortcuts, use
[`setIgnoreMenuShortcuts`](#contentssetignoremenushortcuts): [`setIgnoreMenuShortcuts`](#contentssetignoremenushortcutsignore-experimental):
```javascript ```javascript
const {BrowserWindow} = require('electron') const {BrowserWindow} = require('electron')

View file

@ -528,7 +528,7 @@ can be obtained by subscribing to [`found-in-page`](webview-tag.md#event-found-i
### `<webview>.stopFindInPage(action)` ### `<webview>.stopFindInPage(action)`
* `action` String - Specifies the action to take place when ending * `action` String - Specifies the action to take place when ending
[`<webview>.findInPage`](webview-tag.md#webviewtagfindinpage) request. [`<webview>.findInPage`](#webviewfindinpagetext-options) request.
* `clearSelection` - Clear the selection. * `clearSelection` - Clear the selection.
* `keepSelection` - Translate the selection into a normal selection. * `keepSelection` - Translate the selection into a normal selection.
* `activateSelection` - Focus and click the selection node. * `activateSelection` - Focus and click the selection node.
@ -579,7 +579,7 @@ Send an asynchronous message to renderer process via `channel`, you can also
send arbitrary arguments. The renderer process can handle the message by send arbitrary arguments. The renderer process can handle the message by
listening to the `channel` event with the [`ipcRenderer`](ipc-renderer.md) module. listening to the `channel` event with the [`ipcRenderer`](ipc-renderer.md) module.
See [webContents.send](web-contents.md#webcontentssendchannel-args) for See [webContents.send](web-contents.md#contentssendchannel-arg1-arg2-) for
examples. examples.
### `<webview>.sendInputEvent(event)` ### `<webview>.sendInputEvent(event)`
@ -588,7 +588,7 @@ examples.
Sends an input `event` to the page. Sends an input `event` to the page.
See [webContents.sendInputEvent](web-contents.md#webcontentssendinputeventevent) See [webContents.sendInputEvent](web-contents.md#contentssendinputeventevent)
for detailed description of `event` object. for detailed description of `event` object.
### `<webview>.setZoomFactor(factor)` ### `<webview>.setZoomFactor(factor)`
@ -752,7 +752,7 @@ Returns:
* `finalUpdate` Boolean * `finalUpdate` Boolean
Fired when a result is available for Fired when a result is available for
[`webview.findInPage`](webview-tag.md#webviewtagfindinpage) request. [`webview.findInPage`](#webviewfindinpagetext-options) request.
```javascript ```javascript
const webview = document.querySelector('webview') const webview = document.querySelector('webview')

View file

@ -15,7 +15,7 @@ To ensure that your JavaScript is in compliance with the Electron coding
style, run `npm run lint-js`, which will run `standard` against both style, run `npm run lint-js`, which will run `standard` against both
Electron itself as well as the unit tests. If you are using an editor Electron itself as well as the unit tests. If you are using an editor
with a plugin/addon system, you might want to use one of the many with a plugin/addon system, you might want to use one of the many
[StandardJS addons](standard-addons) to be informed of coding style [StandardJS addons][standard-addons] to be informed of coding style
violations before you ever commit them. violations before you ever commit them.
To run `standard` with parameters, run `npm run lint-js --` followed by To run `standard` with parameters, run `npm run lint-js --` followed by

View file

@ -99,6 +99,8 @@
'atom/app/atom_main_delegate.cc', 'atom/app/atom_main_delegate.cc',
'atom/app/atom_main_delegate.h', 'atom/app/atom_main_delegate.h',
'atom/app/atom_main_delegate_mac.mm', 'atom/app/atom_main_delegate_mac.mm',
'atom/app/command_line_args.cc',
'atom/app/command_line_args.h',
'atom/app/node_main.cc', 'atom/app/node_main.cc',
'atom/app/node_main.h', 'atom/app/node_main.h',
'atom/app/uv_task_runner.cc', 'atom/app/uv_task_runner.cc',

View file

@ -634,6 +634,54 @@ describe('app module', () => {
}) })
}) })
describe('app launch through uri', () => {
before(function () {
if (process.platform !== 'win32') {
this.skip()
}
})
it('does not launch for blacklisted argument', function (done) {
const appPath = path.join(__dirname, 'fixtures', 'api', 'quit-app')
// App should exit with non 123 code.
const first = ChildProcess.spawn(remote.process.execPath, [appPath, 'electron-test://?', '--no-sandbox', '--gpu-launcher=cmd.exe /c start calc'])
first.once('exit', (code) => {
assert.notEqual(code, 123)
done()
})
})
it('launches successfully for multiple uris in cmd args', function (done) {
const appPath = path.join(__dirname, 'fixtures', 'api', 'quit-app')
// App should exit with code 123.
const first = ChildProcess.spawn(remote.process.execPath, [appPath, 'http://electronjs.org', 'electron-test://testdata'])
first.once('exit', (code) => {
assert.equal(code, 123)
done()
})
})
it('does not launch for encoded space', function (done) {
const appPath = path.join(__dirname, 'fixtures', 'api', 'quit-app')
// App should exit with non 123 code.
const first = ChildProcess.spawn(remote.process.execPath, [appPath, 'electron-test://?', '--no-sandbox', '--gpu-launcher%20"cmd.exe /c start calc'])
first.once('exit', (code) => {
assert.notEqual(code, 123)
done()
})
})
it('launches successfully for argnames similar to blacklisted ones', function (done) {
const appPath = path.join(__dirname, 'fixtures', 'api', 'quit-app')
// inspect is blacklisted, but inspector should work, and app launch should succeed
const first = ChildProcess.spawn(remote.process.execPath, [appPath, 'electron-test://?', '--inspector'])
first.once('exit', (code) => {
assert.equal(code, 123)
done()
})
})
})
describe('getFileIcon() API', () => { describe('getFileIcon() API', () => {
const iconPath = path.join(__dirname, 'fixtures/assets/icon.ico') const iconPath = path.join(__dirname, 'fixtures/assets/icon.ico')
const sizes = { const sizes = {