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

New makeSingleInstance API #12782

Merged
merged 7 commits into from May 7, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 34 additions & 5 deletions atom/browser/api/atom_api_app.cc
Expand Up @@ -350,8 +350,8 @@ struct Converter<content::CertificateRequestResultType> {
namespace atom {

ProcessMetric::ProcessMetric(int type,
base::ProcessId pid,
std::unique_ptr<base::ProcessMetrics> metrics) {
base::ProcessId pid,
std::unique_ptr<base::ProcessMetrics> metrics) {
this->type = type;
this->pid = pid;
this->metrics = std::move(metrics);
Expand Down Expand Up @@ -864,25 +864,52 @@ std::string App::GetLocale() {
return g_browser_process->GetApplicationLocale();
}

bool App::MakeSingleInstance(
const ProcessSingleton::NotificationCallback& callback) {
bool App::OnSecondInstance(const base::CommandLine::StringVector& cmd,
const base::FilePath& cwd) {
Emit("second-instance", cmd, cwd);
// This value is used literally no where, but we need it for Reasons(tm)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the kind of comment that makes kittens cry.

What would be good is adding something to planned-breaking-changes.md that discusses the future change (and links to the issues), and having these changes refer to planned-breaking-changes.md

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be fair this comment and the return value still being there is me not being bothered to refactor a little bit more 😆 I'll clear this out and make it return void 👍

return false;
}

bool App::IsPrimaryInstance(mate::Arguments* args) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • method should be const
  • args should be const (we only use args->ThrowError(), which is also const)

if (single_instance_state_ == SingleInstanceState::UNINITIALIZED) {
args->ThrowError(
"You can not check if you are the primary instance without calling "
"makeSingleInstance");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(bikeshed) "Must call makeSingleInstance() before IsPrimaryInstance()"

}
return single_instance_state_ == SingleInstanceState::PRIMARY;
}

bool App::IsSingleInstance() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method should be const

if (process_singleton_)
return true;
return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did return process_singleton_; give warnings?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah

}

bool App::MakeSingleInstance() {
if (IsSingleInstance())
return false;

base::FilePath user_dir;
PathService::Get(brightray::DIR_USER_DATA, &user_dir);

ProcessSingleton::NotificationCallback cb =
base::Bind(&App::OnSecondInstance, base::Unretained(this));

process_singleton_.reset(new ProcessSingleton(
user_dir, base::Bind(NotificationCallbackWrapper, callback)));
user_dir, base::Bind(NotificationCallbackWrapper, cb)));

switch (process_singleton_->NotifyOtherProcessOrCreate()) {
case ProcessSingleton::NotifyResult::LOCK_ERROR:
case ProcessSingleton::NotifyResult::PROFILE_IN_USE:
case ProcessSingleton::NotifyResult::PROCESS_NOTIFIED: {
process_singleton_.reset();
single_instance_state_ = SingleInstanceState::SECONDARY;
return true;
}
case ProcessSingleton::NotifyResult::PROCESS_NONE:
default: // Shouldn't be needed, but VS warns if it is not there.
single_instance_state_ = SingleInstanceState::PRIMARY;
return false;
}
}
Expand Down Expand Up @@ -1252,6 +1279,8 @@ void App::BuildPrototype(v8::Isolate* isolate,
#if defined(USE_NSS_CERTS)
.SetMethod("importCertificate", &App::ImportCertificate)
#endif
.SetMethod("isSingleInstance", &App::IsSingleInstance)
.SetMethod("isPrimaryInstance", &App::IsPrimaryInstance)
.SetMethod("makeSingleInstance", &App::MakeSingleInstance)
.SetMethod("releaseSingleInstance", &App::ReleaseSingleInstance)
.SetMethod("relaunch", &App::Relaunch)
Expand Down
17 changes: 15 additions & 2 deletions atom/browser/api/atom_api_app.h
Expand Up @@ -45,6 +45,12 @@ namespace atom {
enum class JumpListResult : int;
#endif

enum SingleInstanceState {
UNINITIALIZED,
PRIMARY,
SECONDARY,
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider making this an enum class


struct ProcessMetric {
int type;
base::ProcessId pid;
Expand Down Expand Up @@ -178,8 +184,12 @@ class App : public AtomBrowserClient::Delegate,

void SetDesktopName(const std::string& desktop_name);
std::string GetLocale();
bool MakeSingleInstance(
const ProcessSingleton::NotificationCallback& callback);
bool OnSecondInstance(const base::CommandLine::StringVector& cmd,
const base::FilePath& cwd);
bool IsPrimaryInstance(mate::Arguments* args);
bool IsSingleInstance();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do go with this three-method API ... these two functions have names which are confusingly similar to each other. Since we're trying to make this readable without reaching for the docs, is there a clearer way to name these?

// TODO(MarshallOfSound): Remove return value in 4.0
bool MakeSingleInstance();
void ReleaseSingleInstance();
bool Relaunch(mate::Arguments* args);
void DisableHardwareAcceleration(mate::Arguments* args);
Expand Down Expand Up @@ -229,6 +239,9 @@ class App : public AtomBrowserClient::Delegate,
std::unordered_map<base::ProcessId, std::unique_ptr<atom::ProcessMetric>>;
ProcessMetricMap app_metrics_;

SingleInstanceState single_instance_state_ =
SingleInstanceState::UNINITIALIZED;

DISALLOW_COPY_AND_ASSIGN(App);
};

Expand Down
54 changes: 36 additions & 18 deletions docs/api/app.md
Expand Up @@ -373,6 +373,22 @@ app.on('session-created', (event, session) => {
})
```

### Event: 'second-instance'

Returns:

* `argv` String[] - An array of the second instance's command line arguments
* `workingDirectory` String - The second instance's working directory

This event will be emitted inside the primary instance of your application
when a second instance has been executed. `argv` is an Array of the second instance's
command line arguments, and `workingDirectory` is its current working directory. Usually
applications respond to this by making their primary window focused and
non-minimized.

This event is guaranteed to be emitted after the `ready` event of `app`
gets emitted.

## Methods

The `app` object has the following methods:
Expand Down Expand Up @@ -742,33 +758,31 @@ app.setJumpList([
])
```

### `app.makeSingleInstance(callback)`

* `callback` Function
* `argv` String[] - An array of the second instance's command line arguments
* `workingDirectory` String - The second instance's working directory

Returns `Boolean`.
### `app.makeSingleInstance()`

This method makes your application a Single Instance Application - instead of
allowing multiple instances of your app to run, this will ensure that only a
single instance of your app is running, and other instances signal this
instance and exit.

`callback` will be called by the first instance with `callback(argv, workingDirectory)`
when a second instance has been executed. `argv` is an Array of the second instance's
command line arguments, and `workingDirectory` is its current working directory. Usually
applications respond to this by making their primary window focused and
non-minimized.
### `app.isSingleInstance()`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be public API? It's not in your new use example in the PR

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't need to be, but it's helpful for modules that need to know whether or not they are in a single instance app

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK now we're getting somewhere, maybe there is a use case for these that I'm not seeing.

When would this method be useful?


The `callback` is guaranteed to be executed after the `ready` event of `app`
gets emitted.
Returns `Boolean`

This methods returns whether or not `makeSingleInstance` has been called.

This method returns `false` if your process is the primary instance of the
application and your app should continue loading. And returns `true` if your
### `app.isPrimaryInstance()`

Returns `Boolean`

This method returns `true` if your process is the primary instance of the
application and your app should continue loading. And returns `false` if your
process has sent its parameters to another instance, and you should immediately
quit.

This method will throw an error if you call it without first calling
`app.makeSingleInstance()`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • It never makes sense to call makeSingleInstance() without also calling this
  • It never makes sense to call this without calling makeSingleInstance() first

Why not make these a single API call as before? I don't see how that goes against the goal of replacing a callback with a signal?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because makeSingleInstance returning a boolean that indicates whether it is the second instance doesn't semantically make a whole lot of sense IMO. This might be API preference creeping in but I 100% prefer a slightly more verbose self-explanatory API than an API that doesn't make sense without reading the docs to figure out what the return value of makeSingleInstance means.

Like I've seen this before:

if (app.makeSingleInstance(cb)) {
  return app.quit();
}

Which although is API correct, makes no sense to anyone reading that. Especially when most "action" api's that Electron exposes, the boolean value indicates success.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're in agreement the current API is unintuitive -- the proposed API is more readable and has return values that make sense.

However, I'm not sure how useful these new methods are. I don't see a use case for IsSingleInstance() and it doesn't seem that makeSingleInstance() and IsPrimaryInstance() make use except when called as a pair, suggesting that they could be folded into a single function.

Which comes back to the current API: putting aside the unintuitive return value and slightly opaque name for the moment, it does have strengths in that it's impossible to call the pieces out of order, to miss a call, or to forget to register for a signal. It's very difficult to get wrong.

So what if we preserved that while fixing the name and return value? Something like:

if (!app.registerSecondInstanceHandler(cb)) {
  app.quit();
}

This seems pretty readable to me.


On macOS the system enforces single instance automatically when users try to open
a second instance of your app in Finder, and the `open-file` and `open-url`
events will be emitted for that. However when users start your app in command
Expand All @@ -782,15 +796,19 @@ starts:
const {app} = require('electron')
let myWindow = null

const isSecondInstance = app.makeSingleInstance((commandLine, workingDirectory) => {
app.makeSingleInstance()

app.on('second-instance', (commandLine, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (myWindow) {
if (myWindow.isMinimized()) myWindow.restore()
myWindow.focus()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, not a blocker to this review (esp since it's not new to this diff) but what is the difference between win.restore() and win.show()?

It looks like this block could be replaced with if (myWindow) myWindow.show() to restore + focus but it's not completely clear to me from the documentation, so I thought I'd ask...

}
})

if (isSecondInstance) {
const isPrimaryInstance = app.isPrimaryInstance()

if (!isPrimaryInstance) {
app.quit()
}

Expand Down
10 changes: 10 additions & 0 deletions lib/browser/api/app.js
Expand Up @@ -109,6 +109,16 @@ for (let name of events) {
})
}

// TODO(MarshallOfSound): Remove in 4.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be added to docs/tutorial/planned-breaking-changes.md

const originalMakeSingleInstance = app.makeSingleInstance.bind(app)
app.makeSingleInstance = (cb) => {
if (cb && typeof cb === 'function') {
deprecate.warn('app.makeSingleInstance(cb)', 'app.makeSingleInstance() and app.on(\'second-instance\', cb)')
app.on('second-instance', (event, ...args) => cb(...args))
}
return originalMakeSingleInstance()
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a deprecate mesage for app.releaseSingleInstance() too

// Wrappers for native classes.
const {DownloadItem} = process.atomBinding('download_item')
Object.setPrototypeOf(DownloadItem.prototype, EventEmitter.prototype)
2 changes: 1 addition & 1 deletion spec/fixtures/api/singleton/main.js
Expand Up @@ -5,7 +5,7 @@ app.once('ready', () => {
})

const shouldExit = app.makeSingleInstance(() => {
process.nextTick(() => app.exit(0))
setImmediate(() => app.exit(0))
})

if (shouldExit) {
Expand Down