Commits

Jeff Laing committed c2508a2

Renamed some initWithDevice to initWithAMDevice to keep it clearer whats happening - some
other cosmetic changes of the same ilk.
Added some stuff about the installation proxy
Change syslog relay to strip leading newline characters
Correct some comments

  • Participants
  • Parent commits 899af97

Comments (0)

Files changed (2)

MobileDeviceAccess.h

  *
  * The application delegate will usually register itself as a listener to the MobileDeviceAccess
  * singleton.  It will then be called back for every iPhone and iPod Touch that connects.  To access
- * files on the AMDevice, create an AFCDirectoryAccess using \p -newAFCDirectoryAccess:
+ * files on the AMDevice, create one of the subclasses AFCDirectoryAccess using the 
+ * corresponding \p -newAFCxxxDirectory: method.
  *
  * \section refs_sec References
  *
 @protected
 	am_service _service;
 	NSString *_lasterror;
+	id _delegate;
 }
 
 /// The last error that occurred on this service
 /// this property will be nil.
 @property (readonly) NSString *lasterror;
 
+/// The delegate for this service.  Whilst AMService does not use it directly,
+/// some of the subclasses (like AMInstallationProxy) do.  The delegate is
+/// *not* retained - it is the callers responsibility to ensure it remains
+/// valid for the life of the service.
+@property (assign) id delegate;
+
 @end
 
 /// This class represents an installed application on the device.  To retrieve
 
 @end
 
+/// This protocol describes the messages that will be sent by AMInstallationProxy
+/// to its delegate.
+@protocol AMInstallationProxyDelegate
+@optional
+
+/// A new current operation is beginning.
+-(void)operationStarted:(NSDictionary*)info;
+
+/// The current operation is continuing.
+-(void)operationContinues:(NSDictionary*)info;
+
+/// The current operation finished (one way or the other)
+-(void)operationCompleted:(NSDictionary*)info;
+
+@end
+
 /// This class communicates with the mobile_installation_proxy.  It can be used
 /// to retrieve information about installed applications on the device (as well as other
 /// installation operations that are not supported by this framework).
 /// <PRE>
 ///	/usr/libexec/mobile_installation_proxy
 /// </PRE>
+/// That binary seems to vet the incoming requests, then pass them to the installd process
+/// for execution.
+///
+/// The protocol also seems to be a one-shot.  That is, you need to establish multiple connections
+/// if you want to perform multiple operations.
+///
 /// See also: http://iphone-docs.org/doku.php?id=docs:protocols:installation_proxy
 @interface AMInstallationProxy : AMService
 
 /// you can't filter on the value of the attribute, just its existance.
 ///
 /// You probably don't want to use lookupType:withAttribute - see browseFiltered: instead.
-- (NSDictionary*)lookupType:(NSString*)type withAttribute:(NSString*)attr;
+- (NSDictionary*)lookupType:(NSString*)type
+			  withAttribute:(NSString*)attr;
+
+/// Ask the installation daemon on the device to create an archive of a specific
+/// application.  Once finished a corresponding
+/// zip file will be present in the Media/ApplicationArchives directory where it could
+/// be retrieved via AFCMediaDirectory.
+/// The contents of the archive depends on the values of the container: and payload:
+/// arguments.
+/// If the uninstall: argument is YES, the application will also be uninstalled.
+- (BOOL)archive:(NSString*)bundleid
+	  container:(BOOL)container
+		payload:(BOOL)payload
+	  uninstall:(BOOL)uninstall;
+
+/// Ask the installation daemon on the device to restore a previously archived application.
+/// The .zip file must be placed in the Media/ApplicationArchives directory.
+- (BOOL)restore:(NSString*)bundleid;
+
+/// Ask the installation daemon on the device what application archives are
+/// available.  Return an array of bundle ids which can be passed to functions
+/// like restore:
+- (NSArray*)archivedAppBundleIds;
+
+/// Ask the installation daemon on the device what application archives are
+/// available.  Return a dictionary keyed by application bundle id.
+///
+/// This seems to be reading the file /var/mobile/Library/MobileInstallation/ArchivedApplications.plist
+/// which is not otherwise accessible (except using AFCRootDirectory on jailbreaked devices)
+- (NSDictionary*)archivedAppInfo;
+
+/// Remove the archive for a given bundle id.  Note that this is more than just removing
+/// the .zip file from the Media/ApplicationArchives directory - if you do that, the
+/// archive remains "known to" the installation daemon and future requests to archive
+/// this bundle will fail.  Sadly, explicit requests to removeArchive: will file if the
+/// .zip file has been removed as well. (The simplest fix for this scenario is to create
+/// a dummy file before calling removeArchive:)
+- (BOOL)removeArchive:(NSString*)bundleid;
+
+/// Ask the installation daemon on the device to install an application.  pathname
+/// must be the name of a directory located in /var/mobile/Media and must contain
+/// a pre-expanded .app
+- (BOOL)install:(NSString*)pathname;
 
 @end
 
 /// <PRE>
 ///	/usr/libexec/afcd --lockdown -d /var/mobile/Media -u mobile
 /// </PRE>
