feat: add serial api support (#25237)

* feat: add serial api support

resolves #22478

* Put serial port support behind a flag and mark as experimental

* Update docs/api/session.md

Co-authored-by: Jeremy Rose <jeremya@chromium.org>

* Use enable-blink-features=Serial instead of enable-experimental-web-platform-features

* Set enableBlinkFeatures on webPreferences instead of commandline

Co-authored-by: Jeremy Rose <jeremya@chromium.org>
This commit is contained in:
John Kleinschmidt 2020-09-28 12:22:03 -04:00 committed by GitHub
parent bed50bb73c
commit fd63510ca9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 936 additions and 1 deletions

View file

@ -178,6 +178,76 @@ Emitted when a hunspell dictionary file download fails. For details
on the failure you should collect a netlog and inspect the download
request.
#### Event: 'select-serial-port' _Experimental_
Returns:
* `event` Event
* `portList` [SerialPort[]](structures/serial-port.md)
* `webContents` [WebContents](web-contents.md)
* `callback` Function
* `portId` String
Emitted when a serial port needs to be selected when a call to
`navigator.serial.requestPort` is made. `callback` should be called with
`portId` to be selected, passing an empty string to `callback` will
cancel the request. Additionally, permissioning on `navigator.serial` can
be managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler)
with the `serial` permission.
Because this is an experimental feature it is disabled by default. To enable this feature, you
will need to use the `--enable-features=ElectronSerialChooser` command line switch. Additionally
because this is an experimental Chromium feature you will need to set `enableBlinkFeatures: 'Serial'`
on the `webPreferences` property when opening a BrowserWindow.
```javascript
const { app, BrowserWindow } = require('electron')
let win = null
app.commandLine.appendSwitch('enable-features', 'ElectronSerialChooser')
app.whenReady().then(() => {
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
enableBlinkFeatures: 'Serial'
}
})
win.webContents.session.on('select-serial-port', (event, portList, callback) => {
event.preventDefault()
const selectedPort = portList.find((device) => {
return device.vendorId === 0x2341 && device.productId === 0x0043
})
if (!selectedPort) {
callback('')
} else {
callback(result1.portId)
}
})
})
```
#### Event: 'serial-port-added' _Experimental_
Returns:
* `event` Event
* `port` [SerialPort](structures/serial-port.md)
* `webContents` [WebContents](web-contents.md)
Emitted after `navigator.serial.requestPort` has been called and `select-serial-port` has fired if a new serial port becomes available. For example, this event will fire when a new USB device is plugged in.
#### Event: 'serial-port-removed' _Experimental_
Returns:
* `event` Event
* `port` [SerialPort](structures/serial-port.md)
* `webContents` [WebContents](web-contents.md)
Emitted after `navigator.serial.requestPort` has been called and `select-serial-port` has fired if a serial port has been removed. For example, this event will fire when a USB device is unplugged.
### Instance Methods
The following methods are available on instances of `Session`:
@ -420,7 +490,7 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents
* `handler` Function<Boolean> | null
* `webContents` [WebContents](web-contents.md) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin.
* `permission` String - Enum of 'media'.
* `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, or `serial`.
* `requestingOrigin` String - The origin URL of the permission check
* `details` Object - Some properties are only available on certain permission types.
* `securityOrigin` String - The security origin of the `media` check.

View file

@ -0,0 +1,8 @@
# SerialPort Object
* `portId` String - Unique identifier for the port
* `portName` String - Name of the port
* `displayName` String - Addtional information for the port
* `persistentId` String - This platform-specific identifier, if present, can be used to identify the device across restarts of the application and operating system.
* `vendorId` String - Optional USB vendor ID
* `productId` String - Optional USB product ID

View file

@ -115,6 +115,7 @@ auto_filenames = {
"docs/api/structures/referrer.md",
"docs/api/structures/scrubber-item.md",
"docs/api/structures/segmented-control-segment.md",
"docs/api/structures/serial-port.md",
"docs/api/structures/service-worker-info.md",
"docs/api/structures/shared-worker-info.md",
"docs/api/structures/shortcut-details.md",

View file

@ -299,6 +299,14 @@ filenames = {
"shell/browser/relauncher_linux.cc",
"shell/browser/relauncher_mac.cc",
"shell/browser/relauncher_win.cc",
"shell/browser/serial/electron_serial_delegate.cc",
"shell/browser/serial/electron_serial_delegate.h",
"shell/browser/serial/serial_chooser_context.cc",
"shell/browser/serial/serial_chooser_context.h",
"shell/browser/serial/serial_chooser_context_factory.cc",
"shell/browser/serial/serial_chooser_context_factory.h",
"shell/browser/serial/serial_chooser_controller.cc",
"shell/browser/serial/serial_chooser_controller.h",
"shell/browser/session_preferences.cc",
"shell/browser/session_preferences.h",
"shell/browser/special_storage_policy.cc",

View file

@ -86,6 +86,7 @@
#include "shell/browser/notifications/notification_presenter.h"
#include "shell/browser/notifications/platform_notification_service.h"
#include "shell/browser/protocol_registry.h"
#include "shell/browser/serial/electron_serial_delegate.h"
#include "shell/browser/session_preferences.h"
#include "shell/browser/ui/devtools_manager_delegate.h"
#include "shell/browser/web_contents_permission_helper.h"
@ -1747,4 +1748,10 @@ ElectronBrowserClient::GetPluginMimeTypesWithExternalHandlers(
return mime_types;
}
content::SerialDelegate* ElectronBrowserClient::GetSerialDelegate() {
if (!serial_delegate_)
serial_delegate_ = std::make_unique<ElectronSerialDelegate>();
return serial_delegate_.get();
}
} // namespace electron

View file

@ -18,6 +18,7 @@
#include "content/public/browser/web_contents.h"
#include "electron/buildflags/buildflags.h"
#include "net/ssl/client_cert_identity.h"
#include "shell/browser/serial/electron_serial_delegate.h"
namespace content {
class ClientCertificateDelegate;
@ -82,6 +83,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
void SetCanUseCustomSiteInstance(bool should_disable);
bool CanUseCustomSiteInstance() override;
content::SerialDelegate* GetSerialDelegate() override;
protected:
void RenderProcessWillLaunch(content::RenderProcessHost* host) override;
@ -335,6 +337,8 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
// ProxyingWebSocket classes.
uint64_t next_id_ = 0;
std::unique_ptr<ElectronSerialDelegate> serial_delegate_;
DISALLOW_COPY_AND_ASSIGN(ElectronBrowserClient);
};

View file

@ -0,0 +1,121 @@
// Copyright (c) 2020 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/serial/electron_serial_delegate.h"
#include <utility>
#include "base/feature_list.h"
#include "content/public/browser/web_contents.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/serial/serial_chooser_context.h"
#include "shell/browser/serial/serial_chooser_context_factory.h"
#include "shell/browser/serial/serial_chooser_controller.h"
#include "shell/browser/web_contents_permission_helper.h"
namespace features {
const base::Feature kElectronSerialChooser{"ElectronSerialChooser",
base::FEATURE_DISABLED_BY_DEFAULT};
}
namespace electron {
SerialChooserContext* GetChooserContext(content::RenderFrameHost* frame) {
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
auto* browser_context = web_contents->GetBrowserContext();
return SerialChooserContextFactory::GetForBrowserContext(browser_context);
}
ElectronSerialDelegate::ElectronSerialDelegate() = default;
ElectronSerialDelegate::~ElectronSerialDelegate() = default;
std::unique_ptr<content::SerialChooser> ElectronSerialDelegate::RunChooser(
content::RenderFrameHost* frame,
std::vector<blink::mojom::SerialPortFilterPtr> filters,
content::SerialChooser::Callback callback) {
if (base::FeatureList::IsEnabled(features::kElectronSerialChooser)) {
SerialChooserController* controller = ControllerForFrame(frame);
if (controller) {
DeleteControllerForFrame(frame);
}
AddControllerForFrame(frame, std::move(filters), std::move(callback));
} else {
// If feature is disabled, immediately return back with no port selected.
std::move(callback).Run(nullptr);
}
// Return a nullptr because the return value isn't used for anything, eg
// there is no mechanism to cancel navigator.serial.requestPort(). The return
// value is simply used in Chromium to cleanup the chooser UI once the serial
// service is destroyed.
return nullptr;
}
bool ElectronSerialDelegate::CanRequestPortPermission(
content::RenderFrameHost* frame) {
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
auto* permission_helper =
WebContentsPermissionHelper::FromWebContents(web_contents);
return permission_helper->CheckSerialAccessPermission(
web_contents->GetMainFrame()->GetLastCommittedOrigin());
}
bool ElectronSerialDelegate::HasPortPermission(
content::RenderFrameHost* frame,
const device::mojom::SerialPortInfo& port) {
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
auto* browser_context = web_contents->GetBrowserContext();
auto* chooser_context =
SerialChooserContextFactory::GetForBrowserContext(browser_context);
return chooser_context->HasPortPermission(
frame->GetLastCommittedOrigin(),
web_contents->GetMainFrame()->GetLastCommittedOrigin(), port);
}
device::mojom::SerialPortManager* ElectronSerialDelegate::GetPortManager(
content::RenderFrameHost* frame) {
return GetChooserContext(frame)->GetPortManager();
}
void ElectronSerialDelegate::AddObserver(content::RenderFrameHost* frame,
Observer* observer) {
return GetChooserContext(frame)->AddPortObserver(observer);
}
void ElectronSerialDelegate::RemoveObserver(content::RenderFrameHost* frame,
Observer* observer) {
SerialChooserContext* serial_chooser_context = GetChooserContext(frame);
if (serial_chooser_context) {
return serial_chooser_context->RemovePortObserver(observer);
}
}
SerialChooserController* ElectronSerialDelegate::ControllerForFrame(
content::RenderFrameHost* render_frame_host) {
auto mapping = controller_map_.find(render_frame_host);
return mapping == controller_map_.end() ? nullptr : mapping->second.get();
}
SerialChooserController* ElectronSerialDelegate::AddControllerForFrame(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::SerialPortFilterPtr> filters,
content::SerialChooser::Callback callback) {
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
auto controller = std::make_unique<SerialChooserController>(
render_frame_host, std::move(filters), std::move(callback), web_contents,
weak_factory_.GetWeakPtr());
controller_map_.insert(
std::make_pair(render_frame_host, std::move(controller)));
return ControllerForFrame(render_frame_host);
}
void ElectronSerialDelegate::DeleteControllerForFrame(
content::RenderFrameHost* render_frame_host) {
controller_map_.erase(render_frame_host);
}
} // namespace electron

View file

@ -0,0 +1,60 @@
// Copyright (c) 2020 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef SHELL_BROWSER_SERIAL_ELECTRON_SERIAL_DELEGATE_H_
#define SHELL_BROWSER_SERIAL_ELECTRON_SERIAL_DELEGATE_H_
#include <memory>
#include <unordered_map>
#include <vector>
#include "base/memory/weak_ptr.h"
#include "content/public/browser/serial_delegate.h"
#include "shell/browser/serial/serial_chooser_controller.h"
namespace electron {
class SerialChooserController;
class ElectronSerialDelegate : public content::SerialDelegate {
public:
ElectronSerialDelegate();
~ElectronSerialDelegate() override;
std::unique_ptr<content::SerialChooser> RunChooser(
content::RenderFrameHost* frame,
std::vector<blink::mojom::SerialPortFilterPtr> filters,
content::SerialChooser::Callback callback) override;
bool CanRequestPortPermission(content::RenderFrameHost* frame) override;
bool HasPortPermission(content::RenderFrameHost* frame,
const device::mojom::SerialPortInfo& port) override;
device::mojom::SerialPortManager* GetPortManager(
content::RenderFrameHost* frame) override;
void AddObserver(content::RenderFrameHost* frame,
Observer* observer) override;
void RemoveObserver(content::RenderFrameHost* frame,
Observer* observer) override;
void DeleteControllerForFrame(content::RenderFrameHost* render_frame_host);
private:
SerialChooserController* ControllerForFrame(
content::RenderFrameHost* render_frame_host);
SerialChooserController* AddControllerForFrame(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::SerialPortFilterPtr> filters,
content::SerialChooser::Callback callback);
std::unordered_map<content::RenderFrameHost*,
std::unique_ptr<SerialChooserController>>
controller_map_;
base::WeakPtrFactory<ElectronSerialDelegate> weak_factory_{this};
DISALLOW_COPY_AND_ASSIGN(ElectronSerialDelegate);
};
} // namespace electron
#endif // SHELL_BROWSER_SERIAL_ELECTRON_SERIAL_DELEGATE_H_

View file

@ -0,0 +1,162 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "shell/browser/serial/serial_chooser_context.h"
#include <utility>
#include "base/base64.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "content/public/browser/device_service.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
namespace electron {
constexpr char kPortNameKey[] = "name";
constexpr char kPersistentIdKey[] = "persistent_id";
constexpr char kTokenKey[] = "token";
std::string EncodeToken(const base::UnguessableToken& token) {
const uint64_t data[2] = {token.GetHighForSerialization(),
token.GetLowForSerialization()};
std::string buffer;
base::Base64Encode(
base::StringPiece(reinterpret_cast<const char*>(&data[0]), sizeof(data)),
&buffer);
return buffer;
}
base::UnguessableToken DecodeToken(base::StringPiece input) {
std::string buffer;
if (!base::Base64Decode(input, &buffer) ||
buffer.length() != sizeof(uint64_t) * 2) {
return base::UnguessableToken();
}
const uint64_t* data = reinterpret_cast<const uint64_t*>(buffer.data());
return base::UnguessableToken::Deserialize(data[0], data[1]);
}
base::Value PortInfoToValue(const device::mojom::SerialPortInfo& port) {
base::Value value(base::Value::Type::DICTIONARY);
if (port.display_name && !port.display_name->empty())
value.SetStringKey(kPortNameKey, *port.display_name);
else
value.SetStringKey(kPortNameKey, port.path.LossyDisplayName());
if (SerialChooserContext::CanStorePersistentEntry(port))
value.SetStringKey(kPersistentIdKey, port.persistent_id.value());
else
value.SetStringKey(kTokenKey, EncodeToken(port.token));
return value;
}
SerialChooserContext::SerialChooserContext() = default;
SerialChooserContext::~SerialChooserContext() = default;
void SerialChooserContext::GrantPortPermission(
const url::Origin& requesting_origin,
const url::Origin& embedding_origin,
const device::mojom::SerialPortInfo& port) {
base::Value value = PortInfoToValue(port);
port_info_.insert({port.token, value.Clone()});
ephemeral_ports_[{requesting_origin, embedding_origin}].insert(port.token);
}
bool SerialChooserContext::HasPortPermission(
const url::Origin& requesting_origin,
const url::Origin& embedding_origin,
const device::mojom::SerialPortInfo& port) {
auto it = ephemeral_ports_.find({requesting_origin, embedding_origin});
if (it != ephemeral_ports_.end()) {
const std::set<base::UnguessableToken> ports = it->second;
if (base::Contains(ports, port.token))
return true;
}
return false;
}
// static
bool SerialChooserContext::CanStorePersistentEntry(
const device::mojom::SerialPortInfo& port) {
// If there is no display name then the path name will be used instead. The
// path name is not guaranteed to be stable. For example, on Linux the name
// "ttyUSB0" is reused for any USB serial device. A name like that would be
// confusing to show in settings when the device is disconnected.
if (!port.display_name || port.display_name->empty())
return false;
return port.persistent_id && !port.persistent_id->empty();
}
device::mojom::SerialPortManager* SerialChooserContext::GetPortManager() {
EnsurePortManagerConnection();
return port_manager_.get();
}
void SerialChooserContext::AddPortObserver(PortObserver* observer) {
port_observer_list_.AddObserver(observer);
}
void SerialChooserContext::RemovePortObserver(PortObserver* observer) {
port_observer_list_.RemoveObserver(observer);
}
base::WeakPtr<SerialChooserContext> SerialChooserContext::AsWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void SerialChooserContext::OnPortAdded(device::mojom::SerialPortInfoPtr port) {
for (auto& observer : port_observer_list_)
observer.OnPortAdded(*port);
}
void SerialChooserContext::OnPortRemoved(
device::mojom::SerialPortInfoPtr port) {
for (auto& observer : port_observer_list_)
observer.OnPortRemoved(*port);
std::vector<std::pair<url::Origin, url::Origin>> revoked_url_pairs;
for (auto& map_entry : ephemeral_ports_) {
std::set<base::UnguessableToken>& ports = map_entry.second;
if (ports.erase(port->token) > 0) {
revoked_url_pairs.push_back(map_entry.first);
}
}
port_info_.erase(port->token);
}
void SerialChooserContext::EnsurePortManagerConnection() {
if (port_manager_)
return;
mojo::PendingRemote<device::mojom::SerialPortManager> manager;
content::GetDeviceService().BindSerialPortManager(
manager.InitWithNewPipeAndPassReceiver());
SetUpPortManagerConnection(std::move(manager));
}
void SerialChooserContext::SetUpPortManagerConnection(
mojo::PendingRemote<device::mojom::SerialPortManager> manager) {
port_manager_.Bind(std::move(manager));
port_manager_.set_disconnect_handler(
base::BindOnce(&SerialChooserContext::OnPortManagerConnectionError,
base::Unretained(this)));
port_manager_->SetClient(client_receiver_.BindNewPipeAndPassRemote());
}
void SerialChooserContext::OnPortManagerConnectionError() {
port_manager_.reset();
client_receiver_.reset();
port_info_.clear();
ephemeral_ports_.clear();
}
} // namespace electron

View file

@ -0,0 +1,92 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_H_
#define SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_H_
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/unguessable_token.h"
#include "components/keyed_service/core/keyed_service.h"
#include "content/public/browser/serial_delegate.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/device/public/mojom/serial.mojom-forward.h"
#include "third_party/blink/public/mojom/serial/serial.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace base {
class Value;
}
namespace electron {
class SerialChooserContext : public KeyedService,
public device::mojom::SerialPortManagerClient {
public:
using PortObserver = content::SerialDelegate::Observer;
SerialChooserContext();
~SerialChooserContext() override;
// Serial-specific interface for granting and checking permissions.
void GrantPortPermission(const url::Origin& requesting_origin,
const url::Origin& embedding_origin,
const device::mojom::SerialPortInfo& port);
bool HasPortPermission(const url::Origin& requesting_origin,
const url::Origin& embedding_origin,
const device::mojom::SerialPortInfo& port);
static bool CanStorePersistentEntry(
const device::mojom::SerialPortInfo& port);
device::mojom::SerialPortManager* GetPortManager();
void AddPortObserver(PortObserver* observer);
void RemovePortObserver(PortObserver* observer);
base::WeakPtr<SerialChooserContext> AsWeakPtr();
// SerialPortManagerClient implementation.
void OnPortAdded(device::mojom::SerialPortInfoPtr port) override;
void OnPortRemoved(device::mojom::SerialPortInfoPtr port) override;
private:
void EnsurePortManagerConnection();
void SetUpPortManagerConnection(
mojo::PendingRemote<device::mojom::SerialPortManager> manager);
void OnPortManagerConnectionError();
void OnGetPorts(const url::Origin& requesting_origin,
const url::Origin& embedding_origin,
blink::mojom::SerialService::GetPortsCallback callback,
std::vector<device::mojom::SerialPortInfoPtr> ports);
// Tracks the set of ports to which an origin (potentially embedded in another
// origin) has access to. Key is (requesting_origin, embedding_origin).
std::map<std::pair<url::Origin, url::Origin>,
std::set<base::UnguessableToken>>
ephemeral_ports_;
// Holds information about ports in |ephemeral_ports_|.
std::map<base::UnguessableToken, base::Value> port_info_;
mojo::Remote<device::mojom::SerialPortManager> port_manager_;
mojo::Receiver<device::mojom::SerialPortManagerClient> client_receiver_{this};
base::ObserverList<PortObserver> port_observer_list_;
base::WeakPtrFactory<SerialChooserContext> weak_factory_{this};
DISALLOW_COPY_AND_ASSIGN(SerialChooserContext);
};
} // namespace electron
#endif // SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_H_

View file

@ -0,0 +1,41 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "shell/browser/serial/serial_chooser_context_factory.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "shell/browser/serial/serial_chooser_context.h"
namespace electron {
SerialChooserContextFactory::SerialChooserContextFactory()
: BrowserContextKeyedServiceFactory(
"SerialChooserContext",
BrowserContextDependencyManager::GetInstance()) {}
SerialChooserContextFactory::~SerialChooserContextFactory() {}
KeyedService* SerialChooserContextFactory::BuildServiceInstanceFor(
content::BrowserContext* context) const {
return new SerialChooserContext();
}
// static
SerialChooserContextFactory* SerialChooserContextFactory::GetInstance() {
return base::Singleton<SerialChooserContextFactory>::get();
}
// static
SerialChooserContext* SerialChooserContextFactory::GetForBrowserContext(
content::BrowserContext* context) {
return static_cast<SerialChooserContext*>(
GetInstance()->GetServiceForBrowserContext(context, true));
}
content::BrowserContext* SerialChooserContextFactory::GetBrowserContextToUse(
content::BrowserContext* context) const {
return context;
}
} // namespace electron

View file

@ -0,0 +1,40 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_FACTORY_H_
#define SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_FACTORY_H_
#include "base/macros.h"
#include "base/memory/singleton.h"
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
#include "shell/browser/serial/serial_chooser_context.h"
namespace electron {
class SerialChooserContext;
class SerialChooserContextFactory : public BrowserContextKeyedServiceFactory {
public:
static SerialChooserContext* GetForBrowserContext(
content::BrowserContext* context);
static SerialChooserContextFactory* GetInstance();
private:
friend struct base::DefaultSingletonTraits<SerialChooserContextFactory>;
SerialChooserContextFactory();
~SerialChooserContextFactory() override;
// BrowserContextKeyedServiceFactory methods:
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* context) const override;
content::BrowserContext* GetBrowserContextToUse(
content::BrowserContext* context) const override;
DISALLOW_COPY_AND_ASSIGN(SerialChooserContextFactory);
};
} // namespace electron
#endif // SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_FACTORY_H_

View file

@ -0,0 +1,181 @@
// Copyright (c) 2020 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/serial/serial_chooser_controller.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "shell/browser/api/electron_api_session.h"
#include "shell/browser/serial/serial_chooser_context.h"
#include "shell/browser/serial/serial_chooser_context_factory.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/content_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "ui/base/l10n/l10n_util.h"
namespace gin {
template <>
struct Converter<device::mojom::SerialPortInfoPtr> {
static v8::Local<v8::Value> ToV8(
v8::Isolate* isolate,
const device::mojom::SerialPortInfoPtr& port) {
gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(isolate);
dict.Set("portId", port->token.ToString());
dict.Set("portName", port->path.BaseName().LossyDisplayName());
if (port->display_name && !port->display_name->empty()) {
dict.Set("displayName", *port->display_name);
}
if (port->persistent_id && !port->persistent_id->empty()) {
dict.Set("persistentId", *port->persistent_id);
}
if (port->has_vendor_id) {
dict.Set("vendorId", base::StringPrintf("%u", port->vendor_id));
}
if (port->has_product_id) {
dict.Set("productId", base::StringPrintf("%u", port->product_id));
}
return gin::ConvertToV8(isolate, dict);
}
};
} // namespace gin
namespace electron {
SerialChooserController::SerialChooserController(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::SerialPortFilterPtr> filters,
content::SerialChooser::Callback callback,
content::WebContents* web_contents,
base::WeakPtr<ElectronSerialDelegate> serial_delegate)
: WebContentsObserver(web_contents),
filters_(std::move(filters)),
callback_(std::move(callback)),
serial_delegate_(serial_delegate) {
requesting_origin_ = render_frame_host->GetLastCommittedOrigin();
embedding_origin_ = web_contents->GetMainFrame()->GetLastCommittedOrigin();
chooser_context_ = SerialChooserContextFactory::GetForBrowserContext(
web_contents->GetBrowserContext())
->AsWeakPtr();
DCHECK(chooser_context_);
chooser_context_->GetPortManager()->GetDevices(base::BindOnce(
&SerialChooserController::OnGetDevices, weak_factory_.GetWeakPtr()));
observer_.Add(chooser_context_.get());
}
SerialChooserController::~SerialChooserController() {
RunCallback(/*port=*/nullptr);
}
api::Session* SerialChooserController::GetSession() {
if (!web_contents()) {
return nullptr;
}
return api::Session::FromBrowserContext(web_contents()->GetBrowserContext());
}
void SerialChooserController::OnPortAdded(
const device::mojom::SerialPortInfo& port) {
ports_.push_back(port.Clone());
api::Session* session = GetSession();
if (session) {
session->Emit("serial-port-added", port.Clone(), web_contents());
}
}
void SerialChooserController::OnPortRemoved(
const device::mojom::SerialPortInfo& port) {
const auto it = std::find_if(
ports_.begin(), ports_.end(),
[&port](const auto& ptr) { return ptr->token == port.token; });
if (it != ports_.end()) {
api::Session* session = GetSession();
if (session) {
session->Emit("serial-port-removed", port.Clone(), web_contents());
}
ports_.erase(it);
}
}
void SerialChooserController::OnPortManagerConnectionError() {
observer_.RemoveAll();
}
void SerialChooserController::OnDeviceChosen(const std::string& port_id) {
if (port_id.empty()) {
RunCallback(/*port=*/nullptr);
} else {
const auto it =
std::find_if(ports_.begin(), ports_.end(), [&port_id](const auto& ptr) {
return ptr->token.ToString() == port_id;
});
chooser_context_->GrantPortPermission(requesting_origin_, embedding_origin_,
*it->get());
RunCallback(it->Clone());
}
}
void SerialChooserController::OnGetDevices(
std::vector<device::mojom::SerialPortInfoPtr> ports) {
// Sort ports by file paths.
std::sort(ports.begin(), ports.end(),
[](const auto& port1, const auto& port2) {
return port1->path.BaseName() < port2->path.BaseName();
});
for (auto& port : ports) {
if (FilterMatchesAny(*port))
ports_.push_back(std::move(port));
}
bool prevent_default = false;
api::Session* session = GetSession();
if (session) {
prevent_default =
session->Emit("select-serial-port", ports_, web_contents(),
base::AdaptCallbackForRepeating(base::BindOnce(
&SerialChooserController::OnDeviceChosen,
weak_factory_.GetWeakPtr())));
}
if (!prevent_default) {
RunCallback(/*port=*/nullptr);
}
}
bool SerialChooserController::FilterMatchesAny(
const device::mojom::SerialPortInfo& port) const {
if (filters_.empty())
return true;
for (const auto& filter : filters_) {
if (filter->has_vendor_id &&
(!port.has_vendor_id || filter->vendor_id != port.vendor_id)) {
continue;
}
if (filter->has_product_id &&
(!port.has_product_id || filter->product_id != port.product_id)) {
continue;
}
return true;
}
return false;
}
void SerialChooserController::RunCallback(
device::mojom::SerialPortInfoPtr port) {
if (callback_) {
std::move(callback_).Run(std::move(port));
}
}
} // namespace electron

View file

@ -0,0 +1,79 @@
// Copyright (c) 2020 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTROLLER_H_
#define SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTROLLER_H_
#include <string>
#include <vector>
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_observer.h"
#include "base/strings/string16.h"
#include "content/public/browser/serial_chooser.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "services/device/public/mojom/serial.mojom-forward.h"
#include "shell/browser/api/electron_api_session.h"
#include "shell/browser/serial/electron_serial_delegate.h"
#include "shell/browser/serial/serial_chooser_context.h"
#include "third_party/blink/public/mojom/serial/serial.mojom.h"
namespace content {
class RenderFrameHost;
} // namespace content
namespace electron {
class ElectronSerialDelegate;
// SerialChooserController provides data for the Serial API permission prompt.
class SerialChooserController final : public SerialChooserContext::PortObserver,
public content::WebContentsObserver {
public:
SerialChooserController(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::SerialPortFilterPtr> filters,
content::SerialChooser::Callback callback,
content::WebContents* web_contents,
base::WeakPtr<ElectronSerialDelegate> serial_delegate);
~SerialChooserController() override;
// SerialChooserContext::PortObserver:
void OnPortAdded(const device::mojom::SerialPortInfo& port) override;
void OnPortRemoved(const device::mojom::SerialPortInfo& port) override;
void OnPortManagerConnectionError() override;
private:
api::Session* GetSession();
void OnGetDevices(std::vector<device::mojom::SerialPortInfoPtr> ports);
bool FilterMatchesAny(const device::mojom::SerialPortInfo& port) const;
void RunCallback(device::mojom::SerialPortInfoPtr port);
void OnDeviceChosen(const std::string& port_id);
std::vector<blink::mojom::SerialPortFilterPtr> filters_;
content::SerialChooser::Callback callback_;
url::Origin requesting_origin_;
url::Origin embedding_origin_;
base::WeakPtr<SerialChooserContext> chooser_context_;
ScopedObserver<SerialChooserContext,
SerialChooserContext::PortObserver,
&SerialChooserContext::AddPortObserver,
&SerialChooserContext::RemovePortObserver>
observer_{this};
std::vector<device::mojom::SerialPortInfoPtr> ports_;
base::WeakPtr<ElectronSerialDelegate> serial_delegate_;
base::WeakPtrFactory<SerialChooserController> weak_factory_{this};
DISALLOW_COPY_AND_ASSIGN(SerialChooserController);
};
} // namespace electron
#endif // SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTROLLER_H_

View file

@ -160,6 +160,14 @@ bool WebContentsPermissionHelper::CheckMediaAccessPermission(
return CheckPermission(content::PermissionType::AUDIO_CAPTURE, &details);
}
bool WebContentsPermissionHelper::CheckSerialAccessPermission(
const url::Origin& embedding_origin) const {
base::DictionaryValue details;
details.SetString("securityOrigin", embedding_origin.GetURL().spec());
return CheckPermission(
static_cast<content::PermissionType>(PermissionType::SERIAL), &details);
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPermissionHelper)
} // namespace electron

View file

@ -22,6 +22,7 @@ class WebContentsPermissionHelper
POINTER_LOCK = static_cast<int>(content::PermissionType::NUM) + 1,
FULLSCREEN,
OPEN_EXTERNAL,
SERIAL
};
// Asynchronous Requests
@ -38,6 +39,7 @@ class WebContentsPermissionHelper
// Synchronous Checks
bool CheckMediaAccessPermission(const GURL& security_origin,
blink::mojom::MediaStreamType type) const;
bool CheckSerialAccessPermission(const url::Origin& embedding_origin) const;
private:
explicit WebContentsPermissionHelper(content::WebContents* web_contents);

View file

@ -199,6 +199,8 @@ v8::Local<v8::Value> Converter<content::PermissionType>::ToV8(
return StringToV8(isolate, "fullscreen");
case PermissionType::OPEN_EXTERNAL:
return StringToV8(isolate, "openExternal");
case PermissionType::SERIAL:
return StringToV8(isolate, "serial");
default:
return StringToV8(isolate, "unknown");
}

View file

@ -1455,3 +1455,51 @@ describe('iframe using HTML fullscreen API while window is OS-fullscreened', ()
expect(width).to.equal(0);
});
});
describe('navigator.serial', () => {
let w: BrowserWindow;
before(async () => {
w = new BrowserWindow({
show: false,
webPreferences: {
enableBlinkFeatures: 'Serial'
}
});
await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
});
const getPorts: any = () => {
return w.webContents.executeJavaScript(`
navigator.serial.requestPort().then(port => port.toString()).catch(err => err.toString());
`, true);
};
after(closeAllWindows);
afterEach(() => {
session.defaultSession.setPermissionCheckHandler(null);
session.defaultSession.removeAllListeners('select-serial-port');
});
it('does not return a port if select-serial-port event is not defined', async () => {
w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
const port = await getPorts();
expect(port).to.equal('NotFoundError: No port selected by the user.');
});
it('does not return a port when permission denied', async () => {
w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
callback(portList[0].portId);
});
session.defaultSession.setPermissionCheckHandler(() => false);
const port = await getPorts();
expect(port).to.equal('NotFoundError: No port selected by the user.');
});
it('returns a port when select-serial-port event is defined', async () => {
w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
callback(portList[0].portId);
});
const port = await getPorts();
expect(port).to.equal('[object SerialPort]');
});
});

View file

@ -18,6 +18,7 @@ const { app, protocol } = require('electron');
v8.setFlagsFromString('--expose_gc');
app.commandLine.appendSwitch('js-flags', '--expose_gc');
app.commandLine.appendSwitch('enable-features', 'ElectronSerialChooser');
// Prevent the spec runner quiting when the first window closes
app.on('window-all-closed', () => null);