Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

136 changes: 136 additions & 0 deletions cpp/examples/wda.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Jackson Coxson

#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>

#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
#include <idevice++/usbmuxd.hpp>
#include <idevice++/wda.hpp>
#include <idevice++/wda_bridge.hpp>

using namespace IdeviceFFI;

static void print_usage(const char* prog) {
std::cerr << "Usage:\n"
<< " " << prog << " status\n"
<< " " << prog << " screenshot <output.png> [bundle_id]\n"
<< " " << prog << " bridge\n";
}

static Provider build_provider(const std::string& label) {
auto mux = UsbmuxdConnection::default_new(0).expect("failed to connect to usbmuxd");
auto devices = mux.get_devices().expect("failed to list devices");
if (devices.empty()) {
std::cerr << "no devices connected\n";
std::exit(1);
}
auto& dev = devices[0];

auto udid = dev.get_udid();
if (udid.is_none()) {
std::cerr << "device has no UDID\n";
std::exit(1);
}
auto mux_id = dev.get_id();
if (mux_id.is_none()) {
std::cerr << "device has no mux id\n";
std::exit(1);
}

auto addr = UsbmuxdAddr::default_new();
const uint32_t tag = 0;
return Provider::usbmuxd_new(std::move(addr), tag, udid.unwrap(), mux_id.unwrap(), label)
.expect("failed to create provider");
}

int main(int argc, char** argv) {
idevice_init_logger(Debug, Disabled, NULL);

// Usage:
// wda status
// wda screenshot <output.png> [bundle_id]
// wda bridge
if (argc < 2) {
print_usage(argv[0]);
return 2;
}

const std::string command = argv[1];

if (command == "status") {
if (argc != 2) {
print_usage(argv[0]);
return 2;
}

auto wda = Wda::create(build_provider("wda-client")).expect("failed to create Wda client");

auto status = wda.status().expect("failed to fetch /status from WDA");
std::cout << status << "\n";
return 0;
}

if (command == "screenshot") {
if (argc < 3 || argc > 4) {
print_usage(argv[0]);
return 2;
}

const std::string out_path = argv[2];
Option<std::string> bundle_id;
if (argc == 4) {
bundle_id = Some(std::string(argv[3]));
}

auto wda = Wda::create(build_provider("wda-client")).expect("failed to create Wda client");

auto session_id = wda.start_session(bundle_id).expect("failed to start WDA session");
std::cout << "Session: " << session_id << "\n";

auto buf = wda.screenshot(None).expect("failed to capture screenshot");

wda.delete_session(session_id).expect("failed to delete WDA session");

std::ofstream out(out_path, std::ios::binary);
if (!out.is_open()) {
std::cerr << "failed to open output file: " << out_path << "\n";
return 1;
}
out.write(reinterpret_cast<const char*>(buf.data()),
static_cast<std::streamsize>(buf.size()));
out.close();

std::cout << "Screenshot saved to " << out_path << " (" << buf.size() << " bytes)\n";
return 0;
}

if (command == "bridge") {
if (argc != 2) {
print_usage(argv[0]);
return 2;
}

auto bridge = WdaBridge::start(build_provider("wda-bridge"))
.expect("failed to start WDA bridge");
auto endpoints = bridge.endpoints().expect("failed to read bridge endpoints");

if (endpoints.udid.is_some()) {
std::cout << "udid: " << endpoints.udid.unwrap() << "\n";
}
std::cout << "wda_url: " << endpoints.wda_url << "\n"
<< "mjpeg_url: " << endpoints.mjpeg_url << "\n"
<< "device_http: " << endpoints.device_http << "\n"
<< "device_mjpeg:" << endpoints.device_mjpeg << "\n";

std::cout << "\nForwarding active. Press Enter to stop.\n";
std::string line;
std::getline(std::cin, line);
return 0;
}

print_usage(argv[0]);
return 2;
}
128 changes: 128 additions & 0 deletions cpp/include/idevice++/wda.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Jackson Coxson

#pragma once
#include <cstdint>
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/option.hpp>
#include <idevice++/provider.hpp>
#include <idevice++/result.hpp>
#include <memory>
#include <string>
#include <utility>
#include <vector>