+///
+/// Common subdirectories to access within the Media directory are:
+/// - DCIM
+/// - ApplicationArchives
+/// - PublicStaging
 @interface AFCMediaDirectory : AFCDirectoryAccess {
 }
 @end
 - (AMSpringboardServices*)newAMSpringboardServices;
 
 /// Create an installation proxy relay.
-- (AMInstallationProxy*)newAMInstallationProxy;
+/// If an object is passed for the delegate, it will be notified during some of the
+/// operations performed by the proxy.
+- (AMInstallationProxy*)newAMInstallationProxyWithDelegate:(id<AMInstallationProxyDelegate>)delegate;
 
 /// Create a fileset relay.  This can be used to en-masse extract certain groups
 /// of information from the device. For more information, see AMFileRelay.

MobileDeviceAccess.m

 typedef void (*NOTIFY_CALLBACK)(CFStringRef notification, void* data);
 mach_error_t AMDListenForNotifications(am_service socket, NOTIFY_CALLBACK cb, void* data);
 
-#if 0
-Yeah, I’ve just found out how to handle this and terminate sync when user slides cancel switch.
-
-We need following functions (meta-language):
-
-ERROR AMDObserveNotification(HANDLE proxy, CFSTR notification);
-
-ERROR AMDListenForNotifications(HANDLE proxy, NOTIFY_CALLBACK cb, USERDATA data);
-
-and callback delegate:
-
-typedef void (*NOTIFY_CALLBACK)(CFSTR notification, USERDATA data);
-
-First, we need AMDObserveNotification to subscribe notifications about “com.apple.itunes-client.syncCancelRequest”. Then we should start listening for notifications (second function) until we get “AMDNotificationFaceplant”.
-That’s it. When notification got, you should unlock and close lock file handle (don’t sure if you need to post “syncDidFinish” to proxy, seems it doesn’t matter) and terminate sync gracefully.
-
-P.S. The same notification is also got when you unplug your device, so you should always be ready for errors.
-#endif
 @interface AMDevice(Private)
 - (am_service)_startService:(NSString*)name;
 @end
 @implementation AMService
 
 @synthesize lasterror = _lasterror;
+@synthesize delegate = _delegate;
 
 - (void)clearLastError
 {
 	_lasterror = [msg retain];
 }
 
