From c2322176caae5ee65810f4d91b0606b571a7d458 Mon Sep 17 00:00:00 2001 From: artehe <112902041+artehe@users.noreply.github.com> Date: Tue, 25 Jul 2023 13:48:48 +0100 Subject: [PATCH] iOS backup creation --- Netimobiledevice/Backup/BackupFile.cs | 38 + .../Backup/BackupFileErrorEventArgs.cs | 15 + .../Backup/BackupFileEventArgs.cs | 40 + .../Backup/BackupResultEventArgs.cs | 32 + Netimobiledevice/Backup/BackupState.cs | 11 + Netimobiledevice/Backup/BackupStatus.cs | 69 + Netimobiledevice/Backup/DeviceBackup.cs | 1266 +++++++++++++++++ Netimobiledevice/Backup/DeviceLinkMessage.cs | 62 + Netimobiledevice/Backup/ErrNo.cs | 25 + Netimobiledevice/Backup/SnapshotState.cs | 33 + Netimobiledevice/Backup/StatusEventArgs.cs | 24 + .../Exceptions/DeviceDisconnectedException.cs | 12 + README.md | 23 +- 13 files changed, 1649 insertions(+), 1 deletion(-) create mode 100644 Netimobiledevice/Backup/BackupFile.cs create mode 100644 Netimobiledevice/Backup/BackupFileErrorEventArgs.cs create mode 100644 Netimobiledevice/Backup/BackupFileEventArgs.cs create mode 100644 Netimobiledevice/Backup/BackupResultEventArgs.cs create mode 100644 Netimobiledevice/Backup/BackupState.cs create mode 100644 Netimobiledevice/Backup/BackupStatus.cs create mode 100644 Netimobiledevice/Backup/DeviceBackup.cs create mode 100644 Netimobiledevice/Backup/DeviceLinkMessage.cs create mode 100644 Netimobiledevice/Backup/ErrNo.cs create mode 100644 Netimobiledevice/Backup/SnapshotState.cs create mode 100644 Netimobiledevice/Backup/StatusEventArgs.cs create mode 100644 Netimobiledevice/Exceptions/DeviceDisconnectedException.cs diff --git a/Netimobiledevice/Backup/BackupFile.cs b/Netimobiledevice/Backup/BackupFile.cs new file mode 100644 index 0000000..e599383 --- /dev/null +++ b/Netimobiledevice/Backup/BackupFile.cs @@ -0,0 +1,38 @@ +using System.IO; + +namespace Netimobiledevice.Backup +{ + /// + /// Represents a file in the backup process. + /// + public class BackupFile + { + /// + /// The absolute path in the device. + /// + public string DevicePath { get; } + + /// + /// Relative path to the backup folder, includes the device UDID. + /// + public string BackupPath { get; } + + /// + /// The absolute path in the local backup folder. + /// + public string LocalPath { get; } + + /// + /// Creates an instance of a BackupFile + /// + /// The absolute path in the device. + /// Relative path to the backup folder, includes the device UDID. + /// Absolute path to the backup directory + public BackupFile(string devicePath, string backupPath, string backupDirectory) + { + DevicePath = devicePath; + BackupPath = backupPath; + LocalPath = Path.Combine(backupDirectory, backupPath); + } + } +} diff --git a/Netimobiledevice/Backup/BackupFileErrorEventArgs.cs b/Netimobiledevice/Backup/BackupFileErrorEventArgs.cs new file mode 100644 index 0000000..471cc3d --- /dev/null +++ b/Netimobiledevice/Backup/BackupFileErrorEventArgs.cs @@ -0,0 +1,15 @@ +namespace Netimobiledevice.Backup +{ + /// + /// EventArgs for File Transfer Error events. + /// + public class BackupFileErrorEventArgs : BackupFileEventArgs + { + /// + /// Indicates whether the backup should be cancelled. + /// + public bool Cancel { get; set; } + + public BackupFileErrorEventArgs(BackupFile file) : base(file) { } + } +} diff --git a/Netimobiledevice/Backup/BackupFileEventArgs.cs b/Netimobiledevice/Backup/BackupFileEventArgs.cs new file mode 100644 index 0000000..778356c --- /dev/null +++ b/Netimobiledevice/Backup/BackupFileEventArgs.cs @@ -0,0 +1,40 @@ +using System; + +namespace Netimobiledevice.Backup +{ + /// + /// EventArgs for BackupFile related events. + /// + public class BackupFileEventArgs : EventArgs + { + /// + /// The BackupFile related to the event. + /// + public BackupFile File { get; } + /// + /// The file contents associated with the backup file + /// + public byte[] Data { get; } + + /// + /// Creates an instance of the BackupFileEventArgs class. + /// + /// The BackupFile related to the event. + public BackupFileEventArgs(BackupFile file) + { + File = file; + Data = Array.Empty(); + } + + /// + /// Creates an instance of the BackupFileEventArgs class. + /// + /// The BackupFile related to the event. + /// The content of the BackupFile + public BackupFileEventArgs(BackupFile file, byte[] data) + { + File = file; + Data = data; + } + } +} diff --git a/Netimobiledevice/Backup/BackupResultEventArgs.cs b/Netimobiledevice/Backup/BackupResultEventArgs.cs new file mode 100644 index 0000000..6b62b6b --- /dev/null +++ b/Netimobiledevice/Backup/BackupResultEventArgs.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Netimobiledevice.Backup +{ + /// + /// EventArgs including information about the backup process result. + /// + public class BackupResultEventArgs : EventArgs + { + /// + /// Indicates whether the user cancelled the backup process. + /// + public bool UserCancelled { get; } + + /// + /// Indicates whether the backup has finished due to a device disconnection. + /// + public bool DeviceDisconnected { get; } + + /// + /// The files that failed to transfer during the backup. + /// + public IReadOnlyList TransferErrors { get; } + + public BackupResultEventArgs(IEnumerable failedFiles, bool userCancelled, bool deviceDisconnected) + { + UserCancelled = userCancelled; + TransferErrors = new List(failedFiles); + } + } +} diff --git a/Netimobiledevice/Backup/BackupState.cs b/Netimobiledevice/Backup/BackupState.cs new file mode 100644 index 0000000..702a041 --- /dev/null +++ b/Netimobiledevice/Backup/BackupState.cs @@ -0,0 +1,11 @@ +namespace Netimobiledevice.Backup +{ + /// + /// The backup state. + /// + public enum BackupState + { + Empty, + New, + } +} diff --git a/Netimobiledevice/Backup/BackupStatus.cs b/Netimobiledevice/Backup/BackupStatus.cs new file mode 100644 index 0000000..f1fb260 --- /dev/null +++ b/Netimobiledevice/Backup/BackupStatus.cs @@ -0,0 +1,69 @@ +using Netimobiledevice.Plist; +using System; +using System.Diagnostics; +using System.Globalization; + +namespace Netimobiledevice.Backup +{ + /// + /// Represents the Status.plist of a backup + /// + public class BackupStatus + { + /// + /// The backup unique identifier. + /// + public string UUID { get; } + /// + /// The backup timestamp. + /// + public DateTime Date { get; } + /// + /// Indicates whether the backup is a full one or incremental. + /// + public bool IsFullBackup { get; } + /// + /// Version of the backup protocol. + /// + public Version Version { get; } + /// + /// The backup state. + /// + public BackupState BackupState { get; } + /// + /// The snapshot state. + /// + public SnapshotState SnapshotState { get; } + + /// + /// Creates an instance of BackupStatus. + /// + /// The dictionary from the Status.plist file. + public BackupStatus(DictionaryNode status) + { + UUID = status["UUID"].AsStringNode().Value; + Date = status["Date"].AsDateNode().Value; + Version = Version.Parse(status["Version"].AsStringNode().Value); + IsFullBackup = status["IsFullBackup"].AsBooleanNode().Value; + + CultureInfo cultureInfo = CultureInfo.InvariantCulture; + TextInfo textInfo = cultureInfo.TextInfo; + + string backupStateString = textInfo.ToTitleCase(status["BackupState"].AsStringNode().Value); + if (Enum.TryParse(backupStateString, out BackupState state)) { + BackupState = state; + } + else { + Debug.WriteLine($"WARNING: New Backup state found: {backupStateString}"); + } + + string snapshotStateString = textInfo.ToTitleCase(status["SnapshotState"].AsStringNode().Value); + if (Enum.TryParse(snapshotStateString, out SnapshotState snapshotState)) { + SnapshotState = snapshotState; + } + else { + Debug.WriteLine($"WARNING: New Snapshot state found: {snapshotStateString}"); + } + } + } +} diff --git a/Netimobiledevice/Backup/DeviceBackup.cs b/Netimobiledevice/Backup/DeviceBackup.cs new file mode 100644 index 0000000..ff46883 --- /dev/null +++ b/Netimobiledevice/Backup/DeviceBackup.cs @@ -0,0 +1,1266 @@ +using Netimobiledevice.EndianBitConversion; +using Netimobiledevice.Exceptions; +using Netimobiledevice.Lockdown; +using Netimobiledevice.Lockdown.Services; +using Netimobiledevice.Plist; +using Netimobiledevice.Usbmuxd; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Netimobiledevice.Backup +{ + public class DeviceBackup : IDisposable + { + /// + /// iTunes files to be inserted into the Info.plist file. + /// + private static readonly string[] iTunesFiles = new string[] { + "ApertureAlbumPrefs", + "IC-Info.sidb", + "IC-Info.sidv", + "PhotosFolderAlbums", + "PhotosFolderName", + "PhotosFolderPrefs", + "VoiceMemos.plist", + "iPhotoAlbumPrefs", + "iTunesApplicationIDs", + "iTunesPrefs", + "iTunesPrefs.plist" + }; + + /// + /// The AFC service. + /// + private AfcService? afcService; + /// + /// The last backup status received. + /// + private BackupStatus? lastStatus = null; + /// + /// The Notification service. + /// + private NotificationProxyService? notificationProxyService; + /// + /// The current snapshot state for the backup. + /// + private SnapshotState snapshotState = SnapshotState.Uninitialized; + /// + /// The sync lock identifier. + /// + private ulong syncLock; + /// + /// Indicates whether the device was disconnected during the backup process. + /// + protected bool deviceDisconnected = false; + /// + /// The backup service. + /// + protected Mobilebackup2Service? mobilebackup2Service; + /// + /// The exception that caused the backup to fail. + /// + protected Exception? terminatingException; + /// + /// Indicates whether the user cancelled the backup process. + /// + protected bool userCancelled = false; + /// + /// A list of the files whose transfer failed due to a device error. + /// + protected readonly List failedFiles = new List(); + + protected bool IsFinished { get; set; } = false; + /// + /// The Lockdown client. + /// + private LockdownClient LockdownClient { get; } + /// + /// The flag for cancelling the backup process. + /// + protected bool IsCancelling { get; set; } = false; + /// + /// Indicates whether the backup is encrypted. + /// + public bool IsEncrypted { get; protected set; } = false; + public bool IsStopping => IsCancelling || IsFinished; + /// + /// The path to the backup folder, without the device UDID. + /// + public string BackupDirectory { get; } + /// + /// The path to the backup folder, including the device UDID. + /// + public string DeviceBackupPath { get; } + /// + /// Indicates whether the backup is currently in progress. + /// + public bool InProgress { get; protected set; } + /// + /// Indicates the backup progress, in a 0 to 100,000 range (in order to obtain a smoother integer progress). + /// + public double ProgressPercentage { get; protected set; } + /// + /// The time at which the backup started. + /// + public DateTime StartTime { get; protected set; } + + /// + /// Event raised when a file is about to be transferred from the device. + /// + public event EventHandler? BeforeReceivingFile; + /// + /// Event raised when the backup finishes. + /// + public event EventHandler? Completed; + /// + /// Event raised when there is some error during the backup. + /// + public event EventHandler? Error; + /// + /// Event raised when a file is received from the device. + /// + public event EventHandler? FileReceived; + /// + /// Event raised when a part of a file has been received from the device. + /// + public event EventHandler? FileReceiving; + /// + /// Event raised when a file transfer has failed due an internal device error. + /// + public event EventHandler? FileTransferError; + /// + /// Event raised when the device requires a passcode to start the backup + /// + public event EventHandler? PasscodeRequiredForBackup; + /// + /// Event raised for signaling the backup progress. + /// + public event ProgressChangedEventHandler? Progress; + /// + /// Event raised when the backup started. + /// + public event EventHandler? Started; + /// + /// Event raised for signaling different kinds of the backup status. + /// + public event EventHandler? Status; + + /// + /// Creates an instance of a BackupJob class. + /// + /// The lockdown client for the device that will be backed-up. + /// The folder to store the backup data. Without the device UDID. + public DeviceBackup(LockdownClient lockdown, string backupFolder) + { + LockdownClient = lockdown; + BackupDirectory = backupFolder; + DeviceBackupPath = Path.Combine(BackupDirectory, lockdown.UDID); + } + + /// + /// Destructor of the BackupJob class. + /// + ~DeviceBackup() + { + Dispose(); + } + + private async Task AquireBackupLock() + { + notificationProxyService?.Post(Notification.SyncWillStart); + syncLock = afcService?.FileOpen("/com.apple.itunes.lock_sync", "r+") ?? 0; + + if (syncLock != 0) { + notificationProxyService?.Post(Notification.SyncLockRequest); + for (int i = 0; i < 50; i++) { + bool lockAquired = false; + try { + afcService?.Lock(syncLock, AfcLockModes.ExclusiveLock); + lockAquired = true; + } + catch (AfcException e) { + if (e.AfcError == AfcError.OpWouldBlock) { + await Task.Delay(200); + } + else { + afcService?.FileClose(syncLock); + throw; + } + } + catch (Exception) { + throw; + } + + if (lockAquired) { + notificationProxyService?.Post(Notification.SyncDidStart); + break; + } + } + } + else { + // Lock failed + afcService?.FileClose(syncLock); + throw new Exception("Failed to lock iTunes backup sync file"); + } + } + + /// + /// Cleans the used resources. + /// + private void CleanResources() + { + if (InProgress) { + IsCancelling = true; + } + + Unlock(); + + notificationProxyService?.Dispose(); + mobilebackup2Service?.Dispose(); + afcService?.Dispose(); + LockdownClient?.Dispose(); + InProgress = false; + } + + /// + /// Backup creation task entry point. + /// + private async Task CreateBackup() + { + lastStatus = null; + InProgress = true; + IsCancelling = false; + IsFinished = false; + userCancelled = false; + deviceDisconnected = false; + StartTime = DateTime.Now; + ProgressPercentage = 0.0; + terminatingException = null; + snapshotState = SnapshotState.Uninitialized; + + Debug.WriteLine($"Starting backup of device {LockdownClient.GetValue("ProductType")?.AsStringNode().Value} v{LockdownClient.IOSVersion}"); + + if (Directory.Exists(DeviceBackupPath)) { + Directory.Delete(DeviceBackupPath, true); + } + Directory.CreateDirectory(DeviceBackupPath); + + Debug.WriteLine($"Saving at {DeviceBackupPath}"); + try { + IsEncrypted = LockdownClient.GetValue("com.apple.mobile.backup", "WillEncrypt")?.AsBooleanNode().Value ?? false; + Debug.WriteLine($"The backup will{(IsEncrypted ? null : " not")} be encrypted."); + + afcService = new AfcService(LockdownClient); + mobilebackup2Service = new Mobilebackup2Service(LockdownClient); + notificationProxyService = new NotificationProxyService(LockdownClient); + + await mobilebackup2Service.LoadDeviceLink(); + + await AquireBackupLock(); + + OnStatus("Initializing backup ..."); + DictionaryNode options = CreateBackupOptions(); + mobilebackup2Service.SendRequest("Backup", LockdownClient.UDID, LockdownClient.UDID, options); + + // iOS versions 15.7.1 and anything 16.1 or newer will require you to input a passcode before + // it can start a backup so we make sure to notify the user about this. + if (LockdownClient.IOSVersion >= new Version(15, 7, 1)) { + PasscodeRequiredForBackup?.Invoke(this, EventArgs.Empty); + } + + await MessageLoop(); + } + catch (Exception ex) { + OnError(ex); + CleanResources(); + return; + } + } + + /// + /// Creates a dictionary plist instance of the required error report for the device. + /// + /// The errno code. + private static DictionaryNode CreateErrorReport(int errorNo) + { + string errMsg; + int errCode = -errorNo; + + if (errorNo == (int) ErrNo.ENOENT) { + errCode = -6; + errMsg = "No such file or directory."; + } + else if (errorNo == (int) ErrNo.EEXIST) { + errCode = -7; + errMsg = "File or directory already exists."; + } + else { + errMsg = $"Unspecified error: ({errorNo})"; + } + + DictionaryNode dict = new DictionaryNode() { + { "DLFileErrorString", new StringNode(errMsg) }, + { "DLFileErrorCode", new IntegerNode(errCode) } + }; + return dict; + } + + /// + /// Creates the Info.plist dictionary. + /// + /// The created Info.plist as a DictionaryNode. + private async Task CreateInfoPlist() + { + DictionaryNode info = new DictionaryNode(); + + (DictionaryNode appDict, ArrayNode installedApps) = await CreateInstalledAppList(); + info.Add("Applications", appDict); + + DictionaryNode? rootNode = LockdownClient.GetValue()?.AsDictionaryNode(); + if (rootNode != null) { + info.Add("Build Version", rootNode["BuildVersion"]); + info.Add("Device Name", rootNode["DeviceName"]); + info.Add("Display Name", rootNode["DeviceName"]); + info.Add("GUID", new StringNode(Guid.NewGuid().ToString())); + + if (rootNode.ContainsKey("IntegratedCircuitCardIdentity")) { + info.Add("ICCID", rootNode["IntegratedCircuitCardIdentity"]); + } + if (rootNode.ContainsKey("InternationalMobileEquipmentIdentity")) { + info.Add("IMEI", rootNode["InternationalMobileEquipmentIdentity"]); + } + + info.Add("Installed Applications", installedApps); + info.Add("Last Backup Date", new DateNode(StartTime)); + + if (rootNode.ContainsKey("MobileEquipmentIdentifier")) { + info.Add("MEID", rootNode["MobileEquipmentIdentifier"]); + } + if (rootNode.ContainsKey("PhoneNumber")) { + info.Add("Phone Number", rootNode["PhoneNumber"]); + } + + info.Add("Product Type", rootNode["ProductType"]); + info.Add("Product Version", rootNode["ProductVersion"]); + info.Add("Serial Number", rootNode["SerialNumber"]); + + info.Add("Target Identifier", new StringNode(LockdownClient.UDID.ToUpper())); + info.Add("Target Type", new StringNode("Device")); + info.Add("Unique Identifier", new StringNode(LockdownClient.UDID.ToUpper())); + } + + try { + byte[] dataBuffer = afcService?.GetFileContents("/Books/iBooksData2.plist") ?? Array.Empty(); + info.Add("iBooks Data 2", new DataNode(dataBuffer)); + } + catch (AfcException ex) { + if (ex.AfcError != AfcError.ObjectNotFound) { + throw; + } + } + + DictionaryNode files = new DictionaryNode(); + foreach (string iTuneFile in iTunesFiles) { + try { + string filePath = Path.Combine("/iTunes_Control/iTunes", iTuneFile); + byte[] dataBuffer = afcService?.GetFileContents(filePath) ?? Array.Empty(); + files.Add(iTuneFile, new DataNode(dataBuffer)); + } + catch (AfcException ex) { + if (ex.AfcError == AfcError.ObjectNotFound) { + continue; + } + else { + throw; + } + } + } + info.Add("iTunes Files", files); + + PropertyNode? itunesSettings = LockdownClient.GetValue("com.apple.iTunes", null); + info.Add("iTunes Settings", itunesSettings ?? new DictionaryNode()); + + // If we don't have iTunes, then let's get the minimum required iTunes version from the device + PropertyNode? minItunesVersion = LockdownClient.GetValue("com.apple.mobile.iTunes", "MinITunesVersion"); + info.Add("iTunes Version", minItunesVersion ?? new StringNode("10.0.1")); + + return info; + } + + /// + /// Creates the application array and dictionary for the Info.plist file. + /// + /// The application dictionary and array of applications bundle ids. + private async Task<(DictionaryNode, ArrayNode)> CreateInstalledAppList() + { + InstallationProxyService installationProxyService = new InstallationProxyService(LockdownClient); + SpringBoardServicesService springBoardServicesService = new SpringBoardServicesService(LockdownClient); + + DictionaryNode appDict = new DictionaryNode(); + ArrayNode installedApps = new ArrayNode(); + + try { + ArrayNode apps = await installationProxyService.Browse( + new DictionaryNode() { { "ApplicationType", new StringNode("User") } }, + new ArrayNode() { new StringNode("CFBundleIdentifier"), new StringNode("ApplicationSINF"), new StringNode("iTunesMetadata") }); + foreach (DictionaryNode app in apps.Cast()) { + if (app.ContainsKey("CFBundleIdentifier")) { + StringNode bundleId = app["CFBundleIdentifier"].AsStringNode(); + installedApps.Add(bundleId); + if (app.ContainsKey("iTunesMetadata") && app.ContainsKey("ApplicationSINF")) { + appDict.Add(bundleId.Value, new DictionaryNode() { + { "ApplicationSINF", app["ApplicationSINF"] }, + { "iTunesMetadata", app["iTunesMetadata"] }, + { "PlaceholderIcon", springBoardServicesService.GetIconPNGData(bundleId.Value) }, + }); + } + } + } + } + catch (Exception ex) { + Debug.WriteLine($"ERROR: Creating application list for Info.plist"); + Debug.WriteLine(ex); + } + return (appDict, installedApps); + } + + /// + /// Gets the free space on the disk containing the specified path. + /// + /// The path that specifies the disk to retrieve its free space. + /// The number of bytes of free space in the disk specified by path. + private static long GetFreeSpace(string path) + { + var dir = new DirectoryInfo(path); + foreach (DriveInfo drive in DriveInfo.GetDrives()) { + try { + if (drive.IsReady && drive.Name == dir.Root.FullName) { + return drive.AvailableFreeSpace; + } + } + catch (Exception ex) { + Debug.WriteLine($"Error: {ex}"); + } + } + return 0; + } + + /// + /// The main loop for processing messages from the device. + /// + private async Task MessageLoop() + { + bool isFirstMessage = true; + + Debug.WriteLine("Starting the backup message loop."); + while (!IsStopping) { + try { + if (mobilebackup2Service != null) { + ArrayNode msg = await mobilebackup2Service.ReceiveMessage(); + if (msg != null) { + // Reset waiting state + if (snapshotState == SnapshotState.Waiting) { + OnSnapshotStateChanged(snapshotState, snapshotState = lastStatus?.SnapshotState ?? SnapshotState.Waiting); + } + + // If it's the first message that isn't null report that the backup is started + if (isFirstMessage) { + OnBackupStarted(); + await SaveInfoPropertyList(); + isFirstMessage = false; + } + + try { + OnMessageReceived(msg, msg[0].AsStringNode().Value); + } + catch (Exception ex) { + OnError(ex); + } + } + else if (!Usbmux.IsDeviceConnected(LockdownClient.UDID)) { + throw new DeviceDisconnectedException(); + } + } + } + catch (TimeoutException) { + OnSnapshotStateChanged(snapshotState, SnapshotState.Waiting); + OnStatus("Waiting for device to be ready ..."); + await Task.Delay(100); + } + catch (Exception ex) { + Debug.WriteLine($"ERROR Receiving message"); + OnError(ex); + break; + } + } + + // Check if the execution arrived here due to a device disconnection. + if (terminatingException == null && !Usbmux.IsDeviceConnected(LockdownClient.UDID)) { + throw new DeviceDisconnectedException(); + } + + Debug.WriteLine($"Finished message loop. Cancelling = {IsCancelling}, Finished = {IsFinished}"); + OnBackupCompleted(); + } + + /// + /// Manages the CopyItem device message. + /// + /// The message received from the device. + /// The errno result of the operation. + private void OnCopyItem(ArrayNode msg) + { + int errorCode = 0; + string errorDesc = string.Empty; + string srcPath = Path.Combine(BackupDirectory, msg[1].AsStringNode().Value); + string dstPath = Path.Combine(BackupDirectory, msg[2].AsStringNode().Value); + + var source = new FileInfo(srcPath); + if (source.Attributes.HasFlag(FileAttributes.Directory)) { + Debug.WriteLine($"ERROR: Are you really asking me to copy a whole directory?"); + } + else { + File.Copy(source.FullName, new FileInfo(dstPath).FullName); + } + mobilebackup2Service?.SendStatusReport(errorCode, errorDesc); + } + + /// + /// Manages the CreateDirectory device message. + /// + /// The message received from the device. + /// The errno result of the operation. + private void OnCreateDirectory(ArrayNode msg) + { + int errorCode = 0; + string errorMessage = ""; + UpdateProgressForMessage(msg, 3); + var newDir = new DirectoryInfo(Path.Combine(BackupDirectory, msg[1].AsStringNode().Value)); + if (!newDir.Exists) { + newDir.Create(); + } + mobilebackup2Service?.SendStatusReport(errorCode, errorMessage); + } + + /// + /// Manages the DownloadFiles device message. + /// + /// The message received from the device. + private void OnDownloadFiles(ArrayNode msg) + { + UpdateProgressForMessage(msg, 3); + + DictionaryNode errList = new DictionaryNode(); + ArrayNode files = msg[1].AsArrayNode(); + foreach (StringNode filename in files.Cast()) { + if (IsStopping) { + break; + } + else { + SendFile(filename.Value, errList); + } + } + + if (!IsStopping) { + byte[] fileTransferTerminator = new byte[4]; + mobilebackup2Service?.SendRaw(fileTransferTerminator); + if (errList.Count == 0) { + mobilebackup2Service?.SendStatusReport(0, null, null); + } + else { + mobilebackup2Service?.SendStatusReport(-13, "Multi status", errList); + } + } + } + + /// + /// Manages the ListDirectory device message. + /// + /// The message received from the device. + /// Always 0. + private void OnListDirectory(ArrayNode msg) + { + string path = Path.Combine(BackupDirectory, msg[1].AsStringNode().Value); + DictionaryNode dirList = new DictionaryNode(); + var dir = new DirectoryInfo(path); + if (dir.Exists) { + foreach (FileSystemInfo entry in dir.GetFileSystemInfos()) { + if (IsStopping) { + break; + } + var entryDict = new DictionaryNode { + { "DLFileModificationDate", new DateNode(entry.LastWriteTime) }, + { "DLFileSize", new IntegerNode(entry is FileInfo fileInfo ? fileInfo.Length : 0L) }, + { "DLFileType", new StringNode(entry.Attributes.HasFlag(FileAttributes.Directory) ? "DLFileTypeDirectory" : "DLFileTypeRegular") } + }; + dirList.Add(entry.Name, entryDict); + } + } + + if (!IsStopping) { + mobilebackup2Service?.SendStatusReport(0, null, dirList); + } + } + + /// + /// Process the message received from the backup service. + /// + /// The property array received. + /// The string that identifies the message type. + /// Depends on the message type, but a negative value always indicates an error. + private void OnMessageReceived(ArrayNode msg, string message) + { + Debug.WriteLine($"Message Received: {message}"); + switch (message) { + case DeviceLinkMessage.DownloadFiles: { + OnDownloadFiles(msg); + break; + } + case DeviceLinkMessage.GetFreeDiskSpace: { + OnGetFreeDiskSpace(msg); + break; + } + case DeviceLinkMessage.CreateDirectory: { + OnCreateDirectory(msg); + break; + } + case DeviceLinkMessage.UploadFiles: { + OnUploadFiles(msg); + break; + } + case DeviceLinkMessage.ContentsOfDirectory: { + OnListDirectory(msg); + break; + } + case DeviceLinkMessage.MoveFiles: + case DeviceLinkMessage.MoveItems: { + OnMoveItems(msg); + break; + } + case DeviceLinkMessage.RemoveFiles: + case DeviceLinkMessage.RemoveItems: { + OnRemoveItems(msg); + break; + } + case DeviceLinkMessage.CopyItem: { + OnCopyItem(msg); + break; + } + case DeviceLinkMessage.Disconnect: { + IsCancelling = true; + break; + } + case DeviceLinkMessage.ProcessMessage: { + OnProcessMessage(msg); + break; + } + default: { + Debug.WriteLine($"WARNING: Unknown message in MessageLoop: {message}"); + mobilebackup2Service?.SendStatusReport(1, "Operation not supported"); + break; + } + } + } + + /// + /// Manages the MoveItems device message. + /// + /// The message received from the device. + /// The number of items moved. + private void OnMoveItems(ArrayNode msg) + { + int res = 0; + int errorCode = 0; + string errorDesc = string.Empty; + UpdateProgressForMessage(msg, 3); + foreach (KeyValuePair move in msg[1].AsDictionaryNode()) { + if (IsStopping) { + break; + } + + string newPath = move.Value.AsStringNode().Value; + if (!string.IsNullOrEmpty(newPath)) { + res++; + var newFile = new FileInfo(Path.Combine(BackupDirectory, newPath)); + var oldFile = new FileInfo(Path.Combine(BackupDirectory, move.Key)); + var fileInfo = new FileInfo(newPath); + if (fileInfo.Exists) { + if (fileInfo.Attributes.HasFlag(FileAttributes.Directory)) { + new DirectoryInfo(newFile.FullName).Delete(true); + } + else { + fileInfo.Delete(); + } + } + + if (oldFile.Exists) { + oldFile.MoveTo(newFile.FullName); + } + } + } + + mobilebackup2Service?.SendStatusReport(errorCode, errorDesc); + } + + private void OnProcessMessage(ArrayNode msg) + { + int resultCode = ProcessMessage(msg); + switch (resultCode) { + case 0: { + IsFinished = true; + break; + } + case -38: { + OnError(new Exception("Backing up the phone is denied by managing organisation")); + break; + } + case -207: { + OnError(new Exception("No backup encryption password set but is required by managing organisation")); + break; + } + case -208: { + // Device locked which most commonly happens when requesting a backup but the user either + // hit cancel or the screen turned off again locking the phone and cancelling the backup. + OnError(new Exception("Device locked (MBErrorDomain/208)")); + break; + } + default: { + Debug.WriteLine($"ERROR On ProcessMessage: {resultCode}"); + DictionaryNode msgDict = msg[1].AsDictionaryNode(); + if (msgDict.TryGetValue("ErrorDescription", out PropertyNode errDescription)) { + throw new Exception($"Error {resultCode}: {errDescription.AsStringNode().Value}"); + } + else { + throw new Exception($"Error {resultCode}"); + } + } + } + } + + /// + /// Manages the RemoveItems device message. + /// + /// The message received from the device. + /// The number of items removed. + private void OnRemoveItems(ArrayNode msg) + { + UpdateProgressForMessage(msg, 3); + + int errorCode = 0; + string errorDesc = ""; + ArrayNode removes = msg[1].AsArrayNode(); + foreach (StringNode filename in removes.Cast()) { + if (IsStopping) { + break; + } + + if (string.IsNullOrEmpty(filename.Value)) { + Debug.WriteLine("WARNING: Empty file to remove."); + } + else { + var file = new FileInfo(Path.Combine(BackupDirectory, filename.Value)); + if (file.Exists) { + if (file.Attributes.HasFlag(FileAttributes.Directory)) { + Directory.Delete(file.FullName, true); + } + else { + file.Delete(); + } + } + } + } + + if (!IsStopping) { + mobilebackup2Service?.SendStatusReport(errorCode, errorDesc); + } + } + + /// + /// Manages the UploadFiles device message. + /// + /// The message received from the device. + /// The number of files processed. + private void OnUploadFiles(ArrayNode msg) + { + string errorDescription = string.Empty; + int fileCount = 0; + int errorCode = 0; + UpdateProgressForMessage(msg, 2); + + int nlen = 0; + long backupRealSize = 0; + long backupTotalSize = (long) msg[3].AsIntegerNode().Value; + if (backupTotalSize > 0) { + Debug.WriteLine($"Backup total size: {backupTotalSize}"); + } + + while (!IsStopping) { + BackupFile? backupFile = ReceiveBackupFile(); + if (backupFile != null) { + Debug.WriteLine($"Receiving file {backupFile.BackupPath}"); + OnBeforeReceivingFile(backupFile); + ResultCode code = ReceiveFile(backupFile, backupTotalSize, ref backupRealSize, out nlen); + if (code == ResultCode.Success) { + OnFileReceived(backupFile); + } + else if (code != ResultCode.Skipped) { + Debug.WriteLine($"ERROR Receiving {backupFile.BackupPath}: {code}"); + } + fileCount++; + } + else if (Usbmux.IsDeviceConnected(LockdownClient.UDID, UsbmuxdConnectionType.Usb)) { + break; + } + else { + throw new DeviceDisconnectedException(); + } + } + + if (!IsStopping) { + // If there are leftovers to read, finish up cleanly. + if (--nlen > 0) { + mobilebackup2Service?.ReceiveRaw(nlen); + } + mobilebackup2Service?.SendStatusReport(errorCode, errorDescription); + } + } + + + /// + /// Processes a message response received from the backup service. + /// + /// The message received. + /// The result status code from the message. + private static int ProcessMessage(ArrayNode msg) + { + DictionaryNode tmp = msg[1].AsDictionaryNode(); + int errorCode = (int) tmp["ErrorCode"].AsIntegerNode().Value; + string errorDescription = tmp["ErrorDescription"].AsStringNode().Value; + if (errorCode != 0) { + Debug.WriteLine($"ERROR: Code: {errorCode} {errorDescription}"); + } + return -errorCode; + } + + /// + /// Reads the information of the next file that the backup service will send. + /// + /// Returns the file information of the next file to download, or null if there are no more files to download. + private BackupFile? ReceiveBackupFile() + { + int len = ReceiveFilename(out string devicePath); + if (len == 0) { + return null; + } + len = ReceiveFilename(out string backupPath); + if (len <= 0) { + Debug.WriteLine("WARNING Error reading backup file path."); + } + return new BackupFile(devicePath, backupPath, BackupDirectory); + } + + /// + /// Reads a filename from the backup service stream. + /// + /// The filename read from the backup stream, or NULL if there are no more files. + /// The length of the filename read. + private int ReceiveFilename(out string filename) + { + filename = string.Empty; + int len = ReadInt32(); + + // A zero length means no more files to receive. + if (len != 0) { + byte[] buffer = mobilebackup2Service?.ReceiveRaw(len) ?? Array.Empty(); + filename = Encoding.UTF8.GetString(buffer); + } + return len; + } + + private ResultCode ReadCode() + { + byte[] buffer = mobilebackup2Service?.ReceiveRaw(1) ?? Array.Empty(); + + byte code = buffer[0]; + if (!Enum.IsDefined(typeof(ResultCode), code)) { + Debug.WriteLine($"WARNING: New backup code found: {code}"); + } + ResultCode result = (ResultCode) code; + + return result; + } + + /// + /// Reads an Int32 value from the backup service. + /// + /// The Int32 value read. + private int ReadInt32() + { + byte[] buffer = mobilebackup2Service?.ReceiveRaw(4) ?? Array.Empty(); + if (buffer.Length > 0) { + return EndianBitConverter.BigEndian.ToInt32(buffer, 0); + } + return -1; + } + + /// + /// Sends a single file to the device. + /// + /// The relative filename requested to send. + /// The error list to append the eventual local error happening. + /// The errno result of the operation. + private void SendFile(string filename, DictionaryNode errList) + { + Debug.WriteLine($"Sending file: {filename}"); + + mobilebackup2Service?.SendPath(filename); + string localFile = Path.Combine(BackupDirectory, filename); + FileInfo fileInfo = new FileInfo(localFile); + int errorCode; + if (!fileInfo.Exists) { + errorCode = 2; + } + else if (fileInfo.Length == 0) { + errorCode = 0; + } + else { + SendFile(fileInfo); + errorCode = 0; + } + + if (errorCode == 0) { + var bytes = new List(EndianBitConverter.BigEndian.GetBytes(1)) { + (byte) ResultCode.Success + }; + mobilebackup2Service?.SendRaw(bytes.ToArray()); + } + else { + Debug.WriteLine($"Sending Error Code: {errorCode}"); + DictionaryNode errReport = CreateErrorReport(errorCode); + errList.Add(filename, errReport); + mobilebackup2Service?.SendError(errReport); + } + } + + /// + /// Sends the specified file to the device. + /// + /// The FileInfo of the file to send. + /// The MobileBackup2Error result of the native call. + private void SendFile(FileInfo fileInfo) + { + const int maxBufferSize = 32768; + long remaining = fileInfo.Length; + using (FileStream stream = File.OpenRead(fileInfo.FullName)) { + while (remaining > 0) { + int toSend = (int) Math.Min(maxBufferSize, remaining); + var bytes = new List(EndianBitConverter.BigEndian.GetBytes(toSend)) { + (byte) ResultCode.FileData + }; + mobilebackup2Service?.SendRaw(bytes.ToArray()); + + byte[] buffer = new byte[toSend]; + int read = stream.Read(buffer, 0, toSend); + mobilebackup2Service?.SendRaw(buffer); + + remaining -= read; + } + } + } + + /// + /// Unlocks the sync file. + /// + private void Unlock() + { + if (syncLock != 0) { + afcService?.Lock(syncLock, AfcLockModes.Unlock); + syncLock = 0; + } + } + + /// + /// Updates the backup progress as signaled by the backup service. + /// + /// The message received containing the progress information. + /// The index of the element in the array that contains the progress value. + private void UpdateProgressForMessage(ArrayNode msg, int index) + { + double progress = msg[index].AsRealNode().Value; + if (progress > 0.0) { + ProgressPercentage = progress; + OnBackupProgress(); + } + } + + /// + /// Creates a dictionary with the options for the backup. + /// + /// A PropertyDict containing the backup options. + protected virtual DictionaryNode CreateBackupOptions() + { + DictionaryNode options = new DictionaryNode { + { "ForceFullBackup", new BooleanNode(true) } + }; + return options; + } + + /// + /// Event handler called when the backup is completed. + /// + protected virtual void OnBackupCompleted() + { + Debug.WriteLine("Device Backup Completed"); + CleanResources(); + Completed?.Invoke(this, new BackupResultEventArgs(failedFiles, userCancelled, deviceDisconnected)); + } + + /// + /// Event handler called to report progress. + /// + /// The filename related to the progress. + protected virtual void OnBackupProgress() + { + Progress?.Invoke(this, new ProgressChangedEventArgs((int) ProgressPercentage, null)); + } + + /// + /// Event handler called when the backup has actually started. + /// + protected virtual void OnBackupStarted() + { + notificationProxyService?.Post(Notification.SyncDidStart); + Started?.Invoke(this, EventArgs.Empty); + } + + /// + /// Event handler called before a file is to be received from the device. + /// + /// The file to be received. + protected virtual void OnBeforeReceivingFile(BackupFile file) + { + BeforeReceivingFile?.Invoke(this, new BackupFileEventArgs(file)); + } + + /// + /// Event handler called when a terminating error happens during the backup. + /// + /// + protected virtual void OnError(Exception ex) + { + IsCancelling = true; + deviceDisconnected = Usbmux.IsDeviceConnected(LockdownClient.UDID); + Debug.WriteLine($"BackupJob.OnError: {ex.Message}"); + terminatingException = deviceDisconnected ? new DeviceDisconnectedException() : ex; + Error?.Invoke(this, new ErrorEventArgs(terminatingException)); + } + + /// + /// Event handler called after a file has been received from the device. + /// + /// The file received. + protected virtual void OnFileReceived(BackupFile file) + { + FileReceived?.Invoke(this, new BackupFileEventArgs(file)); + if (string.Compare("Status.plist", Path.GetFileName(file.LocalPath), true) == 0) { + using (FileStream fs = File.OpenRead(file.LocalPath)) { + DictionaryNode statusPlist = PropertyList.Load(fs).AsDictionaryNode(); + OnStatusReceived(new BackupStatus(statusPlist)); + } + } + } + + /// + /// Event handler called after a part (or all of) a file has been sent from the device from the device. + /// + /// The file received. + /// The file contents received + protected virtual void OnFileReceiving(BackupFile file, byte[] fileData) + { + FileReceiving?.Invoke(this, new BackupFileEventArgs(file, fileData)); + using (FileStream stream = File.OpenWrite(file.LocalPath)) { + stream.Write(fileData, 0, fileData.Length); + } + } + + /// + /// Event handler called after a file transfer failed due to a device error. + /// + /// The file whose tranfer failed. + protected virtual void OnFileTransferError(BackupFile file) + { + failedFiles.Add(file); + if (FileTransferError != null) { + var e = new BackupFileErrorEventArgs(file); + FileTransferError.Invoke(this, e); + IsCancelling = e.Cancel; + } + } + + /// + /// Manages the GetFreeDiskSpace device message. + /// + /// The message received from the device. + /// Whether the device should abide by the freeSpace value passed or ignore it + /// 0 on success, -1 on error. + protected virtual void OnGetFreeDiskSpace(ArrayNode msg, bool respectFreeSpaceValue = true) + { + long freeSpace = GetFreeSpace(BackupDirectory); + IntegerNode spaceItem = new IntegerNode(freeSpace); + if (respectFreeSpaceValue) { + mobilebackup2Service?.SendStatusReport(0, null, spaceItem); + } + else { + mobilebackup2Service?.SendStatusReport(-1, null, spaceItem); + } + } + + /// + /// Event handler called when the snapshot state of the backup changes. + /// + /// The previous snapshot state. + /// The new snapshot state. + protected virtual void OnSnapshotStateChanged(SnapshotState oldSnapshotState, SnapshotState newSnapshotState) + { + Debug.WriteLine($"Snapshot state changed: {newSnapshotState}"); + OnStatus($"{newSnapshotState} ..."); + if (newSnapshotState == SnapshotState.Finished) { + IsFinished = true; + } + } + + /// + /// Event handler called to report a status messages. + /// + /// The status message to report. + protected virtual void OnStatus(string message) + { + Status?.Invoke(this, new StatusEventArgs(message)); + Debug.WriteLine($"OnStatus: {message}"); + } + + /// + /// Event handler called each time the backup service sends a status report. + /// + /// The status report sent from the backup service. + protected virtual void OnStatusReceived(BackupStatus status) + { + lastStatus = status; + if (snapshotState != status.SnapshotState) { + OnSnapshotStateChanged(snapshotState, snapshotState = status.SnapshotState); + } + } + + /// + /// Receives a single file from the device. + /// + /// The BackupFile to receive. + /// The total size indicated in the device message. + /// The actual bytes transferred. + /// The number of bytes left to read. + /// Indicates whether to skip or save the file. + /// The result code of the transfer. + protected virtual ResultCode ReceiveFile(BackupFile file, long totalSize, ref long realSize, out int nlen, bool skip = false) + { + nlen = 0; + const int bufferLen = 32 * 1024; + ResultCode lastCode = ResultCode.Success; + if (File.Exists(file.LocalPath)) { + File.Delete(file.LocalPath); + } + while (!IsStopping) { + nlen = ReadInt32(); + if (nlen <= 0) { + break; + } + + ResultCode code = ReadCode(); + int blockSize = nlen - 1; + if (code != ResultCode.FileData) { + if (code == ResultCode.Success) { + return code; + } + if (blockSize > 0) { + byte[] msgBuffer = mobilebackup2Service?.ReceiveRaw(blockSize) ?? Array.Empty(); + string msg = Encoding.UTF8.GetString(msgBuffer); + Debug.WriteLine($"ERROR Receving file data: {code}: {msg}"); + } + OnFileTransferError(file); + return code; + } + lastCode = code; + + int done = 0; + while (done < blockSize) { + int toRead = Math.Min(blockSize - done, bufferLen); + byte[] buffer = mobilebackup2Service?.ReceiveRaw(toRead) ?? Array.Empty(); + if (!skip) { + OnFileReceiving(file, buffer); + } + done += buffer.Length; + } + if (done == blockSize) { + realSize += blockSize; + } + } + + return lastCode; + } + + /// + /// Generates and saves the backup Info.plist file. + /// + protected virtual async Task SaveInfoPropertyList() + { + OnStatus("Creating Info.plist"); + BackupFile backupFile = new BackupFile(string.Empty, $"Info.plist", DeviceBackupPath); + + DateTime startTime = DateTime.Now; + + PropertyNode infoPlist = await CreateInfoPlist(); + byte[] infoPlistData = PropertyList.SaveAsByteArray(infoPlist, PlistFormat.Xml); + OnFileReceiving(backupFile, infoPlistData); + + TimeSpan elapsed = DateTime.Now - startTime; + Debug.WriteLine($"Creating Info.plist took {elapsed}"); + + OnFileReceived(backupFile); + } + + /// + /// Disposes the used resources. + /// + public void Dispose() + { + CleanResources(); + GC.SuppressFinalize(this); + } + + /// + /// Starts the backup process. + /// + public async Task Start() + { + if (!InProgress) { + await CreateBackup(); + } + } + + /// + /// Stops the backup process. + /// + public void Stop() + { + if (InProgress && !IsStopping) { + IsCancelling = true; + userCancelled = true; + } + } + } +} diff --git a/Netimobiledevice/Backup/DeviceLinkMessage.cs b/Netimobiledevice/Backup/DeviceLinkMessage.cs new file mode 100644 index 0000000..28f7b52 --- /dev/null +++ b/Netimobiledevice/Backup/DeviceLinkMessage.cs @@ -0,0 +1,62 @@ +namespace Netimobiledevice.Backup +{ + /// + /// DeviceLink Messages received from the device during the backup process. + /// + internal class DeviceLinkMessage + { + /// + /// The device asks to receive files from the host. + /// + public const string DownloadFiles = "DLMessageDownloadFiles"; + /// + /// The device asks to send files to the host. + /// + public const string UploadFiles = "DLMessageUploadFiles"; + /// + /// The device asks for the free space on the host. + /// + public const string GetFreeDiskSpace = "DLMessageGetFreeDiskSpace"; + /// + /// The device asks for the contents of an specific directory. + /// + public const string ContentsOfDirectory = "DLContentsOfDirectory"; + /// + /// The device asks to create an specific directory on the host. + /// + public const string CreateDirectory = "DLMessageCreateDirectory"; + /// + /// The device asks to move files on the host. + /// + public const string MoveFiles = "DLMessageMoveFiles"; + /// + /// The device asks to move items on the host. + /// + public const string MoveItems = "DLMessageMoveItems"; + /// + /// The device asks to remove files on the host. + /// + public const string RemoveFiles = "DLMessageRemoveFiles"; + /// + /// The device asks to remove items on the host. + /// + public const string RemoveItems = "DLMessageRemoveItems"; + /// + /// The device asks to copy items on the host. + /// + public const string CopyItem = "DLMessageCopyItem"; + /// + /// The device asks the host to disconnect. + /// + public const string Disconnect = "DLMessageDisconnect"; + /// + /// The device asks the host to process an error message. + /// An error message with Code 0 is sent when the backup process is finished. + /// + public const string ProcessMessage = "DLMessageProcessMessage"; + /// + /// The device tells the host how many disk space it requires so the host can try to make room. + /// + public const string PurgeDiskSpace = "DLMessagePurgeDiskSpace"; + } +} diff --git a/Netimobiledevice/Backup/ErrNo.cs b/Netimobiledevice/Backup/ErrNo.cs new file mode 100644 index 0000000..c5fa4b2 --- /dev/null +++ b/Netimobiledevice/Backup/ErrNo.cs @@ -0,0 +1,25 @@ +namespace Netimobiledevice.Backup +{ + /// + /// C ErrNo error codes + /// + internal enum ErrNo : int + { + /// + /// No Error. + /// + ENOERR = 0, + /// + /// Not found. + /// + ENOENT = 2, + /// + /// Permission denied. + /// + EACCES = 13, + /// + /// Already exists. + /// + EEXIST = 17, + } +} diff --git a/Netimobiledevice/Backup/SnapshotState.cs b/Netimobiledevice/Backup/SnapshotState.cs new file mode 100644 index 0000000..9fab26c --- /dev/null +++ b/Netimobiledevice/Backup/SnapshotState.cs @@ -0,0 +1,33 @@ +namespace Netimobiledevice.Backup +{ + /// + /// The backup snapshot state. + /// + public enum SnapshotState + { + /// + /// Custom added state to signal the process is waiting for the device to get ready. + /// + Waiting = -2, + /// + /// Custom added state to signal the process has not yet started. + /// + Uninitialized = -1, + /// + /// Status.plist defined state signaling files are being transferred from the device to the host. + /// + Uploading = 0, + /// + /// Status.plist defined state signaling files are moved to its final location on the host. + /// + Moving, + /// + /// Status.plist defined state signaling files are being removed in the host. + /// + Removing, + /// + /// Status.plist defined state signaling that the process has finished. + /// + Finished + } +} diff --git a/Netimobiledevice/Backup/StatusEventArgs.cs b/Netimobiledevice/Backup/StatusEventArgs.cs new file mode 100644 index 0000000..4eb7860 --- /dev/null +++ b/Netimobiledevice/Backup/StatusEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace Netimobiledevice.Backup +{ + /// + /// Generic EventArgs for signaling a status message. + /// + public class StatusEventArgs : EventArgs + { + /// + /// The status message. + /// + public string Message { get; } + + /// + /// Creates an instance of the StatusEventArgs class. + /// + /// The status message. + public StatusEventArgs(string message) + { + Message = message; + } + } +} diff --git a/Netimobiledevice/Exceptions/DeviceDisconnectedException.cs b/Netimobiledevice/Exceptions/DeviceDisconnectedException.cs new file mode 100644 index 0000000..1762153 --- /dev/null +++ b/Netimobiledevice/Exceptions/DeviceDisconnectedException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Netimobiledevice.Exceptions +{ + /// + /// Exception thrown when the device is disconnected. + /// + public class DeviceDisconnectedException : Exception + { + public DeviceDisconnectedException() : base("Device disconnected") { } + } +} diff --git a/README.md b/README.md index 897e15b..6721127 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,33 @@ private static void SubscriptionCallback(UsbmuxdDevice device, UsbmuxdConnection Get the app icon displayed on the home screen as a PNG: ```csharp -using (LockdownClient lockdown = LockdownClient.CreateLockdownClient("60653a518d33eb53b3ca2212cd1f44e162a42069")) { +using (LockdownClient lockdown = LockdownClient.CreateLockdownClient("60653a518d33eb53b3ca2322de3f44e162a42069")) { SpringBoardServicesService springBoard = new SpringBoardServicesService(lockdown); PropertyNode png = springBoard.GetIconPNGData("net.whatsapp.WhatsApp"); } ``` +Create an iTunes backup: + +```csharp +using (LockdownClient lockdown = LockdownClient.CreateLockdownClient("60653a518d33eb53b3ca2322de3f44e162a42069")) { + using (DeviceBackup backupJob = new DeviceBackup(lockdown, @"C:\Users\User\Downloads")) { + backupJob.BeforeReceivingFile += BackupJob_BeforeReceivingFile; + backupJob.Completed += BackupJob_Completed; + backupJob.Error += BackupJob_Error; + backupJob.FileReceived += BackupJob_FileReceived; + backupJob.FileReceiving += BackupJob_FileReceiving; + backupJob.FileTransferError += BackupJob_FileTransferError; + backupJob.PasscodeRequiredForBackup += BackupJob_PasscodeRequiredForBackup; + backupJob.Progress += BackupJob_Progress; + backupJob.Status += BackupJob_Status; + backupJob.Started += BackupJob_Started; + + await backupJob.Start(); + } +} +``` + ## Services The list of all the services from lockdownd which have been implemented and the functions available for each one. Clicking on the service name will take you to it's implementation, to learn more about it.