Android系統Recovery工作原理之使用update.zip升級過程分析

轉載自  http://blog.csdn.net/mu0206mu


         這篇及以後的篇幅將通過分析update.zip包在具體Android系統升級的過程,來理解Android系統中Recovery模式服務的工作原理。我們先從update.zip包的製作開始,然後是Android系統的啓動模式分析,Recovery工作原理,如何從我們上層開始選擇system update到重啓到Recovery服務,以及在Recovery服務中具體怎樣處理update.zip包升級的,我們的安裝腳本updater-script怎樣被解析並執行的等一系列問題。分析過程中所用的Android源碼是gingerbread0919(tcc88xx開發板標配的),測試開發板是tcc88xx。這是在工作中總結的文檔,當然在網上參考了不少內容,如有雷同純屬巧合吧,在分析過程中也存在很多未解決的問題,也希望大家不吝指教。


一、 update.zip包的目錄結構
          |----boot.img
          |----system/
          |----recovery/
                `|----recovery-from-boot.p
                `|----etc/
                        `|----install-recovery.sh
          |---META-INF/
              `|CERT.RSA
              `|CERT.SF
              `|MANIFEST.MF
              `|----com/
                     `|----google/
                             `|----android/
                                    `|----update-binary
                                    `|----updater-script
                             `|----android/
                                    `|----metadata
二、 update.zip包目錄結構詳解
         以上是我們用命令make otapackage 製作的update.zip包的標準目錄結構。
         1、boot.img是更新boot分區所需要的文件。這個boot.img主要包括kernel+ramdisk。

         2、system/目錄的內容在升級後會放在系統的system分區。主要用來更新系統的一些應用或則應用會用到的一些庫等等。可以將Android源碼編譯out/target/product/tcc8800/system/中的所有文件拷貝到這個目錄來代替。

         3、recovery/目錄中的recovery-from-boot.p是boot.img和recovery.img的補丁(patch),主要用來更新recovery分區,其中etc/目錄下的install-recovery.sh是更新腳本。
         4、update-binary是一個二進制文件,相當於一個腳本解釋器,能夠識別updater-script中描述的操作。該文件在Android源碼編譯後out/target/product/tcc8800/system bin/updater生成,可將updater重命名爲update-binary得到。
               該文件在具體的更新包中的名字由源碼中bootable/recovery/install.c中的宏ASSUMED_UPDATE_BINARY_NAME的值而定。
         5、updater-script:此文件是一個腳本文件,具體描述了更新過程。我們可以根據具體情況編寫該腳本來適應我們的具體需求。該文件的命名由源碼中bootable/recovery/updater/updater.c文件中的宏SCRIPT_NAME的值而定。
         6、 metadata文件是描述設備信息及環境變量的元數據。主要包括一些編譯選項,簽名公鑰,時間戳以及設備型號等。
         7、我們還可以在包中添加userdata目錄,來更新系統中的用戶數據部分。這部分內容在更新後會存放在系統的/data目錄下。

         8、update.zip包的簽名:update.zip更新包在製作完成後需要對其簽名,否則在升級時會出現認證失敗的錯誤提示。而且簽名要使用和目標板一致的加密公鑰。加密公鑰及加密需要的三個文件在Android源碼編譯後生成的具體路徑爲:

               out/host/linux-x86/framework/signapk.jar 

               build/target/product/security/testkey.x509.pem         

               build/target/product/security/testkey.pk8 。

              我們用命令make otapackage製作生成的update.zip包是已簽過名的,如果自己做update.zip包時必須手動對其簽名。

              具體的加密方法:$ java –jar gingerbread/out/host/linux/framework/signapk.jar –w gingerbread/build/target/product/security/testkey.x509.pem                                      gingerbread/build/target/product/security/testkey.pk8 update.zip update_signed.zip
              以上命令在update.zip包所在的路徑下執行,其中signapk.jar testkey.x509.pem以及testkey.pk8文件的引用使用絕對路徑。update.zip 是我們已經打好的包,update_signed.zip包是命令執行完生成的已經簽過名的包。
         9、MANIFEST.MF:這個manifest文件定義了與包的組成結構相關的數據。類似Android應用的mainfest.xml文件。
        10、CERT.RSA:與簽名文件相關聯的簽名程序塊文件,它存儲了用於簽名JAR文件的公共簽名。
        11、CERT.SF:這是JAR文件的簽名文件,其中前綴CERT代表簽名者。
        另外,在具體升級時,對update.zip包檢查時大致會分三步:①檢驗SF文件與RSA文件是否匹配。②檢驗MANIFEST.MF與簽名文件中的digest是否一致。③檢驗包中的文件與MANIFEST中所描述的是否一致。
三、 Android升級包update.zip的生成過程分析

         1) 對於update.zip包的製作有兩種方式,即手動製作和命令生成。

          第一種手動製作:即按照update.zip的目錄結構手動創建我們需要的目錄。然後將對應的文件拷貝到相應的目錄下,比如我們向系統中新加一個應用程序。可以將新增的應用拷貝到我們新建的update/system/app/下(system目錄是事先拷貝編譯源碼後生成的system目錄),打包並簽名後,拷貝到SD卡就可以使用了。這種方式在實際的tcc8800開發板中未測試成功。簽名部分未通過,可能與具體的開發板相關。

          第二種製作方式:命令製作。Android源碼系統中爲我們提供了製作update.zip刷機包的命令,即make otapackage。該命令在編譯源碼完成後並在源碼根目錄下執行。 具體操作方式:在源碼根目錄下執行

                ①$ . build/envsetup.sh。 

                ②$ lunch 然後選擇你需要的配置(如17)。

                ③$ make otapackage。

          在編譯完源碼後最好再執行一遍上面的①、②步防止執行③時出現未找到對應規則的錯誤提示。命令執行完成後生成的升級包所在位置在out/target/product/full_tcc8800_evm_target_files-eng.mumu.20120309.111059.zip將這個包重新命名爲update.zip,並拷貝到SD卡中即可使用。

           這種方式(即完全升級)在tcc8800開發板中已測試成功。

       2) 使用make otapackage命令生成update.zip的過程分析。
            在源碼根目錄下執行make otapackage命令生成update.zip包主要分爲兩步,第一步是根據Makefile執行編譯生成一個update原包(zip格式)。第二步是運行一個python腳本,並以上一步準備的zip包作爲輸入,最終生成我們需要的升級包。下面進一步分析這兩個過程。

            第一步:編譯Makefile。對應的Makefile文件所在位置:build/core/Makefile。從該文件的884行(tcc8800,gingerbread0919)開始會生成一個zip包,這個包最後會用來製作OTA package 或者filesystem image。先將這部分的對應的Makefile貼出來如下:


# -----------------------------------------------------------------  
# A zip of the directories that map to the target filesystem.  
# This zip can be used to create an OTA package or filesystem image  
# as a post-build step.  
#  
name := $(TARGET_PRODUCT)  
ifeq ($(TARGET_BUILD_TYPE),debug)  
  name := $(name)_debug  
endif  
name := $(name)-target_files-$(FILE_NAME_TAG)  
  
intermediates := $(call intermediates-dir-for,PACKAGING,target_files)  
BUILT_TARGET_FILES_PACKAGE := $(intermediates)/$(name).zip  
$(BUILT_TARGET_FILES_PACKAGE): intermediates := $(intermediates)  
$(BUILT_TARGET_FILES_PACKAGE): \  
        zip_root := $(intermediates)/$(name)  
  
# $(1): Directory to copy  
# $(2): Location to copy it to  
# The "ls -A" is to prevent "acp s/* d" from failing if s is empty.  
define package_files-copy-root  
  if [ -d "$(strip $(1))" -a "$$(ls -A $(1))" ]; then \  
    mkdir -p $(2) && \  
    $(ACP) -rd $(strip $(1))/* $(2); \  
  fi  
endef  
  
built_ota_tools := \  
    $(call intermediates-dir-for,EXECUTABLES,applypatch)/applypatch \  
    $(call intermediates-dir-for,EXECUTABLES,applypatch_static)/applypatch_static \  
    $(call intermediates-dir-for,EXECUTABLES,check_prereq)/check_prereq \  
    $(call intermediates-dir-for,EXECUTABLES,updater)/updater  
$(BUILT_TARGET_FILES_PACKAGE): PRIVATE_OTA_TOOLS := $(built_ota_tools)  
  
$(BUILT_TARGET_FILES_PACKAGE): PRIVATE_RECOVERY_API_VERSION := $(RECOVERY_API_VERSION)  
  
ifeq ($(TARGET_RELEASETOOLS_EXTENSIONS),)  
# default to common dir for device vendor  
$(BUILT_TARGET_FILES_PACKAGE): tool_extensions := $(TARGET_DEVICE_DIR)/../common  
else  
$(BUILT_TARGET_FILES_PACKAGE): tool_extensions := $(TARGET_RELEASETOOLS_EXTENSIONS)  
endif  
  
# Depending on the various images guarantees that the underlying  
# directories are up-to-date.  
$(BUILT_TARGET_FILES_PACKAGE): \  
        $(INSTALLED_BOOTIMAGE_TARGET) \  
        $(INSTALLED_RADIOIMAGE_TARGET) \  
        $(INSTALLED_RECOVERYIMAGE_TARGET) \  
        $(INSTALLED_SYSTEMIMAGE) \  
        $(INSTALLED_USERDATAIMAGE_TARGET) \  
        $(INSTALLED_ANDROID_INFO_TXT_TARGET) \  
        $(built_ota_tools) \  
        $(APKCERTS_FILE) \  
        $(HOST_OUT_EXECUTABLES)/fs_config \  
        | $(ACP)  
    @echo "Package target files: $@"  
    $(hide) rm -rf $@ $(zip_root)  
    $(hide) mkdir -p $(dir $@) $(zip_root)  
    @# Components of the recovery image  
    $(hide) mkdir -p $(zip_root)/RECOVERY  
    $(hide) $(call package_files-copy-root, \  
        $(TARGET_RECOVERY_ROOT_OUT),$(zip_root)/RECOVERY/RAMDISK)  
ifdef INSTALLED_KERNEL_TARGET  
    $(hide) $(ACP) $(INSTALLED_KERNEL_TARGET) $(zip_root)/RECOVERY/kernel  
endif  
ifdef INSTALLED_2NDBOOTLOADER_TARGET  
    $(hide) $(ACP) \  
        $(INSTALLED_2NDBOOTLOADER_TARGET) $(zip_root)/RECOVERY/second  
endif  
ifdef BOARD_KERNEL_CMDLINE  
    $(hide) echo "$(BOARD_KERNEL_CMDLINE)" > $(zip_root)/RECOVERY/cmdline  
endif  
ifdef BOARD_KERNEL_BASE  
    $(hide) echo "$(BOARD_KERNEL_BASE)" > $(zip_root)/RECOVERY/base  
endif  
ifdef BOARD_KERNEL_PAGESIZE  
    $(hide) echo "$(BOARD_KERNEL_PAGESIZE)" > $(zip_root)/RECOVERY/pagesize  
endif  
    @# Components of the boot image  
    $(hide) mkdir -p $(zip_root)/BOOT  
    $(hide) $(call package_files-copy-root, \  
        $(TARGET_ROOT_OUT),$(zip_root)/BOOT/RAMDISK)  
ifdef INSTALLED_KERNEL_TARGET  
    $(hide) $(ACP) $(INSTALLED_KERNEL_TARGET) $(zip_root)/BOOT/kernel  
endif  
ifdef INSTALLED_2NDBOOTLOADER_TARGET  
    $(hide) $(ACP) \  
        $(INSTALLED_2NDBOOTLOADER_TARGET) $(zip_root)/BOOT/second  
endif  
ifdef BOARD_KERNEL_CMDLINE  
    $(hide) echo "$(BOARD_KERNEL_CMDLINE)" > $(zip_root)/BOOT/cmdline  
endif  
ifdef BOARD_KERNEL_BASE  
    $(hide) echo "$(BOARD_KERNEL_BASE)" > $(zip_root)/BOOT/base  
endif  
ifdef BOARD_KERNEL_PAGESIZE  
    $(hide) echo "$(BOARD_KERNEL_PAGESIZE)" > $(zip_root)/BOOT/pagesize  
endif  
    $(hide) $(foreach t,$(INSTALLED_RADIOIMAGE_TARGET),\  
                mkdir -p $(zip_root)/RADIO; \  
                $(ACP) $(t) $(zip_root)/RADIO/$(notdir $(t));)  
    @# Contents of the system image  
    $(hide) $(call package_files-copy-root, \  
        $(SYSTEMIMAGE_SOURCE_DIR),$(zip_root)/SYSTEM)  
    @# Contents of the data image  
    $(hide) $(call package_files-copy-root, \  
        $(TARGET_OUT_DATA),$(zip_root)/DATA)  
    @# Extra contents of the OTA package  
    $(hide) mkdir -p $(zip_root)/OTA/bin  
    $(hide) $(ACP) $(INSTALLED_ANDROID_INFO_TXT_TARGET) $(zip_root)/OTA/  
    $(hide) $(ACP) $(PRIVATE_OTA_TOOLS) $(zip_root)/OTA/bin/  
    @# Files that do not end up in any images, but are necessary to  
    @# build them.  
    $(hide) mkdir -p $(zip_root)/META  
    $(hide) $(ACP) $(APKCERTS_FILE) $(zip_root)/META/apkcerts.txt  
    $(hide) echo "$(PRODUCT_OTA_PUBLIC_KEYS)" > $(zip_root)/META/otakeys.txt  
    $(hide) echo "recovery_api_version=$(PRIVATE_RECOVERY_API_VERSION)" > $(zip_root)/META/misc_info.txt  
ifdef BOARD_FLASH_BLOCK_SIZE  
    $(hide) echo "blocksize=$(BOARD_FLASH_BLOCK_SIZE)" >> $(zip_root)/META/misc_info.txt  
endif  
ifdef BOARD_BOOTIMAGE_PARTITION_SIZE  
    $(hide) echo "boot_size=$(BOARD_BOOTIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt  
endif  
ifdef BOARD_RECOVERYIMAGE_PARTITION_SIZE  
    $(hide) echo "recovery_size=$(BOARD_RECOVERYIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt  
endif  
ifdef BOARD_SYSTEMIMAGE_PARTITION_SIZE  
    $(hide) echo "system_size=$(BOARD_SYSTEMIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt  
endif  
ifdef BOARD_USERDATAIMAGE_PARTITION_SIZE  
    $(hide) echo "userdata_size=$(BOARD_USERDATAIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt  
endif  
    $(hide) echo "tool_extensions=$(tool_extensions)" >> $(zip_root)/META/misc_info.txt  
ifdef mkyaffs2_extra_flags  
    $(hide) echo "mkyaffs2_extra_flags=$(mkyaffs2_extra_flags)" >> $(zip_root)/META/misc_info.txt  
endif  
    @# Zip everything up, preserving symlinks  
    $(hide) (cd $(zip_root) && zip -qry ../$(notdir $@) .)  
    @# Run fs_config on all the system files in the zip, and save the output  
    $(hide) zipinfo -1 $@ | awk -F/ 'BEGIN { OFS="/" } /^SYSTEM\// {$$1 = "system"; print}' | $(HOST_OUT_EXECUTABLES)/fs_config > $(zip_root)/META/filesystem_config.txt  
    $(hide) (cd $(zip_root) && zip -q ../$(notdir $@) META/filesystem_config.txt)  
  
  
target-files-package: $(BUILT_TARGET_FILES_PACKAGE)  
  
  
ifneq ($(TARGET_SIMULATOR),true)  
ifneq ($(TARGET_PRODUCT),sdk)  
ifneq ($(TARGET_DEVICE),generic)  
ifneq ($(TARGET_NO_KERNEL),true)  
ifneq ($(recovery_fstab),)  


            根據上面的Makefile可以分析這個包的生成過程:

            首先創建一個root_zip根目錄,並依次在此目錄下創建所需要的如下其他目錄

            ①創建RECOVERY目錄,並填充該目錄的內容,包括kernel的鏡像和recovery根文件系統的鏡像。此目錄最終用於生成recovery.img。

            ②創建並填充BOOT目錄。包含kernel和cmdline以及pagesize大小等,該目錄最終用來生成boot.img。
            ③向SYSTEM目錄填充system image。
            ④向DATA填充data image。
            ⑤用於生成OTA package包所需要的額外的內容。主要包括一些bin命令。
            ⑥創建META目錄並向該目錄下添加一些文本文件,如apkcerts.txt(描述apk文件用到的認證證書),misc_info.txt(描述Flash內存的塊大小以及boot、recovery、system、userdata等分區的大小信息)。
            ⑦使用保留連接選項壓縮我們在上面獲得的root_zip目錄。
            ⑧使用fs_config(build/tools/fs_config)配置上面的zip包內所有的系統文件(system/下各目錄、文件)的權限屬主等信息。fs_config包含了一個頭文件#include“private/android_filesystem_config.h”。在這個頭文件中以硬編碼的方式設定了system目錄下各文件的權限、屬主。執行完配置後會將配置後的信息以文本方式輸出 到META/filesystem_config.txt中。並再一次zip壓縮成我們最終需要的原始包。

             第二步:上面的zip包只是一個編譯過程中生成的原始包。這個原始zip包在實際的編譯過程中有兩個作用,一是用來生成OTA update升級包,二是用來生成系統鏡像。在編譯過程中若生成OTA update升級包時會調用(具體位置在Makefile的1037行到1058行)一個名爲ota_from_target_files的python腳本,位置在/build/tools/releasetools/ota_from_target_files。這個腳本的作用是以第一步生成的zip原始包作爲輸入,最終生成可用的OTA升級zip包。

             下面我們分析使用這個腳本生成最終OTA升級包的過程。

                   ㈠ 首先看一下這個腳本開始部分的幫助文檔。代碼如下:

#!/usr/bin/env python  
#  
# Copyright (C) 2008 The Android Open Source Project  
#  
# Licensed under the Apache License, Version 2.0 (the "License");  
# you may not use this file except in compliance with the License.  
# You may obtain a copy of the License at  
#  
#      http://www.apache.org/licenses/LICENSE-2.0  
#  
# Unless required by applicable law or agreed to in writing, software  
# distributed under the License is distributed on an "AS IS" BASIS,  
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
# See the License for the specific language governing permissions and  
# limitations under the License.  
  
""" 
Given a target-files zipfile, produces an OTA package that installs 
that build.  An incremental OTA is produced if -i is given, otherwise 
a full OTA is produced. 
 
Usage:  ota_from_target_files [flags] input_target_files output_ota_package 
 
  -b  (--board_config)  <file> 
      Deprecated. 
 
  -k  (--package_key)  <key> 
      Key to use to sign the package (default is 
      "build/target/product/security/testkey"). 
 
  -i  (--incremental_from)  <file> 
      Generate an incremental OTA using the given target-files zip as 
      the starting build. 
 
  -w  (--wipe_user_data) 
      Generate an OTA package that will wipe the user data partition 
      when installed. 
 
  -n  (--no_prereq) 
      Omit the timestamp prereq check normally included at the top of 
      the build scripts (used for developer OTA packages which 
      legitimately need to go back and forth). 
 
  -e  (--extra_script)  <file> 
      Insert the contents of file at the end of the update script. 
 
""" 

                        下面簡單翻譯一下他們的使用方法以及選項的作用。

                        Usage: ota_from_target_files [flags] input_target_files output_ota_package
                        -b 過時的。
                        -k簽名所使用的密鑰
                        -i生成增量OTA包時使用此選項。後面我們會用到這個選項來生成OTA增量包。
                        -w是否清除userdata分區
                        -n在升級時是否不檢查時間戳,缺省要檢查,即缺省情況下只能基於舊版本升級。
                        -e是否有額外運行的腳本
                        -m執行過程中生成腳本(updater-script)所需要的格式,目前有兩種即amend和edify。對應上兩種版本升級時會採用不同的解釋器。缺省會同時生成兩種格式的腳 本。
                        -p定義腳本用到的一些可執行文件的路徑。
                        -s定義額外運行腳本的路徑。
                        -x定義額外運行的腳本可能用的鍵值對。
                        -v執行過程中打印出執行的命令。
                        -h命令幫助

                   ㈡ 下面我們分析ota_from_target_files這個python腳本是怎樣生成最終zip包的。先講這個腳本的代碼貼出來如下:

import sys  
  
if sys.hexversion < 0x02040000:  
  print >> sys.stderr, "Python 2.4 or newer is required."  
  sys.exit(1)  
  
import copy  
import errno  
import os  
import re  
import sha  
import subprocess  
import tempfile  
import time  
import zipfile  
  
import common  
import edify_generator  
  
OPTIONS = common.OPTIONS  
OPTIONS.package_key = "build/target/product/security/testkey"  
OPTIONS.incremental_source = None  
OPTIONS.require_verbatim = set()  
OPTIONS.prohibit_verbatim = set(("system/build.prop",))  
OPTIONS.patch_threshold = 0.95  
OPTIONS.wipe_user_data = False  
OPTIONS.omit_prereq = False  
OPTIONS.extra_script = None  
OPTIONS.worker_threads = 3  
  
def MostPopularKey(d, default):  
  """Given a dict, return the key corresponding to the largest 
  value.  Returns 'default' if the dict is empty."""  
  x = [(v, k) for (k, v) in d.iteritems()]  
  if not x: return default  
  x.sort()  
  return x[-1][1]  
  
  
def IsSymlink(info):  
  """Return true if the zipfile.ZipInfo object passed in represents a 
  symlink."""  
  return (info.external_attr >> 16) == 0120777  
  
  
class Item:  
  """Items represent the metadata (user, group, mode) of files and 
  directories in the system image."""  
  ITEMS = {}  
  def __init__(self, name, dir=False):  
    self.name = name  
    self.uid = None  
    self.gid = None  
    self.mode = None  
    self.dir = dir  
  
    if name:  
      self.parent = Item.Get(os.path.dirname(name), dir=True)  
      self.parent.children.append(self)  
    else:  
      self.parent = None  
    if dir:  
      self.children = []  
  
  def Dump(self, indent=0):  
    if self.uid is not None:  
      print "%s%s %d %d %o" % ("  "*indent, self.name, self.uid, self.gid, self.mode)  
    else:  
      print "%s%s %s %s %s" % ("  "*indent, self.name, self.uid, self.gid, self.mode)  
    if self.dir:  
      print "%s%s" % ("  "*indent, self.descendants)  
      print "%s%s" % ("  "*indent, self.best_subtree)  
      for i in self.children:  
        i.Dump(indent=indent+1)  
 
  @classmethod  
  def Get(cls, name, dir=False):  
    if name not in cls.ITEMS:  
      cls.ITEMS[name] = Item(name, dir=dir)  
    return cls.ITEMS[name]  
 
  @classmethod  
  def GetMetadata(cls, input_zip):  
  
    try:  
      # See if the target_files contains a record of what the uid,  
      # gid, and mode is supposed to be.  
      output = input_zip.read("META/filesystem_config.txt")  
    except KeyError:  
      # Run the external 'fs_config' program to determine the desired  
      # uid, gid, and mode for every Item object.  Note this uses the  
      # one in the client now, which might not be the same as the one  
      # used when this target_files was built.  
      p = common.Run(["fs_config"], stdin=subprocess.PIPE,  
                     stdout=subprocess.PIPE, stderr=subprocess.PIPE)  
      suffix = { False: "", True: "/" }  
      input = "".join(["%s%s\n" % (i.name, suffix[i.dir])  
                       for i in cls.ITEMS.itervalues() if i.name])  
      output, error = p.communicate(input)  
      assert not error  
  
    for line in output.split("\n"):  
      if not line: continue  
      name, uid, gid, mode = line.split()  
      i = cls.ITEMS.get(name, None)  
      if i is not None:  
        i.uid = int(uid)  
        i.gid = int(gid)  
        i.mode = int(mode, 8)  
        if i.dir:  
          i.children.sort(key=lambda i: i.name)  
  
    # set metadata for the files generated by this script.  
    i = cls.ITEMS.get("system/recovery-from-boot.p", None)  
    if i: i.uid, i.gid, i.mode = 0, 0, 0644  
    i = cls.ITEMS.get("system/etc/install-recovery.sh", None)  
    if i: i.uid, i.gid, i.mode = 0, 0, 0544  
  
  def CountChildMetadata(self):  
    """Count up the (uid, gid, mode) tuples for all children and 
    determine the best strategy for using set_perm_recursive and 
    set_perm to correctly chown/chmod all the files to their desired 
    values.  Recursively calls itself for all descendants. 
 
    Returns a dict of {(uid, gid, dmode, fmode): count} counting up 
    all descendants of this node.  (dmode or fmode may be None.)  Also 
    sets the best_subtree of each directory Item to the (uid, gid, 
    dmode, fmode) tuple that will match the most descendants of that 
    Item. 
    """  
  
    assert self.dir  
    d = self.descendants = {(self.uid, self.gid, self.mode, None): 1}  
    for i in self.children:  
      if i.dir:  
        for k, v in i.CountChildMetadata().iteritems():  
          d[k] = d.get(k, 0) + v  
      else:  
        k = (i.uid, i.gid, None, i.mode)  
        d[k] = d.get(k, 0) + 1  
  
    # Find the (uid, gid, dmode, fmode) tuple that matches the most  
    # descendants.  
  
    # First, find the (uid, gid) pair that matches the most  
    # descendants.  
    ug = {}  
    for (uid, gid, _, _), count in d.iteritems():  
      ug[(uid, gid)] = ug.get((uid, gid), 0) + count  
    ug = MostPopularKey(ug, (0, 0))  
  
    # Now find the dmode and fmode that match the most descendants  
    # with that (uid, gid), and choose those.  
    best_dmode = (0, 0755)  
    best_fmode = (0, 0644)  
    for k, count in d.iteritems():  
      if k[:2] != ug: continue  
      if k[2] is not None and count >= best_dmode[0]: best_dmode = (count, k[2])  
      if k[3] is not None and count >= best_fmode[0]: best_fmode = (count, k[3])  
    self.best_subtree = ug + (best_dmode[1], best_fmode[1])  
  
    return d  
  
  def SetPermissions(self, script):  
    """Append set_perm/set_perm_recursive commands to 'script' to 
    set all permissions, users, and groups for the tree of files 
    rooted at 'self'."""  
  
    self.CountChildMetadata()  
  
    def recurse(item, current):  
      # current is the (uid, gid, dmode, fmode) tuple that the current  
      # item (and all its children) have already been set to.  We only  
      # need to issue set_perm/set_perm_recursive commands if we're  
      # supposed to be something different.  
      if item.dir:  
        if current != item.best_subtree:  
          script.SetPermissionsRecursive("/"+item.name, *item.best_subtree)  
          current = item.best_subtree  
  
        if item.uid != current[0] or item.gid != current[1] or \  
           item.mode != current[2]:  
          script.SetPermissions("/"+item.name, item.uid, item.gid, item.mode)  
  
        for i in item.children:  
          recurse(i, current)  
      else:  
        if item.uid != current[0] or item.gid != current[1] or \  
               item.mode != current[3]:  
          script.SetPermissions("/"+item.name, item.uid, item.gid, item.mode)  
  
    recurse(self, (-1, -1, -1, -1))  
  
  
def CopySystemFiles(input_zip, output_zip=None,  
                    substitute=None):  
  """Copies files underneath system/ in the input zip to the output 
  zip.  Populates the Item class with their metadata, and returns a 
  list of symlinks.  output_zip may be None, in which case the copy is 
  skipped (but the other side effects still happen).  substitute is an 
  optional dict of {output filename: contents} to be output instead of 
  certain input files. 
  """  
  
  symlinks = []  
  
  for info in input_zip.infolist():  
    if info.filename.startswith("SYSTEM/"):  
      basefilename = info.filename[7:]  
      if IsSymlink(info):  
        symlinks.append((input_zip.read(info.filename),  
                         "/system/" + basefilename))  
      else:  
        info2 = copy.copy(info)  
        fn = info2.filename = "system/" + basefilename  
        if substitute and fn in substitute and substitute[fn] is None:  
          continue  
        if output_zip is not None:  
          if substitute and fn in substitute:  
            data = substitute[fn]  
          else:  
            data = input_zip.read(info.filename)  
          output_zip.writestr(info2, data)  
        if fn.endswith("/"):  
          Item.Get(fn[:-1], dir=True)  
        else:  
          Item.Get(fn, dir=False)  
  
  symlinks.sort()  
  return symlinks  
  
  
def SignOutput(temp_zip_name, output_zip_name):  
  key_passwords = common.GetKeyPasswords([OPTIONS.package_key])  
  pw = key_passwords[OPTIONS.package_key]  
  
  common.SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw,  
                  whole_file=True)  
  
  
def AppendAssertions(script, input_zip):  
  device = GetBuildProp("ro.product.device", input_zip)  
  script.AssertDevice(device)  
  
  
def MakeRecoveryPatch(output_zip, recovery_img, boot_img):  
  """Generate a binary patch that creates the recovery image starting 
  with the boot image.  (Most of the space in these images is just the 
  kernel, which is identical for the two, so the resulting patch 
  should be efficient.)  Add it to the output zip, along with a shell 
  script that is run from init.rc on first boot to actually do the 
  patching and install the new recovery image. 
 
  recovery_img and boot_img should be File objects for the 
  corresponding images. 
 
  Returns an Item for the shell script, which must be made 
  executable. 
  """  
  
  d = common.Difference(recovery_img, boot_img)  
  _, _, patch = d.ComputePatch()  
  common.ZipWriteStr(output_zip, "recovery/recovery-from-boot.p", patch)  
  Item.Get("system/recovery-from-boot.p", dir=False)  
  
  boot_type, boot_device = common.GetTypeAndDevice("/boot", OPTIONS.info_dict)  
  recovery_type, recovery_device = common.GetTypeAndDevice("/recovery", OPTIONS.info_dict)  
  
  # Images with different content will have a different first page, so  
  # we check to see if this recovery has already been installed by  
  # testing just the first 2k.  
  HEADER_SIZE = 2048  
  header_sha1 = sha.sha(recovery_img.data[:HEADER_SIZE]).hexdigest()  
  sh = """#!/system/bin/sh 
if ! applypatch -c %(recovery_type)s:%(recovery_device)s:%(header_size)d:%(header_sha1)s; then 
  log -t recovery "Installing new recovery image" 
  applypatch %(boot_type)s:%(boot_device)s:%(boot_size)d:%(boot_sha1)s %(recovery_type)s:%(recovery_device)s %(recovery_sha1)s %(recovery_size)d %(boot_sha1)s:/system/recovery-from-boot.p 
else 
  log -t recovery "Recovery image already installed" 
fi 
""" % { 'boot_size': boot_img.size,  
        'boot_sha1': boot_img.sha1,  
        'header_size': HEADER_SIZE,  
        'header_sha1': header_sha1,  
        'recovery_size': recovery_img.size,  
        'recovery_sha1': recovery_img.sha1,  
        'boot_type': boot_type,  
        'boot_device': boot_device,  
        'recovery_type': recovery_type,  
        'recovery_device': recovery_device,  
        }  
  common.ZipWriteStr(output_zip, "recovery/etc/install-recovery.sh", sh)  
  return Item.Get("system/etc/install-recovery.sh", dir=False)  
  
  
def WriteFullOTAPackage(input_zip, output_zip):  
  # TODO: how to determine this?  We don't know what version it will  
  # be installed on top of.  For now, we expect the API just won't  
  # change very often.  
  script = edify_generator.EdifyGenerator(3, OPTIONS.info_dict)  
  
  metadata = {"post-build": GetBuildProp("ro.build.fingerprint", input_zip),  
              "pre-device": GetBuildProp("ro.product.device", input_zip),  
              "post-timestamp": GetBuildProp("ro.build.date.utc", input_zip),  
              }  
  
  device_specific = common.DeviceSpecificParams(  
      input_zip=input_zip,  
      input_version=OPTIONS.info_dict["recovery_api_version"],  
      output_zip=output_zip,  
      script=script,  
      input_tmp=OPTIONS.input_tmp,  
      metadata=metadata,  
      info_dict=OPTIONS.info_dict)  
  
  if not OPTIONS.omit_prereq:  
    ts = GetBuildProp("ro.build.date.utc", input_zip)  
    script.AssertOlderBuild(ts)  
  
  AppendAssertions(script, input_zip)  
  device_specific.FullOTA_Assertions()  
  
  script.ShowProgress(0.5, 0)  
  
  if OPTIONS.wipe_user_data:  
    script.FormatPartition("/data")  
  
  script.FormatPartition("/system")  
  script.Mount("/system")  
  script.UnpackPackageDir("recovery", "/system")  
  script.UnpackPackageDir("system", "/system")  
  
  symlinks = CopySystemFiles(input_zip, output_zip)  
  script.MakeSymlinks(symlinks)  
  
  boot_img = common.File("boot.img", common.BuildBootableImage(  
      os.path.join(OPTIONS.input_tmp, "BOOT")))  
  recovery_img = common.File("recovery.img", common.BuildBootableImage(  
      os.path.join(OPTIONS.input_tmp, "RECOVERY")))  
  MakeRecoveryPatch(output_zip, recovery_img, boot_img)  
  
  Item.GetMetadata(input_zip)  
  Item.Get("system").SetPermissions(script)  
  
  common.CheckSize(boot_img.data, "boot.img", OPTIONS.info_dict)  
  common.ZipWriteStr(output_zip, "boot.img", boot_img.data)  
  script.ShowProgress(0.2, 0)  
  
  script.ShowProgress(0.2, 10)  
  script.WriteRawImage("/boot", "boot.img")  
  
  script.ShowProgress(0.1, 0)  
  device_specific.FullOTA_InstallEnd()  
  
  if OPTIONS.extra_script is not None:  
    script.AppendExtra(OPTIONS.extra_script)  
  
  script.UnmountAll()  
  script.AddToZip(input_zip, output_zip)  
  WriteMetadata(metadata, output_zip)  
  
  
def WriteMetadata(metadata, output_zip):  
  common.ZipWriteStr(output_zip, "META-INF/com/android/metadata",  
                     "".join(["%s=%s\n" % kv  
                              for kv in sorted(metadata.iteritems())]))  
  
  
  
  
def LoadSystemFiles(z):  
  """Load all the files from SYSTEM/... in a given target-files 
  ZipFile, and return a dict of {filename: File object}."""  
  out = {}  
  for info in z.infolist():  
    if info.filename.startswith("SYSTEM/") and not IsSymlink(info):  
      fn = "system/" + info.filename[7:]  
      data = z.read(info.filename)  
      out[fn] = common.File(fn, data)  
  return out  
  
  
def GetBuildProp(property, z):  
  """Return the fingerprint of the build of a given target-files 
  ZipFile object."""  
  bp = z.read("SYSTEM/build.prop")  
  if not property:  
    return bp  
  m = re.search(re.escape(property) + r"=(.*)\n", bp)  
  if not m:  
    raise common.ExternalError("couldn't find %s in build.prop" % (property,))  
  return m.group(1).strip()  
  
  
def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip):  
  source_version = OPTIONS.source_info_dict["recovery_api_version"]  
  target_version = OPTIONS.target_info_dict["recovery_api_version"]  
  
  if source_version == 0:  
    print ("WARNING: generating edify script for a source that "  
           "can't install it.")  
  script = edify_generator.EdifyGenerator(source_version, OPTIONS.info_dict)  
  
  metadata = {"pre-device": GetBuildProp("ro.product.device", source_zip),  
              "post-timestamp": GetBuildProp("ro.build.date.utc", target_zip),  
              }  
  
  device_specific = common.DeviceSpecificParams(  
      source_zip=source_zip,  
      source_version=source_version,  
      target_zip=target_zip,  
      target_version=target_version,  
      output_zip=output_zip,  
      script=script,  
      metadata=metadata,  
      info_dict=OPTIONS.info_dict)  
  
  print "Loading target..."  
  target_data = LoadSystemFiles(target_zip)  
  print "Loading source..."  
  source_data = LoadSystemFiles(source_zip)  
  
  verbatim_targets = []  
  patch_list = []  
  diffs = []  
  largest_source_size = 0  
  for fn in sorted(target_data.keys()):  
    tf = target_data[fn]  
    assert fn == tf.name  
    sf = source_data.get(fn, None)  
  
    if sf is None or fn in OPTIONS.require_verbatim:  
      # This file should be included verbatim  
      if fn in OPTIONS.prohibit_verbatim:  
        raise common.ExternalError("\"%s\" must be sent verbatim" % (fn,))  
      print "send", fn, "verbatim"  
      tf.AddToZip(output_zip)  
      verbatim_targets.append((fn, tf.size))  
    elif tf.sha1 != sf.sha1:  
      # File is different; consider sending as a patch  
      diffs.append(common.Difference(tf, sf))  
    else:  
      # Target file identical to source.  
      pass  
  
  common.ComputeDifferences(diffs)  
  
  for diff in diffs:  
    tf, sf, d = diff.GetPatch()  
    if d is None or len(d) > tf.size * OPTIONS.patch_threshold:  
      # patch is almost as big as the file; don't bother patching  
      tf.AddToZip(output_zip)  
      verbatim_targets.append((tf.name, tf.size))  
    else:  
      common.ZipWriteStr(output_zip, "patch/" + tf.name + ".p", d)  
      patch_list.append((tf.name, tf, sf, tf.size, sha.sha(d).hexdigest()))  
      largest_source_size = max(largest_source_size, sf.size)  
  
  source_fp = GetBuildProp("ro.build.fingerprint", source_zip)  
  target_fp = GetBuildProp("ro.build.fingerprint", target_zip)  
  metadata["pre-build"] = source_fp  
  metadata["post-build"] = target_fp  
  
  script.Mount("/system")  
  script.AssertSomeFingerprint(source_fp, target_fp)  
  
  source_boot = common.File("/tmp/boot.img",  
                            common.BuildBootableImage(  
                                os.path.join(OPTIONS.source_tmp, "BOOT")))  
  target_boot = common.File("/tmp/boot.img",  
                            common.BuildBootableImage(  
                                os.path.join(OPTIONS.target_tmp, "BOOT")))  
  updating_boot = (source_boot.data != target_boot.data)  
  
  source_recovery = common.File("system/recovery.img",  
                                common.BuildBootableImage(  
                                    os.path.join(OPTIONS.source_tmp, "RECOVERY")))  
  target_recovery = common.File("system/recovery.img",  
                                common.BuildBootableImage(  
                                    os.path.join(OPTIONS.target_tmp, "RECOVERY")))  
  updating_recovery = (source_recovery.data != target_recovery.data)  
  
  # Here's how we divide up the progress bar:  
  #  0.1 for verifying the start state (PatchCheck calls)  
  #  0.8 for applying patches (ApplyPatch calls)  
  #  0.1 for unpacking verbatim files, symlinking, and doing the  
  #      device-specific commands.  
  
  AppendAssertions(script, target_zip)  
  device_specific.IncrementalOTA_Assertions()  
  
  script.Print("Verifying current system...")  
  
  script.ShowProgress(0.1, 0)  
  total_verify_size = float(sum([i[2].size for i in patch_list]) + 1)  
  if updating_boot:  
    total_verify_size += source_boot.size  
  so_far = 0  
  
  for fn, tf, sf, size, patch_sha in patch_list:  
    script.PatchCheck("/"+fn, tf.sha1, sf.sha1)  
    so_far += sf.size  
    script.SetProgress(so_far / total_verify_size)  
  
  if updating_boot:  
    d = common.Difference(target_boot, source_boot)  
    _, _, d = d.ComputePatch()  
    print "boot      target: %d  source: %d  diff: %d" % (  
        target_boot.size, source_boot.size, len(d))  
  
    common.ZipWriteStr(output_zip, "patch/boot.img.p", d)  
  
    boot_type, boot_device = common.GetTypeAndDevice("/boot", OPTIONS.info_dict)  
  
    script.PatchCheck("%s:%s:%d:%s:%d:%s" %  
                      (boot_type, boot_device,  
                       source_boot.size, source_boot.sha1,  
                       target_boot.size, target_boot.sha1))  
    so_far += source_boot.size  
    script.SetProgress(so_far / total_verify_size)  
  
  if patch_list or updating_recovery or updating_boot:  
    script.CacheFreeSpaceCheck(largest_source_size)  
  
  device_specific.IncrementalOTA_VerifyEnd()  
  
  script.Comment("---- start making changes here ----")  
  
  if OPTIONS.wipe_user_data:  
    script.Print("Erasing user data...")  
    script.FormatPartition("/data")  
  
  script.Print("Removing unneeded files...")  
  script.DeleteFiles(["/"+i[0] for i in verbatim_targets] +  
                     ["/"+i for i in sorted(source_data)  
                            if i not in target_data] +  
                     ["/system/recovery.img"])  
  
  script.ShowProgress(0.8, 0)  
  total_patch_size = float(sum([i[1].size for i in patch_list]) + 1)  
  if updating_boot:  
    total_patch_size += target_boot.size  
  so_far = 0  
  
  script.Print("Patching system files...")  
  for fn, tf, sf, size, _ in patch_list:  
    script.ApplyPatch("/"+fn, "-", tf.size, tf.sha1, sf.sha1, "patch/"+fn+".p")  
    so_far += tf.size  
    script.SetProgress(so_far / total_patch_size)  
  
  if updating_boot:  
    # Produce the boot image by applying a patch to the current  
    # contents of the boot partition, and write it back to the  
    # partition.  
    script.Print("Patching boot image...")  
    script.ApplyPatch("%s:%s:%d:%s:%d:%s"  
                      % (boot_type, boot_device,  
                         source_boot.size, source_boot.sha1,  
                         target_boot.size, target_boot.sha1),  
                      "-",  
                      target_boot.size, target_boot.sha1,  
                      source_boot.sha1, "patch/boot.img.p")  
    so_far += target_boot.size  
    script.SetProgress(so_far / total_patch_size)  
    print "boot image changed; including."  
  else:  
    print "boot image unchanged; skipping."  
  
  if updating_recovery:  
    # Is it better to generate recovery as a patch from the current  
    # boot image, or from the previous recovery image?  For large  
    # updates with significant kernel changes, probably the former.  
    # For small updates where the kernel hasn't changed, almost  
    # certainly the latter.  We pick the first option.  Future  
    # complicated schemes may let us effectively use both.  
    #  
    # A wacky possibility: as long as there is room in the boot  
    # partition, include the binaries and image files from recovery in  
    # the boot image (though not in the ramdisk) so they can be used  
    # as fodder for constructing the recovery image.  
    MakeRecoveryPatch(output_zip, target_recovery, target_boot)  
    script.DeleteFiles(["/system/recovery-from-boot.p",  
                        "/system/etc/install-recovery.sh"])  
    print "recovery image changed; including as patch from boot."  
  else:  
    print "recovery image unchanged; skipping."  
  
  script.ShowProgress(0.1, 10)  
  
  target_symlinks = CopySystemFiles(target_zip, None)  
  
  target_symlinks_d = dict([(i[1], i[0]) for i in target_symlinks])  
  temp_script = script.MakeTemporary()  
  Item.GetMetadata(target_zip)  
  Item.Get("system").SetPermissions(temp_script)  
  
  # Note that this call will mess up the tree of Items, so make sure  
  # we're done with it.  
  source_symlinks = CopySystemFiles(source_zip, None)  
  source_symlinks_d = dict([(i[1], i[0]) for i in source_symlinks])  
  
  # Delete all the symlinks in source that aren't in target.  This  
  # needs to happen before verbatim files are unpacked, in case a  
  # symlink in the source is replaced by a real file in the target.  
  to_delete = []  
  for dest, link in source_symlinks:  
    if link not in target_symlinks_d:  
      to_delete.append(link)  
  script.DeleteFiles(to_delete)  
  
  if verbatim_targets:  
    script.Print("Unpacking new files...")  
    script.UnpackPackageDir("system", "/system")  
  
  if updating_recovery:  
    script.Print("Unpacking new recovery...")  
    script.UnpackPackageDir("recovery", "/system")  
  
  script.Print("Symlinks and permissions...")  
  
  # Create all the symlinks that don't already exist, or point to  
  # somewhere different than what we want.  Delete each symlink before  
  # creating it, since the 'symlink' command won't overwrite.  
  to_create = []  
  for dest, link in target_symlinks:  
    if link in source_symlinks_d:  
      if dest != source_symlinks_d[link]:  
        to_create.append((dest, link))  
    else:  
      to_create.append((dest, link))  
  script.DeleteFiles([i[1] for i in to_create])  
  script.MakeSymlinks(to_create)  
  
  # Now that the symlinks are created, we can set all the  
  # permissions.  
  script.AppendScript(temp_script)  
  
  # Do device-specific installation (eg, write radio image).  
  device_specific.IncrementalOTA_InstallEnd()  
  
  if OPTIONS.extra_script is not None:  
    scirpt.AppendExtra(OPTIONS.extra_script)  
  
  script.AddToZip(target_zip, output_zip)  
  WriteMetadata(metadata, output_zip)  
  
  
def main(argv):  
  
  def option_handler(o, a):  
    if o in ("-b", "--board_config"):  
      pass   # deprecated  
    elif o in ("-k", "--package_key"):  
      OPTIONS.package_key = a  
    elif o in ("-i", "--incremental_from"):  
      OPTIONS.incremental_source = a  
    elif o in ("-w", "--wipe_user_data"):  
      OPTIONS.wipe_user_data = True  
    elif o in ("-n", "--no_prereq"):  
      OPTIONS.omit_prereq = True  
    elif o in ("-e", "--extra_script"):  
      OPTIONS.extra_script = a  
    elif o in ("--worker_threads"):  
      OPTIONS.worker_threads = int(a)  
    else:  
      return False  
    return True  
  
  args = common.ParseOptions(argv, __doc__,  
                             extra_opts="b:k:i:d:wne:",  
                             extra_long_opts=["board_config=",  
                                              "package_key=",  
                                              "incremental_from=",  
                                              "wipe_user_data",  
                                              "no_prereq",  
                                              "extra_script=",  
                                              "worker_threads="],  
                             extra_option_handler=option_handler)  
  
  if len(args) != 2:  
    common.Usage(__doc__)  
    sys.exit(1)  
  
  if OPTIONS.extra_script is not None:  
    OPTIONS.extra_script = open(OPTIONS.extra_script).read()  
  
  print "unzipping target target-files..."  
  OPTIONS.input_tmp = common.UnzipTemp(args[0])  
  
  OPTIONS.target_tmp = OPTIONS.input_tmp  
  input_zip = zipfile.ZipFile(args[0], "r")  
  OPTIONS.info_dict = common.LoadInfoDict(input_zip)  
  if OPTIONS.verbose:  
    print "--- target info ---"  
    common.DumpInfoDict(OPTIONS.info_dict)  
  
  if OPTIONS.device_specific is None:  
    OPTIONS.device_specific = OPTIONS.info_dict.get("tool_extensions", None)  
  if OPTIONS.device_specific is not None:  
    OPTIONS.device_specific = os.path.normpath(OPTIONS.device_specific)  
    print "using device-specific extensions in", OPTIONS.device_specific  
  
  if OPTIONS.package_key:  
    temp_zip_file = tempfile.NamedTemporaryFile()  
    output_zip = zipfile.ZipFile(temp_zip_file, "w",  
                                 compression=zipfile.ZIP_DEFLATED)  
  else:  
    output_zip = zipfile.ZipFile(args[1], "w",  
                                 compression=zipfile.ZIP_DEFLATED)  
  
  if OPTIONS.incremental_source is None:  
    WriteFullOTAPackage(input_zip, output_zip)  
  else:  
    print "unzipping source target-files..."  
    OPTIONS.source_tmp = common.UnzipTemp(OPTIONS.incremental_source)  
    source_zip = zipfile.ZipFile(OPTIONS.incremental_source, "r")  
    OPTIONS.target_info_dict = OPTIONS.info_dict  
    OPTIONS.source_info_dict = common.LoadInfoDict(source_zip)  
    if OPTIONS.verbose:  
      print "--- source info ---"  
      common.DumpInfoDict(OPTIONS.source_info_dict)  
    WriteIncrementalOTAPackage(input_zip, source_zip, output_zip)  
  
  output_zip.close()  
  if OPTIONS.package_key:  
    SignOutput(temp_zip_file.name, args[1])  
    temp_zip_file.close()  
  
  common.Cleanup()  
  
  print "done."  
  
  
if __name__ == '__main__':  
  try:  
    common.CloseInheritedPipes()  
    main(sys.argv[1:])  
  except common.ExternalError, e:  
    print  
    print "   ERROR: %s" % (e,)  
    print  
    sys.exit(1)  


                       主函數main是python的入口函數,我們從main函數開始看,大概看一下main函數(腳本最後)裏的流程就能知道腳本的執行過程了。

                       ① 在main函數的開頭,首先將用戶設定的option選項存入OPTIONS變量中,它是一個python中的類。緊接着判斷有沒有額外的腳本,如果有就讀入到OPTIONS變量中。
                       ② 解壓縮輸入的zip包,即我們在上文生成的原始zip包。然後判斷是否用到device-specific extensions(設備擴展)如果用到,隨即讀入到OPTIONS變量中。
                       ③ 判斷是否簽名,然後判斷是否有新內容的增量源,有的話就解壓該增量源包放入一個臨時變量中(source_zip)。自此,所有的準備工作已完畢,隨即會調用該 腳本中最主要的函數WriteFullOTAPackage(input_zip,output_zip)
                       ④ WriteFullOTAPackage函數的處理過程是先獲得腳本的生成器。默認格式是edify。然後獲得metadata元數據,此數據來至於Android的一些環境變量。然後獲得設備配置參數比如api函數的版本。然後判斷是否忽略時間戳。
                       ⑤ WriteFullOTAPackage函數做完準備工作後就開始生成升級用的腳本文件(updater-script)了。生成腳本文件後將上一步獲得的metadata元數據寫入到輸出包out_zip。
                       ⑥至此一個完整的update.zip升級包就生成了。生成位置在:out/target/product/tcc8800/full_tcc8800_evm-ota-eng.mumu.20120315.155326.zip。將升級包拷貝到SD卡中就可以用來升級了。
四、 Android OTA增量包update.zip的生成

         在上面的過程中生成的update.zip升級包是全部系統的升級包。大小有80M多。這對手機用戶來說,用來升級的流量是很大的。而且在實際升級中,我們只希望能夠升級我們改變的那部分內容。這就需要使用增量包來升級。生成增量包的過程也需要上文中提到的ota_from_target_files.py的參與。

         下面是製作update.zip增量包的過程。

          ① 在源碼根目錄下依次執行下列命令
           $ . build/envsetup.sh
           $ lunch 選擇17
           $ make
           $ make otapackage
           執行上面的命令後會在out/target/product/tcc8800/下生成我們第一個系統升級包。我們先將其命名爲A.zip
          ② 在源碼中修改我們需要改變的部分,比如修改內核配置,增加新的驅動等等。修改後再一次執行上面的命令。就會生成第二個我們修改後生成的update.zip升級包。將  其命名爲B.zip。

          ③ 在上文中我們看了ota_from_target_files.py腳本的使用幫助,其中選項-i就是用來生成差分增量包的。使用方法是以上面的A.zip 和B.zip包作爲輸入,以update.zip包作  爲輸出。生成的update.zip就是我們最後需要的增量包。

              具體使用方式是:將上述兩個包拷貝到源碼根目錄下,然後執行下面的命令。

              $ ./build/tools/releasetools/ota_from_target_files -i A.zip B.zip update.zip。

              在執行上述命令時會出現未找到recovery_api_version的錯誤。原因是在執行上面的腳本時如果使用選項i則會調用WriteIncrementalOTAPackage會從A包和B包中的META目錄下搜索misc_info.txt來讀取recovery_api_version的值。但是在執行make  otapackage命令時生成的update.zip包中沒有這個目錄更沒有這個文檔。

              此時我們就需要使用執行make otapackage生成的原始的zip包。這個包的位置在out/target/product/tcc8800/obj/PACKAGING/target_files_intermediates/目錄下,它是在用命令make otapackage之後的中間生產物,是最原始的升級包。我們將兩次編譯的生成的包分別重命名爲A.zip和B.zip,並拷貝到SD卡根目錄下重複執行上面的命令:

               $ ./build/tools/releasetools/ota_form_target_files -i A.zip B.zip update.zip。

              在上述命令即將執行完畢時,在device/telechips/common/releasetools.py會調用IncrementalOTA_InstallEnd,在這個函數中讀取包中的RADIO/bootloader.img。

              而包中是沒有這個目錄和bootloader.img的。所以執行失敗,未能生成對應的update.zip。可能與我們未修改bootloader(升級firmware)有關。此問題在下一篇博客已經解決。


             在下一篇中講解制作增量包失敗的原因,以及解決方案。

-------------------------------------------


                      Android系統Recovery工作原理之使用update.zip升級過程分析(二)---update.zip差分包問題的解決

       在上一篇末尾提到的生成差分包時出現的問題,現已解決,由於最近比較忙,相隔的時間也比較長,所以單列一個篇幅提示大家。這個問題居然是源碼中的問題,可能你已經製作成功了,不過我的這個問題確實是源碼中的一個問題,不知道是不是一個bug,下文會具體分析!

一、生成OTA增量包失敗的解決方案

           在上一篇中末尾使用ota_from_target_files腳本製作update.zip增量包時失敗,我們先將出現的錯誤貼出來

                 

    

              在執行這個腳本的最後讀取input_zip中RADIO/bootloader.img時出現錯誤,顯示DeviceSpecifiParams這個對象中沒有input_zip屬性。

         我們先從腳本中出現錯誤的調用函數中開始查找。出現錯誤的調用地方是在函WriteIncrementalOTAPackage(443行)中的device_specific.IncrementalOTA_InstallEnd(),其位於WriteIncrementalOTAPackage()中的末尾。進一步跟蹤源碼發現,這是一個回調函數,他的具體執行方法位於源碼中/device/telechips/common/releasetools.py腳本中的IncrementalOTA_InstallEnd()函數。下面就分析這個函數的作用。

          releasetools.py腳本中的兩個函數FullOTA_InstallEnd()和IncrementalOTA_InstallEnd()的作用都是從輸入包中讀取RADIO/下的bootloader.img文件寫到輸出包中,同時生成安裝bootloader.img時執行腳本的那部分命令。只不過一個是直接將輸入包中的bootloader.img鏡像寫到輸出包中,一個是先比較target_zip和source_zip中的bootloader.img是否不同(使用選項-i生成差分包時),然後將新的鏡像寫入輸出包中。下面先將這個函數(位於/device/telechips/common/releasetools.py)的具體實現貼出來:

                    

               我們的實際情況是,在用命令make otapackage時生成的包中是沒有這個RADIO目錄下的bootloader.img鏡像文件(因爲這部分更新已被屏蔽掉了)。但是這個函數中對於從包中未讀取到bootloader.img文件的情況是有錯誤處理的,即返回。所以我們要從  出現的實際錯誤中尋找問題的原由。

         真正出現錯誤的地方是:

          target_bootloader=info.input_zip.read(“RADIO/bootloader.img”)。

         出現錯誤的原因是:AttributeError:‘DeviceSpecificParams’object has no attribute  ‘input_zip’,提示我們DeviceSpecificParams對象沒有input_zip這個屬性。

         在用ota_from_target_files腳本製作差分包時使用了選項-i,並且只有這種情況有三個參數,即target_zip 、source_zip、 out_zip。而出現錯誤的地方是target_bootloader=info.input_zip_read(“RADIO/bootloader.img”),它使用的是input_zip,我們要懷疑這個地方是不是使用錯了,而應該使用info.target_zip.read()。下面可以證實一下我們的猜測。

         從ota_from_target_files腳本中WriteFullOTAPackage()和WriteIncrementalOTAPackage這兩個函數(分別用來生成全包和差分包)可以發現,在他們的開始部分都對device_specific進行了賦值。其中WriteFullOTAPackage()對應的參數是input_zip和out_zip,而WriteIncrementalOTAPackage對應的是target_zip,source_zip,out_zip,我們可以看一下在WriteIncrementalOTAPackage函數中這部分的具體實現:

                  

            從上圖可以發現,在WriteIncrementalOTAPackage函數對DeviceSpecificParams對象進行初始化時確實使用的是target_zip而不是input_zip。而在releasetools.py腳本中使用的卻是info.input_zip.read(),所以纔會出現DeviceSpecificParams對象沒有input_zip這個屬性。由此我們找到了問題的所在(這是不是源碼中的一個Bug?)。

        將releasetools.py腳本IncrementalOTA_InstallEnd(info)函數中的 target_bootloader=info.input_zip.

read(“RADIO/bootloader.img”)爲:target_bootloader=info.target_zip.read(“RADIO/bootloader.img”),然後重新執行上面提到的製作差分包命令。就生成了我們需要的差分包update.zip。

二、          差分包update.zip的更新測試     

                在上面製作差分包腳本命令中,生成差分包的原理是,參照第一個參數(target_zip),將第二個參數(source_zip)中不同的部分輸出到第三個參數(output_zip)中。其中target_zip與source_zip的先後順序不同,產生的差分包也將不同。

          在實際的測試過程中,我們的增量包要刪除之前添加的一個應用(在使用update.zip全包升級時增加的),其他的部分如內核都沒有改動,所以生成的差分包很簡單,只有META-INF這個文件夾。主要的不同都體現在updater-script腳本中,其中的#----start make changes  here----之後的部分就是做出改變的部分,最主要的腳本命令是: delete(“/system/app/CheckUpdateAll.apk” , “/system/recovery.img”);在具體更新時它將刪除CheckUpdateAll.apk這個應用。

          爲了大家參考,還是把這個差分包的升級腳本貼出來,其對應的完全升級的腳本在第九篇已貼出

mount("yaffs2", "MTD", "system", "/system");  
assert(file_getprop("/system/build.prop", "ro.build.fingerprint") == "telechips/full_tcc8800_evm/tcc8800:2.3.5/GRJ90/eng.mumu.20120309.100232:eng/test-keys" ||  
       file_getprop("/system/build.prop", "ro.build.fingerprint") == "telechips/full_tcc8800_evm/tcc8800:2.3.5/GRJ90/eng.mumu.20120309.100232:eng/test-keys");  
assert(getprop("ro.product.device") == "tcc8800" ||  
       getprop("ro.build.product") == "tcc8800");  
ui_print("Verifying current system...");  
show_progress(0.100000, 0);  
  
# ---- start making changes here ----  
  
ui_print("Removing unneeded files...");  
delete("/system/app/CheckUpdateAll.apk",  
       "/system/recovery.img");  
show_progress(0.800000, 0);  
ui_print("Patching system files...");  
show_progress(0.100000, 10);  
ui_print("Symlinks and permissions...");  
set_perm_recursive(0, 0, 0755, 0644, "/system");  
set_perm_recursive(0, 2000, 0755, 0755, "/system/bin");  
set_perm(0, 3003, 02750, "/system/bin/netcfg");  
set_perm(0, 3004, 02755, "/system/bin/ping");  
set_perm(0, 2000, 06750, "/system/bin/run-as");  
set_perm_recursive(1002, 1002, 0755, 0440, "/system/etc/bluetooth");  
set_perm(0, 0, 0755, "/system/etc/bluetooth");  
set_perm(1000, 1000, 0640, "/system/etc/bluetooth/auto_pairing.conf");  
set_perm(3002, 3002, 0444, "/system/etc/bluetooth/blacklist.conf");  
set_perm(1002, 1002, 0440, "/system/etc/dbus.conf");  
set_perm(1014, 2000, 0550, "/system/etc/dhcpcd/dhcpcd-run-hooks");  
set_perm(0, 2000, 0550, "/system/etc/init.goldfish.sh");  
set_perm_recursive(0, 0, 0755, 0555, "/system/etc/ppp");  
set_perm_recursive(0, 2000, 0755, 0755, "/system/xbin");  
set_perm(0, 0, 06755, "/system/xbin/librank");  
set_perm(0, 0, 06755, "/system/xbin/procmem");  
set_perm(0, 0, 06755, "/system/xbin/procrank");  
set_perm(0, 0, 06755, "/system/xbin/su");  
set_perm(0, 0, 06755, "/system/xbin/tcpdump");  
unmount("/system");  

         在做更新測試時,我們要以target_zip系統爲依據,也就是更新之前的開發板系統是用target_zip包升級後的系統。否則會更新就會失敗,因爲在更新時會從系統對應的目錄下讀取設備以及時間戳等信息(updater-script腳本一開始的部分),進行匹配正確後才進行下一步的安裝。

         所有準備都完成後,將我們製作的差分包放到SD卡中,在Settings-->About Phone-->System Update-->Installed From SDCARD執行更新。最後更新完成並重啓後,我們會發現之前的CheckUpdateAll.apk被成功刪掉了,大功告成!

        至此終於將update.zip包以及其對應的差分包製作成功了,下面的文章開始具體分析製作的update.zip包在實際的更新中所走的過程!


----------------------------------------

            Android系統Recovery工作原理之使用update.zip升級過程分析(三)---Android系統的三種啓動模式

        

        以下的篇幅開始分析我們在上兩個篇幅中生成的update.zip包在具體更新中所經過的過程,並根據源碼分析每一部分的工作原理。

一、       系統更新update.zip包的兩種方式

    1.  通過上一個文檔,我們知道了怎樣製作一個update.zip升級包用於升級系統。Android在升級系統時獲得update.zip包的方式有兩種。一種是離線升級,即手動拷貝升級包到SD卡(或NAND)中,通過settings-->About phone-->System Update-->選擇從SD卡升級。另一種是在線升級,即OTA  Install(over  the  air)。用戶通過在線下載升級包到本地,然後更新。這種方式下的update.zip包一般被下載到系統的/CACHE分區下。

     2.  無論將升級包放在什麼位置,在使用update.zip更新時都會重啓並進入Recovery模式,然後啓動recovery服務(/sbin/recovery)來安裝我們的update.zip包。

    3.  爲此,我們必須瞭解Recovery模式的工作原理以及Android系統重啓時怎樣進入Recovery工作模式而不是其他模式(如正常模式)。

二、       Android系統中三種啓動模式

       首先我們要了解Android系統啓動後可能會進入的幾種工作模式。先看下圖:

                

       由上圖可知Android系統啓動後可能進入的模式有以下幾種:

       (一) MAGIC KEY(組合鍵):

              即用戶在啓動後通過按下組合鍵,進入不同的工作模式,具體有兩種:             

             ① camera + power:若用戶在啓動剛開始按了camera+power組合鍵則會進入bootloader模式,並可進一步進入fastboot(快速刷機模式)。

        ② home + power :若用戶在啓動剛開始按了home+power組合鍵,系統會直接進入Recovery模式。以這種方式進入Recovery模式時系統會進入一個簡單的UI(使用了minui)界面,用來提示用戶進一步操作。在tcc8800開發板中提供了一下幾種選項操作:

                                “reboot system now”

                                “apply update from sdcard”

                                “wipe data/factory reset”

                                “wipe cache partition”

       (二)正常啓動:

                 若啓動過程中用戶沒有按下任何組合鍵,bootloader會讀取位於MISC分區的啓動控制信息塊BCB(Bootloader Control Block)。它是一個結構體,存放着啓動命令command。根據不同的命令,系統又 可以進入三種不同的啓動模式。我們先看一下這個結構體的定義。

             struct bootloader_message{

                      char command[32];  //存放不同的啓動命令

                      char status[32];   //update-radio或update-hboot完成存放執行結果

                      char recovery[1024]; //存放/cache/recovery/command中的命令

             };

         我們先看command可能的值,其他的在後文具體分析。command可能的值有兩種,與值爲空(即沒有命令)一起區分三種啓動模式。

         ①command=="boot-recovery"時,系統會進入Recovery模式。Recovery服務會具體根據/cache/recovery/command中的命令執行相應的操作(例如,升級update.zip或擦除cache,data等)。

         ②command=="update-radia"或"update-hboot"時,系統會進入更新firmware(更新bootloader),具體由bootloader完成。

         ③command爲空時,即沒有任何命令,系統會進入正常的啓動,最後進入主系統(main system)。這種是最通常的啓動流程。

         Android系統不同的啓動模式的進入是在不同的情形下觸發的,我們從SD卡中升級我們的update.zip時會進入Recovery模式是其中一種,其他的比如:系統崩潰,或則在命令行輸入啓動命令式也會進入Recovery或其他的啓動模式。

        爲了解我們的update.zip包具體是怎樣在Recovery模式中更新完成,並重啓到主系統的,我們還要分析Android中Recovery模式的工作原理。

     

             下一篇幅開始看具體的Recovery模式工作原理,以及其在更新中的重要作用。



-------------------------------

                             Android系統Recovery模式的工作原理


        在使用update.zip包升級時怎樣從主系統(main system)重啓進入Recovery模式,進入Recovery模式後怎樣判斷做何種操作,以及怎樣獲得主系統發送給Recovery服務的命令,這一系列問題的解決是通過整個軟件平臺的不同部分之間的密切通信配合來完成的。爲此,我們必須要了解Recovery模式的工作原理,這樣才能知道我們的update.zip包是怎樣一步步進入Recovery中升級並最後到達主系統的。

一、Recovery模式中的三個部分

         Recovery的工作需要整個軟件平臺的配合,從通信架構上來看,主要有三個部分。

        ①MainSystem:即上面提到的正常啓動模式(BCB中無命令),是用boot.img啓動的系統,Android的正常工作模式。更新時,在這種模式中我們的上層操作就是使用OTA或則從SD卡中升級update.zip包。在重啓進入Recovery模式之前,會向BCB中寫入命令,以便在重啓後告訴bootloader進入Recovery模式。

        ②Recovery:系統進入Recovery模式後會裝載Recovery分區,該分區包含recovery.img(同boot.img相同,包含了標準的內核和根文件系統)。進入該模式後主要是運行Recovery服務(/sbin/recovery)來做相應的操作(重啓、升級update.zip、擦除cache分區等)。

        ③Bootloader:除了正常的加載啓動系統之外,還會通過讀取MISC分區(BCB)獲得來至Main system和Recovery的消息。

二、Recovery模式中的兩個通信接口

       在Recovery服務中上述的三個實體之間的通信是必不可少的,他們相互之間又有以下兩個通信接口。

       (一)通過CACHE分區中的三個文件:

             Recovery通過/cache/recovery/目錄下的三個文件與mian system通信。具體如下             

            ①/cache/recovery/command:這個文件保存着Main system傳給Recovery的命令行,每一行就是一條命令,支持一下幾種的組合。

                --send_intent=anystring   //write the text out to recovery/intent     在Recovery結束時在finish_recovery函數中將定義的intent字符串作爲參數傳進來,並寫入到/cache/recovery/intent中

                --update_package=root:path   //verify install an OTA package file     Main system將這條命令寫入時,代表系統需要升級,在進入Recovery模式後,將該文件中的命令讀取並寫入BCB中,然後進行相應的更新update.zip包的操作。

                --wipe_data    //erase user data(and cache),then reboot。擦除用戶數據。擦除data分區時必須要擦除cache分區。

                --wipe_cache   //wipe cache(but not user data),then reboot。擦除cache分區。

            ②/cache/recovery/log:Recovery模式在工作中的log打印。在recovery服務運行過程中,stdout以及stderr會重定位到/tmp/recovery.log在recovery退出之前會將其轉存到/cache/recovery/log中,供查看。

            ③/cache/recovery/intent:Recovery傳遞給Main system的信息。作用不詳。

       (二)通過BCB(Bootloader Control Block):

             BCB是bootloader與Recovery的通信接口,也是Bootloader與Main system之間的通信接口。存儲在flash中的MISC分區,佔用三個page,其本身就是一個結構體,具體成員以及各成員含義如下:

             struct bootloader_message{

                       char command[32];

                       char status[32];

                       char recovery[1024];

              };

            ①command成員:其可能的取值我們在上文已經分析過了,即當我們想要在重啓進入Recovery模式時,會更新這個成員的值。另外在成功更新後結束Recovery時,會清除這個成員的值,防止重啓時再次進入Recovery模式。

            ②status:在完成相應的更新後,Bootloader會將執行結果寫入到這個字段。

            ③recovery:可被Main System寫入,也可被Recovery服務程序寫入。該文件的內容格式爲:

                               “recovery\n

                               <recovery command>\n

                               <recovery command>”

            該文件存儲的就是一個字符串,必須以recovery\n開頭,否則這個字段的所有內容域會被忽略。“recovery\n”之後的部分,是/cache/recovery/command支持的命令。可以將其理解爲Recovery操作過程中對命令操作的備份。Recovery對其操作的過程爲:先讀取BCB然後讀取/cache/recovery/command,然後將二者重新寫回BCB,這樣在進入Main system之前,確保操作被執行。在操作之後進入Main system之前,Recovery又會清空BCB的command域和recovery域,這樣確保重啓後不再進入Recovery模式。

三、如何從Main System重啓並進入Recovery模式

       我們先看一下以上三個部分是怎樣進行通信的,先看下圖:

                       

               我們只看從Main System如何進入Recovery模式,其他的通信在後文中詳述。先從Main System開始看,當我們在Main System使用update.zip包進行升級時,系統會重啓並進入Recovery模式。在系統重啓之前,我們可以看到,Main System定會向BCB中的command域寫入boot-recovery(粉紅色線),用來告知Bootloader重啓後進入recovery模式。這一步是必須的。至於Main System是否向recovery域寫入值我們在源碼中不能肯定這一點。即便如此,重啓進入Recovery模式後Bootloader會從/cache/recovery/command中讀取值並放入到BCB的recovery域。而Main System在重啓之前肯定會向/cache/recovery/command中寫入Recovery將要進行的操作命令。

         至此,我們就大概知道了,在上層使用update.zip升級時,主系統是怎樣告知重啓後的系統進入Recovery模式的,以及在Recovery模式中完成什麼樣的操作。

         下一篇開始分析第一個階段,即我們在上層使用update.zip包升級時,Main System怎樣重啓並進入Recovery服務的細節流程。


               文章開頭我們就提到update.zip包來源有兩種,一個是OTA在線下載(一般下載到/CACHE分區),一個是手動拷貝到SD卡中。不論是哪種方式獲得update.zip包,在進入Recovery模式前,都未對這個zip包做處理。只是在重啓之前將zip包的路徑告訴了Recovery服務(通過將--update_package=CACHE:some_filename.zip或--update_package=SDCARD:update.zip命令寫入到/cache/recovery/command中)。在這裏我們假設update.zip包已經製作好並拷貝到了SD卡中,並以Settings-->About Phone-->System Update-->Installed From SDCARD方式升級。

         我們的測試開發板是TCC8800,使用的Android源碼是gingerbread0919,在這種方式下升級的源碼位於gingerbread/device/telechips/common/apps/TelechipsSystemUpdater/src/com/telechips/android/systemupdater/下。         下面我們具體分析這種升級方式下,我們的update.zip是怎樣從上層一步步進入到Recovery模式的。


一、從System Update到Reboot


        當我們依次選擇Settings-->About Phone-->System Update-->Installed From SDCARD後會彈出一個對話框,提示已有update.zip包是否現在更新,我們從這個地方跟蹤。這個對話框的源碼是SystemUpdateInstallDialog.java。


        ①在mNowButton按鈕的監聽事件裏,會調用mService.rebootAndUpdate(new  File(mFile))。這個mService就是SystemUpdateService的實例。 這  個類所在的源碼文件是SystemUpdateService.java。這個函數的參數是一個文件。它肯定就是我們的update.zip包了。我們可以證實一下這個猜想。

        ②mFile的值:在SystemUpdateInstallDialog.java中的ServiceConnection中我們可以看到這個mFile的值有兩個來源。

                 來源一:

                  mFile的一個來源是這個是否立即更新提示框接受的上一個Activity以“file”標記傳來的值。這個Activity就是SystemUpdate.java。它是一個PreferenceActivity類型的。在其onPreferenceChange函數中定義了向下一個Activity傳送的值,這個值是根據我們不同的選擇而定的。如果我們在之前選擇了從SD卡安裝,則這個傳下去的“file”值爲“/sdcard/update.zip”。如果選擇了從NAND安裝,則對應的值爲“/nand/update.zip”。


                 來源二:

                 另個一來源是從mService.getInstallFile()獲得。我們進一步跟蹤就可發現上面這個函數獲得的值就是“/cache”+ mUpdateFileURL.getFile();這就是OTA在線下載後對應的文件路徑。不論參數mFile的來源如何,我們可以發現在mNowButton按鈕的監聽事件裏是將整個文件,也就是我們的update.zip包作爲參數往rebootAndUpdate()中傳遞的。

        ③rebootAndUpdate:在這個函數中Main System做了重啓前的準備。繼續跟蹤下去會發現,在SystemUpdateService.java中的rebootAndUpdate函數中新建了一個線程,在這個線程中最後調用的就是RecoverySystem.installPackage(mContext,mFile),我們的update.zip包也被傳遞進來了。

        ④RecoverySystem類:RecoverySystem類的源碼所在文件路徑爲:gingerbread0919/frameworks/base/core/java/android/os/RecoverySystem.java。我們關心的是installPackage(Context context,FilepackageFile)函數。這個函數首先根據我們傳過來的包文件,獲取這個包文件的絕對路徑filename。然後將其拼成arg=“--update_package=”+filename。它最終會被寫入到BCB中。這個就是重啓進入Recovery模式後,Recovery服務要進行的操作。它被傳遞到函數bootCommand(context,arg)。

        ⑤bootCommand():在這個函數中才是Main System在重啓前真正做的準備。主要做了以下事情,首先創建/cache/recovery/目錄,刪除這個目錄下的command和log(可能不存在)文件在sqlite數據庫中的備份。然後將上面④步中的arg命令寫入到/cache/recovery/command文件中。下一步就是真正重啓了。接下來看一下在重啓函數reboot中所做的事情。

         ⑥pm.reboot():重啓之前先獲得了PowerManager(電源管理)並進一步獲得其系統服務。然後調用了pm.reboot(“recovery”)函數。他就是/gingerbread0919/bionic/libc/unistd/reboot.c中的reboot函數。這個函數實際上是一個系統調用,即__reboot(LINUX_REBOOT_MAGIC1,LINUX_REBOOT_MAGIC2,mode,NULL);從這個函數我們可以看出前兩個參數就代表了我們的組合鍵,mode就是我們傳過來的“recovery”。再進一步跟蹤就到了彙編代碼了,我們無法直接查看它的具體實現細節。但可以肯定的是 這個函數只將“recovery”參數傳遞過去了,之後將“boot-recovery”寫入到了MISC分區的BCB數據塊的command域中。這樣在重啓之後Bootloader才知道要進入Recovery模式。


         在這裏我們無法肯定Main System在重啓之前對BCB的recovery域是否進行了操作。其實在重啓前是否更新BCB的recovery域是不重要的,因爲進入Recovery服務後,Recovery會自動去/cache/recovery/command中讀取要進行的操作然後寫入到BCB的recovery域中。

         至此,Main System就開始重啓並進入Recovery模式。在這之前Main System做的最實質的就是兩件事,一是將“boot-recovery”寫入BCB的command域,二是將--update_package=/cache/update.zip”或則“--update_package=/sdcard/update.zip”寫入/cache/recovery/command文件中。下面的部分就開始重啓並進入Recovery服務了。


二、從reboot到Recovery服務

       

            這個過程我們在上文(對照第一個圖)已經講過了。從Bootloader開始如果沒有組合鍵按下,就從MISC分區讀取BCB塊的command域(在主系統時已經將“boot-recovery”寫入)。然後就以Recovery模式開始啓動。與正常啓動不同的是Recovery模式下加載的鏡像是recovery.img。這個鏡像同boot.img類似,也包含了標準的內核和根文件系統。其後就與正常的啓動系統類似,也是啓動內核,然後啓動文件系統。在進入文件系統後會執行/init,init的配置文件就是/init.rc。這個配置文件來自bootable/recovery/etc/init.rc。查看這個文件我們可以看到它做的事情很簡單:

       ①設置環境變量。

       ②建立etc連接。

       ③新建目錄,備用。

       ④掛載/tmp爲內存文件系統tmpfs

       ⑤啓動recovery(/sbin/recovery)服務。

       ⑥啓動adbd服務(用於調試)。

       這裏最重要的就是當然就recovery服務了。在Recovery服務中將要完成我們的升級工作。

       我們將在下一篇詳細分析Recovery服務的流程細節。





         Recovery服務毫無疑問是Recovery啓動模式中最核心的部分。它完成Recovery模式所有的工作。Recovery程序對應的源碼文件位於:/gingerbread0919/bootable/recovery/recovery.c。


一、 Recovery的三類服務:

         先看一下在這個源碼文件中開始部分的一大段註釋,這將對我們理解Recovery服務的主要功能有很大幫助。代碼如下:

         

/* 
 * The recovery tool communicates with the main system through /cache files. 
 *   /cache/recovery/command - INPUT - command line for tool, one arg per line 
 *   /cache/recovery/log - OUTPUT - combined log file from recovery run(s) 
 *   /cache/recovery/intent - OUTPUT - intent that was passed in 
 * 
 * The arguments which may be supplied in the recovery.command file: 
 *   --send_intent=anystring - write the text out to recovery.intent 
 *   --update_package=path - verify install an OTA package file 
 *   --wipe_data - erase user data (and cache), then reboot 
 *   --wipe_cache - wipe cache (but not user data), then reboot 
 *   --set_encrypted_filesystem=on|off - enables / diasables encrypted fs 
 * 
 * After completing, we remove /cache/recovery/command and reboot. 
 * Arguments may also be supplied in the bootloader control block (BCB). 
 * These important scenarios must be safely restartable at any point: 
 * 
 * FACTORY RESET 
 * 1. user selects "factory reset" 
 * 2. main system writes "--wipe_data" to /cache/recovery/command 
 * 3. main system reboots into recovery 
 * 4. get_args() writes BCB with "boot-recovery" and "--wipe_data" 
 *    -- after this, rebooting will restart the erase -- 
 * 5. erase_volume() reformats /data 
 * 6. erase_volume() reformats /cache 
 * 7. finish_recovery() erases BCB 
 *    -- after this, rebooting will restart the main system -- 
 * 8. main() calls reboot() to boot main system 
 * 
 * OTA INSTALL 
 * 1. main system downloads OTA package to /cache/some-filename.zip 
 * 2. main system writes "--update_package=/cache/some-filename.zip" 
 * 3. main system reboots into recovery 
 * 4. get_args() writes BCB with "boot-recovery" and "--update_package=..." 
 *    -- after this, rebooting will attempt to reinstall the update -- 
 * 5. install_package() attempts to install the update 
 *    NOTE: the package install must itself be restartable from any point 
 * 6. finish_recovery() erases BCB 
 *    -- after this, rebooting will (try to) restart the main system -- 
 * 7. ** if install failed ** 
 *    7a. prompt_and_wait() shows an error icon and waits for the user 
 *    7b; the user reboots (pulling the battery, etc) into the main system 
 * 8. main() calls maybe_install_firmware_update() 
 *    ** if the update contained radio/hboot firmware **: 
 *    8a. m_i_f_u() writes BCB with "boot-recovery" and "--wipe_cache" 
 *        -- after this, rebooting will reformat cache & restart main system -- 
 *    8b. m_i_f_u() writes firmware image into raw cache partition 
 *    8c. m_i_f_u() writes BCB with "update-radio/hboot" and "--wipe_cache" 
 *        -- after this, rebooting will attempt to reinstall firmware -- 
 *    8d. bootloader tries to flash firmware 
 *    8e. bootloader writes BCB with "boot-recovery" (keeping "--wipe_cache") 
 *        -- after this, rebooting will reformat cache & restart main system -- 
 *    8f. erase_volume() reformats /cache 
 *    8g. finish_recovery() erases BCB 
 *        -- after this, rebooting will (try to) restart the main system -- 
 * 9. main() calls reboot() to boot main system 
 * 
 * SECURE FILE SYSTEMS ENABLE/DISABLE 
 * 1. user selects "enable encrypted file systems" 
 * 2. main system writes "--set_encrypted_filesystems=on|off" to 
 *    /cache/recovery/command 
 * 3. main system reboots into recovery 
 * 4. get_args() writes BCB with "boot-recovery" and 
 *    "--set_encrypted_filesystems=on|off" 
 *    -- after this, rebooting will restart the transition -- 
 * 5. read_encrypted_fs_info() retrieves encrypted file systems settings from /data 
 *    Settings include: property to specify the Encrypted FS istatus and 
 *    FS encryption key if enabled (not yet implemented) 
 * 6. erase_volume() reformats /data 
 * 7. erase_volume() reformats /cache 
 * 8. restore_encrypted_fs_info() writes required encrypted file systems settings to /data 
 *    Settings include: property to specify the Encrypted FS status and 
 *    FS encryption key if enabled (not yet implemented) 
 * 9. finish_recovery() erases BCB 
 *    -- after this, rebooting will restart the main system -- 
 * 10. main() calls reboot() to boot main system 
 */  


          從註釋中我們可以看到Recovery的服務內容主要有三類:

         ①FACTORY RESET,恢復出廠設置。

         ②OTA INSTALL,即我們的update.zip包升級。

         ③ENCRYPTED FILE SYSTEM ENABLE/DISABLE,使能/關閉加密文件系統。具體的每一類服務的大概工作流程,註釋中都有,我們在下文中會詳細講解OTA INSTALL的工作流程。這三類服務的大概的流程都是通用的,只是不同操作體現與不同的操作細節。下面我們看Recovery服務的通用流程。


二、Recovery服務的通用流程:

        在這裏我們以OTA INSTALL的流程爲例具體分析。並從相關函數的調用過程圖開始,如下圖:


          

          我們順着流程圖分析,從recovery.c的main函數開始:

          1.    ui_init():Recovery服務使用了一個基於framebuffer的簡單ui(miniui)系統。這個函數對其進行了簡單的初始化。在Recovery服務的過程中主要用於顯示一個背景圖片(正在安裝或安裝失敗)和一個進度條(用於顯示進度)。另外還啓動了兩個線程,一個用於處理進度條的顯示(progress_thread),另一個用於響應用戶的按鍵(input_thread)。

          2.    get_arg():這個函數主要做了上圖中get_arg()往右往下直到parse arg/v的工作。我們對照着流程一個一個看。

                ①get_bootloader_message():主要工作是根據分區的文件格式類型(mtd或emmc)從MISC分區中讀取BCB數據塊到一個臨時的變量中。

                ②然後開始判斷Recovery服務是否有帶命令行的參數(/sbin/recovery,根據現有的邏輯是沒有的),若沒有就從BCB中讀取recovery域。如果讀取失敗則從/cache/recovery/command中讀取然後。這樣這個BCB的臨時變量中的recovery域就被更新了。在將這個BCB的臨時變量寫回真實的BCB之前,又更新的這個BCB臨時變量的command域爲“boot-recovery”。這樣做的目的是如果在升級失敗(比如升級還未結束就斷電了)時,系統在重啓之後還會進入Recovery模式,直到升級完成。

                ③在這個BCB臨時變量的各個域都更新完成後使用set_bootloader_message()寫回到真正的BCB塊中。

                這個過程可以用一個簡單的圖來概括,這樣更清晰:

                                 


          3.     parserargc/argv:解析我們獲得參數。註冊所解析的命令(register_update_command),在下面的操作中會根據這一步解析的值進行一步步的判斷,然後進行相應的操作。

          4.    if(update_package):判斷update_package是否有值,若有就表示需要升級更新包,此時就會調用install_package()(即圖中紅色的第二個階段)。在這一步中將要完成安裝實際的升級包。這是最爲複雜,也是升級update.zip包最爲核心的部分。我們在下一節詳細分析這一過程。爲從宏觀上理解Recovery服務的框架,我們將這一步先略過,假設已經安裝完成了。我們接着往下走,看安裝完成後Recovery怎樣一步步結束服務,並重啓到新的主系統的。

          5.    if(wipe_data/wipe_cache):這一步判斷實際是兩步,在源碼中是先判斷是否擦除data分區(用戶數據部分)的,然後再判斷是否擦除cache分區。值得注意的是在擦除data分區的時候必須連帶擦除cache分區。在只擦除cache分區的情形下可以不擦除data分區。

          6.    maybe_install_firmware_update():如果升級包中包含/radio/hboot firmware的更新,則會調用這個函數。查看源碼發現,在註釋中(OTA INSTALL)有這一個流程。但是main函數中並沒有顯示調用這個函數。目前尚未發現到底是在什麼地方處理。但是其流程還是向上面的圖示一樣。即,① 先向BCB中寫入“boot-recovery”和“—wipe_cache”之後將cache分區格式化,然後將firmware image 寫入原始的cache分區中。②將命令“update-radio/hboot”和“—wipe_cache”寫入BCB中,然後開始重新安裝firmware並刷新firmware。③之後又會進入圖示中的末尾,即finish_recovery()。

          7.    prompt_and_wait():這個函數是在一個判斷中被調用的。其意義是如果安裝失敗(update.zip包錯誤或驗證簽名失敗),則等待用戶的輸入處理(如通過組合鍵reboot等)。

          8.    finish_recovery():這是Recovery關閉並進入Main System的必經之路。其大體流程如下:

                                               

               ① 將intent(字符串)的內容作爲參數傳進finish_recovery中。如果有intent需要告知Main System,則將其寫入/cache/recovery/intent中。這個intent的作用尚不知有何用。

               ② 將內存文件系統中的Recovery服務的日誌(/tmp/recovery.log)拷貝到cache(/cache/recovery/log)分區中,以便告知重啓後的Main System發生過什麼。

               ③ 擦除MISC分區中的BCB數據塊的內容,以便系統重啓後不在進入Recovery模式而是進入更新後的主系統。

               ④ 刪除/cache/recovery/command文件。這一步也是很重要的,因爲重啓後Bootloader會自動檢索這個文件,如果未刪除的話又會進入Recovery模式。原理在上面已經講的很清楚了。


          9.    reboot():這是一個系統調用。在這一步Recovery完成其服務重啓並進入Main System。這次重啓和在主系統中重啓進入Recovery模式調用的函數是一樣的,但是其方向是不一樣的。所以參數也就不一樣。查看源碼發現,其重啓模式是RB_AUTOBOOT。這是一個系統的宏。

 

            至此,我們對Recovery服務的整個流程框架已有了大概的認識。下面就是升級update.zip包時特有的也是Recovery服務中關於安裝升級包最核心的第二個階段。即我們圖例中的紅色2的那個分支。


           我們將在下一篇詳細講解這一部分,即Recovery服務的核心部分install_package函數



一、       Recovery服務的核心install_package(升級update.zip特有)


              和Recovery服務中的wipe_data、wipe_cache不同,install_package()是升級update.zip特有的一部分,也是最核心的部分。在這一步才真正開始對我們的update.zip包進行處理。下面就開始分析這一部分。還是先看圖例:

                          

            這一部分的源碼文件位於:/gingerbread0919/bootable/recovery/install.c。這是一個沒有main函數的源碼文件,還是把源碼先貼出來如下:

/* 
 * Copyright (C) 2007 The Android Open Source Project 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */  
  
#include <ctype.h>  
#include <errno.h>  
#include <fcntl.h>  
#include <limits.h>  
#include <sys/stat.h>  
#include <sys/wait.h>  
#include <unistd.h>  
  
#include "common.h"  
#include "install.h"  
#include "mincrypt/rsa.h"  
#include "minui/minui.h"  
#include "minzip/SysUtil.h"  
#include "minzip/Zip.h"  
#include "mtdutils/mounts.h"  
#include "mtdutils/mtdutils.h"  
#include "roots.h"  
#include "verifier.h"  
  
#define ASSUMED_UPDATE_BINARY_NAME  "META-INF/com/google/android/update-binary"  
#define PUBLIC_KEYS_FILE "/res/keys"  
  
// If the package contains an update binary, extract it and run it.  
static int  
try_update_binary(const char *path, ZipArchive *zip) {  
    const ZipEntry* binary_entry =  
            mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME);  
    if (binary_entry == NULL) {  
        mzCloseZipArchive(zip);  
        return INSTALL_CORRUPT;  
    }  
  
    char* binary = "/tmp/update_binary";  
    unlink(binary);  
    int fd = creat(binary, 0755);  
    if (fd < 0) {  
        mzCloseZipArchive(zip);  
        LOGE("Can't make %s\n", binary);  
        return 1;  
    }  
    bool ok = mzExtractZipEntryToFile(zip, binary_entry, fd);  
    close(fd);  
    mzCloseZipArchive(zip);  
  
    if (!ok) {  
        LOGE("Can't copy %s\n", ASSUMED_UPDATE_BINARY_NAME);  
        return 1;  
    }  
  
    int pipefd[2];  
    pipe(pipefd);  
  
    // When executing the update binary contained in the package, the  
    // arguments passed are:  
    //  
    //   - the version number for this interface  
    //  
    //   - an fd to which the program can write in order to update the  
    //     progress bar.  The program can write single-line commands:  
    //  
    //        progress <frac> <secs>  
    //            fill up the next <frac> part of of the progress bar  
    //            over <secs> seconds.  If <secs> is zero, use  
    //            set_progress commands to manually control the  
    //            progress of this segment of the bar  
    //  
    //        set_progress <frac>  
    //            <frac> should be between 0.0 and 1.0; sets the  
    //            progress bar within the segment defined by the most  
    //            recent progress command.  
    //  
    //        firmware <"hboot"|"radio"> <filename>  
    //            arrange to install the contents of <filename> in the  
    //            given partition on reboot.  
    //  
    //            (API v2: <filename> may start with "PACKAGE:" to  
    //            indicate taking a file from the OTA package.)  
    //  
    //            (API v3: this command no longer exists.)  
    //  
    //        ui_print <string>  
    //            display <string> on the screen.  
    //  
    //   - the name of the package zip file.  
    //  
  
    char** args = malloc(sizeof(char*) * 5);  
    args[0] = binary;  
    args[1] = EXPAND(RECOVERY_API_VERSION);   // defined in Android.mk  
    args[2] = malloc(10);  
    sprintf(args[2], "%d", pipefd[1]);  
    args[3] = (char*)path;  
    args[4] = NULL;  
  
    pid_t pid = fork();  
    if (pid == 0) {  
        close(pipefd[0]);  
        execv(binary, args);  
        fprintf(stdout, "E:Can't run %s (%s)\n", binary, strerror(errno));  
        _exit(-1);  
    }  
    close(pipefd[1]);  
  
    char buffer[1024];  
    FILE* from_child = fdopen(pipefd[0], "r");  
    while (fgets(buffer, sizeof(buffer), from_child) != NULL) {  
        char* command = strtok(buffer, " \n");  
        if (command == NULL) {  
            continue;  
        } else if (strcmp(command, "progress") == 0) {  
            char* fraction_s = strtok(NULL, " \n");  
            char* seconds_s = strtok(NULL, " \n");  
  
            float fraction = strtof(fraction_s, NULL);  
            int seconds = strtol(seconds_s, NULL, 10);  
  
            ui_show_progress(fraction * (1-VERIFICATION_PROGRESS_FRACTION),  
                             seconds);  
        } else if (strcmp(command, "set_progress") == 0) {  
            char* fraction_s = strtok(NULL, " \n");  
            float fraction = strtof(fraction_s, NULL);  
            ui_set_progress(fraction);  
        } else if (strcmp(command, "ui_print") == 0) {  
            char* str = strtok(NULL, "\n");  
            if (str) {  
                ui_print("%s", str);  
            } else {  
                ui_print("\n");  
            }  
        } else {  
            LOGE("unknown command [%s]\n", command);  
        }  
    }  
    fclose(from_child);  
  
    int status;  
    waitpid(pid, &status, 0);  
    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {  
        LOGE("Error in %s\n(Status %d)\n", path, WEXITSTATUS(status));  
        return INSTALL_ERROR;  
    }  
  
    return INSTALL_SUCCESS;  
}  
  
// Reads a file containing one or more public keys as produced by  
// DumpPublicKey:  this is an RSAPublicKey struct as it would appear  
// as a C source literal, eg:  
//  
//  "{64,0xc926ad21,{1795090719,...,-695002876},{-857949815,...,1175080310}}"  
//  
// (Note that the braces and commas in this example are actual  
// characters the parser expects to find in the file; the ellipses  
// indicate more numbers omitted from this example.)  
//  
// The file may contain multiple keys in this format, separated by  
// commas.  The last key must not be followed by a comma.  
//  
// Returns NULL if the file failed to parse, or if it contain zero keys.  
static RSAPublicKey*  
load_keys(const char* filename, int* numKeys) {  
    RSAPublicKey* out = NULL;  
    *numKeys = 0;  
  
    FILE* f = fopen(filename, "r");  
    if (f == NULL) {  
        LOGE("opening %s: %s\n", filename, strerror(errno));  
        goto exit;  
    }  
  
    int i;  
    bool done = false;  
    while (!done) {  
        ++*numKeys;  
        out = realloc(out, *numKeys * sizeof(RSAPublicKey));  
        RSAPublicKey* key = out + (*numKeys - 1);  
        if (fscanf(f, " { %i , 0x%x , { %u",  
                   &(key->len), &(key->n0inv), &(key->n[0])) != 3) {  
            goto exit;  
        }  
        if (key->len != RSANUMWORDS) {  
            LOGE("key length (%d) does not match expected size\n", key->len);  
            goto exit;  
        }  
        for (i = 1; i < key->len; ++i) {  
            if (fscanf(f, " , %u", &(key->n[i])) != 1) goto exit;  
        }  
        if (fscanf(f, " } , { %u", &(key->rr[0])) != 1) goto exit;  
        for (i = 1; i < key->len; ++i) {  
            if (fscanf(f, " , %u", &(key->rr[i])) != 1) goto exit;  
        }  
        fscanf(f, " } } ");  
  
        // if the line ends in a comma, this file has more keys.  
        switch (fgetc(f)) {  
            case ',':  
                // more keys to come.  
                break;  
  
            case EOF:  
                done = true;  
                break;  
  
            default:  
                LOGE("unexpected character between keys\n");  
                goto exit;  
        }  
    }  
  
    fclose(f);  
    return out;  
  
exit:  
    if (f) fclose(f);  
    free(out);  
    *numKeys = 0;  
    return NULL;  
}  
  
int  
install_package(const char *path)  
{  
    ui_set_background(BACKGROUND_ICON_INSTALLING);  
    ui_print("Finding update package...\n");  
    ui_show_indeterminate_progress();  
    LOGI("Update location: %s\n", path);  
  
    if (ensure_path_mounted(path) != 0) {  
        LOGE("Can't mount %s\n", path);  
        return INSTALL_CORRUPT;  
    }  
  
    ui_print("Opening update package...\n");  
  
    int numKeys;  
    RSAPublicKey* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys);  
    if (loadedKeys == NULL) {  
        LOGE("Failed to load keys\n");  
        return INSTALL_CORRUPT;  
    }  
    LOGI("%d key(s) loaded from %s\n", numKeys, PUBLIC_KEYS_FILE);  
  
    // Give verification half the progress bar...  
    ui_print("Verifying update package...\n");  
    ui_show_progress(  
            VERIFICATION_PROGRESS_FRACTION,  
            VERIFICATION_PROGRESS_TIME);  
  
    int err;  
    err = verify_file(path, loadedKeys, numKeys);  
    free(loadedKeys);  
    LOGI("verify_file returned %d\n", err);  
    if (err != VERIFY_SUCCESS) {  
        LOGE("signature verification failed\n");  
        return INSTALL_CORRUPT;  
    }  
  
    /* Try to open the package. 
     */  
    ZipArchive zip;  
    err = mzOpenZipArchive(path, &zip);  
    if (err != 0) {  
        LOGE("Can't open %s\n(%s)\n", path, err != -1 ? strerror(err) : "bad");  
        return INSTALL_CORRUPT;  
    }  
  
    /* Verify and install the contents of the package. 
     */  
    ui_print("Installing update...\n");  
    return try_update_binary(path, &zip);  
}  


             下面順着上面的流程圖和源碼來分析這一流程:

            ①ensure_path_mount():先判斷所傳的update.zip包路徑所在的分區是否已經掛載。如果沒有則先掛載。

            ②load_keys():加載公鑰源文件,路徑位於/res/keys。這個文件在Recovery鏡像的根文件系統中。

            ③verify_file():對升級包update.zip包進行簽名驗證。

            ④mzOpenZipArchive():打開升級包,並將相關的信息拷貝到一個臨時的ZipArchinve變量中。這一步並未對我們的update.zip包解壓。

            ⑤try_update_binary():在這個函數中才是對我們的update.zip升級的地方。這個函數一開始先根據我們上一步獲得的zip包信息,以及升級包的絕對路徑將update_binary文件拷貝到內存文件系統的/tmp/update_binary中。以便後面使用。

            ⑥pipe():創建管道,用於下面的子進程和父進程之間的通信。

            ⑦fork():創建子進程。其中的子進程主要負責執行binary(execv(binary,args),即執行我們的安裝命令腳本),父進程負責接受子進程發送的命令去更新ui顯示(顯示當前的進度)。子父進程間通信依靠管道。

            ⑧其中,在創建子進程後,父進程有兩個作用。一是通過管道接受子進程發送的命令來更新UI顯示。二是等待子進程退出並返回INSTALL SUCCESS。其中子進程在解析執行安裝腳本的同時所發送的命令有以下幾種:

                       progress  <frac> <secs>:根據第二個參數secs(秒)來設置進度條。

                       set_progress  <frac>:直接設置進度條,frac取值在0.0到0.1之間。

                       firmware <”hboot”|”radio”><filename>:升級firmware時使用,在API  V3中不再使用。

                       ui_print <string>:在屏幕上顯示字符串,即打印更新過程。

                 execv(binary,args)的作用就是去執行binary程序,這個程序的實質就是去解析update.zip包中的updater-script腳本中的命令並執行。由此,Recovery服務就進入了實際安裝update.zip包的過程。

                 下一篇繼續分析使用update-binary解析並執行updater-script的過程。



一、update_binary的執行過程分析

       上一篇幅中的子進程所執行的程序binary實際上就是update.zip包中的update-binary。我們在上文中也說過,Recovery服務在做這一部分工作的時候是先將包中update-binary拷貝到內存文件系統中的/tmp/update_binary,然後再執行的。update_binary程序的源碼位於gingerbread0919/bootable/recovery/updater/updater.c,源碼如下:

/* 
 * Copyright (C) 2009 The Android Open Source Project 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */  
  
#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
  
#include "edify/expr.h"  
#include "updater.h"  
#include "install.h"  
#include "minzip/Zip.h"  
  
// Generated by the makefile, this function defines the  
// RegisterDeviceExtensions() function, which calls all the  
// registration functions for device-specific extensions.  
#include "register.inc"  
  
// Where in the package we expect to find the edify script to execute.  
// (Note it's "updateR-script", not the older "update-script".)  
#define SCRIPT_NAME "META-INF/com/google/android/updater-script"  
  
int main(int argc, char** argv) {  
    // Various things log information to stdout or stderr more or less  
    // at random.  The log file makes more sense if buffering is  
    // turned off so things appear in the right order.  
    setbuf(stdout, NULL);  
    setbuf(stderr, NULL);  
  
    if (argc != 4) {  
        fprintf(stderr, "unexpected number of arguments (%d)\n", argc);  
        return 1;  
    }  
  
    char* version = argv[1];  
    if ((version[0] != '1' && version[0] != '2' && version[0] != '3') ||  
        version[1] != '\0') {  
        // We support version 1, 2, or 3.  
        fprintf(stderr, "wrong updater binary API; expected 1, 2, or 3; "  
                        "got %s\n",  
                argv[1]);  
        return 2;  
    }  
  
    // Set up the pipe for sending commands back to the parent process.  
  
    int fd = atoi(argv[2]);  
    FILE* cmd_pipe = fdopen(fd, "wb");  
    setlinebuf(cmd_pipe);  
  
    // Extract the script from the package.  
  
    char* package_data = argv[3];  
    ZipArchive za;  
    int err;  
    err = mzOpenZipArchive(package_data, &za);  
    if (err != 0) {  
        fprintf(stderr, "failed to open package %s: %s\n",  
                package_data, strerror(err));  
        return 3;  
    }  
  
    const ZipEntry* script_entry = mzFindZipEntry(&za, SCRIPT_NAME);  
    if (script_entry == NULL) {  
        fprintf(stderr, "failed to find %s in %s\n", SCRIPT_NAME, package_data);  
        return 4;  
    }  
  
    char* script = malloc(script_entry->uncompLen+1);  
    if (!mzReadZipEntry(&za, script_entry, script, script_entry->uncompLen)) {  
        fprintf(stderr, "failed to read script from package\n");  
        return 5;  
    }  
    script[script_entry->uncompLen] = '\0';  
  
    // Configure edify's functions.  
  
    RegisterBuiltins();  
    RegisterInstallFunctions();  
    RegisterDeviceExtensions();  
    FinishRegistration();  
  
    // Parse the script.  
  
    Expr* root;  
    int error_count = 0;  
    yy_scan_string(script);  
    int error = yyparse(&root, &error_count);  
    if (error != 0 || error_count > 0) {  
        fprintf(stderr, "%d parse errors\n", error_count);  
        return 6;  
    }  
  
    // Evaluate the parsed script.  
  
    UpdaterInfo updater_info;  
    updater_info.cmd_pipe = cmd_pipe;  
    updater_info.package_zip = &za;  
    updater_info.version = atoi(version);  
  
    State state;  
    state.cookie = &updater_info;  
    state.script = script;  
    state.errmsg = NULL;  
  
    char* result = Evaluate(&state, root);  
    if (result == NULL) {  
        if (state.errmsg == NULL) {  
            fprintf(stderr, "script aborted (no error message)\n");  
            fprintf(cmd_pipe, "ui_print script aborted (no error message)\n");  
        } else {  
            fprintf(stderr, "script aborted: %s\n", state.errmsg);  
            char* line = strtok(state.errmsg, "\n");  
            while (line) {  
                fprintf(cmd_pipe, "ui_print %s\n", line);  
                line = strtok(NULL, "\n");  
            }  
            fprintf(cmd_pipe, "ui_print\n");  
        }  
        free(state.errmsg);  
        return 7;  
    } else {  
        fprintf(stderr, "script result was [%s]\n", result);  
        free(result);  
    }  
  
    if (updater_info.package_zip) {  
        mzCloseZipArchive(updater_info.package_zip);  
    }  
    free(script);  
  
    return 0;  
}  

        通過上面的源碼來分析下這個程序的執行過程:

        ①函數參數以及版本的檢查:當前updater binary API所支持的版本號有1,2,3這三個。

        ②獲取管道並打開:在執行此程序的過程中向該管道寫入命令,用於通知其父進程根據命令去更新UI顯示。

        ③讀取updater-script腳本:從update.zip包中將updater-script腳本讀到一塊動態內存中,供後面執行。

        ④Configure edify’s functions:註冊腳本中的語句處理函數,即識別腳本中命令的函數。主要有以下幾類

                   RegisterBuiltins():註冊程序中控制流程的語句,如ifelse、assert、abort、stdout等。

                            RegisterInstallFunctions():實際安裝過程中安裝所需的功能函數,比如mount、format、set_progress、set_perm等等。

                   RegisterDeviceExtensions():與設備相關的額外添加項,在源碼中並沒有任何實現。

                   FinishRegistration():結束註冊。

        ⑤Parsethe script:調用yy*庫函數解析腳本,並將解析後的內容存放到一個Expr類型的python類中。主要函數是yy_scan_string()和yyparse()。

        ⑥執行腳本:核心函數是Evaluate(),它會調用其他的callback函數,而這些callback函數又會去調用Evaluate去解析不同的腳本片段,從而實現一個簡單的腳本解釋器。

        ⑦錯誤信息提示:最後就是根據Evaluate()執行後的返回值,給出一些打印信息。


            這一執行過程非常簡單,最主要的函數就是Evaluate。它負責最終執行解析的腳本命令。而安裝過程中的命令就是updater-script。

            下一篇幅將介紹updater-script腳本中的語法以及這個腳本在具體升級中的執行流程。




          目前updater-script腳本格式是edify,其與amend有何區別,暫不討論,我們只分析其中主要的語法,以及腳本的流程控制。

一、updater-script腳本語法簡介:

        我們順着所生成的腳本來看其中主要涉及的語法。

        1.assert(condition):如果condition參數的計算結果爲False,則停止腳本執行,否則繼續執行腳本。

        2.show_progress(frac,sec):frac表示進度完成的數值,sec表示整個過程的總秒數。主要用與顯示UI上的進度條。

        3.format(fs_type,partition_type,location):fs_type,文件系統類型,取值一般爲“yaffs2”或“ext4”。Partition_type,分區類型,一般取值爲“MTD”或則“EMMC”。主要用於格式化爲指定的文件系統。事例如下:format(”yaffs2”,”MTD”,”system”)。

        4.mount(fs_type,partition_type,location,mount_point):前兩個參數同上,location要掛載的設備,mount_point掛載點。作用:掛載一個文件系統到指定的掛載點。

        5.package_extract_dir(src_path,destination_path):src_path,要提取的目錄,destination_path目標目錄。作用:從升級包內,提取目錄到指定的位置。示例:package_extract_dir(“system”,”/system”)。

        6.symlink(target,src1,src2,……,srcN):target,字符串類型,是符號連接的目標。SrcX代表要創建的符號連接的目標點。示例:symlink(“toolbox”,”/system/bin/ps”),建立指向toolbox符號連接/system/bin/ps,值得注意的是,在建立新的符號連接之前,要斷開已經存在的符號連接。

        7.set_perm(uid,gid,mode,file1,file2,……,fileN):作用是設置單個文件或則一系列文件的權限,最少要指定一個文件。

        8.set_perm_recursive(uid,gid,mode,dir1,dir2,……,dirN):作用同上,但是這裏同時改變的是一個或多個目錄及其文件的權限。

        9.package_extract_file(srcfile_path,desfile_paht):srcfile_path,要提取的文件,desfile_path,提取文件的目標位置。示例:package_extract_file(“boot.img”,”/tmp/boot.img”)將升級包中的boot.img文件拷貝到內存文件系統的/tmp下。

      10.write_raw_image(src-image,partition):src-image源鏡像文件,partition,目標分區。作用:將鏡像寫入目標分區。示例:write_raw_image(“/tmp/boot.img”,”boot”)將boot.img鏡像寫入到系統的boot分區。

      11.getprop(key):通過指定key的值來獲取對應的屬性信息。示例:getprop(“ro.product.device”)獲取ro.product.device的屬性值。

二、updater-script腳本執行流程分析:

          先看一下在測試過程中用命令make otapackage生成的升級腳本如下:

assert(!less_than_int(1331176658, getprop("ro.build.date.utc")));  
assert(getprop("ro.product.device") == "tcc8800" ||  
       getprop("ro.build.product") == "tcc8800");  
show_progress(0.500000, 0);  
format("yaffs2", "MTD", "system");  
mount("yaffs2", "MTD", "system", "/system");  
package_extract_dir("recovery", "/system");  
package_extract_dir("system", "/system");  
symlink("busybox", "/system/bin/cp", "/system/bin/grep",  
        "/system/bin/tar", "/system/bin/unzip",  
        "/system/bin/vi");  
symlink("toolbox", "/system/bin/cat", "/system/bin/chmod",  
        "/system/bin/chown", "/system/bin/cmp", "/system/bin/date",  
        "/system/bin/dd", "/system/bin/df", "/system/bin/dmesg",  
        "/system/bin/getevent", "/system/bin/getprop", "/system/bin/hd",  
        "/system/bin/id", "/system/bin/ifconfig", "/system/bin/iftop",  
        "/system/bin/insmod", "/system/bin/ioctl", "/system/bin/ionice",  
        "/system/bin/kill", "/system/bin/ln", "/system/bin/log",  
        "/system/bin/ls", "/system/bin/lsmod", "/system/bin/lsof",  
        "/system/bin/mkdir", "/system/bin/mount", "/system/bin/mv",  
        "/system/bin/nandread", "/system/bin/netstat",  
        "/system/bin/newfs_msdos", "/system/bin/notify", "/system/bin/printenv",  
        "/system/bin/ps", "/system/bin/reboot", "/system/bin/renice",  
        "/system/bin/rm", "/system/bin/rmdir", "/system/bin/rmmod",  
        "/system/bin/route", "/system/bin/schedtop", "/system/bin/sendevent",  
        "/system/bin/setconsole", "/system/bin/setprop", "/system/bin/sleep",  
        "/system/bin/smd", "/system/bin/start", "/system/bin/stop",  
        "/system/bin/sync", "/system/bin/top", "/system/bin/umount",  
        "/system/bin/uptime", "/system/bin/vmstat", "/system/bin/watchprops",  
        "/system/bin/wipe");  
set_perm_recursive(0, 0, 0755, 0644, "/system");  
set_perm_recursive(0, 2000, 0755, 0755, "/system/bin");  
set_perm(0, 3003, 02750, "/system/bin/netcfg");  
set_perm(0, 3004, 02755, "/system/bin/ping");  
set_perm(0, 2000, 06750, "/system/bin/run-as");  
set_perm_recursive(1002, 1002, 0755, 0440, "/system/etc/bluetooth");  
set_perm(0, 0, 0755, "/system/etc/bluetooth");  
set_perm(1000, 1000, 0640, "/system/etc/bluetooth/auto_pairing.conf");  
set_perm(3002, 3002, 0444, "/system/etc/bluetooth/blacklist.conf");  
set_perm(1002, 1002, 0440, "/system/etc/dbus.conf");  
set_perm(1014, 2000, 0550, "/system/etc/dhcpcd/dhcpcd-run-hooks");  
set_perm(0, 2000, 0550, "/system/etc/init.goldfish.sh");  
set_perm(0, 0, 0544, "/system/etc/install-recovery.sh");  
set_perm_recursive(0, 0, 0755, 0555, "/system/etc/ppp");  
set_perm_recursive(0, 2000, 0755, 0755, "/system/xbin");  
set_perm(0, 0, 06755, "/system/xbin/librank");  
set_perm(0, 0, 06755, "/system/xbin/procmem");  
set_perm(0, 0, 06755, "/system/xbin/procrank");  
set_perm(0, 0, 06755, "/system/xbin/su");  
set_perm(0, 0, 06755, "/system/xbin/tcpdump");  
show_progress(0.200000, 0);  
show_progress(0.200000, 10);  
assert(package_extract_file("boot.img", "/tmp/boot.img"),  
       write_raw_image("/tmp/boot.img", "boot"),  
       delete("/tmp/boot.img"));  
show_progress(0.100000, 0);  
unmount("/system");  

          下面分析下這個腳本的執行過程:

         ①比較時間戳:如果升級包較舊則終止腳本的執行。

         ②匹配設備信息:如果和當前的設備信息不一致,則停止腳本的執行。

         ③顯示進度條:如果以上兩步匹配則開始顯示升級進度條。

         ④格式化system分區並掛載。

         ⑤提取包中的recovery以及system目錄下的內容到系統的/system下。

         爲/system/bin/下的命令文件建立符號連接。

         ⑦設置/system/下目錄以及文件的屬性。

         ⑧將包中的boot.img提取到/tmp/boot.img。

         ⑨將/tmp/boot.img鏡像文件寫入到boot分區。

         ⑩完成後卸載/system。

         以上就是updater-script腳本中的語法,及其執行的具體過程。通過分析其執行流程,我們可以發現在執行過程中,並未將升級包另外解壓到一個地方,而是需要什麼提取什麼。值得主要的是在提取recovery和system目錄中的內容時,一併放在了/system/下。在操作的過程中,並未刪除或改變update.zip包中的任何內容。在實際的更新完成後,我們的update.zip包確實還存在於原來的位置。


三、總結

        以上的九篇着重分析了Android系統中Recovery模式中的一種,即我們做好的update.zip包在系統更新時所走過的流程。其核心部分就是Recovery服務的工作原理。其他兩種FACTORY RESET、ENCRYPTED FILE SYSTEM ENABLE/DISABLE與OTA INSTALL是相通的。重點是要理解Recovery服務的工作原理。另外詳細分析其升級過程,對於我們在實際升級時,可以根據我們的需要做出相應的修改。

         不足之處,請大家不吝指正!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章