+- (void)performDelegateSelector:(SEL)sel
+			  		 withObject:(id)info
+{
+	if (_delegate) {
+		if ([_delegate respondsToSelector:sel]) {
+			[_delegate performSelector:sel withObject:info];
+		}
+	}
+}
+
 - (void)dealloc
 {
 	[_lasterror release];
 - (id)initWithName:(NSString*)name onDevice:(AMDevice*)device
 {
 	if (self = [super init]) {
+		_delegate = nil;
 		_service = [device _startService:name];
 		if (_service == nil) {
 			[self release];
 
 @implementation AFCMediaDirectory
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.afc" onDevice:device]) {
 		int ret = AFCConnectionOpen(_service, 0/*timeout*/, &_afc);
 
 @implementation AFCCrashLogDirectory
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.crashreportcopymobile" onDevice:device]) {
 		int ret = AFCConnectionOpen(_service, 0/*timeout*/, &_afc);
 
 @implementation AFCRootDirectory
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.afc2" onDevice:device]) {
 		int ret = AFCConnectionOpen(_service, 0/*timeout*/, &_afc);
 
 @implementation AFCApplicationDirectory
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 			 andName:(NSString*)identifier
 {
-	MyLogMethodEntry();
-
 	if (self = [super initWithName:@"com.apple.mobile.house_arrest" onDevice:device]) {
 		NSDictionary *message;
 		message = [NSDictionary dictionaryWithObjectsAndKeys:
 
 @implementation AMInstallationProxy
 
+#if 0
+Yeah, I’ve just found out how to handle this and terminate sync when user slides cancel switch.
+
+We need following functions (meta-language):
+
+ERROR AMDObserveNotification(HANDLE proxy, CFSTR notification);
+
+ERROR AMDListenForNotifications(HANDLE proxy, NOTIFY_CALLBACK cb, USERDATA data);
+
+and callback delegate:
+
+typedef void (*NOTIFY_CALLBACK)(CFSTR notification, USERDATA data);
+
+First, we need AMDObserveNotification to subscribe notifications about “com.apple.itunes-client.syncCancelRequest”. Then we should start listening for notifications (second function) until we get “AMDNotificationFaceplant”.
+That’s it. When notification got, you should unlock and close lock file handle (don’t sure if you need to post “syncDidFinish” to proxy, seems it doesn’t matter) and terminate sync gracefully.
+
+P.S. The same notification is also got when you unplug your device, so you should always be ready for errors.
+#endif
+
 - (NSArray *)browse:(NSString*)type
 {
 	return [self browseFiltered:nil];
 	return result;
 }
 
+- (BOOL)archive:(NSString*)bundleid
+		container:(BOOL)container
+		payload:(BOOL)payload
+		uninstall:(BOOL)uninstall
+{
+	BOOL result = NO;
+	NSDictionary *message;
+	NSString *mode;
+
+	if (container) {
+		if (payload) {
+			mode = @"All";
+		} else {
+			mode = @"DocumentsOnly";
+		}
+	} else {
+		if (payload) {
+			mode = @"ApplicationOnly";
+		} else {
+			return NO;
+		}
+	}
+
+	// { "Command" => "Archive";	"ApplicationIdentifier" => ...; ClientOptions => ... }
+	// it also takes {"ArchiveType" => oneof "All", "DocumentsOnly", "ApplicationOnly" }
+	// it also takes {"SkipUninstall" => True / False}
+	message = [NSDictionary dictionaryWithObjectsAndKeys:
+					// value																key
+					@"Archive",																@"Command",
+					bundleid,																@"ApplicationIdentifier",
+					[NSDictionary dictionaryWithObjectsAndKeys:
+						// value										key
+						mode,											@"ArchiveType",
+						uninstall ? kCFBooleanFalse : kCFBooleanTrue,	@"SkipUninstall",
+						nil],																@"ClientOptions",
+					nil];
+
+	[self performDelegateSelector:@selector(operationStarted:) withObject:message];
+	if ([self sendXMLRequest:message]) {
+		for (;;) {
+			// read next slab of information
+			// The reply will contain an entry for Status, a string from the following:
+			//	"TakingInstallLock"
+			//	"EmptyingApplication"
+			//	"ArchivingApplication"
+			//	"RemovingApplication"			/__ only if the application was uninstalled
+			//	"GeneratingApplicationMap"		\
+			//	"Complete"
+			// All except "Complete" also include PercentageComplete, an integer between 0 and 100 (but it goes up and down)
+			//
+			// If its already been archived, we might get:
+			//    Error = AlreadyArchived;
+			// If we keep listening we'll then get
+			//    Error = APIInternalError;
+			NSDictionary *reply = [self readXMLReply];
+			if (!reply) break;
+			[self performDelegateSelector:@selector(operationContinues:) withObject:reply];
+
+			NSString *s = [reply objectForKey:@"Status"];
+			if ([s isEqual:@"Complete"]) break;
+		}
+		result = YES;
+	}
+	[self performDelegateSelector:@selector(operationCompleted:) withObject:message];
+	return result;
+}
+
+- (BOOL)restore:(NSString*)bundleid
+{
+	BOOL result = NO;
+	NSDictionary *message;
+	message = [NSDictionary dictionaryWithObjectsAndKeys:
+					// value																key
+					@"Restore",																@"Command",
+					bundleid,																@"ApplicationIdentifier",
+					nil];
+	[self performDelegateSelector:@selector(operationStarted:) withObject:message];
+	if ([self sendXMLRequest:message]) {
+		for (;;) {
+			// read next slab of information
+			// The reply will contain an entry for Status, a string from the following:
+			//	"TakingInstallLock"
+			//	"RestoringApplication"
+			//	"SandboxingApplication"
+			//	"GeneratingApplicationMap"
+			//	"Complete"
+			// All except "Complete" also include PercentageComplete, an integer between 0 and 100 (but it goes up and down)
+			//
+			// Looks like instead of Status, we can get:
+			// Error = APIInternalError
+			// (that happened if I passed in ClientOptions.  Bad...
+			// Error = APIEpicFail;
+			// (that happened if I passed in a missing bundle-id
+			NSDictionary *reply = [self readXMLReply];
+			if (!reply) break;
+			[self performDelegateSelector:@selector(operationContinues:) withObject:reply];
+			NSString *s = [reply objectForKey:@"Status"];
+			if ([s isEqual:@"Complete"]) break;
+		}
+		result = YES;
+	}
+	[self performDelegateSelector:@selector(operationCompleted:) withObject:message];
+	return result;
+}
+
+- (NSArray*)archivedAppBundleIds
+{
+	NSDictionary *appInfo = [self archivedAppInfo];
+	return [appInfo allKeys];
+}
+
+- (NSDictionary*)archivedAppInfo
+{
+	NSDictionary *message;
+	message = [NSDictionary dictionaryWithObjectsAndKeys:
+					// value					key
+					@"LookupArchives",			@"Command",
+					nil];
+	if ([self sendXMLRequest:message]) {
+		NSDictionary *reply = [self readXMLReply];
+		if (reply) {
+			return [[[reply objectForKey:@"LookupResult"] retain] autorelease];
+		}
+	}
+	return nil;
+}
+
+/// Remove the archive for a given bundle id.
+- (BOOL)removeArchive:(NSString*)bundleid
+{
+	NSDictionary *message;
+	message = [NSDictionary dictionaryWithObjectsAndKeys:
+					// value					key
+					@"RemoveArchive",			@"Command",
+					bundleid,					@"ApplicationIdentifier",
+					nil];
+
+	[self performDelegateSelector:@selector(operationStarted:) withObject:message];
+	if ([self sendXMLRequest:message]) {
+		for (;;) {
+			// read next slab of information
+			// The reply will contain an entry for Status, a string from the following:
+			//	"RemovingArchive"
+			//	"Complete"
+			// All except "Complete" also include PercentageComplete, an integer between 0 and 100 (but it goes up and down)
+			//
+			// Looks like instead of Status, we can get:
+			// Error = APIEpicFail;
+			// (that happened if I passed in a missing bundle-id
+			// Error = APIInternalError
+			// (that happened if I kept reading?
+			NSDictionary *reply = [self readXMLReply];
+			if (!reply) break;
+			[self performDelegateSelector:@selector(operationContinues:) withObject:reply];
+			NSString *s = [reply objectForKey:@"Status"];
+			if ([s isEqual:@"Complete"]) break;
+		}
+	}
+	[self performDelegateSelector:@selector(operationCompleted:) withObject:message];
+	return NO;
+}
+
 //
 // lookup is similiar to browse - it looks like it has clever filtering
 // capability but its not that useful.  You can look up all attributes that
 						attr, @"Attribute",
 						nil],				@"ClientOptions",
 					nil];
+	[self performDelegateSelector:@selector(operationStarted:) withObject:message];
 	if ([self sendXMLRequest:message]) {
 		NSDictionary *reply = [self readXMLReply];
 		if (reply) {
+			[self performDelegateSelector:@selector(operationContinues:) withObject:reply];
 			NSMutableDictionary *result = [NSMutableDictionary new];
 			NSDictionary *lookup_result = [reply objectForKey:@"LookupResult"];
 			for (NSString *key in lookup_result) {
 			return [NSDictionary dictionaryWithDictionary:result];
 		}
 	}
+	[self performDelegateSelector:@selector(operationCompleted:) withObject:message];
 	return nil;
 }
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.mobile.installation_proxy" onDevice:device]) {
 	}
 #if 0
 http://libiphone.lighthouseapp.com/projects/27916/tickets/104/a/365185/0001-new-installation_proxy-interface.patch
 
-// { "Command" => "Browse";	ClientOptions => { "ApplicationType" => "User"||"System"||"Any"||"Internal" }  }
-// { "Command" => "Lookup";	ClientOptions => { "ApplicationType" => "User"||"System"||"Any"||"Internal" "Attribute"="attrname" } }
-//		<- { Status => Complete; LookupResult => ... }
-//		<- { Error => LookupFailed; }
-// { "Command" => "Install"; "PackagePath" => "..."; "ClientOptions" = "..." }
+// { "Command" => "Install";
+//			"PackagePath" => "...";	// Will be prefixed with /var/mobile/Media/
+//									// if PackageType="Developer", it should be a pointer to an expanded .app
+//									// containing code signature stuff, etc.
+//			"ClientOptions" = { "PackageType" = "Developer"; "ApplicationSINF" = ... "; "iTunesMetadata" = "...",  }
+//							  { "PackageType" = "Customer";
+//							  { "PackageType" = "CarrierBundle"; ...
+//		<- { Status => Complete; }
+//		<- { Status => "..."; PercentComplete = ... }
 // { "Command" => "Upgrade"; "PackagePath" => "..." }
+//
+
+2010-06-08 20:01:34.628 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 0;
+    Status = TakingInstallLock;
+}
+2010-06-08 20:01:34.640 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 5;
+    Status = CreatingStagingDirectory;
+}
+2010-06-08 20:01:34.656 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 10;
+    Status = StagingPackage;
+}
+2010-06-08 20:01:34.694 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 15;
+    Status = ExtractingPackage;
+}
+2010-06-08 20:01:34.697 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 20;
+    Status = InspectingPackage;
+}
+2010-06-08 20:01:34.701 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 30;
+    Status = PreflightingApplication;
+}
+2010-06-08 20:01:34.704 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 30;
+    Status = InstallingEmbeddedProfile;
+}
+2010-06-08 20:01:34.708 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 40;
+    Status = VerifyingApplication;
+}
+2010-06-08 20:01:34.722 iPodBackup[40130:a0f] operationContinues::{
+    Error = BundleVerificationFailed;
+}
+2010-06-08 20:01:34.728 iPodBackup[40130:a0f] operationContinues::{
+    Error = APIInternalError;
+}
+2010-06-08 20:01:34.730 iPodBackup[40130:a0f] operationCompleted::{
+    ClientOptions =     {
+        PackageType = Developer;
+    };
+    Command = Install;
+    PackagePath = "PublicStaging/AncientFrogHD.app";
+}
+
 // { "Command" => "Uninstall";	"ApplicationIdentifier" => ...; ClientOptions => ... }
