diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index fe65446..f832b74 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -84,7 +84,6 @@ if (WIN32) ) endif() -# Optional: position independent code if you later turn idevice_cpp into a shared lib set_target_properties(idevice_cpp PROPERTIES POSITION_INDEPENDENT_CODE ON) # ---- Build each example and link against idevice_cpp ------------------------ diff --git a/cpp/examples/app_service.cpp b/cpp/examples/app_service.cpp new file mode 100644 index 0000000..6b5c861 --- /dev/null +++ b/cpp/examples/app_service.cpp @@ -0,0 +1,221 @@ +// Jackson Coxson + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace IdeviceFFI; + +static void die(const char* msg, const FfiError& e) { + std::cerr << msg << ": " << e.message << "\n"; + std::exit(1); +} + +static void usage(const char* argv0) { + std::cerr << "Usage:\n" + << " " << argv0 << " list\n" + << " " << argv0 << " launch \n" + << " " << argv0 << " processes\n" + << " " << argv0 << " uninstall \n" + << " " << argv0 << " signal \n" + << " " << argv0 << " icon [hw=1.0] [scale=1.0]\n"; +} + +int main(int argc, char** argv) { + if (argc < 2) { + usage(argv[0]); + return 2; + } + + std::string cmd = argv[1]; + + FfiError err; + + // 1) Connect to usbmuxd and pick first device + auto mux = UsbmuxdConnection::default_new(/*tag*/ 0, err); + if (!mux) + die("failed to connect to usbmuxd", err); + + auto devices = mux->get_devices(err); + if (!devices) + die("failed to list devices", err); + if (devices->empty()) { + std::cerr << "no devices connected\n"; + return 1; + } + auto& dev = (*devices)[0]; + + auto udid = dev.get_udid(); + if (!udid) { + std::cerr << "device has no UDID\n"; + return 1; + } + auto mux_id = dev.get_id(); + if (!mux_id) { + std::cerr << "device has no mux id\n"; + return 1; + } + + // 2) Provider via default usbmuxd addr + auto addr = UsbmuxdAddr::default_new(); + + const uint32_t tag = 0; + const std::string label = "app_service-jkcoxson"; + + auto provider = Provider::usbmuxd_new(std::move(addr), tag, *udid, *mux_id, label, err); + if (!provider) + die("failed to create provider", err); + + // 3) CoreDeviceProxy + auto cdp = CoreDeviceProxy::connect(*provider, err); + if (!cdp) + die("failed to connect CoreDeviceProxy", err); + + auto rsd_port = cdp->get_server_rsd_port(err); + if (!rsd_port) + die("failed to get server RSD port", err); + + // 4) Create software tunnel adapter (consumes proxy) + auto adapter = std::move(*cdp).create_tcp_adapter(err); + if (!adapter) + die("failed to create software tunnel adapter", err); + + // 5) Connect adapter to RSD → ReadWrite stream + auto stream = adapter->connect(*rsd_port, err); + if (!stream) + die("failed to connect RSD stream", err); + + // 6) RSD handshake (consumes stream) + auto rsd = RsdHandshake::from_socket(std::move(*stream), err); + if (!rsd) + die("failed RSD handshake", err); + + // 7) AppService over RSD (borrows adapter + handshake) + auto app = AppService::connect_rsd(*adapter, *rsd, err); + if (!app) + die("failed to connect AppService", err); + + // 8) Commands + if (cmd == "list") { + auto apps = app->list_apps(/*app_clips*/ true, + /*removable*/ true, + /*hidden*/ true, + /*internal*/ true, + /*default_apps*/ true, + err); + if (!apps) + die("list_apps failed", err); + + for (const auto& a : *apps) { + std::cout << "- " << a.bundle_identifier << " | name=" << a.name + << " | version=" << (a.version ? *a.version : std::string("")) + << " | dev=" << (a.is_developer_app ? "y" : "n") + << " | hidden=" << (a.is_hidden ? "y" : "n") << "\n"; + } + return 0; + + } else if (cmd == "launch") { + if (argc < 3) { + std::cerr << "No bundle ID passed\n"; + return 2; + } + std::string bundle_id = argv[2]; + + std::vector args; // empty in this example + auto resp = app->launch(bundle_id, + args, + /*kill_existing*/ false, + /*start_suspended*/ false, + err); + if (!resp) + die("launch failed", err); + + std::cout << "Launched pid=" << resp->pid << " exe=" << resp->executable_url + << " piv=" << resp->process_identifier_version + << " audit_token_len=" << resp->audit_token.size() << "\n"; + return 0; + + } else if (cmd == "processes") { + auto procs = app->list_processes(err); + if (!procs) + die("list_processes failed", err); + + for (const auto& p : *procs) { + std::cout << p.pid << " : " + << (p.executable_url ? *p.executable_url : std::string("")) << "\n"; + } + return 0; + + } else if (cmd == "uninstall") { + if (argc < 3) { + std::cerr << "No bundle ID passed\n"; + return 2; + } + std::string bundle_id = argv[2]; + + if (!app->uninstall(bundle_id, err)) + die("uninstall failed", err); + std::cout << "Uninstalled " << bundle_id << "\n"; + return 0; + + } else if (cmd == "signal") { + if (argc < 4) { + std::cerr << "Usage: signal \n"; + return 2; + } + uint32_t pid = static_cast(std::stoul(argv[2])); + uint32_t signal = static_cast(std::stoul(argv[3])); + + auto res = app->send_signal(pid, signal, err); + if (!res) + die("send_signal failed", err); + + std::cout << "Signaled pid=" << res->pid << " signal=" << res->signal + << " ts_ms=" << res->device_timestamp_ms + << " exe=" << (res->executable_url ? *res->executable_url : std::string("")) + << "\n"; + return 0; + + } else if (cmd == "icon") { + if (argc < 4) { + std::cerr << "Usage: icon [hw=1.0] [scale=1.0]\n"; + return 2; + } + std::string bundle_id = argv[2]; + std::string save_path = argv[3]; + float hw = (argc >= 5) ? std::stof(argv[4]) : 1.0f; + float scale = (argc >= 6) ? std::stof(argv[5]) : 1.0f; + + auto icon = app->fetch_icon(bundle_id, hw, hw, scale, /*allow_placeholder*/ true, err); + if (!icon) + die("fetch_app_icon failed", err); + + std::ofstream out(save_path, std::ios::binary); + if (!out) { + std::cerr << "Failed to open " << save_path << " for writing\n"; + return 1; + } + out.write(reinterpret_cast(icon->data.data()), + static_cast(icon->data.size())); + out.close(); + + std::cout << "Saved icon to " << save_path << " (" << icon->data.size() << " bytes, " + << icon->icon_width << "x" << icon->icon_height << ", min " << icon->minimum_width + << "x" << icon->minimum_height << ")\n"; + return 0; + + } else { + usage(argv[0]); + return 2; + } +} diff --git a/cpp/include/idevice++/app_service.hpp b/cpp/include/idevice++/app_service.hpp new file mode 100644 index 0000000..12c2de9 --- /dev/null +++ b/cpp/include/idevice++/app_service.hpp @@ -0,0 +1,117 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using AppServicePtr = + std::unique_ptr>; + +struct AppInfo { + bool is_removable{}; + std::string name; + bool is_first_party{}; + std::string path; + std::string bundle_identifier; + bool is_developer_app{}; + std::optional bundle_version; + bool is_internal{}; + bool is_hidden{}; + bool is_app_clip{}; + std::optional version; +}; + +struct LaunchResponse { + uint32_t process_identifier_version{}; + uint32_t pid{}; + std::string executable_url; + std::vector audit_token; // raw words +}; + +struct ProcessToken { + uint32_t pid{}; + std::optional executable_url; +}; + +struct SignalResponse { + uint32_t pid{}; + std::optional executable_url; + uint64_t device_timestamp_ms{}; + uint32_t signal{}; +}; + +struct IconData { + std::vector data; + double icon_width{}; + double icon_height{}; + double minimum_width{}; + double minimum_height{}; +}; + +class AppService { + public: + // Factory: connect via RSD (borrows adapter & handshake) + static std::optional + connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err); + + // Factory: from socket Box (consumes it). + // Only use if you actually obtain such a pointer from C. + static std::optional from_readwrite_ptr(ReadWriteOpaque* consumed, FfiError& err); + + // nice ergonomic overload: consume a C++ ReadWrite by releasing it + static std::optional from_readwrite(ReadWrite&& rw, FfiError& err); + + // API + std::optional> list_apps(bool app_clips, + bool removable, + bool hidden, + bool internal, + bool default_apps, + FfiError& err) const; + + std::optional launch(const std::string& bundle_id, + const std::vector& argv, + bool kill_existing, + bool start_suspended, + FfiError& err); + + std::optional> list_processes(FfiError& err) const; + + bool uninstall(const std::string& bundle_id, FfiError& err); + + std::optional send_signal(uint32_t pid, uint32_t signal, FfiError& err); + + std::optional fetch_icon(const std::string& bundle_id, + float width, + float height, + float scale, + bool allow_placeholder, + FfiError& err); + + // RAII / moves + ~AppService() noexcept = default; + AppService(AppService&&) noexcept = default; + AppService& operator=(AppService&&) noexcept = default; + AppService(const AppService&) = delete; + AppService& operator=(const AppService&) = delete; + + AppServiceHandle* raw() const noexcept { return handle_.get(); } + static AppService adopt(AppServiceHandle* h) noexcept { return AppService(h); } + + private: + explicit AppService(AppServiceHandle* h) noexcept : handle_(h) {} + AppServicePtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/app_service.cpp b/cpp/src/app_service.cpp new file mode 100644 index 0000000..725c1d4 --- /dev/null +++ b/cpp/src/app_service.cpp @@ -0,0 +1,196 @@ +// Jackson Coxson + +#include + +namespace IdeviceFFI { + +// ---- Factories ---- +std::optional +AppService::connect_rsd(Adapter& adapter, RsdHandshake& rsd, FfiError& err) { + AppServiceHandle* out = nullptr; + if (IdeviceFfiError* e = ::app_service_connect_rsd(adapter.raw(), rsd.raw(), &out)) { + err = FfiError(e); + return std::nullopt; + } + return AppService::adopt(out); +} + +std::optional AppService::from_readwrite_ptr(ReadWriteOpaque* consumed, FfiError& err) { + AppServiceHandle* out = nullptr; + if (IdeviceFfiError* e = ::app_service_new(consumed, &out)) { + err = FfiError(e); + return std::nullopt; + } + return AppService::adopt(out); +} + +std::optional AppService::from_readwrite(ReadWrite&& rw, FfiError& err) { + // Rust consumes the stream regardless of result → release BEFORE call + return from_readwrite_ptr(rw.release(), err); +} + +// ---- Helpers to copy/free C arrays ---- +static std::vector copy_and_free_app_list(AppListEntryC* arr, size_t n) { + std::vector out; + out.reserve(n); + for (size_t i = 0; i < n; ++i) { + const auto& c = arr[i]; + AppInfo a; + a.is_removable = c.is_removable != 0; + if (c.name) + a.name = c.name; + a.is_first_party = c.is_first_party != 0; + if (c.path) + a.path = c.path; + if (c.bundle_identifier) + a.bundle_identifier = c.bundle_identifier; + a.is_developer_app = c.is_developer_app != 0; + if (c.bundle_version) + a.bundle_version = std::string(c.bundle_version); + a.is_internal = c.is_internal != 0; + a.is_hidden = c.is_hidden != 0; + a.is_app_clip = c.is_app_clip != 0; + if (c.version) + a.version = std::string(c.version); + out.emplace_back(std::move(a)); + } + ::app_service_free_app_list(arr, n); + return out; +} + +static std::vector copy_and_free_process_list(ProcessTokenC* arr, size_t n) { + std::vector out; + out.reserve(n); + for (size_t i = 0; i < n; ++i) { + ProcessToken p; + p.pid = arr[i].pid; + if (arr[i].executable_url) + p.executable_url = std::string(arr[i].executable_url); + out.emplace_back(std::move(p)); + } + ::app_service_free_process_list(arr, n); + return out; +} + +// ---- API impls ---- +std::optional> AppService::list_apps(bool app_clips, + bool removable, + bool hidden, + bool internal, + bool default_apps, + FfiError& err) const { + AppListEntryC* arr = nullptr; + size_t n = 0; + if (IdeviceFfiError* e = ::app_service_list_apps(handle_.get(), + app_clips ? 1 : 0, + removable ? 1 : 0, + hidden ? 1 : 0, + internal ? 1 : 0, + default_apps ? 1 : 0, + &arr, + &n)) { + err = FfiError(e); + return std::nullopt; + } + return copy_and_free_app_list(arr, n); +} + +std::optional AppService::launch(const std::string& bundle_id, + const std::vector& argv, + bool kill_existing, + bool start_suspended, + FfiError& err) { + std::vector c_argv; + c_argv.reserve(argv.size()); + for (auto& s : argv) + c_argv.push_back(s.c_str()); + + LaunchResponseC* resp = nullptr; + if (IdeviceFfiError* e = ::app_service_launch_app(handle_.get(), + bundle_id.c_str(), + c_argv.empty() ? nullptr : c_argv.data(), + c_argv.size(), + kill_existing ? 1 : 0, + start_suspended ? 1 : 0, + &resp)) { + err = FfiError(e); + return std::nullopt; + } + + LaunchResponse out; + out.process_identifier_version = resp->process_identifier_version; + out.pid = resp->pid; + if (resp->executable_url) + out.executable_url = resp->executable_url; + if (resp->audit_token && resp->audit_token_len > 0) { + out.audit_token.assign(resp->audit_token, resp->audit_token + resp->audit_token_len); + } + ::app_service_free_launch_response(resp); + return out; +} + +std::optional> AppService::list_processes(FfiError& err) const { + ProcessTokenC* arr = nullptr; + size_t n = 0; + if (IdeviceFfiError* e = ::app_service_list_processes(handle_.get(), &arr, &n)) { + err = FfiError(e); + return std::nullopt; + } + return copy_and_free_process_list(arr, n); +} + +bool AppService::uninstall(const std::string& bundle_id, FfiError& err) { + if (IdeviceFfiError* e = ::app_service_uninstall_app(handle_.get(), bundle_id.c_str())) { + err = FfiError(e); + return false; + } + return true; +} + +std::optional +AppService::send_signal(uint32_t pid, uint32_t signal, FfiError& err) { + SignalResponseC* c = nullptr; + if (IdeviceFfiError* e = ::app_service_send_signal(handle_.get(), pid, signal, &c)) { + err = FfiError(e); + return std::nullopt; + } + SignalResponse out; + out.pid = c->pid; + if (c->executable_url) + out.executable_url = std::string(c->executable_url); + out.device_timestamp_ms = c->device_timestamp; + out.signal = c->signal; + ::app_service_free_signal_response(c); + return out; +} + +std::optional AppService::fetch_icon(const std::string& bundle_id, + float width, + float height, + float scale, + bool allow_placeholder, + FfiError& err) { + IconDataC* c = nullptr; + if (IdeviceFfiError* e = ::app_service_fetch_app_icon(handle_.get(), + bundle_id.c_str(), + width, + height, + scale, + allow_placeholder ? 1 : 0, + &c)) { + err = FfiError(e); + return std::nullopt; + } + IconData out; + if (c->data && c->data_len) { + out.data.assign(c->data, c->data + c->data_len); + } + out.icon_width = c->icon_width; + out.icon_height = c->icon_height; + out.minimum_width = c->minimum_width; + out.minimum_height = c->minimum_height; + ::app_service_free_icon_data(c); + return out; +} + +} // namespace IdeviceFFI diff --git a/ffi/src/core_device/app_service.rs b/ffi/src/core_device/app_service.rs index 908bd26..c8f0616 100644 --- a/ffi/src/core_device/app_service.rs +++ b/ffi/src/core_device/app_service.rs @@ -9,7 +9,7 @@ use idevice::{IdeviceError, ReadWrite, RsdService}; use crate::core_device_proxy::AdapterHandle; use crate::rsd::RsdHandshakeHandle; -use crate::{IdeviceFfiError, RUNTIME, ffi_err}; +use crate::{IdeviceFfiError, RUNTIME, ReadWriteOpaque, ffi_err}; /// Opaque handle to an AppServiceClient pub struct AppServiceHandle(pub AppServiceClient>); @@ -122,7 +122,7 @@ pub unsafe extern "C" fn app_service_connect_rsd( /// `handle` must be a valid pointer to a location where the handle will be stored #[unsafe(no_mangle)] pub unsafe extern "C" fn app_service_new( - socket: *mut Box, + socket: *mut ReadWriteOpaque, handle: *mut *mut AppServiceHandle, ) -> *mut IdeviceFfiError { if socket.is_null() || handle.is_null() { @@ -130,7 +130,7 @@ pub unsafe extern "C" fn app_service_new( } let socket = unsafe { Box::from_raw(socket) }; - let res = RUNTIME.block_on(async move { AppServiceClient::new(*socket).await }); + let res = RUNTIME.block_on(async move { AppServiceClient::new(socket.inner.unwrap()).await }); match res { Ok(client) => { diff --git a/tools/src/app_service.rs b/tools/src/app_service.rs index 9751e28..9951666 100644 --- a/tools/src/app_service.rs +++ b/tools/src/app_service.rs @@ -97,7 +97,7 @@ async fn main() { let host = matches.get_one::("host"); let provider = - match common::get_provider(udid, host, pairing_file, "debug-proxy-jkcoxson").await { + match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await { Ok(p) => p, Err(e) => { eprintln!("{e}");