gidevice icon indicating copy to clipboard operation
gidevice copied to clipboard

Fail when installing big ipa file

Open jessehardy opened this issue 4 years ago • 9 comments

When installing a big ipa file (3.3G), it will report error after uploading to PublicStaging:

device_test.go:104: receive packet: read tcp 127.0.0.1:15119->127.0.0.1:27015: wsarecv: An established connection was aborted by the software in your host machine.

It seems like lockdown connection has been closed by the phone. A simple fix would work: `

func (d *device) AppInstall(ipaPath string) (err error) { if _, err = d.AfcService(); err != nil { return err }

stagingPath := "PublicStaging"
if _, err = d.afc.Stat(stagingPath); err != nil {
	if err != ErrAfcStatNotExist {
		return err
	}
	if err = d.afc.Mkdir(stagingPath); err != nil {
		return fmt.Errorf("app install: %w", err)
	}
}

var info map[string]interface{}
if info, err = ipa.Info(ipaPath); err != nil {
	return err
}
bundleID, ok := info["CFBundleIdentifier"]
if !ok {
	return errors.New("can't find 'CFBundleIdentifier'")
}

installationPath := path.Join(stagingPath, fmt.Sprintf("%s.ipa", bundleID))

var data []byte
if data, err = os.ReadFile(ipaPath); err != nil {
	return err
}
if err = d.afc.WriteFile(installationPath, data, AfcFileModeWr); err != nil {
	return err
}

d.lockdown = nil

if _, err = d.installationProxyService(); err != nil {
	return err
}

return d.installationProxy.Install(fmt.Sprintf("%s", bundleID), installationPath)

}

`

Add a d.lockdown = nil before loading installation proxy service. But I'm not sure that's the best way of fixing.

jessehardy avatar May 18 '21 06:05 jessehardy

Is the device locked when an error is reported?

electricbubble avatar May 18 '21 07:05 electricbubble

No, it will fail without the added d.lockdown = nil, whether locked or not.

jessehardy avatar May 18 '21 13:05 jessehardy

Was it an error at about 30s?

Maybe I made a mistake here 👇

https://github.com/electricbubble/gidevice/blob/8c797a42a78b7c0ff7aff753dd87da4590eb00b1/device.go#L158


Maybe that's right 👇

