You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							174 lines
						
					
					
						
							5.0 KiB
						
					
					
				
			
		
		
	
	
							174 lines
						
					
					
						
							5.0 KiB
						
					
					
				| #!/usr/bin/env python3
 | |
| import json
 | |
| import lzma
 | |
| import hashlib
 | |
| import requests
 | |
| import struct
 | |
| import subprocess
 | |
| import os
 | |
| from typing import Generator
 | |
| 
 | |
| from common.spinner import Spinner
 | |
| 
 | |
| 
 | |
| class StreamingDecompressor:
 | |
|   def __init__(self, url: str) -> None:
 | |
|     self.buf = b""
 | |
| 
 | |
|     self.req = requests.get(url, stream=True, headers={'Accept-Encoding': None})
 | |
|     self.it = self.req.iter_content(chunk_size=1024 * 1024)
 | |
|     self.decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO)
 | |
|     self.eof = False
 | |
|     self.sha256 = hashlib.sha256()
 | |
| 
 | |
|   def read(self, length: int) -> bytes:
 | |
|     while len(self.buf) < length:
 | |
|       self.req.raise_for_status()
 | |
| 
 | |
|       try:
 | |
|         compressed = next(self.it)
 | |
|       except StopIteration:
 | |
|         self.eof = True
 | |
|         break
 | |
|       out = self.decompressor.decompress(compressed)
 | |
|       self.buf += out
 | |
| 
 | |
|     result = self.buf[:length]
 | |
|     self.buf = self.buf[length:]
 | |
| 
 | |
|     self.sha256.update(result)
 | |
|     return result
 | |
| 
 | |
| SPARSE_CHUNK_FMT = struct.Struct('H2xI4x')
 | |
| def unsparsify(f: StreamingDecompressor) -> Generator[bytes, None, None]:
 | |
|   # https://source.android.com/devices/bootloader/images#sparse-format
 | |
|   magic = struct.unpack("I", f.read(4))[0]
 | |
|   assert(magic == 0xed26ff3a)
 | |
| 
 | |
|   # Version
 | |
|   major = struct.unpack("H", f.read(2))[0]
 | |
|   minor = struct.unpack("H", f.read(2))[0]
 | |
|   assert(major == 1 and minor == 0)
 | |
| 
 | |
|   f.read(2)  # file header size
 | |
|   f.read(2)  # chunk header size
 | |
| 
 | |
|   block_sz = struct.unpack("I", f.read(4))[0]
 | |
|   f.read(4)  # total blocks
 | |
|   num_chunks = struct.unpack("I", f.read(4))[0]
 | |
|   f.read(4)  # crc checksum
 | |
| 
 | |
|   for _ in range(num_chunks):
 | |
|     chunk_type, out_blocks = SPARSE_CHUNK_FMT.unpack(f.read(12))
 | |
| 
 | |
|     if chunk_type == 0xcac1:  # Raw
 | |
|       # TODO: yield in smaller chunks. Yielding only block_sz is too slow. Largest observed data chunk is 252 MB.
 | |
|       yield f.read(out_blocks * block_sz)
 | |
|     elif chunk_type == 0xcac2:  # Fill
 | |