-// { "Command" => "Archive";	"ApplicationIdentifier" => ...; ClientOptions => ... }
-// { "Command" => "Restore";	"ApplicationIdentifier" => ...; ClientOptions => ... }
+
 // { "Command" => "RemoveArchive";	"ApplicationIdentifier" => ...; ClientOptions => ... }
-// { "Command" => "LookupArchives"; ClientOptions => ... }
-//		<- { Status => Complete; LookupResult => {...} }
 // { "Command" => "CheckCapabilitiesMatch"; Capabilities => ...; ClientOptions => ... }
 //		<- { Status => Complete; LookupResult => ... }
 //		<- { Error = APIInternalError; }
+//
+
+2010-06-08 20:16:59.431 iPodBackup[40417:a0f] operationStarted::{
+    ClientOptions =     {
+        PackageType = Developer;
+    };
+    Command = Install;
+    PackagePath = "PublicStaging/Rooms.app";
+}
+2010-06-08 20:16:59.462 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 0;
+    Status = TakingInstallLock;
+}
+2010-06-08 20:16:59.479 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 5;
+    Status = CreatingStagingDirectory;
+}
+2010-06-08 20:16:59.488 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 10;
+    Status = StagingPackage;
+}
+2010-06-08 20:16:59.504 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 15;
+    Status = ExtractingPackage;
+}
+2010-06-08 20:16:59.511 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 20;
+    Status = InspectingPackage;
+}
+2010-06-08 20:16:59.518 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 30;
+    Status = PreflightingApplication;
+}
+2010-06-08 20:16:59.526 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 30;
+    Status = InstallingEmbeddedProfile;
+}
+2010-06-08 20:16:59.533 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 40;
+    Status = VerifyingApplication;
+}
+2010-06-08 20:17:02.915 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 50;
+    Status = CreatingContainer;
+}
+2010-06-08 20:17:02.926 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 60;
+    Status = InstallingApplication;
+}
+2010-06-08 20:17:02.935 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 70;
+    Status = PostflightingApplication;
+}
+2010-06-08 20:17:02.939 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 80;
+    Status = SandboxingApplication;
+}
+2010-06-08 20:17:02.977 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 90;
+    Status = GeneratingApplicationMap;
+}
+2010-06-08 20:17:04.943 iPodBackup[40417:a0f] operationContinues::{
+    Status = Complete;
+}
 #endif
 