namespace IdeviceFFI {

using WdaClientPtr =
std::unique_ptr<WdaClientHandle, FnDeleter<WdaClientHandle, wda_client_free>>;

/// Minimal WebDriverAgent client over a single device's direct connections.
///
/// `Wda::create` consumes the `Provider` argument; subsequent calls reuse the
/// owned provider to open per-request sockets, matching the underlying Rust
/// `WdaClient` semantics.
class Wda {
public:
static Result<Wda, FfiError> create(Provider&& provider);

// Builder-style configuration
Result<void, FfiError> set_ports(uint16_t http, uint16_t mjpeg);
Result<void, FfiError> set_timeout_ms(uint64_t ms);
Result<std::pair<uint16_t, uint16_t>, FfiError> get_ports() const;
Option<std::string> session_id() const;

// Status / session
Result<std::string, FfiError> status();
Result<std::string, FfiError> wait_until_ready(uint64_t timeout_ms);
Result<std::string, FfiError> start_session(Option<std::string> bundle_id);
Result<void, FfiError> delete_session(const std::string& session_id);

// Element finding & state
Result<std::string, FfiError> find_element(const std::string& using_,
const std::string& value,
Option<std::string> session_id);
Result<std::vector<std::string>, FfiError> find_elements(const std::string& using_,
const std::string& value,
Option<std::string> session_id);
Result<std::string, FfiError> element_attribute(const std::string& element_id,
const std::string& name,
Option<std::string> session_id);
Result<std::string, FfiError> element_text(const std::string& element_id,
Option<std::string> session_id);
Result<std::string, FfiError> element_rect(const std::string& element_id,
Option<std::string> session_id);
Result<bool, FfiError> element_displayed(const std::string& element_id,
Option<std::string> session_id);
Result<bool, FfiError> element_enabled(const std::string& element_id,
Option<std::string> session_id);
Result<bool, FfiError> element_selected(const std::string& element_id,
Option<std::string> session_id);

// Interaction
Result<void, FfiError> click(const std::string& element_id, Option<std::string> session_id);
Result<void, FfiError> send_keys(const std::string& text, Option<std::string> session_id);
Result<void, FfiError> press_button(const std::string& name, Option<std::string> session_id);
Result<void, FfiError> unlock(Option<std::string> session_id);
Result<void, FfiError> swipe(int64_t start_x,
int64_t start_y,
int64_t end_x,
int64_t end_y,
double duration,
Option<std::string> session_id);
Result<void, FfiError> tap(Option<double> x,
Option<double> y,
Option<std::string> element_id,
Option<std::string> session_id);
Result<void, FfiError> double_tap(Option<double> x,
Option<double> y,
Option<std::string> element_id,
Option<std::string> session_id);
Result<void, FfiError> touch_and_hold(double duration,
Option<double> x,
Option<double> y,
Option<std::string> element_id,
Option<std::string> session_id);
Result<void, FfiError> scroll(Option<std::string> direction,
Option<std::string> name,
Option<std::string> predicate_string,
Option<bool> to_visible,
Option<std::string> element_id,
Option<std::string> session_id);

// Output / app
Result<std::string, FfiError> source(Option<std::string> session_id);
Result<std::vector<uint8_t>, FfiError> screenshot(Option<std::string> session_id);
Result<std::string, FfiError> window_size(Option<std::string> session_id);
Result<std::string, FfiError> viewport_rect(Option<std::string> session_id);
Result<std::string, FfiError> orientation(Option<std::string> session_id);
Result<std::string, FfiError> launch_app(const std::string& bundle_id,
const std::vector<std::string>& arguments,
Option<std::string> environment_json,
Option<std::string> session_id);
Result<std::string, FfiError> activate_app(const std::string& bundle_id,
Option<std::string> session_id);
Result<bool, FfiError> terminate_app(const std::string& bundle_id,
Option<std::string> session_id);
Result<int64_t, FfiError> query_app_state(const std::string& bundle_id,
Option<std::string> session_id);
Result<std::string, FfiError> background_app(Option<double> seconds,
Option<std::string> session_id);
Result<bool, FfiError> is_locked(Option<std::string> session_id);

// RAII / moves
~Wda() noexcept = default;
Wda(Wda&&) noexcept = default;
Wda& operator=(Wda&&) noexcept = default;
Wda(const Wda&) = delete;
Wda& operator=(const Wda&) = delete;

WdaClientHandle* raw() const noexcept { return handle_.get(); }
static Wda adopt(WdaClientHandle* h) noexcept { return Wda(h); }

private:
explicit Wda(WdaClientHandle* h) noexcept : handle_(h) {}
WdaClientPtr handle_{};
};

} // namespace IdeviceFFI
56 changes: 56 additions & 0 deletions cpp/include/idevice++/wda_bridge.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Jackson Coxson

#pragma once
#include <cstdint>
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/option.hpp>
#include <idevice++/provider.hpp>
#include <idevice++/result.hpp>
#include <memory>
#include <string>

namespace IdeviceFFI {

using WdaBridgePtr =
std::unique_ptr<WdaBridgeHandle, FnDeleter<WdaBridgeHandle, wda_bridge_free>>;

/// Localhost endpoints exposed by a running WDA bridge.
struct WdaBridgeEndpoints {
Option<std::string> udid;
std::string wda_url;
std::string mjpeg_url;
uint16_t local_http = 0;
uint16_t local_mjpeg = 0;
uint16_t device_http = 0;
uint16_t device_mjpeg = 0;
};

/// Dynamic localhost bridge for a single device's WDA endpoints.
///
/// Both factories consume the `Provider` argument; dropping the bridge aborts
/// the underlying forwarder tasks.
class WdaBridge {
public:
static Result<WdaBridge, FfiError> start(Provider&& provider);
static Result<WdaBridge, FfiError> start_with_ports(Provider&& provider,
uint16_t device_http,
uint16_t device_mjpeg);

Result<WdaBridgeEndpoints, FfiError> endpoints() const;

~WdaBridge() noexcept = default;
WdaBridge(WdaBridge&&) noexcept = default;
WdaBridge& operator=(WdaBridge&&) noexcept = default;
WdaBridge(const WdaBridge&) = delete;
WdaBridge& operator=(const WdaBridge&) = delete;

WdaBridgeHandle* raw() const noexcept { return handle_.get(); }
static WdaBridge adopt(WdaBridgeHandle* h) noexcept { return WdaBridge(h); }

private:
explicit WdaBridge(WdaBridgeHandle* h) noexcept : handle_(h) {}
WdaBridgePtr handle_{};
};

} // namespace IdeviceFFI
Loading
Loading