if innerConn, err = d.NewConnect(LockdownPort, 0); err != nil {

Can you give it a try?

If it works as expected, PR welcomed 🥳

electricbubble avatar May 18 '21 13:05 electricbubble

if innerConn, err = d.NewConnect(LockdownPort, 0); err != nil {

I changed all NewConnect() calls to include a 0 timeout. But it didn't work. I think that the timeout here, is for the action of connecting itself, and read/write timeout, but not for how long iOS allows the connection to live. So maybe we have to reconnect?

jessehardy avatar May 18 '21 14:05 jessehardy

🤔 Maybe AFCService needs a separate connection?

electricbubble avatar May 18 '21 15:05 electricbubble

Tried another method, sending a 'ping' message to lockdown service, and it worked.

func (c *afc) WriteFile(filename string, data []byte, perm AfcFileMode, l Lockdown) (err error) {
	var file *AfcFile
	if file, err = c.Open(filename, perm); err != nil {
		return err
	}
	defer func() {
		err = file.Close()
	}()

	chunk_size := 1 << 26
	buf := bytes.NewBuffer(data)

	for buf.Len() > 0 {
		if _, err = file.Write(buf.Next(chunk_size)); err != nil {
			return err
		}
		log.Println(buf.Len())
		l.QueryType()
	}

	return
}

jessehardy avatar May 19 '21 10:05 jessehardy

You seem to have found another mistake I made

I may not have written all the data correctly

Can you try this? Will it still report an error?

func (c *afc) WriteFile(filename string, data []byte, perm AfcFileMode) (err error) {
	var file *AfcFile
	if file, err = c.Open(filename, perm); err != nil {
		return err
	}
	defer func() {
		err = file.Close()
	}()

	if _, err = io.Copy(file, bytes.NewReader(data)); err != nil {
		return err
	}
	return
}

electricbubble avatar May 19 '21 15:05 electricbubble

This still don't work. Actually the data was written correctly in my case. I added some log to see the problem:

2021/05/21 10:47:51 Exit lockdown._startService
2021/05/21 10:47:51 PublicStaging
2021/05/21 10:47:52 Enter afc.WriteFile
2021/05/21 10:47:52 afc.Open
2021/05/21 10:47:52 Begin Copy
2021/05/21 10:49:40 End Copy
2021/05/21 10:49:40 file.Close
2021/05/21 10:49:40 Exit afc.WriteFile
2021/05/21 10:49:40 Enter installationProxyService
2021/05/21 10:49:40 Reuse d.lockdown
2021/05/21 10:49:40 lockdown.InstallationProxyService()
2021/05/21 10:49:40 Enter lockdown._startService
2021/05/21 10:49:40 handshake
2021/05/21 10:49:40 safeConn.Read Error read tcp 127.0.0.1:28673->127.0.0.1:27015: wsarecv: An established connection was aborted by the software in your host machine.
2021/05/21 10:49:40 handshake Error: receive packet: read tcp 127.0.0.1:28673->127.0.0.1:27015: wsarecv: An established connection was aborted by the software in your host machine.
2021/05/21 10:49:40 Exit installationProxyService
--- FAIL: Test_device_AppInstall (109.08s)
    device_test.go:105: receive packet: read tcp 127.0.0.1:28673->127.0.0.1:27015: wsarecv: An established connection was aborted by the software in your host machine.
FAIL

Call stack:

github.com/electricbubble/gidevice/pkg/libimobiledevice.(*safeConn).Read (f:\Trunk\gidevice\pkg\libimobiledevice\usbmux.go:345)
github.com/electricbubble/gidevice/pkg/libimobiledevice.(*servicePacketClient).ReceivePacket (f:\Trunk\gidevice\pkg\libimobiledevice\client_servicepacket.go:50)
github.com/electricbubble/gidevice/pkg/libimobiledevice.(*LockdownClient).ReceivePacket (f:\Trunk\gidevice\pkg\libimobiledevice\lockdown.go:104)
github.com/electricbubble/gidevice.(*lockdown).QueryType (f:\Trunk\gidevice\lockdown.go:55)
github.com/electricbubble/gidevice.(*lockdown).handshake (f:\Trunk\gidevice\lockdown.go:143)
github.com/electricbubble/gidevice.(*lockdown)._startService (f:\Trunk\gidevice\lockdown.go:476)
github.com/electricbubble/gidevice.(*lockdown).InstallationProxyService (f:\Trunk\gidevice\lockdown.go:367)
github.com/electricbubble/gidevice.(*device).installationProxyService (f:\Trunk\gidevice\device.go:283)
github.com/electricbubble/gidevice.(*device).AppInstall (f:\Trunk\gidevice\device.go:418)
github.com/electricbubble/gidevice.Test_device_AppInstall (f:\Trunk\gidevice\device_test.go:103)
testing.tRunner (c:\Program Files\Go\src\testing\testing.go:1193)
runtime.goexit (c:\Program Files\Go\src\runtime\asm_amd64.s:1371)

There are 3 connections: a physical connection to usbmuxd, a tunneled connection to lockdown service, and a tunneled connection to afc service. The connection to installation service was never established. So the problem is with the tunneled connection to lockdown service. After applying afc service, we have never talked to the lockdown service, for a long time. So maybe the phone has closed the tunneled connection. When we want to talk to the lockdown service again, applying installation service, the error is reported.

So we have to keep the connection alive, or reconnect when appropriate. But I didn't find a way to check if the connection was alive. So maybe we should alway reconnect to lockdown service again before talking to it, or reconnect when we waited too long?

jessehardy avatar May 21 '21 03:05 jessehardy

Nice!

I quite agree with you 👍

2021/05/21 10:47:52 Begin Copy
2021/05/21 10:49:40 End Copy

2mins


Maybe we have two ways to choose

Looking forward to your PR

  1. like https://github.com/electricbubble/gidevice/issues/4#issuecomment-843970622
func (d *device) AppInstall(ipaPath string) (err error) {
	if _, err = d.AfcService(); err != nil {
		return err
	}

	stagingPath := "PublicStaging"
	if _, err = d.afc.Stat(stagingPath); err != nil {
		if err != ErrAfcStatNotExist {
			return err
		}
		if err = d.afc.Mkdir(stagingPath); err != nil {
			return fmt.Errorf("app install: %w", err)
		}
	}

	var info map[string]interface{}
	if info, err = ipa.Info(ipaPath); err != nil {
		return err
	}
	bundleID, ok := info["CFBundleIdentifier"]
	if !ok {
		return errors.New("can't find 'CFBundleIdentifier'")
	}

	installationPath := path.Join(stagingPath, fmt.Sprintf("%s.ipa", bundleID))

	chUploaded := make(chan bool)

	go func() {
		for {
			select {
			case <-chUploaded:
				return
			default:
				if _, err := newLockdown(d).QueryType(); err != nil {
					debugLog(fmt.Sprintf("AppInstall 'ping': %s", err))
				}
				time.Sleep(time.Second * 5)
			}
		}
	}()

	var data []byte
	if data, err = os.ReadFile(ipaPath); err != nil {
		return err
	}
	if err = d.afc.WriteFile(installationPath, data, AfcFileModeWr); err != nil {
		chUploaded <- true
		return err
	}
	chUploaded <- true

	if _, err = d.installationProxyService(); err != nil {
		return err
	}

	return d.installationProxy.Install(fmt.Sprintf("%s", bundleID), installationPath)
}
  1. Ensure that other functions do not have similar situations

https://github.com/electricbubble/gidevice/blob/main/device.go#L152

func (d *device) lockdownService() (lockdown Lockdown, err error) {
	if d.lockdown != nil {
		return d.lockdown, nil
	}

	var innerConn InnerConn
	if innerConn, err = d.NewConnect(LockdownPort, 0); err != nil {
		return nil, err
	}
	d.lockdownClient = libimobiledevice.NewLockdownClient(innerConn)
	d.lockdown = newLockdown(d)
	_, err = d.lockdown._getProductVersion()
	lockdown = d.lockdown

	go func() {
		for {
			if _, err := lockdown.QueryType(); err != nil {
				if strings.Contains(err.Error(), io.EOF.Error()) {
					return
				}
				debugLog(fmt.Sprintf("lockdownService 'ping': %s", err))
			}
			time.Sleep(time.Second * 10)
		}
	}()
	return
}

electricbubble avatar May 21 '21 03:05 electricbubble