+- (BOOL)install:(NSString*)pathname
+{
+	NSDictionary *message;
+	message = [NSDictionary dictionaryWithObjectsAndKeys:
+					// value					key
+					@"Install",				@"Command",
+					pathname,				@"PackagePath",
+					[NSDictionary dictionaryWithObjectsAndKeys:
+						@"Developer", @"PackageType",
+						nil],				@"ClientOptions",
+					nil];
+	[self performDelegateSelector:@selector(operationStarted:) withObject:message];
+	if ([self sendXMLRequest:message]) {
+		for (;;) {
+			// read next slab of information
+			// The reply will contain an entry for Status, a string from the following:
+			//	"RemovingArchive"
+			//	"Complete"
+			// All except "Complete" also include PercentageComplete, an integer between 0 and 100 (but it goes up and down)
+			//
+			// Looks like instead of Status, we can get:
+			// Error = APIEpicFail;
+			// (that happened if I passed in a missing bundle-id
+			// Error = APIInternalError
+			// (that happened if I kept reading?
+			NSDictionary *reply = [self readXMLReply];
+			if (!reply) break;
+			[self performDelegateSelector:@selector(operationContinues:) withObject:reply];
+			NSString *s = [reply objectForKey:@"Status"];
+			if ([s isEqual:@"Complete"]) break;
+		}
+	}
+	[self performDelegateSelector:@selector(operationCompleted:) withObject:message];
+	return NO;
+}
+
 @end
 
 @implementation AMSyslogRelay
 			// which are \0 terminated - they may contain \n characters within
 			// a record, and they never seem to send us an unterminated message
 			// (again, the server code seems to preclude it)
