[feat] add host platform metadata and iOS platform placeholders

This commit is contained in:
dijunkun
2026-09-28 23:49:55 +08:00
parent f8535a797b
commit c345c69e81
14 changed files with 323 additions and 44 deletions
@@ -322,6 +322,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
if (props && source_id == props->control_data_label_) {
std::lock_guard lock(props->remote_version_mutex_);
props->remote_host_info_received_ = true;
props->remote_platform_ = remote_action.i.platform;
props->remote_app_version_ = IsValidAppVersion(remote_action.i.app_version)
? remote_action.i.app_version : "";
}
@@ -32,6 +32,19 @@
#endif
namespace crossdesk {
namespace {
#if defined(_WIN32)
constexpr HostPlatform kClientPlatform = HostPlatform::Windows;
#elif defined(__APPLE__)
constexpr HostPlatform kClientPlatform = HostPlatform::MacOS;
#elif defined(__linux__)
constexpr HostPlatform kClientPlatform = HostPlatform::Linux;
#else
constexpr HostPlatform kClientPlatform = HostPlatform::Unknown;
#endif
} // namespace
void PeerEventHandler::SendClientInfo(PeerPtr* peer,
const std::string& client_id) {
@@ -39,19 +52,9 @@ void PeerEventHandler::SendClientInfo(PeerPtr* peer,
return;
}
#if defined(_WIN32)
constexpr const char* kClientPlatform = "windows";
#elif defined(__APPLE__)
constexpr const char* kClientPlatform = "macos";
#elif defined(__linux__)
constexpr const char* kClientPlatform = "linux";
#else
constexpr const char* kClientPlatform = "unknown";
#endif
const nlohmann::json message = {{"type", "client_info"},
{"version", CROSSDESK_VERSION},
{"platform", kClientPlatform}};
{"platform", HostPlatformName(kClientPlatform)}};
const std::string payload = message.dump();
if (SendSignalMessage(peer, payload.data(), payload.size()) != 0) {
LOG_WARN("[{}] failed to report client information", client_id);
@@ -270,7 +273,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
{
RemoteAction remote_action = MakeHostInformation(
GetHostName(), runtime->devices_.host_display_list(),
/*supports_privacy_screen=*/true, CROSSDESK_VERSION);
/*supports_privacy_screen=*/true, CROSSDESK_VERSION,
kClientPlatform);
remote_action.i.supports_video_settings = true;
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
@@ -304,6 +308,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::lock_guard lock(props->remote_version_mutex_);
props->remote_host_info_received_ = false;
props->remote_app_version_.clear();
props->remote_platform_ = HostPlatform::Unknown;
props->remote_update_dismissed_ = false;
}
props->net_traffic_stats_.Reset();
@@ -401,7 +406,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
{
RemoteAction remote_action = MakeHostInformation(
GetHostName(), runtime->devices_.host_display_list(),
/*supports_privacy_screen=*/true, CROSSDESK_VERSION);
/*supports_privacy_screen=*/true, CROSSDESK_VERSION,
kClientPlatform);
remote_action.i.supports_video_settings = true;
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
@@ -124,10 +124,12 @@ struct RemoteSession {
bool tab_selected_ = false;
bool tab_opened_ = true;
std::string remote_host_name_;
// Received version and dismissal state belong to this connection.
// Host metadata and dismissal state belong to this connection and are
// protected by remote_version_mutex_.
std::mutex remote_version_mutex_;
bool remote_host_info_received_ = false;
std::string remote_app_version_;
HostPlatform remote_platform_ = HostPlatform::Unknown;
bool remote_update_dismissed_ = false;
bool remote_service_status_received_ = false;
std::mutex video_settings_mutex_;
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
100000000000000000000018 /* RecentConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000018 /* RecentConnection.swift */; };
100000000000000000000016 /* RemoteVideoSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000016 /* RemoteVideoSettings.swift */; };
100000000000000000000017 /* RemoteNetworkStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000017 /* RemoteNetworkStatistics.swift */; };
100000000000000000000001 /* CrossDeskMobileApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000001 /* CrossDeskMobileApp.swift */; };
@@ -29,6 +30,7 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
200000000000000000000018 /* RecentConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentConnection.swift; sourceTree = "<group>"; };
200000000000000000000016 /* RemoteVideoSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteVideoSettings.swift; sourceTree = "<group>"; };
200000000000000000000017 /* RemoteNetworkStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteNetworkStatistics.swift; sourceTree = "<group>"; };
200000000000000000000001 /* CrossDeskMobileApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossDeskMobileApp.swift; sourceTree = "<group>"; };
@@ -96,6 +98,7 @@
200000000000000000000013 /* ThumbnailFileStore.swift */,
200000000000000000000012 /* PrivacyPreferences.swift */,
200000000000000000000002 /* RemoteSessionModel.swift */,
200000000000000000000018 /* RecentConnection.swift */,
200000000000000000000016 /* RemoteVideoSettings.swift */,
200000000000000000000017 /* RemoteNetworkStatistics.swift */,
200000000000000000000003 /* ContentView.swift */,
@@ -261,6 +264,7 @@
files = (
100000000000000000000001 /* CrossDeskMobileApp.swift in Sources */,
100000000000000000000002 /* RemoteSessionModel.swift in Sources */,
100000000000000000000018 /* RecentConnection.swift in Sources */,
100000000000000000000016 /* RemoteVideoSettings.swift in Sources */,
100000000000000000000017 /* RemoteNetworkStatistics.swift in Sources */,
100000000000000000000003 /* ContentView.swift in Sources */,
@@ -86,6 +86,7 @@ typedef struct {
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didReceiveHostName:(NSString *)hostName
appVersion:(NSString *)appVersion
platform:(NSString *)platform
displayNames:(NSArray<NSString *> *)displayNames
displaySizes:(NSArray<NSValue *> *)displaySizes
supportsVideoSettings:(BOOL)supportsVideoSettings;
@@ -1343,6 +1343,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
: "iPhone-" + self->_state->identity_base;
RemoteAction action{};
action.type = ControlType::host_infomation;
action.i.platform = crossdesk::HostPlatform::IOS;
CopyCString(action.i.host_name, sizeof(action.i.host_name),
controller_name);
action.i.host_name_size = std::strlen(action.i.host_name);
@@ -1634,6 +1635,8 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
}
if (action.type != ControlType::host_infomation) return;
const BOOL supports_video_settings = action.i.supports_video_settings;
NSString *platform = [NSString stringWithUTF8String:
crossdesk::HostPlatformName(action.i.platform)];
NSString *host_name = [[NSString alloc]
initWithBytes:action.i.host_name
length:action.i.host_name_size
@@ -1663,11 +1666,12 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
if (![self isControllerGenerationActive:generation]) return;
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:
@selector(rtcBridge:didReceiveHostName:appVersion:displayNames:
@selector(rtcBridge:didReceiveHostName:appVersion:platform:displayNames:
displaySizes:supportsVideoSettings:)]) {
[delegate rtcBridge:self
didReceiveHostName:host_name ?: @""
appVersion:app_version ?: @""
platform:platform
displayNames:display_names
displaySizes:display_sizes
supportsVideoSettings:supports_video_settings];
+30 -7
View File
@@ -542,6 +542,26 @@ private struct RecentConnectionCard: View {
let connect: () -> Void
let delete: () -> Void
private var platformColors: [Color] {
switch connection.platform {
case .windows:
return [Color(red: 0.12, green: 0.38, blue: 0.70),
Color(red: 0.20, green: 0.48, blue: 0.77)]
case .linux:
return [Color(red: 0.63, green: 0.31, blue: 0.08),
Color(red: 0.73, green: 0.39, blue: 0.12)]
case .macos:
return [Color(red: 0.43, green: 0.29, blue: 0.68),
Color(red: 0.55, green: 0.39, blue: 0.77)]
case .ios:
return [Color(red: 0.08, green: 0.43, blue: 0.40),
Color(red: 0.12, green: 0.51, blue: 0.47)]
case .unknown:
return [Color(red: 0.35, green: 0.40, blue: 0.46),
Color(red: 0.45, green: 0.49, blue: 0.55)]
}
}
var body: some View {
Button(action: connect) {
VStack(alignment: .leading, spacing: 0) {
@@ -551,13 +571,16 @@ private struct RecentConnectionCard: View {
.resizable()
.scaledToFill()
} else {
LinearGradient(colors: [
Color(red: 0.18, green: 0.48, blue: 0.86),
Color(red: 0.38, green: 0.68, blue: 0.96)
], startPoint: .topLeading, endPoint: .bottomTrailing)
Image(systemName: "display")
.font(.system(size: 34, weight: .medium))
.foregroundStyle(.white.opacity(0.9))
LinearGradient(colors: platformColors,
startPoint: .topLeading,
endPoint: .bottomTrailing)
Text(connection.platform.displayName)
.font(.title3.weight(.semibold))
.lineLimit(1)
.minimumScaleFactor(0.8)
.foregroundStyle(.white)
.padding(8)
.accessibilityLabel("被控端平台:\(connection.platform.displayName)")
}
}
.frame(maxWidth: .infinity)
@@ -0,0 +1,58 @@
import Foundation
enum RemoteHostPlatform: String, Codable {
case unknown, windows, macos, linux, ios
var displayName: String {
switch self {
case .windows: return "Windows"
case .macos: return "macOS"
case .linux: return "Linux"
case .ios: return "iOS"
case .unknown: return "未知"
}
}
}
struct RecentConnection: Codable, Identifiable, Equatable {
let remoteID: String
var displayName: String
var lastConnectedAt: Date
var remembersPassword: Bool
var thumbnailFileName: String?
var platform: RemoteHostPlatform
var id: String { remoteID }
init(remoteID: String, displayName: String, lastConnectedAt: Date,
remembersPassword: Bool, thumbnailFileName: String?,
platform: RemoteHostPlatform = .unknown) {
self.remoteID = remoteID
self.displayName = displayName
self.lastConnectedAt = lastConnectedAt
self.remembersPassword = remembersPassword
self.thumbnailFileName = thumbnailFileName
self.platform = platform
}
private enum CodingKeys: String, CodingKey {
case remoteID, displayName, lastConnectedAt, remembersPassword, thumbnailFileName, platform
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
remoteID = try values.decode(String.self, forKey: .remoteID)
displayName = try values.decode(String.self, forKey: .displayName)
lastConnectedAt = try values.decode(Date.self, forKey: .lastConnectedAt)
remembersPassword = try values.decode(Bool.self, forKey: .remembersPassword)
thumbnailFileName = try values.decodeIfPresent(String.self, forKey: .thumbnailFileName)
// Adding platform metadata must not discard records saved by older apps.
platform = (try? values.decode(RemoteHostPlatform.self, forKey: .platform)) ?? .unknown
}
mutating func updateHostInfo(name: String, platform: RemoteHostPlatform) {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty { displayName = trimmed }
self.platform = platform
}
}
@@ -98,16 +98,6 @@ private struct RemoteVideoFrame {
let captureUptime: TimeInterval
}
struct RecentConnection: Codable, Identifiable, Equatable {
let remoteID: String
var displayName: String
var lastConnectedAt: Date
var remembersPassword: Bool
var thumbnailFileName: String?
var id: String { remoteID }
}
private enum RecentConnectionStore {
private static let defaultsKey = "crossdesk.mobile.recent-connections.v1"
private static let thumbnails = ThumbnailFileStore(directory:
@@ -345,6 +335,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
@Published private(set) var deviceOfflineAlertVisible = false
@Published private(set) var connectionFailureMessage: String?
@Published private(set) var remoteUpdateMessage: String?
@Published private(set) var remotePlatform: RemoteHostPlatform = .unknown
let bridge = CrossDeskRTCBridge()
private let audioPlayer = RemoteAudioPlayer()
@@ -553,7 +544,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
private func beginRemoteConnection(_ identifier: String) {
networkStatistics = RemoteNetworkStatistics()
resetRemoteVersionCheck()
resetRemoteHostInfo()
cancelVideoRecovery()
resetVideoSettings()
cancelPresenceProbe()
@@ -647,7 +638,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
}
private func resetConnection() {
resetRemoteVersionCheck()
resetRemoteHostInfo()
resetVideoSettings()
connectionFailureMessage = nil
cancelVideoRecovery()
@@ -673,7 +664,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
}
func retry() {
resetRemoteVersionCheck()
resetRemoteHostInfo()
cancelVideoRecovery()
bridge.disconnect()
connect()
@@ -683,12 +674,13 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
remoteUpdateMessage = nil
}
private func resetRemoteVersionCheck() {
private func resetRemoteHostInfo() {
remoteVersionCheckTask?.cancel()
remoteVersionCheckTask = nil
remoteVersionCheckGeneration = UUID()
remoteVersionCheckAttempted = false
remoteAppVersion = ""
remotePlatform = .unknown
remoteHostInfoReceived = false
remoteUpdateMessage = nil
}
@@ -891,7 +883,8 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
displayName: title,
lastConnectedAt: Date(),
remembersPassword: pendingRememberPassword,
thumbnailFileName: previous?.thumbnailFileName
thumbnailFileName: previous?.thumbnailFileName,
platform: remoteHostInfoReceived ? remotePlatform : (previous?.platform ?? .unknown)
)
recentConnections.removeAll { $0.remoteID == activeRemoteID }
recentConnections.insert(connection, at: 0)
@@ -899,14 +892,14 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
refreshRecentConnectionPresence()
}
private func updateActiveConnectionName(_ name: String) {
private func updateActiveConnectionHostInfo(_ name: String) {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, !activeRemoteID.isEmpty else { return }
activeDisplayName = trimmed
guard !activeRemoteID.isEmpty else { return }
if !trimmed.isEmpty { activeDisplayName = trimmed }
guard let index = recentConnections.firstIndex(where: {
$0.remoteID == activeRemoteID
}) else { return }
recentConnections[index].displayName = trimmed
recentConnections[index].updateHostInfo(name: trimmed, platform: remotePlatform)
RecentConnectionStore.save(recentConnections)
}
@@ -1183,6 +1176,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
func rtcBridge(_ bridge: CrossDeskRTCBridge,
didReceiveHostName hostName: String,
appVersion: String,
platform: String,
displayNames remoteDisplayNames: [String],
displaySizes remoteDisplaySizes: [NSValue],
supportsVideoSettings: Bool) {
@@ -1196,8 +1190,9 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
}
remoteHostInfoReceived = true
remoteAppVersion = appVersion
remotePlatform = RemoteHostPlatform(rawValue: platform) ?? .unknown
checkRemoteVersionIfNeeded()
updateActiveConnectionName(hostName)
updateActiveConnectionHostInfo(hostName)
let names = remoteDisplayNames.enumerated().map { index, displayName in
let name = displayName.trimmingCharacters(in: .whitespacesAndNewlines)
return name.isEmpty ? "显示器 \(index + 1)" : name
+11
View File
@@ -237,6 +237,17 @@ Receive loss rates are fractions converted to percentages. As on desktop,
MiniRTC's total loss is the sum of the media loss rates, not a packet-weighted
overall loss rate.
The reliable control channel's `host_info` also advertises `platform` as
`windows`, `linux`, `macos`, `ios`, or `unknown`. Desktop and iOS controllers
retain the received platform for the current session. Missing, malformed, or
unrecognized values from older or other peers map to unknown (`未知` on iOS),
without rejecting the remaining host information. Reconnecting clears the
previous peer's platform; iOS sends `ios` in its own host information.
On iOS, the received platform is also saved with each recent connection.
When preview saving is off or no preview is available, the thumbnail placeholder
shows the platform name. Existing history without platform metadata shows
`未知` until host information is received from a subsequent connection.
For black-screen diagnosis, keep Xcode's debug console open and look for
`CrossDesk` and `VideoToolbox` messages. With VideoToolbox H.264 decoding selected, look for
`VideoToolbox decoded frame` from MiniRTC and `CrossDesk delivered latest frame`
+95
View File
@@ -0,0 +1,95 @@
"""Check platform persistence without losing existing recent connections."""
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
@unittest.skipUnless(sys.platform == "darwin", "requires the Apple Swift runtime")
class RecentConnectionTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temp = tempfile.TemporaryDirectory(prefix="crossdesk-recent-connections-")
cls.addClassCleanup(cls.temp.cleanup)
root = Path(cls.temp.name)
harness = root / "RecentConnectionTests.swift"
harness.write_text(r'''
import Foundation
func expect(_ condition: @autoclosure () -> Bool, _ message: String) {
if !condition() { fatalError(message) }
}
@main enum RecentConnectionTests {
static func main() throws {
let test = CommandLine.arguments[1]
let decoder = JSONDecoder()
let legacy = #"{"remoteID":"123456","displayName":"Work PC","lastConnectedAt":100,"remembersPassword":true,"thumbnailFileName":"123456.jpg"}"#
if test == "legacy" {
for field in ["", #", "platform":null"#, #", "platform":23"#,
#", "platform":"future-os""#] {
let record = String(legacy.dropLast()) + field + "}"
let connections = try decoder.decode([RecentConnection].self,
from: Data("[\(record)]".utf8))
expect(connections.count == 1, "Old history must not be discarded")
let connection = connections[0]
expect(connection.platform == .unknown, "Missing or invalid platform is unknown")
expect(connection.platform.displayName == "未知", "Unknown has a readable placeholder")
expect(connection.remoteID == "123456" && connection.displayName == "Work PC",
"Existing identity and name survive migration")
expect(connection.lastConnectedAt == Date(timeIntervalSinceReferenceDate: 100) &&
connection.remembersPassword && connection.thumbnailFileName == "123456.jpg",
"Migration preserves dates, saved-password flags and previews")
}
} else if test == "round_trip" {
let platforms: [RemoteHostPlatform] = [.windows, .linux, .macos, .ios, .unknown]
let connections = platforms.enumerated().map { index, platform in
RecentConnection(remoteID: String(index), displayName: "Host \(index)",
lastConnectedAt: Date(timeIntervalSinceReferenceDate: Double(index)),
remembersPassword: false, thumbnailFileName: nil, platform: platform)
}
let data = try JSONEncoder().encode(connections)
let reloaded = try decoder.decode([RecentConnection].self, from: data)
expect(reloaded == connections, "Platforms persist per device without requiring an image")
} else if test == "host_info" {
var connection = try decoder.decode(RecentConnection.self, from: Data(legacy.utf8))
connection.updateHostInfo(name: " ", platform: .windows)
expect(connection.displayName == "Work PC" && connection.platform == .windows,
"An empty host name must not prevent a platform update")
connection.updateHostInfo(name: " Linux workstation ", platform: .linux)
expect(connection.displayName == "Linux workstation" && connection.platform == .linux,
"New host information replaces the cached platform")
connection.updateHostInfo(name: "", platform: .unknown)
let data = try JSONEncoder().encode(connection)
let reloaded = try decoder.decode(RecentConnection.self, from: data)
expect(reloaded.platform == .unknown && reloaded.thumbnailFileName == "123456.jpg",
"Legacy peers replace the old platform without removing a saved preview")
} else {
fatalError("Unknown test")
}
}
}
''')
source = Path(__file__).resolve().parents[1] / "CrossDeskMobile/RecentConnection.swift"
cls.binary = root / "recent-connection-tests"
subprocess.run(["xcrun", "swiftc", str(source), str(harness), "-o", str(cls.binary)],
check=True, capture_output=True, text=True)
def run_case(self, case):
result = subprocess.run([str(self.binary), case], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr or result.stdout)
def test_existing_history_survives_platform_migration(self):
self.run_case("legacy")
def test_platforms_persist_without_saved_previews(self):
self.run_case("round_trip")
def test_received_host_info_replaces_cached_platform(self):
self.run_case("host_info")
if __name__ == "__main__":
unittest.main()
+9 -1
View File
@@ -116,11 +116,18 @@ struct CursorState {
CursorHiddenReason hidden_reason;
};
enum class HostPlatform { Unknown, Windows, MacOS, Linux, IOS };
const char* HostPlatformName(HostPlatform platform);
HostPlatform ParseHostPlatform(const std::string& platform);
struct HostInfo {
char host_name[64];
std::size_t host_name_size;
// Optional; an empty string means the peer version is unknown.
char app_version[64];
// Optional on the wire; missing or unrecognized values are unknown.
HostPlatform platform;
char** display_list;
std::size_t display_num;
int* left;
@@ -219,7 +226,8 @@ struct HostDisplay {
RemoteAction MakeHostInformation(const std::string& host_name,
const std::vector<HostDisplay>& displays,
bool supports_privacy_screen,
const std::string& app_version = {});
const std::string& app_version = {},
HostPlatform platform = HostPlatform::Unknown);
} // namespace crossdesk
+28 -1
View File
@@ -17,6 +17,7 @@ void ResetHostInfo(HostInfo& info) {
info.host_name[0] = '\0';
info.host_name_size = 0;
info.app_version[0] = '\0';
info.platform = HostPlatform::Unknown;
info.display_list = nullptr;
info.display_num = 0;
info.left = nullptr;
@@ -49,10 +50,29 @@ char* DuplicateString(const std::string& value) {
} // namespace
const char* HostPlatformName(HostPlatform platform) {
switch (platform) {
case HostPlatform::Windows: return "windows";
case HostPlatform::MacOS: return "macos";
case HostPlatform::Linux: return "linux";
case HostPlatform::IOS: return "ios";
default: return "unknown";
}
}
HostPlatform ParseHostPlatform(const std::string& platform) {
if (platform == "windows") return HostPlatform::Windows;
if (platform == "macos") return HostPlatform::MacOS;
if (platform == "linux") return HostPlatform::Linux;
if (platform == "ios") return HostPlatform::IOS;
return HostPlatform::Unknown;
}
RemoteAction MakeHostInformation(const std::string& host_name,
const std::vector<HostDisplay>& displays,
bool supports_privacy_screen,
const std::string& app_version) {
const std::string& app_version,
HostPlatform platform) {
RemoteAction action{};
action.type = ControlType::host_infomation;
ResetHostInfo(action.i);
@@ -73,6 +93,7 @@ RemoteAction MakeHostInformation(const std::string& host_name,
}
action.i.supports_privacy_screen = supports_privacy_screen;
action.i.platform = platform;
const std::size_t name_size =
std::min(host_name.size(), sizeof(action.i.host_name) - 1);
std::memcpy(action.i.host_name, host_name.data(), name_size);
@@ -192,6 +213,7 @@ std::string RemoteAction::ToJson(const RemoteAction& action) {
object["host_info"] = {
{"host_name", action.i.host_name},
{"app_version", action.i.app_version},
{"platform", HostPlatformName(action.i.platform)},
{"display_num", action.i.display_num},
{"displays", displays},
{"supports_privacy_screen", action.i.supports_privacy_screen},
@@ -368,6 +390,11 @@ bool RemoteAction::FromJson(const std::string& json_string,
ResetHostInfo(output.i);
owns_host_info = true;
const auto& host_info_object = object.at("host_info");
const auto platform = host_info_object.find("platform");
if (platform != host_info_object.end() && platform->is_string()) {
output.i.platform =
ParseHostPlatform(platform->get_ref<const std::string&>());
}
const auto app_version = host_info_object.find("app_version");
if (app_version != host_info_object.end() && app_version->is_string()) {
const auto& version = app_version->get_ref<const std::string&>();
+44
View File
@@ -135,6 +135,7 @@ int main() {
host.type = crossdesk::ControlType::host_infomation;
std::strcpy(host.i.host_name, "Test Mac");
std::strcpy(host.i.app_version, "1.5.3-2-20260928");
host.i.platform = crossdesk::HostPlatform::MacOS;
host.i.host_name_size = std::strlen(host.i.host_name);
host.i.display_list = display_names;
host.i.display_num = 1;
@@ -147,6 +148,7 @@ int main() {
parsed_host.i.display_num == 1 &&
std::string(parsed_host.i.host_name) == "Test Mac" &&
std::string(parsed_host.i.app_version) == "1.5.3-2-20260928" &&
parsed_host.i.platform == crossdesk::HostPlatform::MacOS &&
std::string(parsed_host.i.display_list[0]) ==
"Built-in Display" &&
parsed_host.i.right[0] == 2560 &&
@@ -162,6 +164,48 @@ int main() {
crossdesk::FreeRemoteAction(parsed_host);
crossdesk::FreeRemoteAction(advertised);
// The platform is optional metadata, not a requirement for connecting.
for (const auto& platform : std::vector<std::string>{
"windows", "linux", "macos", "ios", "unknown"}) {
advertised = crossdesk::MakeHostInformation(
"Host", {}, true, "1.5.3-20260928",
crossdesk::ParseHostPlatform(platform));
const auto message = advertised.to_json();
ok &= Expect(message.find("\"platform\":\"" + platform + "\"") !=
std::string::npos,
"host factory must advertise the canonical platform string");
ok &= Expect(parsed_host.from_json(message) &&
crossdesk::HostPlatformName(parsed_host.i.platform) == platform,
"host platform must survive a JSON round trip");
crossdesk::FreeRemoteAction(parsed_host);
crossdesk::FreeRemoteAction(advertised);
}
for (const auto& platform_field : std::vector<std::string>{
"", ",\"platform\":null", ",\"platform\":123", ",\"platform\":true",
",\"platform\":[]", ",\"platform\":{}", ",\"platform\":\"\"",
",\"platform\":\"future-os\"", ",\"platform\":\"windows\\u0000extra\"",
",\"platform\":\"" + std::string(256, 'x') + "\""}) {
const auto message =
"{\"type\":3,\"host_info\":{\"host_name\":\"Legacy\","
"\"display_num\":1,\"displays\":[{\"name\":\"Screen\",\"left\":0,"
"\"top\":0,\"right\":1920,\"bottom\":1080}]" + platform_field + "}}";
parsed_host.i.platform = crossdesk::HostPlatform::Windows;
ok &= Expect(parsed_host.from_json(message) &&
parsed_host.i.platform == crossdesk::HostPlatform::Unknown &&
std::string(parsed_host.i.host_name) == "Legacy" &&
parsed_host.i.display_num == 1 &&
parsed_host.i.right[0] == 1920 && parsed_host.i.bottom[0] == 1080,
"missing or invalid platform must be unknown without losing host info");
crossdesk::FreeRemoteAction(parsed_host);
}
advertised = crossdesk::MakeHostInformation("Legacy", {}, false);
ok &= Expect(advertised.i.platform == crossdesk::HostPlatform::Unknown &&
advertised.to_json().find("\"platform\":\"unknown\"") !=
std::string::npos,
"unspecified platform must be advertised as unknown");
crossdesk::FreeRemoteAction(advertised);
// Missing and malformed optional versions must not break legacy host info.
for (const auto& version_field : std::vector<std::string>{
"", ",\"app_version\":null", ",\"app_version\":123",