mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-17 09:23:10 +08:00
[feat] add host platform metadata and iOS platform placeholders
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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&>();
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user