+			//
+			// Control characters seem to be escaped with \ - ie, tab comes through as \ followed by t
 			UInt8 buffer[0x4000];
 			const CFIndex len = CFReadStreamRead(stream,buffer,sizeof(buffer));
 			if (len) {
 				p = buffer;
 				buffer[sizeof(buffer)-1] = '\000';
 				while (left>0) {
+					// remove leading newlines
+					while (*p == '\n') {
+						if (--left) p++;
+						else break;
+					}
 					q = p;
 					while (*q && left>0) {q++;left--;}
 					if (left) {
 	[super dealloc];
 }
 
-- (id)initWithDevice:(AMDevice*)device listener:(id)listener message:(SEL)message
+- (id)initWithAMDevice:(AMDevice*)device listener:(id)listener message:(SEL)message
 {
 	if (self = [super initWithName:@"com.apple.syslog_relay" onDevice:device]) {
 		_listener = listener;
 	return(result);
 }
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.mobile.file_relay" onDevice:device]) {
 		_used = NO;
 	if (status != ERR_SUCCESS) NSLog(@"AMDListenForNotifications returned %lx",status);
 }
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.mobile.notification_proxy" onDevice:device]) {
 		_messages = [NSMutableDictionary new];
 	return nil;
 }
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.springboardservices" onDevice:device]) {
 		// nothing special
 
 @implementation AMMobileSync
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.mobilesync" onDevice:device]) {
 		// wait for the version exchange
 
 http://iphone-docs.org/doku.php?id=docs:protocols:screenshot
 
+http://libimobiledevice.org/docs/mobilesync.html
+
 Like other DeviceLink protocols, it starts with a simple handshake (binary plists represented as ruby objects):
 
 < ["DLMessageVersionExchange", 100, 0]
 	AFCMediaDirectory *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AFCMediaDirectory alloc] initWithDevice:self];
