diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 756b6128b3d..6dce3f81c98 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -141,28 +141,57 @@ script will not be able to install conan for you.) ## Running Orbit -Like mentioned before, the collector currently only works for Linux. So the following -only applies there: +Documentation about how to use Orbit's UI to connect to OrbitService can be found +[here](documentation/DOCUMENTATION.md#connect-orbit). -1. Start Orbit via +Alternatively you can use command line flags to automate the connection setup. This +is explained in the next sections. + +> **Note:** As mentioned before, the collector currently only works for Linux. So +> Local Profiling as described in the next section only works on Linux. + +### Local Profiling + +1. Start OrbitService +```bash +sudo ./build_default_relwithdebinfo/bin/OrbitService +``` +2. Start Orbit with the `--process_name` flag ```bash -./build_default_relwithdebinfo/bin/Orbit +./build_default_relwithdebinfo/bin/Orbit --process_name="" ``` -2. Start OrbitService by clicking the button `Start OrbitService`. To obtain scheduling - information, the collector needs to run as root, hence this will prompt you for a - password (via [pkexec](https://linux.die.net/man/1/pkexec)). Alternatively, you can - start OrbitService yourself: +3. Click `Start Session` and continue to the **Main Window** + + + +### Remote Profiling (SSH connection) + +To automate remote profiling, start Orbit with the following flags: ```bash -sudo ./build_default_relwithdebinfo/bin/OrbitService # Start the collector +./build_default_relwithdebinfo/bin/Orbit \ + --ssh_hostname="" \ + --ssh_port=1234 \ # default 22 + --ssh_user="" \ + --ssh_key_path="" \ + --ssh_known_host_path="" \ + --ssh_target_process="" \ + --collector_root_password="" ``` -The frontend currently has no graphical user interface to connect to a generic -remote instance. Only Stadia is supported as a special case. +This does the following: +1. Start Orbit on the local machine +2. Start a SSH connection to the remote machine +3. Upload OrbitService to the remote machine +4. Start OrbitService with sudo +5. Select the target process +6. Open the **Main Window** + +> **Note**: If you omit the `--ssh_target_process` flag, Orbit will start with the +**Connection Window** prefilled with the other flags. -If you needed remote profiling support you could tunnel the mentioned TCP port through -a SSH connection to an arbitrary Linux server. There are plans on adding generic -SSH tunneling support but we can't promise any timeframe for that. +> **Note**: If you omit the `--collector_root_password` flag, Orbit will assume OrbitService is +already running and skip steps 3 and 4. ## Consistent code styling diff --git a/documentation/DOCUMENTATION.md b/documentation/DOCUMENTATION.md index 49780cc496e..5509c811fba 100644 --- a/documentation/DOCUMENTATION.md +++ b/documentation/DOCUMENTATION.md @@ -14,38 +14,17 @@ have feature requests, consider creating an > **Note** Orbit is now mainly a **Linux** profiler. Windows support is > experimental and not all features are available on Windows. -## Prerequisites +## Start Orbit and OrbitService -You need to have `Orbit` and `OrbitService` -[built](../DEVELOPMENT.md#building-orbit) and -[running](../DEVELOPMENT.md#building-orbit). +When starting Orbit, the so-called **Connection Window** (screenshot below) is +shown. From here, you can start a local or remote profiling session. -> **Note** For reasonable feature support, `OrbitService` needs to run as -> **root** (or Administrator on Windows). +![Connection Window][orbit_connection_window] -> **Note** For remote profiling support you can tunnel the TCP port `44765` -> through an SSH connection to an arbitrary Linux server. - -## Profile your application - -In this section, we will go through a capture session showing you how to select -your target process for profiling, dynamically instrument functions, and record -a capture. Note that captures are saved automatically. - -### Connect Orbit - -This section shows you how to select your target process for profiling or load a -saved capture. - -> **Note** Your application must already be running on the target instance. - -1. Launch `Orbit` and `OrbitService` as described - [here](../DEVELOPMENT.md#building-orbit). - Orbit will open the so-called **Connection Window** (screenshot below). - - ![Choose Process in Orbit][orbit_processes] -2. Select **Local profiling**. +### Local Profiling +1. Select `Local Profiling`, click `Start OrbitService` and enter your root password + Once the connection gets established, the right pane shows the processes running on the target machine. Note that Orbit sorts processes by CPU usage and automatically selects the first process in the list. @@ -55,7 +34,9 @@ saved capture. > window. For ease of access, Orbit maintains a list of captures that you > saved recently. -3. To confirm the selected process and continue to the main window, click + ![Choose Process in Orbit][orbit_processes] + +2. To confirm the selected process and continue to the main window, click **Start Session** or Double-Click the process. In the **Main Window**, you can see the following: @@ -68,12 +49,42 @@ saved capture. ![Orbit's main window with processes and modules][orbit_main_window_startup] -4. To return to the startup window and select a different process, or connect - to a different instance, click **End Session**. +3. To return to the **Connection Window** and select a different process, or + connect to a different machine, click **End Session**. > **Note** If you loaded a capture from file, the right side of the menu bar displays the filename of the capture instead of connection details. +### Remote Profiling (SSH) + +Orbit supports setting up and using a secure communication channel between the +locally running UI and OrbitService running on a remote machine. To do that, +use the **Connection Window** and + +1. Select `Remote Profiling (SSH)` + +2. Fill all fields on the left side with the values you want to use for the SSH + connection. Orbit only supports file based authentication. + +3. Choose either `OrbitService started manually` or + `Start OrbitService with sudo`. + + Pick the former if you manually started OrbitService on the remote machine. + + If you want Orbit to upload and start OrbitService, choose the other option + and provide a sudo password. + +4. Click `Connect` and wait for Orbit to establish the ssh connection. + +5. Continue by selecting your target process and starting the session in the + same way as described [above](#local-profiling) + +## Profile your application + +In this section, we will go through a capture session. This includes recording +a capture with callstack sampling, dynamically instrumenting functions and +more. Note that captures are saved automatically. + ### Record a capture with callstack sampling This section shows you how to record a basic capture that samples callstacks in @@ -835,6 +846,7 @@ we highly recommend that you take a look at the references provided above. [linux-proc]: https://man7.org/linux/man-pages/man5/proc.5.html [linux-vmstat]: https://man7.org/linux/man-pages/man8/vmstat.8.html [linux-cgroup]: https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt +[orbit_connection_window]: orbit_connection_window.png [orbit_processes]: orbit_processes.png [orbit_main_window_default_capture]: orbit_main_window_default_capture.png [orbit_main_window_capture]: orbit_main_window_capture.png diff --git a/documentation/orbit_connection_window.png b/documentation/orbit_connection_window.png new file mode 100644 index 00000000000..d0dafb47ca2 Binary files /dev/null and b/documentation/orbit_connection_window.png differ diff --git a/documentation/orbit_processes.png b/documentation/orbit_processes.png index 276a66a0de1..3eda0c99bb4 100644 Binary files a/documentation/orbit_processes.png and b/documentation/orbit_processes.png differ diff --git a/src/ClientFlags/ClientFlags.cpp b/src/ClientFlags/ClientFlags.cpp index 1546254f4a4..851911d4f02 100644 --- a/src/ClientFlags/ClientFlags.cpp +++ b/src/ClientFlags/ClientFlags.cpp @@ -12,7 +12,9 @@ ABSL_FLAG(bool, devmode, false, "Enable developer mode in the client's UI"); -ABSL_FLAG(bool, nodeploy, false, "Disable automatic deployment of OrbitService"); +ABSL_FLAG(bool, signed_debian_package_deployment, false, + "Deploy OrbitService via the signed debian package deployment method. (Use this for " + "connecting to a Stadia instance)."); ABSL_FLAG(std::string, collector, "", "Full path of collector to be deployed"); diff --git a/src/ClientFlags/include/ClientFlags/ClientFlags.h b/src/ClientFlags/include/ClientFlags/ClientFlags.h index 14d6eed77dd..b0f9edc6ce3 100644 --- a/src/ClientFlags/include/ClientFlags/ClientFlags.h +++ b/src/ClientFlags/include/ClientFlags/ClientFlags.h @@ -13,7 +13,7 @@ ABSL_DECLARE_FLAG(bool, devmode); -ABSL_DECLARE_FLAG(bool, nodeploy); +ABSL_DECLARE_FLAG(bool, signed_debian_package_deployment); ABSL_DECLARE_FLAG(std::string, collector); diff --git a/src/SessionSetup/CMakeLists.txt b/src/SessionSetup/CMakeLists.txt index e8dea1d5960..ae230dacd8d 100644 --- a/src/SessionSetup/CMakeLists.txt +++ b/src/SessionSetup/CMakeLists.txt @@ -11,6 +11,7 @@ target_sources( SessionSetup PUBLIC include/SessionSetup/Connections.h include/SessionSetup/ConnectToLocalWidget.h + include/SessionSetup/ConnectToSshWidget.h include/SessionSetup/ConnectToTargetDialog.h include/SessionSetup/DeploymentConfigurations.h include/SessionSetup/DoubleClickableLabel.h @@ -34,6 +35,8 @@ target_sources( SessionSetup PRIVATE ConnectToLocalWidget.cpp ConnectToLocalWidget.ui + ConnectToSshWidget.cpp + ConnectToSshWidget.ui ConnectToTargetDialog.cpp ConnectToTargetDialog.ui DeploymentConfigurations.cpp diff --git a/src/SessionSetup/ConnectToSshWidget.cpp b/src/SessionSetup/ConnectToSshWidget.cpp new file mode 100644 index 00000000000..62bd04f90c4 --- /dev/null +++ b/src/SessionSetup/ConnectToSshWidget.cpp @@ -0,0 +1,244 @@ +// Copyright (c) 2022 The Orbit 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 "SessionSetup/ConnectToSshWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "ClientFlags/ClientFlags.h" +#include "GrpcProtos/process.pb.h" +#include "OrbitBase/Result.h" +#include "OrbitSsh/AddrAndPort.h" +#include "OrbitSsh/Credentials.h" +#include "OrbitSshQt/ScopedConnection.h" +#include "SessionSetup/Connections.h" +#include "SessionSetup/DeploymentConfigurations.h" +#include "SessionSetup/OverlayWidget.h" +#include "SessionSetup/ServiceDeployManager.h" +#include "SessionSetup/SessionSetupUtils.h" +#include "ui_ConnectToSshWidget.h" + +namespace orbit_session_setup { + +// The destructor needs to be defined here because it needs to see the type +// `Ui::ConnectToSshWidget`. The header file only contains a forward declaration. +ConnectToSshWidget::~ConnectToSshWidget() = default; + +ConnectToSshWidget::ConnectToSshWidget(QWidget* parent) + : QWidget(parent), ui_(std::make_unique()) { + ui_->setupUi(this); + ui_->overlay->raise(); + + ui_->signedDeploymentButton->setVisible(false); + + QObject::connect(ui_->radioButton, &QRadioButton::toggled, ui_->contentContainer, + &QWidget::setEnabled); + + QObject::connect(ui_->sudoButton, &QRadioButton::toggled, ui_->sudoPassword, + &QLineEdit::setEnabled); + + QObject::connect(ui_->connectButton, &QPushButton::clicked, this, + &ConnectToSshWidget::OnConnectClicked); + + QButtonGroup* button_group = new QButtonGroup(this); + button_group->addButton(ui_->noDeploymentButton); + button_group->addButton(ui_->signedDeploymentButton); + button_group->addButton(ui_->sudoButton); + + ui_->port->setValidator(new QIntValidator(1, 65535, this)); + ui_->port->setText(QString::number(absl::GetFlag(FLAGS_ssh_port))); + + if (!absl::GetFlag(FLAGS_ssh_hostname).empty()) { + ui_->hostname->setText(QString::fromStdString(absl::GetFlag(FLAGS_ssh_hostname))); + } + if (!absl::GetFlag(FLAGS_ssh_user).empty()) { + ui_->user->setText(QString::fromStdString(absl::GetFlag(FLAGS_ssh_user))); + } + if (!absl::GetFlag(FLAGS_ssh_known_host_path).empty()) { + ui_->knownHostsPath->setText(QString::fromStdString(absl::GetFlag(FLAGS_ssh_known_host_path))); + } + if (!absl::GetFlag(FLAGS_ssh_key_path).empty()) { + ui_->keyPath->setText(QString::fromStdString(absl::GetFlag(FLAGS_ssh_key_path))); + } +} + +std::optional ConnectToSshWidget::GetTargetAddrAndPort() const { + if (!ssh_connection_.has_value()) return std::nullopt; + + return ssh_connection_.value().GetAddrAndPort(); +} + +QRadioButton* ConnectToSshWidget::GetRadioButton() { return ui_->radioButton; } + +void ConnectToSshWidget::SetSshConnectionArtifacts( + const SshConnectionArtifacts& connection_artifacts) { + // Make a copy of the DeploymentConfiguration and ssh_connection_artifacts_. + deployment_configuration_ = *connection_artifacts.GetDeploymentConfiguration(); + ssh_connection_artifacts_.emplace(SshConnectionArtifacts{connection_artifacts.GetSshContext(), + connection_artifacts.GetGrpcPort(), + &deployment_configuration_}); + + if (std::holds_alternative(deployment_configuration_)) { + ui_->noDeploymentButton->setChecked(true); + } + if (std::holds_alternative(deployment_configuration_)) { + ui_->sudoButton->setChecked(true); + ui_->sudoPassword->setText(QString::fromStdString( + std::get(deployment_configuration_) + .root_password)); + } + if (std::holds_alternative(deployment_configuration_)) { + ui_->signedDeploymentButton->setVisible(true); + ui_->signedDeploymentButton->setChecked(true); + } +} + +void ConnectToSshWidget::SetConnection(std::optional connection_opt) { + ssh_connection_ = std::move(connection_opt); + if (!ssh_connection_.has_value()) { + emit Disconnected(); + ui_->overlay->setVisible(false); + return; + } + + ui_->overlay->setVisible(true); + ui_->overlay->SetSpinning(false); + ui_->overlay->SetStatusMessage( + QString("Connected to %1") + .arg(QString::fromStdString(ssh_connection_->GetAddrAndPort().GetHumanReadable()))); + ui_->overlay->SetButtonMessage("Disconnect"); + + QObject::connect(ui_->overlay, &OverlayWidget::Cancelled, this, + &ConnectToSshWidget::OnDisconnectClicked, Qt::UniqueConnection); + + QObject::connect( + ssh_connection_->GetServiceDeployManager(), &ServiceDeployManager::socketErrorOccurred, this, + [self = QPointer(this)](std::error_code error) { + if (self == nullptr) return; + + // Only show a warning message if the widget is enabled. + if (self->ui_->contentContainer->isEnabled()) { + QMessageBox::critical(self, "Connection Error", + QString("The connection to %1 failed with error message: %2") + .arg(QString::fromStdString( + self->ssh_connection_->GetAddrAndPort().GetHumanReadable())) + .arg(QString::fromStdString(error.message()))); + } + self->SetConnection(std::nullopt); + }); + + ssh_connection_->GetProcessManager()->SetProcessListUpdateListener( + [self = QPointer(this)]( + std::vector process_list) { + if (self == nullptr) return; + emit self->ProcessListUpdated( + QVector(process_list.begin(), process_list.end())); + }); + + emit Connected(); +} + +[[nodiscard]] SshConnection ConnectToSshWidget::TakeConnection() { + ORBIT_CHECK(ssh_connection_.has_value()); + return std::move(ssh_connection_).value(); +} + +void ConnectToSshWidget::OnConnectClicked() { + ErrorMessageOr connected_or_error = TryConnect(); + + if (connected_or_error.has_error()) { + QMessageBox::critical(this, "Error while connecting", + QString::fromStdString(connected_or_error.error().message())); + ui_->overlay->setVisible(false); + } +} + +ErrorMessageOr ConnectToSshWidget::GetCredentialsFromUi() { + bool port_ok = false; + int port = ui_->port->text().toInt(&port_ok); + + if (ui_->hostname->text().isEmpty() || ui_->user->text().isEmpty() || + ui_->knownHostsPath->text().isEmpty() || ui_->keyPath->text().isEmpty() || !port_ok) { + return ErrorMessage{ + R"(The fields "hostname", "port", "user", "path to known_host file" and "path to private key file" are mandatory)"}; + } + + orbit_ssh::AddrAndPort addr_and_port{ui_->hostname->text().toStdString(), port}; + return orbit_ssh::Credentials{addr_and_port, ui_->user->text().toStdString(), + ui_->knownHostsPath->text().toStdString(), + ui_->keyPath->text().toStdString()}; +} + +void ConnectToSshWidget::UpdateDeploymentConfigurationFromUi() { + if (ui_->sudoButton->isChecked()) { + const std::filesystem::path orbit_service_path = + !absl::GetFlag(FLAGS_collector).empty() + ? std::filesystem::path{absl::GetFlag(FLAGS_collector)} + : std::filesystem::path{QCoreApplication::applicationDirPath().toStdString()} / + "OrbitService"; + + deployment_configuration_ = BareExecutableAndRootPasswordDeployment{ + orbit_service_path, ui_->sudoPassword->text().toStdString()}; + } else if (ui_->noDeploymentButton->isChecked()) { + deployment_configuration_ = NoDeployment{}; + } +} + +ErrorMessageOr ConnectToSshWidget::TryConnect() { + OUTCOME_TRY(orbit_ssh::Credentials credentials, GetCredentialsFromUi()); + UpdateDeploymentConfigurationFromUi(); + + auto service_deploy_manager = std::make_unique( + ssh_connection_artifacts_->GetDeploymentConfiguration(), + ssh_connection_artifacts_->GetSshContext(), credentials, + ssh_connection_artifacts_->GetGrpcPort()); + + ui_->overlay->SetSpinning(true); + ui_->overlay->SetCancelable(true); + ui_->overlay->SetStatusMessage( + QString("Connecting to %1 ...") + .arg(QString::fromStdString(credentials.addr_and_port.GetHumanReadable()))); + ui_->overlay->SetButtonMessage("Cancel"); + ui_->overlay->setVisible(true); + + orbit_ssh_qt::ScopedConnection cancel_connection{ + QObject::connect(ui_->overlay, &OverlayWidget::Cancelled, service_deploy_manager.get(), + &ServiceDeployManager::Cancel)}; + + orbit_ssh_qt::ScopedConnection status_message_connection{ + QObject::connect(service_deploy_manager.get(), &ServiceDeployManager::statusMessage, + ui_->overlay, &OverlayWidget::SetStatusMessage)}; + + OUTCOME_TRY(const ServiceDeployManager::GrpcPort grpc_port, service_deploy_manager->Exec()); + + auto grpc_channel = CreateGrpcChannel(grpc_port.grpc_port); + + SetConnection(orbit_session_setup::SshConnection( + credentials.addr_and_port, std::move(service_deploy_manager), std::move(grpc_channel))); + + return outcome::success(); +} + +void ConnectToSshWidget::OnDisconnectClicked() { + ssh_connection_->GetServiceDeployManager()->Shutdown(); + SetConnection(std::nullopt); +} + +} // namespace orbit_session_setup \ No newline at end of file diff --git a/src/SessionSetup/ConnectToSshWidget.ui b/src/SessionSetup/ConnectToSshWidget.ui new file mode 100644 index 00000000000..2df4baf2135 --- /dev/null +++ b/src/SessionSetup/ConnectToSshWidget.ui @@ -0,0 +1,235 @@ + + + ConnectToSshWidget + + + + 0 + 0 + 528 + 351 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Remote profiling (SSH) + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 17 + 204 + + + + + + + + 6 + + + + + Hostname (IP address) + + + + + + + Port (default 22) + + + + + + + User + + + + + + + Path to known_hosts file + + + + + + + Path to private key file + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 6 + + + 20 + + + + + Signed Deployment + + + + + + + + 0 + 27 + + + + OrbitService started manually + + + + + + + + 0 + 27 + + + + Start OrbitService with sudo + + + + + + + QLineEdit::Password + + + sudo password + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Connect + + + + + + + + + + 0 + 0 + 100 + 30 + + + + false + + + + + + + + + + + + orbit_session_setup::OverlayWidget + QWidget +
SessionSetup/OverlayWidget.h
+ 1 +
+
+ + +
diff --git a/src/SessionSetup/DeploymentConfigurations.cpp b/src/SessionSetup/DeploymentConfigurations.cpp index 55415c1c202..033e9c9016f 100644 --- a/src/SessionSetup/DeploymentConfigurations.cpp +++ b/src/SessionSetup/DeploymentConfigurations.cpp @@ -73,33 +73,32 @@ SignedDebianPackageDeployment::SignedDebianPackageDeployment() { } DeploymentConfiguration FigureOutDeploymentConfiguration() { - if (absl::GetFlag(FLAGS_nodeploy)) { - return NoDeployment{}; + if (absl::GetFlag(FLAGS_signed_debian_package_deployment)) { + return SignedDebianPackageDeployment{}; } constexpr const char* kEnvPackagePath = "ORBIT_COLLECTOR_PACKAGE_PATH"; constexpr const char* kEnvSignaturePath = "ORBIT_COLLECTOR_SIGNATURE_PATH"; - constexpr const char* kEnvNoDeployment = "ORBIT_COLLECTOR_NO_DEPLOYMENT"; QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); std::optional collector_path = GetCollectorPath(env); std::optional collector_password = GetCollectorRootPassword(env); - if (collector_path.has_value() && collector_password.has_value()) { - return orbit_session_setup::BareExecutableAndRootPasswordDeployment{collector_path.value(), - collector_password.value()}; + if (collector_path.has_value() || collector_password.has_value()) { + const std::filesystem::path orbit_service_default_location = + std::filesystem::path{QCoreApplication::applicationDirPath().toStdString()} / + "OrbitService"; + return BareExecutableAndRootPasswordDeployment{ + collector_path.value_or(orbit_service_default_location.string()), + collector_password.value_or("")}; } if (env.contains(kEnvPackagePath) && env.contains(kEnvSignaturePath)) { - return orbit_session_setup::SignedDebianPackageDeployment{ - env.value(kEnvPackagePath).toStdString(), env.value(kEnvSignaturePath).toStdString()}; + return SignedDebianPackageDeployment{env.value(kEnvPackagePath).toStdString(), + env.value(kEnvSignaturePath).toStdString()}; } - if (env.contains(kEnvNoDeployment)) { - return NoDeployment{}; - } - - return orbit_session_setup::SignedDebianPackageDeployment{}; + return NoDeployment{}; } } // namespace orbit_session_setup diff --git a/src/SessionSetup/LoadCaptureWidget.ui b/src/SessionSetup/LoadCaptureWidget.ui index 05de834dde5..80249587bfc 100644 --- a/src/SessionSetup/LoadCaptureWidget.ui +++ b/src/SessionSetup/LoadCaptureWidget.ui @@ -117,6 +117,18 @@ FilterCaptureFiles + + + QLineEdit { + background-image: url(:/actions/search_small_offset); + background-position: left center; + background-repeat: no-repeat; + height: 22px; + padding-left: 20px; + padding-bottom: 1px; + } + + Filter capture files diff --git a/src/SessionSetup/ProcessListWidget.ui b/src/SessionSetup/ProcessListWidget.ui index fe6b8528dac..eabd65fc224 100644 --- a/src/SessionSetup/ProcessListWidget.ui +++ b/src/SessionSetup/ProcessListWidget.ui @@ -41,6 +41,18 @@ FilterProcesses + + + QLineEdit { + background-image: url(:/actions/search_small_offset); + background-position: left center; + background-repeat: no-repeat; + height: 22px; + padding-left: 20px; + padding-bottom: 1px; + } + + Filter diff --git a/src/SessionSetup/SessionSetupDialog.cpp b/src/SessionSetup/SessionSetupDialog.cpp index 901802abbc6..cf6b007826a 100644 --- a/src/SessionSetup/SessionSetupDialog.cpp +++ b/src/SessionSetup/SessionSetupDialog.cpp @@ -26,8 +26,11 @@ #include "GrpcProtos/process.pb.h" #include "OrbitBase/Logging.h" #include "OrbitBase/Result.h" +#include "OrbitSsh/AddrAndPort.h" #include "SessionSetup/ConnectToLocalWidget.h" +#include "SessionSetup/ConnectToSshWidget.h" #include "SessionSetup/Connections.h" +#include "SessionSetup/DeploymentConfigurations.h" #include "SessionSetup/LoadCaptureWidget.h" #include "SessionSetup/OrbitServiceInstance.h" #include "SessionSetup/ProcessListWidget.h" @@ -53,7 +56,13 @@ SessionSetupDialog::SessionSetupDialog(SshConnectionArtifacts* ssh_connection_ar state_local_connecting_(&state_local_), state_local_connected_(&state_local_), state_local_no_process_selected_(&state_local_connected_), - state_local_process_selected_(&state_local_connected_) { + state_local_process_selected_(&state_local_connected_), + state_ssh_(&state_machine_), + state_ssh_history_(&state_ssh_), + state_ssh_connecting_(&state_ssh_), + state_ssh_connected_(&state_ssh_), + state_ssh_no_process_selected_(&state_ssh_connected_), + state_ssh_process_selected_(&state_ssh_connected_) { ORBIT_CHECK(ssh_connection_artifacts != nullptr); ui_->setupUi(this); @@ -63,14 +72,15 @@ SessionSetupDialog::SessionSetupDialog(SshConnectionArtifacts* ssh_connection_ar state_machine_.setGlobalRestorePolicy(QStateMachine::RestoreProperties); SetupFileStates(); SetupLocalStates(); + SetupSshStates(); QObject::connect(ui_->confirmButton, &QPushButton::clicked, this, &QDialog::accept); QObject::connect(ui_->loadCaptureWidget, &LoadCaptureWidget::FileSelected, this, [this](std::filesystem::path path) { selected_file_path_ = std::move(path); }); QObject::connect(ui_->loadCaptureWidget, &LoadCaptureWidget::SelectionConfirmed, this, &QDialog::accept); - QObject::connect(ui_->processListWidget, &ProcessListWidget::ProcessSelected, ui_->targetLabel, - qOverload(&TargetLabel::ChangeToLocalTarget)); + QObject::connect(ui_->processListWidget, &ProcessListWidget::ProcessSelected, this, + &SessionSetupDialog::UpdateTargetLabelWithProcess); QObject::connect(ui_->processListWidget, &ProcessListWidget::ProcessSelectionCleared, ui_->targetLabel, &TargetLabel::Clear); QObject::connect(ui_->processListWidget, &ProcessListWidget::ProcessConfirmed, this, @@ -78,17 +88,32 @@ SessionSetupDialog::SessionSetupDialog(SshConnectionArtifacts* ssh_connection_ar button_group_.addButton(ui_->localProfilingWidget->GetRadioButton()); button_group_.addButton(ui_->loadCaptureWidget->GetRadioButton()); + button_group_.addButton(ui_->sshWidget->GetRadioButton()); + + ui_->sshWidget->SetSshConnectionArtifacts(*ssh_connection_artifacts); if (target_configuration_opt.has_value()) { - TargetConfiguration config = std::move(target_configuration_opt.value()); - target_configuration_opt = std::nullopt; - std::visit([this](auto&& target) { SetTargetAndStateMachineInitialState(std::move(target)); }, - config); - } else { - state_machine_.setInitialState(&state_local_); - ui_->localProfilingWidget->GetRadioButton()->setChecked(true); - ui_->processListWidget->SetProcessNameToSelect(absl::GetFlag(FLAGS_process_name)); + TargetConfiguration config = std::move(target_configuration_opt).value(); + std::visit([this](auto target) { SetTargetAndStateMachineInitialState(std::move(target)); }, + std::move(config)); + return; + } + + ui_->processListWidget->SetProcessNameToSelect(absl::GetFlag(FLAGS_process_name)); + + // If ssh_connection_artifacts contains a deployment method that is different that the default + // (NoDeployment), it means they specified the deployment method via flags or environment + // variables. In this case the "Connect to SSH" widget is pre-selected. + if (!std::holds_alternative( + *ssh_connection_artifacts->GetDeploymentConfiguration())) { + state_machine_.setInitialState(&state_ssh_); + ui_->sshWidget->GetRadioButton()->setChecked(true); + return; } + + // Otherwise "Local Profiling" is pre-selected. + state_machine_.setInitialState(&state_local_); + ui_->localProfilingWidget->GetRadioButton()->setChecked(true); } SessionSetupDialog::~SessionSetupDialog() = default; @@ -107,12 +132,61 @@ std::optional SessionSetupDialog::Exec() { std::make_unique(process_info_opt.value())); } else if (state_machine_.configuration().contains(&state_file_)) { return FileTarget(selected_file_path_); + } else if (state_machine_.configuration().contains(&state_ssh_)) { + std::optional process_info_opt = ui_->processListWidget->GetSelectedProcess(); + return SshTarget(ui_->sshWidget->TakeConnection(), + std::make_unique(process_info_opt.value())); } else { ORBIT_UNREACHABLE(); return std::nullopt; } } +void SessionSetupDialog::SetupSshStates() { + // Setup initial and default + state_ssh_.setInitialState(&state_ssh_connecting_); + state_ssh_history_.setDefaultState(&state_ssh_connecting_); + state_ssh_connected_.setInitialState(&state_ssh_no_process_selected_); + + // PROPERTIES + // STATE state_ssh_ + state_ssh_.assignProperty(ui_->confirmButton, "enabled", false); + state_ssh_.assignProperty(ui_->confirmButton, "toolTip", + "Please establish a SSH connection and select a process."); + + // STATE state_ssh_process_selected_ + state_ssh_process_selected_.assignProperty(ui_->confirmButton, "enabled", true); + state_ssh_process_selected_.assignProperty(ui_->confirmButton, "toolTip", ""); + + // TRANSITIONS (and entered/exit events) + // STATE state_ssh_ + state_ssh_.addTransition(ui_->loadCaptureWidget->GetRadioButton(), &QRadioButton::clicked, + &state_file_history_); + state_ssh_.addTransition(ui_->localProfilingWidget->GetRadioButton(), &QRadioButton::clicked, + &state_local_history_); + + // STATE state_ssh_connecting_ + state_ssh_connecting_.addTransition(ui_->sshWidget, &ConnectToSshWidget::Connected, + &state_ssh_connected_); + + // STATE state_ssh_connected_ + state_ssh_connected_.addTransition(ui_->sshWidget, &ConnectToSshWidget::Disconnected, + &state_ssh_connecting_); + QObject::connect(&state_ssh_connected_, &QState::entered, this, + &SessionSetupDialog::ConnectSshAndProcessWidget); + QObject::connect(&state_ssh_connected_, &QState::exited, this, + &SessionSetupDialog::DisconnectSshAndProcessWidget); + + // STATE state_ssh_no_process_selected_ + state_ssh_no_process_selected_.addTransition( + ui_->processListWidget, &ProcessListWidget::ProcessSelected, &state_ssh_process_selected_); + + // STATE state_ssh_process_selected_ + state_ssh_process_selected_.addTransition(ui_->processListWidget, + &ProcessListWidget::ProcessSelectionCleared, + &state_ssh_no_process_selected_); +} + void SessionSetupDialog::SetupLocalStates() { // Setup initial and default state_local_.setInitialState(&state_local_connecting_); @@ -134,6 +208,8 @@ void SessionSetupDialog::SetupLocalStates() { // STATE state_local_ state_local_.addTransition(ui_->loadCaptureWidget->GetRadioButton(), &QRadioButton::clicked, &state_file_history_); + state_local_.addTransition(ui_->sshWidget->GetRadioButton(), &QRadioButton::clicked, + &state_ssh_history_); // STATE state_local_connecting_ state_local_connecting_.addTransition(ui_->localProfilingWidget, &ConnectToLocalWidget::Connected, @@ -176,6 +252,8 @@ void SessionSetupDialog::SetupFileStates() { // STATE state_file_ state_file_.addTransition(ui_->localProfilingWidget->GetRadioButton(), &QRadioButton::clicked, &state_local_history_); + state_file_.addTransition(ui_->sshWidget->GetRadioButton(), &QRadioButton::clicked, + &state_ssh_history_); state_file_.addTransition(ui_->loadCaptureWidget, &LoadCaptureWidget::FileSelected, &state_file_selected_); @@ -196,8 +274,25 @@ void SessionSetupDialog::DisconnectLocalAndProcessWidget() { ui_->processListWidget, &ProcessListWidget::UpdateList); } -void SessionSetupDialog::SetTargetAndStateMachineInitialState(SshTarget /*target*/) { - ORBIT_FATAL("not implemented"); +void SessionSetupDialog::ConnectSshAndProcessWidget() { + QObject::connect(ui_->sshWidget, &ConnectToSshWidget::ProcessListUpdated, ui_->processListWidget, + &ProcessListWidget::UpdateList); +} + +void SessionSetupDialog::DisconnectSshAndProcessWidget() { + ui_->processListWidget->Clear(); + QObject::disconnect(ui_->sshWidget, &ConnectToSshWidget::ProcessListUpdated, + ui_->processListWidget, &ProcessListWidget::UpdateList); +} + +void SessionSetupDialog::SetTargetAndStateMachineInitialState(SshTarget target) { + ui_->processListWidget->SetProcessNameToSelect(target.process_->name()); + ui_->sshWidget->SetConnection(std::move(target.connection_)); + ui_->sshWidget->GetRadioButton()->setChecked(true); + + state_ssh_.setInitialState(&state_ssh_connected_); + state_ssh_history_.setDefaultState(&state_ssh_connected_); + state_machine_.setInitialState(&state_ssh_); } void SessionSetupDialog::SetTargetAndStateMachineInitialState(LocalTarget target) { @@ -218,4 +313,16 @@ void SessionSetupDialog::SetTargetAndStateMachineInitialState(FileTarget target) state_machine_.setInitialState(&state_file_); } +void SessionSetupDialog::UpdateTargetLabelWithProcess( + const orbit_grpc_protos::ProcessInfo& process_info) { + if (state_machine_.configuration().contains(&state_local_)) { + ui_->targetLabel->ChangeToLocalTarget(process_info); + } else if (state_machine_.configuration().contains(&state_ssh_)) { + std::optional addr_and_port = ui_->sshWidget->GetTargetAddrAndPort(); + ui_->targetLabel->ChangeToSshTarget(process_info, addr_and_port.value().GetHumanReadable()); + } else { + ORBIT_UNREACHABLE(); + } +} + } // namespace orbit_session_setup diff --git a/src/SessionSetup/SessionSetupDialog.ui b/src/SessionSetup/SessionSetupDialog.ui index 171de285fc4..e4597536cae 100644 --- a/src/SessionSetup/SessionSetupDialog.ui +++ b/src/SessionSetup/SessionSetupDialog.ui @@ -6,7 +6,7 @@ 0 0 - 1289 + 1084 721 @@ -16,16 +16,6 @@ Orbit Profiler - - QLineEdit { - background-image: url(:/actions/search_small_offset); - background-position: left center; - background-repeat: no-repeat; - height: 22px; - padding-left: 20px; - padding-bottom: 1px; -} - 0 @@ -77,13 +67,16 @@ 0 - + 0 + + + @@ -153,6 +146,11 @@ QWidget
SessionSetup/ConnectToLocalWidget.h
+ + orbit_session_setup::ConnectToSshWidget + QWidget +
SessionSetup/ConnectToSshWidget.h
+
orbit_session_setup::ProcessListWidget QWidget diff --git a/src/SessionSetup/TargetLabel.cpp b/src/SessionSetup/TargetLabel.cpp index 0d76c2bd32d..3eaab5ea890 100644 --- a/src/SessionSetup/TargetLabel.cpp +++ b/src/SessionSetup/TargetLabel.cpp @@ -126,12 +126,23 @@ void TargetLabel::ChangeToSshTarget(const SshTarget& ssh_target) { ssh_target.GetConnection()->GetAddrAndPort().GetHumanReadable()); } +void TargetLabel::ChangeToSshTarget(const orbit_grpc_protos::ProcessInfo& process_info, + std::string_view ssh_target_id) { + return ChangeToSshTarget(process_info.name(), process_info.full_path(), process_info.cpu_usage(), + ssh_target_id); +} + void TargetLabel::ChangeToSshTarget(const orbit_client_data::ProcessData& process, std::string_view ssh_target_id) { + return ChangeToSshTarget(process.name(), process.full_path(), process.cpu_usage(), ssh_target_id); +} + +void TargetLabel::ChangeToSshTarget(std::string_view process_name, std::string_view process_path, + double cpu_usage, std::string_view ssh_target_id) { Clear(); - process_ = QString::fromStdString(process.name()); + process_ = QString::fromUtf8(process_name.data(), process_name.size()); machine_ = QString::fromUtf8(ssh_target_id.data(), ssh_target_id.size()); - SetProcessCpuUsageInPercent(process.cpu_usage()); + SetProcessCpuUsageInPercent(cpu_usage); ui_->targetLabel->setVisible(true); ui_->fileLabel->setVisible(false); @@ -139,7 +150,7 @@ void TargetLabel::ChangeToSshTarget(const orbit_client_data::ProcessData& proces QString{"Connection active.

" "Machine: %1
" "Process: %2 (%3)"} - .arg(machine_, process_, QString::fromStdString(process.full_path()))); + .arg(machine_, process_, QString::fromUtf8(process_path.data(), process_path.size()))); setAccessibleName("Ssh target"); } diff --git a/src/SessionSetup/include/SessionSetup/ConnectToSshWidget.h b/src/SessionSetup/include/SessionSetup/ConnectToSshWidget.h new file mode 100644 index 00000000000..974e7f07903 --- /dev/null +++ b/src/SessionSetup/include/SessionSetup/ConnectToSshWidget.h @@ -0,0 +1,77 @@ +// Copyright (c) 2022 The Orbit 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 SESSION_SETUP_CONNECT_TO_SSH_WIDGET_H_ +#define SESSION_SETUP_CONNECT_TO_SSH_WIDGET_H_ + +#include +#include +#include + +#include "GrpcProtos/process.pb.h" +#include "OrbitBase/Result.h" +#include "OrbitSsh/AddrAndPort.h" +#include "OrbitSsh/Credentials.h" +#include "SessionSetup/Connections.h" +#include "SessionSetup/DeploymentConfigurations.h" + +namespace Ui { +class ConnectToSshWidget; +} + +namespace orbit_session_setup { + +// ConnectToSshWidget provides a UI and functionality to connect to a remote machine via SSH and +// deploy OrbitService. +// +// * For the SSH connection, the user needs to provide 5 fields: Hostname, Port, User, Path to +// known_hosts file and Path to private key file. This can be done in the UI directly, or the UI +// fields can be prefilled via cmd line flags (See `--ssh_...` in ClientFlags.h). +// +// * For the OrbitService deployment the widget provides 2 UI options: "OrbitService started +// manually" (NoDeployment - default) or "Start OrbitService with sudo" +// (BareExecutableAndRootPasswordDeployment). There is also the hidden "Signed Deployment" option, +// which is only visible when the user started Orbit with `--signed_debian_package_deployment`. The +// UI will also be prefilled when the user chooses their deployment method via flags or environment +// variables (see `FigureOutDeploymentConfiguration` in `DeploymentConfigurations.h`). +// +// * After construction of this class, the method `SetSshConnectionArtifacts` needs to be called. +// +// * To reuse an existing `SshConnection` call `SetConnection`. +class ConnectToSshWidget : public QWidget { + Q_OBJECT + + public: + explicit ConnectToSshWidget(QWidget* parent = nullptr); + ~ConnectToSshWidget() override; + + void SetSshConnectionArtifacts(const SshConnectionArtifacts& connection_artifacts); + void SetConnection(std::optional connection_opt); + [[nodiscard]] SshConnection TakeConnection(); + [[nodiscard]] std::optional GetTargetAddrAndPort() const; + + [[nodiscard]] QRadioButton* GetRadioButton(); + + signals: + void Connected(); + void Disconnected(); + void ProcessListUpdated(QVector process_list); + + private: + std::unique_ptr ui_; + std::optional ssh_connection_; + DeploymentConfiguration deployment_configuration_; + std::optional ssh_connection_artifacts_; + + void OnConnectClicked(); + void OnDisconnectClicked(); + ErrorMessageOr GetCredentialsFromUi(); + void UpdateDeploymentConfigurationFromUi(); + + ErrorMessageOr TryConnect(); +}; + +} // namespace orbit_session_setup + +#endif // SESSION_SETUP_CONNECT_TO_SSH_WIDGET_H_ \ No newline at end of file diff --git a/src/SessionSetup/include/SessionSetup/SessionSetupDialog.h b/src/SessionSetup/include/SessionSetup/SessionSetupDialog.h index 7e55fc84101..178d5907a68 100644 --- a/src/SessionSetup/include/SessionSetup/SessionSetupDialog.h +++ b/src/SessionSetup/include/SessionSetup/SessionSetupDialog.h @@ -46,6 +46,8 @@ class SessionSetupDialog : public QDialog { private slots: void ConnectLocalAndProcessWidget(); void DisconnectLocalAndProcessWidget(); + void ConnectSshAndProcessWidget(); + void DisconnectSshAndProcessWidget(); signals: void ProcessSelected(); @@ -73,11 +75,20 @@ class SessionSetupDialog : public QDialog { QState state_local_no_process_selected_; QState state_local_process_selected_; + QState state_ssh_; + QHistoryState state_ssh_history_; + QState state_ssh_connecting_; + QState state_ssh_connected_; + QState state_ssh_no_process_selected_; + QState state_ssh_process_selected_; + void SetupFileStates(); void SetupLocalStates(); + void SetupSshStates(); void SetTargetAndStateMachineInitialState(SshTarget target); void SetTargetAndStateMachineInitialState(LocalTarget target); void SetTargetAndStateMachineInitialState(FileTarget target); + void UpdateTargetLabelWithProcess(const orbit_grpc_protos::ProcessInfo& process_info); }; } // namespace orbit_session_setup diff --git a/src/SessionSetup/include/SessionSetup/TargetLabel.h b/src/SessionSetup/include/SessionSetup/TargetLabel.h index 7f41607e7a3..48d8c38616a 100644 --- a/src/SessionSetup/include/SessionSetup/TargetLabel.h +++ b/src/SessionSetup/include/SessionSetup/TargetLabel.h @@ -37,6 +37,10 @@ class TargetLabel : public QWidget { void ChangeToSshTarget(const SshTarget& ssh_target); void ChangeToSshTarget(const orbit_client_data::ProcessData& process, std::string_view ssh_target_id); + void ChangeToSshTarget(const orbit_grpc_protos::ProcessInfo& process_info, + std::string_view ssh_target_id); + void ChangeToSshTarget(std::string_view process_name, std::string_view process_path, + double cpu_usage, std::string_view ssh_target_id); void ChangeToLocalTarget(const LocalTarget& local_target); void ChangeToLocalTarget(const orbit_client_data::ProcessData& process); void ChangeToLocalTarget(const orbit_grpc_protos::ProcessInfo& process_info);