blob: 83943af732b83a6e5da50073513d24272d39201b [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/desktop_session_agent.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "base/process/process_handle.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "ipc/ipc_channel_proxy.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "mojo/public/cpp/bindings/scoped_interface_endpoint_handle.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "remoting/base/auto_thread_task_runner.h"
#include "remoting/base/errors.h"
#include "remoting/host/action_executor.h"
#include "remoting/host/audio_capturer.h"
#include "remoting/host/base/desktop_environment_options.h"
#include "remoting/host/base/screen_controls.h"
#include "remoting/host/base/screen_resolution.h"
#include "remoting/host/crash_process.h"
#include "remoting/host/desktop_display_info_monitor.h"
#include "remoting/host/desktop_environment.h"
#include "remoting/host/input_injector.h"
#include "remoting/host/keyboard_layout_monitor.h"
#include "remoting/host/mojom/desktop_session.mojom-shared.h"
#include "remoting/host/mojom/desktop_session.mojom.h"
#include "remoting/host/mouse_shape_pump.h"
#include "remoting/host/remote_input_filter.h"
#include "remoting/host/remote_open_url/url_forwarder_configurator.h"
#include "remoting/host/webauthn/remote_webauthn_state_change_notifier.h"
#include "remoting/proto/action.pb.h"
#include "remoting/proto/audio.pb.h"
#include "remoting/proto/control.pb.h"
#include "remoting/proto/event.pb.h"
#include "remoting/proto/url_forwarder_control.pb.h"
#include "remoting/protocol/clipboard_stub.h"
#include "remoting/protocol/input_event_tracker.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
#include "third_party/webrtc/modules/desktop_capture/mouse_cursor.h"
#include "ui/events/types/event_type.h"
namespace remoting {
namespace {
using SetUpUrlForwarderResponse =
protocol::UrlForwarderControl::SetUpUrlForwarderResponse;
// Routes local clipboard events though the IPC channel to the network process.
class DesktopSessionClipboardStub : public protocol::ClipboardStub {
public:
explicit DesktopSessionClipboardStub(
scoped_refptr<DesktopSessionAgent> desktop_session_agent);
DesktopSessionClipboardStub(const DesktopSessionClipboardStub&) = delete;
DesktopSessionClipboardStub& operator=(const DesktopSessionClipboardStub&) =
delete;
~DesktopSessionClipboardStub() override;
// protocol::ClipboardStub implementation.
void InjectClipboardEvent(const protocol::ClipboardEvent& event) override;
private:
scoped_refptr<DesktopSessionAgent> desktop_session_agent_;
};
DesktopSessionClipboardStub::DesktopSessionClipboardStub(
scoped_refptr<DesktopSessionAgent> desktop_session_agent)
: desktop_session_agent_(desktop_session_agent) {}
DesktopSessionClipboardStub::~DesktopSessionClipboardStub() = default;
void DesktopSessionClipboardStub::InjectClipboardEvent(
const protocol::ClipboardEvent& event) {
desktop_session_agent_->OnClipboardEvent(event);
}
} // namespace
DesktopSessionAgent::Delegate::~Delegate() = default;
DesktopSessionAgent::DesktopSessionAgent(
scoped_refptr<AutoThreadTaskRunner> audio_capture_task_runner,
scoped_refptr<AutoThreadTaskRunner> caller_task_runner,
scoped_refptr<AutoThreadTaskRunner> input_task_runner,
scoped_refptr<AutoThreadTaskRunner> io_task_runner)
: audio_capture_task_runner_(audio_capture_task_runner),
caller_task_runner_(caller_task_runner),
input_task_runner_(input_task_runner),
io_task_runner_(io_task_runner) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
}
void DesktopSessionAgent::OnChannelConnected(std::int32_t peer_pid) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
VLOG(1) << "IPC: desktop <- network (" << peer_pid << ")";
}
void DesktopSessionAgent::OnChannelError() {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
// Make sure the channel is closed.
network_channel_.reset();
// Notify the caller that the channel has been disconnected.
if (delegate_.get()) {
delegate_->OnNetworkProcessDisconnected();
}
}
void DesktopSessionAgent::OnAssociatedInterfaceRequest(
const std::string& interface_name,
mojo::ScopedInterfaceEndpointHandle handle) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
if (interface_name == mojom::DesktopSessionAgent::Name_) {
if (desktop_session_agent_.is_bound()) {
LOG(ERROR) << "Receiver already bound for associated interface: "
<< mojom::DesktopSessionAgent::Name_;
delegate_->CrashNetworkProcess(base::Location::Current());
}
mojo::PendingAssociatedReceiver<mojom::DesktopSessionAgent>
pending_receiver(std::move(handle));
desktop_session_agent_.Bind(std::move(pending_receiver));
} else {
LOG(ERROR) << "Unknown associated interface requested: " << interface_name
<< ", crashing the network process";
delegate_->CrashNetworkProcess(base::Location::Current());
}
}
DesktopSessionAgent::~DesktopSessionAgent() {
DCHECK(!audio_capturer_);
DCHECK(!desktop_environment_);
DCHECK(!network_channel_);
DCHECK(!screen_controls_);
DCHECK(video_capturers_.IsEmpty());
DCHECK(!session_file_operations_handler_);
}
const std::string& DesktopSessionAgent::client_jid() const {
return client_jid_;
}
void DesktopSessionAgent::DisconnectSession(
ErrorCode error,
std::string_view error_details,
const SourceLocation& error_location) {
if (desktop_session_state_handler_) {
desktop_session_state_handler_->DisconnectSession(
error, std::string(error_details), error_location);
}
}
void DesktopSessionAgent::OnLocalKeyPressed(std::uint32_t usb_keycode) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
remote_input_filter_->LocalKeyPressed(usb_keycode);
if (desktop_session_event_handler_) {
desktop_session_event_handler_->OnLocalKeyboardInputDetected(usb_keycode);
}
}
void DesktopSessionAgent::OnLocalPointerMoved(
const webrtc::DesktopVector& new_pos,
ui::EventType type) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
remote_input_filter_->LocalPointerMoved(new_pos, type);
if (desktop_session_event_handler_) {
// |type| is always kMouseMoved, if this changes, we need to convey this
// information to the network process.
DCHECK_EQ(type, ui::EventType::kMouseMoved);
desktop_session_event_handler_->OnLocalMouseMoveDetected(new_pos);
}
}
void DesktopSessionAgent::SetDisableInputs(bool disable_inputs) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
// Do not expect this method to be called because it is only used by It2Me.
NOTREACHED();
}
void DesktopSessionAgent::OnDesktopDisplayChanged(
std::unique_ptr<protocol::VideoLayout> layout) {
LOG(INFO) << "DSA::OnDesktopDisplayChanged";
for (int display_id = 0; display_id < layout->video_track_size();
display_id++) {
protocol::VideoTrackLayout track = layout->video_track(display_id);
LOG(INFO) << " #" << display_id << " : "
<< " [" << track.x_dpi() << "," << track.y_dpi() << "]";
}
if (desktop_session_event_handler_) {
desktop_session_event_handler_->OnDesktopDisplayChanged(*layout);
}
}
void DesktopSessionAgent::Start(
const std::string& authenticated_jid,
const ScreenResolution& resolution,
const remoting::DesktopEnvironmentOptions& options,
StartCallback callback) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
DCHECK(!audio_capturer_);
DCHECK(!desktop_environment_);
DCHECK(!input_injector_);
DCHECK(!screen_controls_);
DCHECK(video_capturers_.IsEmpty());
DCHECK(!session_file_operations_handler_);
if (started_) {
LOG(ERROR) << __func__ << " called more than once for the current process.";
delegate_->CrashNetworkProcess(base::Location::Current());
// No need to run the callback since it just calls into the process we are
// asking the daemon process to crash.
callback.Reset();
return;
}
started_ = true;
client_jid_ = authenticated_jid;
// Hook up the associated interfaces.
network_channel_->GetRemoteAssociatedInterface(
&desktop_session_event_handler_);
network_channel_->GetRemoteAssociatedInterface(
&desktop_session_state_handler_);
// Create a desktop environment for the new session.
delegate_->desktop_environment_factory().Create(
weak_factory_.GetWeakPtr(), /* client_session_events= */ nullptr, options,
base::BindOnce(&DesktopSessionAgent::OnDesktopEnvironmentCreated,
weak_factory_.GetWeakPtr(), resolution,
std::move(callback)));
}
void DesktopSessionAgent::OnMouseCursor(webrtc::MouseCursor* cursor) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
std::unique_ptr<webrtc::MouseCursor> owned_cursor(cursor);
if (desktop_session_event_handler_) {
desktop_session_event_handler_->OnMouseCursorChanged(*owned_cursor);
}
video_capturers_.SetMouseCursor(*owned_cursor);
}
void DesktopSessionAgent::OnMouseCursorPosition(
const webrtc::DesktopVector& position) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
video_capturers_.SetMouseCursorPosition(position);
}
void DesktopSessionAgent::OnClipboardEvent(
const protocol::ClipboardEvent& event) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_) << "OnClipboardEvent called before agent was started.";
if (desktop_session_event_handler_) {
desktop_session_event_handler_->OnClipboardEvent(event);
}
}
void DesktopSessionAgent::ProcessAudioPacket(
std::unique_ptr<AudioPacket> packet) {
// AudioPackets are received on the audio_capture task runner but must be sent
// over IPC on the same task_runner the mojo remote was bound on.
if (!caller_task_runner_->BelongsToCurrentThread()) {
DCHECK(audio_capture_task_runner_->BelongsToCurrentThread());
caller_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&DesktopSessionAgent::ProcessAudioPacket,
this, std::move(packet)));
return;
}
if (desktop_session_event_handler_) {
desktop_session_event_handler_->OnAudioPacket(std::move(packet));
}
}
mojo::ScopedMessagePipeHandle DesktopSessionAgent::Initialize(
const base::WeakPtr<Delegate>& delegate) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
DCHECK(delegate);
DCHECK(!delegate_);
delegate_ = delegate;
mojo::MessagePipe pipe;
network_channel_ = IPC::ChannelProxy::Create(
pipe.handle0.release(), IPC::Channel::MODE_SERVER, this, io_task_runner_,
base::SingleThreadTaskRunner::GetCurrentDefault());
return std::move(pipe.handle1);
}
void DesktopSessionAgent::Stop() {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
delegate_.reset();
// Make sure the channel is closed.
network_channel_.reset();
if (started_) {
started_ = false;
// Ignore any further callbacks.
weak_factory_.InvalidateWeakPtrs();
client_jid_.clear();
desktop_session_event_handler_.reset();
desktop_session_state_handler_.reset();
desktop_session_control_.reset();
desktop_session_agent_.reset();
url_forwarder_configurator_.reset();
webauthn_state_change_notifier_.reset();
remote_input_filter_.reset();
session_file_operations_handler_.reset();
// Ensure that any pressed keys or buttons are released.
input_tracker_->ReleaseAll();
input_tracker_.reset();
desktop_environment_.reset();
action_executor_.reset();
input_injector_.reset();
screen_controls_.reset();
keyboard_layout_monitor_.reset();
// Stop the audio capturer.
audio_capture_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&DesktopSessionAgent::StopAudioCapturer, this));
// Stop the video capturers.
video_capturers_.Clear();
mouse_shape_pump_.reset();
}
}
void DesktopSessionAgent::CreateVideoCapturer(
std::int64_t desktop_display_id,
CreateVideoCapturerCallback callback) {
std::move(callback).Run(video_capturers_.CreateVideoCapturer(
desktop_display_id, desktop_environment_.get(), caller_task_runner_));
}
void DesktopSessionAgent::InjectClipboardEvent(
const protocol::ClipboardEvent& event) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
// InputStub implementations must verify events themselves, so we don't need
// verification here. This matches HostEventDispatcher.
input_injector_->InjectClipboardEvent(event);
}
void DesktopSessionAgent::InjectKeyEvent(const protocol::KeyEvent& event) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
// InputStub implementations must verify events themselves, so we need only
// basic verification here. This matches HostEventDispatcher.
if (!event.has_usb_keycode() || !event.has_pressed()) {
LOG(ERROR) << "Received invalid key event.";
return;
}
remote_input_filter_->InjectKeyEvent(event);
}
void DesktopSessionAgent::InjectTextEvent(const protocol::TextEvent& event) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
// InputStub implementations must verify events themselves, so we need only
// basic verification here. This matches HostEventDispatcher.
if (!event.has_text()) {
LOG(ERROR) << "Received invalid TextEvent.";
return;
}
remote_input_filter_->InjectTextEvent(event);
}
void DesktopSessionAgent::InjectMouseEvent(const protocol::MouseEvent& event) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
video_capturers_.SetComposeEnabled(event.has_delta_x() ||
event.has_delta_y());
// InputStub implementations must verify events themselves, so we don't need
// verification here. This matches HostEventDispatcher.
remote_input_filter_->InjectMouseEvent(event);
}
void DesktopSessionAgent::InjectTouchEvent(const protocol::TouchEvent& event) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
remote_input_filter_->InjectTouchEvent(event);
}
void DesktopSessionAgent::InjectSendAttentionSequence() {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
protocol::ActionRequest request;
request.set_action(protocol::ActionRequest::SEND_ATTENTION_SEQUENCE);
action_executor_->ExecuteAction(request);
}
void DesktopSessionAgent::LockWorkstation() {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
protocol::ActionRequest request;
request.set_action(protocol::ActionRequest::LOCK_WORKSTATION);
action_executor_->ExecuteAction(request);
}
void DesktopSessionAgent::OnKeyboardLayoutChange(
const protocol::KeyboardLayout& layout) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
if (desktop_session_event_handler_) {
desktop_session_event_handler_->OnKeyboardLayoutChanged(layout);
}
}
void DesktopSessionAgent::SetScreenResolution(
const ScreenResolution& resolution) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
if (screen_controls_) {
screen_controls_->SetScreenResolution(resolution, std::nullopt);
}
}
void DesktopSessionAgent::StartAudioCapturer() {
DCHECK(audio_capture_task_runner_->BelongsToCurrentThread());
if (audio_capturer_) {
audio_capturer_->Start(
base::BindRepeating(&DesktopSessionAgent::ProcessAudioPacket, this));
}
}
void DesktopSessionAgent::StopAudioCapturer() {
DCHECK(audio_capture_task_runner_->BelongsToCurrentThread());
audio_capturer_.reset();
}
void DesktopSessionAgent::SetUpUrlForwarder() {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
url_forwarder_configurator_->SetUpUrlForwarder(base::BindRepeating(
&DesktopSessionAgent::OnUrlForwarderSetUpStateChanged, this));
}
void DesktopSessionAgent::SignalWebAuthnExtension() {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
webauthn_state_change_notifier_->NotifyStateChange();
}
void DesktopSessionAgent::BeginFileRead(BeginFileReadCallback callback) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
session_file_operations_handler_->BeginFileRead(std::move(callback));
}
void DesktopSessionAgent::BeginFileWrite(const base::FilePath& file_path,
BeginFileWriteCallback callback) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
CHECK(started_);
session_file_operations_handler_->BeginFileWrite(file_path,
std::move(callback));
}
void DesktopSessionAgent::OnDesktopEnvironmentCreated(
const ScreenResolution& resolution,
StartCallback callback,
std::unique_ptr<DesktopEnvironment> desktop_environment) {
// TODO(rkjnsn): Handle null desktop_environment once multiprocess is
// is supported on platforms where remote desktop session creation can fail.
desktop_environment_ = std::move(desktop_environment);
// Create the session controller and set the initial screen resolution.
screen_controls_ = desktop_environment_->CreateScreenControls();
SetScreenResolution(resolution);
// Create the input injector.
input_injector_ = desktop_environment_->CreateInputInjector();
action_executor_ = desktop_environment_->CreateActionExecutor();
// Hook up the input filter.
input_tracker_ =
std::make_unique<protocol::InputEventTracker>(input_injector_.get());
remote_input_filter_ =
std::make_unique<RemoteInputFilter>(input_tracker_.get());
#if BUILDFLAG(IS_WIN)
// LocalInputMonitorWin filters out an echo of the injected input before it
// reaches |remote_input_filter_|.
remote_input_filter_->SetExpectLocalEcho(false);
#endif // BUILDFLAG(IS_WIN)
// Start the input injector.
std::unique_ptr<protocol::ClipboardStub> clipboard_stub(
new DesktopSessionClipboardStub(this));
input_injector_->Start(std::move(clipboard_stub));
// Start the audio capturer.
if (delegate_->desktop_environment_factory().SupportsAudioCapture()) {
audio_capturer_ = desktop_environment_->CreateAudioCapturer();
audio_capture_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&DesktopSessionAgent::StartAudioCapturer, this));
}
// Start the mouse cursor monitor.
mouse_shape_pump_ = std::make_unique<MouseShapePump>(
desktop_environment_->CreateMouseCursorMonitor(),
/*CursorShapeStub*/ nullptr);
mouse_shape_pump_->SetMouseCursorMonitorCallback(this);
// Unretained is sound because callback will never be invoked after
// |keyboard_layout_monitor_| is destroyed.
keyboard_layout_monitor_ = desktop_environment_->CreateKeyboardLayoutMonitor(
base::BindRepeating(&DesktopSessionAgent::OnKeyboardLayoutChange,
base::Unretained(this)));
keyboard_layout_monitor_->Start();
// Begin observing the desktop display(s) for changes. Note that some
// desktop environments may not provide a display info monitor.
auto* display_info_monitor = desktop_environment_->GetDisplayInfoMonitor();
if (display_info_monitor) {
display_info_monitor->Start();
}
// Set up the message handler for file transfers.
session_file_operations_handler_.emplace(
desktop_environment_->CreateFileOperations());
url_forwarder_configurator_ =
desktop_environment_->CreateUrlForwarderConfigurator();
// Check and report the initial URL forwarder setup state.
url_forwarder_configurator_->IsUrlForwarderSetUp(base::BindOnce(
&DesktopSessionAgent::OnCheckUrlForwarderSetUpResult, this));
webauthn_state_change_notifier_ =
desktop_environment_->CreateRemoteWebAuthnStateChangeNotifier();
std::move(callback).Run(
desktop_session_control_.BindNewEndpointAndPassRemote());
}
void DesktopSessionAgent::OnCheckUrlForwarderSetUpResult(bool is_set_up) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
if (!desktop_session_event_handler_) {
// Callback passed to UrlForwarderConfiguratorWin::IsUrlForwarderSetUp()
// may be called after the configurator is deleted and the agent is
// stopped, so we need to null-check |desktop_session_event_handler_|.
//
// TODO(yuweih): Scope callback of IsUrlForwarderSetUp() to the lifetime of
// the UrlForwarderConfiguratorWin instance.
return;
}
desktop_session_event_handler_->OnUrlForwarderStateChange(
is_set_up ? mojom::UrlForwarderState::kSetUp
: mojom::UrlForwarderState::kNotSetUp);
}
void DesktopSessionAgent::OnUrlForwarderSetUpStateChanged(
SetUpUrlForwarderResponse::State state) {
DCHECK(caller_task_runner_->BelongsToCurrentThread());
mojom::UrlForwarderState mojo_state;
switch (state) {
case SetUpUrlForwarderResponse::COMPLETE:
mojo_state = mojom::UrlForwarderState::kSetUp;
break;
case SetUpUrlForwarderResponse::FAILED:
mojo_state = mojom::UrlForwarderState::kFailed;
break;
case SetUpUrlForwarderResponse::USER_INTERVENTION_REQUIRED:
mojo_state = mojom::UrlForwarderState::kSetupPendingUserIntervention;
break;
default:
NOTREACHED() << "Unknown state: " << state;
}
desktop_session_event_handler_->OnUrlForwarderStateChange(mojo_state);
}
} // namespace remoting