+			result = [[AFCMediaDirectory alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AFCCrashLogDirectory *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AFCCrashLogDirectory alloc] initWithDevice:self];
+			result = [[AFCCrashLogDirectory alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AFCRootDirectory *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AFCRootDirectory alloc] initWithDevice:self];
+			result = [[AFCRootDirectory alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AFCApplicationDirectory *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AFCApplicationDirectory alloc] initWithDevice:self andName:name];
+			result = [[AFCApplicationDirectory alloc] initWithAMDevice:self andName:name];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	return result;
 }
 
-- (AMInstallationProxy*)newAMInstallationProxy
+- (AMInstallationProxy*)newAMInstallationProxyWithDelegate:(id<AMInstallationProxyDelegate>)delegate
 {
 	AMInstallationProxy *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMInstallationProxy alloc] initWithDevice:self];
+			result = [[AMInstallationProxy alloc] initWithAMDevice:self];
+			result.delegate = delegate;
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AMNotificationProxy *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMNotificationProxy alloc] initWithDevice:self];
+			result = [[AMNotificationProxy alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AMSpringboardServices *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMSpringboardServices alloc] initWithDevice:self];
+			result = [[AMSpringboardServices alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AMSyslogRelay *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMSyslogRelay alloc] initWithDevice:self listener:listener message:message];
+			result = [[AMSyslogRelay alloc] initWithAMDevice:self listener:listener message:message];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AMFileRelay *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMFileRelay alloc] initWithDevice:self];
+			result = [[AMFileRelay alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AMMobileSync *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMMobileSync alloc] initWithDevice:self];
+			result = [[AMMobileSync alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 		if ([self startSession]) {
 			CFDictionaryRef dict = nil;
 			if (
-				// "User", "System", "Internal" ??
 				[self checkStatus:AMDeviceLookupApplications(_device, nil, &dict)
 							 from:"AMDeviceLookupApplications"]
 			) {
 				result = [[NSMutableArray new] autorelease];
 				for (NSString *key in (NSDictionary*)dict) {
 					NSDictionary *info = [(NSDictionary*)dict objectForKey:key];
+					// "User", "System", "Internal" ??
 					if ([[info objectForKey:@"ApplicationType"] isEqual:@"User"]) {
 						AMApplication *newapp = [[AMApplication alloc] initWithDictionary:info];
 						[result addObject:newapp];
 + (MobileDeviceAccess*)singleton
 {
 	static MobileDeviceAccess *_singleton = nil;
-
-	if (_singleton == nil) {
-		_singleton = [[MobileDeviceAccess alloc] init];
-		
+	@synchronized(self) {
+		if (_singleton == nil) {
+			_singleton = [[MobileDeviceAccess alloc] init];
+			
+		}
 	}
 	return _singleton;
 }