|       filler = f.read(4) * (block_sz // 4)
 | |
|       for _ in range(out_blocks):
 | |
|         yield filler
 | |
|     elif chunk_type == 0xcac3:  # Don't care
 | |
|       yield b""
 | |
|     else:
 | |
|       raise Exception("Unhandled sparse chunk type")
 | |
| 
 | |
| 
 | |
| def flash_partition(cloudlog, spinner, target_slot, partition):
 | |
|   cloudlog.info(f"Downloading and writing {partition['name']}")
 | |
| 
 | |
|   downloader = StreamingDecompressor(partition['url'])
 | |
|   with open(f"/dev/disk/by-partlabel/{partition['name']}{target_slot}", 'wb+') as out:
 | |
|     partition_size = partition['size']
 | |
| 
 | |
|     # Check if partition is already flashed
 | |
|     out.seek(partition_size)
 | |
|     if out.read(64) == partition['hash_raw'].lower().encode():
 | |
|       cloudlog.info(f"Already flashed {partition['name']}")
 | |
|       return
 | |
| 
 | |
|     # Clear hash before flashing
 | |
|     out.seek(partition_size)
 | |
|     out.write(b"\x00" * 64)
 | |
|     out.seek(0)
 | |
|     os.sync()
 | |
| 
 | |
|     # Flash partition
 | |
|     if partition['sparse']:
 | |
|       raw_hash = hashlib.sha256()
 | |
|       for chunk in unsparsify(downloader):
 | |
|         raw_hash.update(chunk)
 | |
|         out.write(chunk)
 | |
| 
 | |
|         if spinner is not None:
 | |
|           spinner.update_progress(out.tell(), partition_size)
 | |
| 
 | |
|       if raw_hash.hexdigest().lower() != partition['hash_raw'].lower():
 | |
|         raise Exception(f"Unsparse hash mismatch '{raw_hash.hexdigest().lower()}'")
 | |
|     else:
 | |
|       while not downloader.eof:
 | |
|         out.write(downloader.read(1024 * 1024))
 | |
| 
 | |
|         if spinner is not None:
 | |
|           spinner.update_progress(out.tell(), partition_size)
 | |
| 
 | |
|     if downloader.sha256.hexdigest().lower() != partition['hash'].lower():
 | |
|       raise Exception("Uncompressed hash mismatch")
 | |
| 
 | |
|     if out.tell() != partition['size']:
 | |
|       raise Exception("Uncompressed size mismatch")
 | |
| 
 | |
|     # Write hash after successfull flash
 | |
|     os.sync()
 | |
|     out.write(partition['hash_raw'].lower().encode())
 | |
| 
 | |
| 
 | |
| def flash_agnos_update(manifest_path, cloudlog, spinner=None):
 | |
|   update = json.load(open(manifest_path))
 | |
| 
 | |
|   current_slot = subprocess.check_output(["abctl", "--boot_slot"], encoding='utf-8').strip()
 | |
|   target_slot = "_b" if current_slot == "_a" else "_a"
 | |
|   target_slot_number = "0" if target_slot == "_a" else "1"
 | |
| 
 | |
|   cloudlog.info(f"Current slot {current_slot}, target slot {target_slot}")
 | |
| 
 | |
|   # set target slot as unbootable
 | |
|   os.system(f"abctl --set_unbootable {target_slot_number}")
 | |
| 
 | |
|   for partition in update:
 | |
|     success = False
 | |
| 
 | |
|     for retries in range(10):
 | |
|       try:
 | |
|         flash_partition(cloudlog, spinner, target_slot, partition)
 | |
|         success = True
 | |
|         break
 | |
| 
 | |
|       except requests.exceptions.RequestException:
 | |
|         cloudlog.exception("Failed")
 | |
|         spinner.update("Waiting for internet...")
 | |
|         cloudlog.info(f"Failed to download {partition['name']}, retrying ({retries})")
 | |
|         time.sleep(10)
 | |
| 
 | |
|     if not success:
 | |
|       cloudlog.info(f"Failed to flash {partition['name']}, aborting")
 | |
|       raise Exception("Maximum retries exceeded")
 | |
| 
 | |
|   cloudlog.info(f"AGNOS ready on slot {target_slot}")
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|   import logging
 | |
|   import time
 | |
|   import sys
 | |
| 
 | |
|   if len(sys.argv) != 2:
 | |
|     print("Usage: ./agnos.py <manifest.json>")
 | |
|     exit(1)
 | |
| 
 | |
|   spinner = Spinner()
 | |
|   spinner.update("Updating AGNOS")
 | |
|   time.sleep(5)
 | |
| 
 | |
|   logging.basicConfig(level=logging.INFO)
 | |
|   flash_agnos_update(sys.argv[1], logging, spinner)
 | |
| 
 |