Revision: 16516
          http://sourceforge.net/p/edk2/code/16516
Author:   oliviermartin
Date:     2014-12-12 19:14:22 +0000 (Fri, 12 Dec 2014)
Log Message:
-----------
ArmPkg/BdsLib: Rework TFTP boot

Rework the downloading of an image from a TFTP server to do not
depend on any "PXE specific" setting of the DHCP server.

Contributed-under: TianoCore Contribution Agreement 1.0
Signed-off-by: Ronald Cron <[email protected]>
Reviewed-by: Olivier Martin <[email protected]>

Modified Paths:
--------------
    trunk/edk2/ArmPkg/ArmPkg.dsc
    trunk/edk2/ArmPkg/Library/BdsLib/BdsFilePath.c
    trunk/edk2/ArmPkg/Library/BdsLib/BdsInternal.h
    trunk/edk2/ArmPkg/Library/BdsLib/BdsLib.inf
    trunk/edk2/ArmPlatformPkg/Bds/Bds.inf
    trunk/edk2/ArmPlatformPkg/Bds/BootOptionSupport.c

Modified: trunk/edk2/ArmPkg/ArmPkg.dsc
===================================================================
--- trunk/edk2/ArmPkg/ArmPkg.dsc        2014-12-12 19:13:04 UTC (rev 16515)
+++ trunk/edk2/ArmPkg/ArmPkg.dsc        2014-12-12 19:14:22 UTC (rev 16516)
@@ -55,6 +55,10 @@
   PeCoffLib|MdePkg/Library/BasePeCoffLib/BasePeCoffLib.inf
   
PeCoffGetEntryPointLib|MdePkg/Library/BasePeCoffGetEntryPointLib/BasePeCoffGetEntryPointLib.inf
 
+  NetLib|MdeModulePkg/Library/DxeNetLib/DxeNetLib.inf
+  
UefiHiiServicesLib|MdeModulePkg/Library/UefiHiiServicesLib/UefiHiiServicesLib.inf
+  HiiLib|MdeModulePkg/Library/UefiHiiLib/UefiHiiLib.inf
+
   SemihostLib|ArmPkg/Library/SemihostLib/SemihostLib.inf
   
UncachedMemoryAllocationLib|ArmPkg/Library/UncachedMemoryAllocationLib/UncachedMemoryAllocationLib.inf
   
DxeServicesTableLib|MdePkg/Library/DxeServicesTableLib/DxeServicesTableLib.inf

Modified: trunk/edk2/ArmPkg/Library/BdsLib/BdsFilePath.c
===================================================================
--- trunk/edk2/ArmPkg/Library/BdsLib/BdsFilePath.c      2014-12-12 19:13:04 UTC 
(rev 16515)
+++ trunk/edk2/ArmPkg/Library/BdsLib/BdsFilePath.c      2014-12-12 19:14:22 UTC 
(rev 16516)
@@ -14,13 +14,41 @@
 
 #include "BdsInternal.h"
 
+#include <Library/NetLib.h>
+
+#include <Protocol/Bds.h>
 #include <Protocol/UsbIo.h>
 #include <Protocol/DiskIo.h>
 #include <Protocol/LoadedImage.h>
 #include <Protocol/SimpleNetwork.h>
+#include <Protocol/Dhcp4.h>
+#include <Protocol/Mtftp4.h>
 
 #define IS_DEVICE_PATH_NODE(node,type,subtype) (((node)->Type == (type)) && 
((node)->SubType == (subtype)))
 
+/*
+   Constant strings and define related to the message indicating the amount of
+   progress in the dowloading of a TFTP file.
+*/
+
+// Frame for the progression slider
+STATIC CONST CHAR16 mTftpProgressFrame[] = L"[                                 
       ]";
+
+// Number of steps in the progression slider
+#define TFTP_PROGRESS_SLIDER_STEPS  ((sizeof (mTftpProgressFrame) / sizeof 
(CHAR16)) - 3)
+
+// Size in number of characters plus one (final zero) of the message to
+// indicate the progress of a tftp download. The format is "[(progress slider:
+// 40 characters)] (nb of KBytes downloaded so far: 7 characters) Kb". There
+// are thus the number of characters in mTftpProgressFrame[] plus 11 characters
+// (2 // spaces, "Kb" and seven characters for the number of KBytes).
+#define TFTP_PROGRESS_MESSAGE_SIZE  ((sizeof (mTftpProgressFrame) / sizeof 
(CHAR16)) + 12)
+
+// String to delete the tftp progress message to be able to update it :
+// (TFTP_PROGRESS_MESSAGE_SIZE-1) '\b'
+STATIC CONST CHAR16 mTftpProgressDelete[] = 
L"\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b";
+
+
 // Extract the FilePath from the Device Path
 CHAR16*
 BdsExtractFilePathFromDevicePath (
@@ -723,14 +751,14 @@
 
 BOOLEAN
 BdsTftpSupport (
-  IN EFI_DEVICE_PATH*           DevicePath,
-  IN EFI_HANDLE                 Handle,
-  IN EFI_DEVICE_PATH*           RemainingDevicePath
+  IN EFI_DEVICE_PATH  *DevicePath,
+  IN EFI_HANDLE       Handle,
+  IN EFI_DEVICE_PATH  *RemainingDevicePath
   )
 {
-  EFI_STATUS  Status;
+  EFI_STATUS       Status;
   EFI_DEVICE_PATH  *NextDevicePath;
-  EFI_PXE_BASE_CODE_PROTOCOL  *PxeBcProtocol;
+  VOID             *Interface;
 
   // Validate the Remaining Device Path
   if (IsDevicePathEnd (RemainingDevicePath)) {
@@ -748,151 +776,412 @@
     return FALSE;
   }
 
-  Status = gBS->HandleProtocol (Handle, &gEfiPxeBaseCodeProtocolGuid, (VOID 
**)&PxeBcProtocol);
+  Status = gBS->HandleProtocol (
+                  Handle, &gEfiDevicePathProtocolGuid,
+                  &Interface
+                  );
   if (EFI_ERROR (Status)) {
     return FALSE;
-  } else {
-    return TRUE;
   }
+
+  //
+  // Check that the controller (identified by its handle "Handle") supports the
+  // MTFTPv4 Service Binding Protocol. If it does, it means that it supports 
the
+  // EFI MTFTPv4 Protocol needed to download the image through TFTP.
+  //
+  Status = gBS->HandleProtocol (
+                  Handle, &gEfiMtftp4ServiceBindingProtocolGuid,
+                  &Interface
+                  );
+  if (EFI_ERROR (Status)) {
+    return FALSE;
+  }
+
+  return TRUE;
 }
 
+/**
+  Worker function that get the size in numbers of bytes of a file from a TFTP
+  server before to download the file.
+
+  @param[in]   Mtftp4    MTFTP4 protocol interface
+  @param[in]   FilePath  Path of the file, Ascii encoded
+  @param[out]  FileSize  Address where to store the file size in number of
+                         bytes.
+
+  @retval  EFI_SUCCESS   The size of the file was returned.
+  @retval  !EFI_SUCCESS  The size of the file was not returned.
+
+**/
+STATIC
 EFI_STATUS
+Mtftp4GetFileSize (
+  IN  EFI_MTFTP4_PROTOCOL  *Mtftp4,
+  IN  CHAR8                *FilePath,
+  OUT UINT64               *FileSize
+  )
+{
+  EFI_STATUS         Status;
+  EFI_MTFTP4_OPTION  ReqOpt[1];
+  EFI_MTFTP4_PACKET  *Packet;
+  UINT32             PktLen;
+  EFI_MTFTP4_OPTION  *TableOfOptions;
+  EFI_MTFTP4_OPTION  *Option;
+  UINT32             OptCnt;
+  UINT8              OptBuf[128];
+
+  ReqOpt[0].OptionStr = (UINT8*)"tsize";
+  OptBuf[0] = '0';
+  OptBuf[1] = 0;
+  ReqOpt[0].ValueStr = OptBuf;
+
+  Status = Mtftp4->GetInfo (
+             Mtftp4,
+             NULL,
+             (UINT8*)FilePath,
+             NULL,
+             1,
+             ReqOpt,
+             &PktLen,
+             &Packet
+             );
+
+  if (EFI_ERROR (Status)) {
+    goto Error;
+  }
+
+  Status = Mtftp4->ParseOptions (
+                     Mtftp4,
+                     PktLen,
+                     Packet,
+                     (UINT32 *) &OptCnt,
+                     &TableOfOptions
+                     );
+  if (EFI_ERROR (Status)) {
+    goto Error;
+  }
+
+  Option = TableOfOptions;
+  while (OptCnt != 0) {
+    if (AsciiStrnCmp ((CHAR8 *)Option->OptionStr, "tsize", 5) == 0) {
+      *FileSize = AsciiStrDecimalToUint64 ((CHAR8 *)Option->ValueStr);
+      break;
+    }
+    OptCnt--;
+    Option++;
+  }
+  FreePool (TableOfOptions);
+
+  if (OptCnt == 0) {
+    Status = EFI_UNSUPPORTED;
+  }
+
+Error :
+
+  return Status;
+}
+
+/**
+  Update the progress of a file download
+  This procedure is called each time a new TFTP packet is received.
+
+  @param[in]  This       MTFTP4 protocol interface
+  @param[in]  Token      Parameters for the download of the file
+  @param[in]  PacketLen  Length of the packet
+  @param[in]  Packet     Address of the packet
+
+  @retval  EFI_SUCCESS  All packets are accepted.
+
+**/
+STATIC
+EFI_STATUS
+Mtftp4CheckPacket (
+  IN EFI_MTFTP4_PROTOCOL  *This,
+  IN EFI_MTFTP4_TOKEN     *Token,
+  IN UINT16               PacketLen,
+  IN EFI_MTFTP4_PACKET    *Packet
+  )
+{
+  BDS_TFTP_CONTEXT  *Context;
+  CHAR16            Progress[TFTP_PROGRESS_MESSAGE_SIZE];
+  UINT64            NbOfKb;
+  UINTN             Index;
+  UINTN             LastStep;
+  UINTN             Step;
+  UINT64            LastNbOf50Kb;
+  UINT64            NbOf50Kb;
+
+  if ((NTOHS (Packet->OpCode)) == EFI_MTFTP4_OPCODE_DATA) {
+    Context = (BDS_TFTP_CONTEXT*)Token->Context;
+
+    if (Context->DownloadedNbOfBytes == 0) {
+      if (Context->FileSize > 0) {
+        Print (L"%s       0 Kb", mTftpProgressFrame);
+      } else {
+        Print (L"    0 Kb");
+      }
+    }
+
+    //
+    // The data is the packet are prepended with two UINT16 :
+    // . OpCode = EFI_MTFTP4_OPCODE_DATA
+    // . Block  = the number of this block of data
+    //
+    Context->DownloadedNbOfBytes += PacketLen - sizeof (Packet->OpCode) - 
sizeof (Packet->Data.Block);
+    NbOfKb = Context->DownloadedNbOfBytes / 1024;
+
+    Progress[0] = L'\0';
+    if (Context->FileSize > 0) {
+      LastStep  = (Context->LastReportedNbOfBytes * 
TFTP_PROGRESS_SLIDER_STEPS) / Context->FileSize;
+      Step      = (Context->DownloadedNbOfBytes   * 
TFTP_PROGRESS_SLIDER_STEPS) / Context->FileSize;
+      if (Step > LastStep) {
+        Print (mTftpProgressDelete);
+        StrCpy (Progress, mTftpProgressFrame);
+        for (Index = 1; Index < Step; Index++) {
+          Progress[Index] = L'=';
+        }
+        Progress[Step] = L'>';
+
+        UnicodeSPrint (
+          Progress + (sizeof (mTftpProgressFrame) / sizeof (CHAR16)) - 1,
+          sizeof (Progress) - sizeof (mTftpProgressFrame),
+          L" %7d Kb",
+          NbOfKb
+          );
+        Context->LastReportedNbOfBytes = Context->DownloadedNbOfBytes;
+      }
+    } else {
+      //
+      // Case when we do not know the size of the final file.
+      // We print the updated size every 50KB of downloaded data
+      //
+      LastNbOf50Kb = Context->LastReportedNbOfBytes / (50*1024);
+      NbOf50Kb     = Context->DownloadedNbOfBytes   / (50*1024);
+      if (NbOf50Kb > LastNbOf50Kb) {
+        Print (L"\b\b\b\b\b\b\b\b\b\b");
+        UnicodeSPrint (Progress, sizeof (Progress), L"%7d Kb", NbOfKb);
+        Context->LastReportedNbOfBytes = Context->DownloadedNbOfBytes;
+      }
+    }
+    if (Progress[0] != L'\0') {
+      Print (L"%s", Progress);
+    }
+  }
+
+  return EFI_SUCCESS;
+}
+
+/**
+  Download an image from a TFTP server
+
+  @param[in]   DevicePath           Device path of the TFTP boot option
+  @param[in]   ControllerHandle     Handle of the network controller
+  @param[in]   RemainingDevicePath  Device path of the TFTP boot option but
+                                    the first node that identifies the network 
controller
+  @param[in]   Type                 Type to allocate memory pages
+  @param[out]  Image                Address of the bufer where the image is 
stored in
+                                    case of success
+  @param[out]  ImageSize            Size in number of bytes of the i;age in 
case of
+                                    success
+
+  @retval  EFI_SUCCESS   The image was returned.
+  @retval  !EFI_SUCCESS  Something went wrong.
+
+**/
+EFI_STATUS
 BdsTftpLoadImage (
   IN     EFI_DEVICE_PATH*       DevicePath,
-  IN     EFI_HANDLE             Handle,
+  IN     EFI_HANDLE             ControllerHandle,
   IN     EFI_DEVICE_PATH*       RemainingDevicePath,
   IN     EFI_ALLOCATE_TYPE      Type,
   IN OUT EFI_PHYSICAL_ADDRESS   *Image,
   OUT    UINTN                  *ImageSize
   )
 {
-  EFI_STATUS                  Status;
-  EFI_PXE_BASE_CODE_PROTOCOL  *Pxe;
-  UINT64                      TftpBufferSize;
-  UINT64                      TftpTransferSize;
-  EFI_IP_ADDRESS              ServerIp;
-  IPv4_DEVICE_PATH*           IPv4DevicePathNode;
-  FILEPATH_DEVICE_PATH*       FilePathDevicePath;
-  EFI_IP_ADDRESS              LocalIp;
-  CHAR8*                      AsciiPathName;
-  EFI_SIMPLE_NETWORK_PROTOCOL *Snp;
+  EFI_STATUS              Status;
+  EFI_HANDLE              Dhcp4ChildHandle;
+  EFI_DHCP4_PROTOCOL      *Dhcp4;
+  BOOLEAN                 Dhcp4ToStop;
+  EFI_HANDLE              Mtftp4ChildHandle;
+  EFI_MTFTP4_PROTOCOL     *Mtftp4;
+  EFI_DHCP4_CONFIG_DATA   Dhcp4CfgData;
+  EFI_DHCP4_MODE_DATA     Dhcp4Mode;
+  EFI_MTFTP4_CONFIG_DATA  Mtftp4CfgData;
+  IPv4_DEVICE_PATH        *IPv4DevicePathNode;
+  FILEPATH_DEVICE_PATH    *FilePathDevicePathNode;
+  CHAR8                   *AsciiFilePath;
+  EFI_MTFTP4_TOKEN        Mtftp4Token;
+  UINT64                  FileSize;
+  UINT64                  TftpBufferSize;
+  BDS_TFTP_CONTEXT        *TftpContext;
 
   ASSERT(IS_DEVICE_PATH_NODE (RemainingDevicePath, MESSAGING_DEVICE_PATH, 
MSG_IPv4_DP));
-
   IPv4DevicePathNode = (IPv4_DEVICE_PATH*)RemainingDevicePath;
-  FilePathDevicePath = (FILEPATH_DEVICE_PATH*)(IPv4DevicePathNode + 1);
 
-  Status = gBS->LocateProtocol (&gEfiPxeBaseCodeProtocolGuid, NULL, (VOID 
**)&Pxe);
-  if (EFI_ERROR (Status)) {
-    return Status;
+  Dhcp4ChildHandle  = NULL;
+  Dhcp4             = NULL;
+  Dhcp4ToStop       = FALSE;
+  Mtftp4ChildHandle = NULL;
+  Mtftp4            = NULL;
+  AsciiFilePath     = NULL;
+  TftpContext       = NULL;
+
+  if (!IPv4DevicePathNode->StaticIpAddress) {
+    //
+    // Using the DHCP4 Service Binding Protocol, create a child handle of the 
DHCP4 service and
+    // install the DHCP4 protocol on it. Then, open the DHCP protocol.
+    //
+    Status = NetLibCreateServiceChild (
+               ControllerHandle,
+               gImageHandle,
+               &gEfiDhcp4ServiceBindingProtocolGuid,
+               &Dhcp4ChildHandle
+               );
+    if (!EFI_ERROR (Status)) {
+      Status = gBS->OpenProtocol (
+                      Dhcp4ChildHandle,
+                      &gEfiDhcp4ProtocolGuid,
+                      (VOID **) &Dhcp4,
+                      gImageHandle,
+                      ControllerHandle,
+                      EFI_OPEN_PROTOCOL_BY_DRIVER
+                      );
+    }
+    if (EFI_ERROR (Status)) {
+      Print (L"Unable to open DHCP4 protocol\n");
+      goto Error;
+    }
   }
 
-  Status = Pxe->Start (Pxe, FALSE);
-  if (EFI_ERROR (Status) && (Status != EFI_ALREADY_STARTED)) {
-    return Status;
+  //
+  // Using the MTFTP4 Service Binding Protocol, create a child handle of the 
MTFTP4 service and
+  // install the MTFTP4 protocol on it. Then, open the MTFTP4 protocol.
+  //
+  Status = NetLibCreateServiceChild (
+             ControllerHandle,
+             gImageHandle,
+             &gEfiMtftp4ServiceBindingProtocolGuid,
+             &Mtftp4ChildHandle
+             );
+  if (!EFI_ERROR (Status)) {
+    Status = gBS->OpenProtocol (
+                    Mtftp4ChildHandle,
+                    &gEfiMtftp4ProtocolGuid,
+                    (VOID **) &Mtftp4,
+                    gImageHandle,
+                    ControllerHandle,
+                    EFI_OPEN_PROTOCOL_BY_DRIVER
+                    );
   }
+  if (EFI_ERROR (Status)) {
+    Print (L"Unable to open MTFTP4 protocol\n");
+    goto Error;
+  }
 
-  do {
-    if (!IPv4DevicePathNode->StaticIpAddress) {
-      Status = Pxe->Dhcp (Pxe, TRUE);
-    } else {
-      CopyMem (&LocalIp.v4, &IPv4DevicePathNode->LocalIpAddress, sizeof 
(EFI_IPv4_ADDRESS));
-      Status = Pxe->SetStationIp (Pxe, &LocalIp, NULL);
+  if (!IPv4DevicePathNode->StaticIpAddress) {
+    //
+    // Configure the DHCP4, all default settings. It is acceptable for the 
configuration to
+    // fail if the return code is equal to EFI_ACCESS_DENIED which means that 
the configuration
+    // has been done by another instance of the DHCP4 protocol or that the 
DHCP configuration
+    // process has been started but is not completed yet.
+    //
+    ZeroMem (&Dhcp4CfgData, sizeof (EFI_DHCP4_CONFIG_DATA));
+    Status = Dhcp4->Configure (Dhcp4, &Dhcp4CfgData);
+    if (EFI_ERROR (Status)) {
+      if (Status != EFI_ACCESS_DENIED) {
+        Print (L"Error while configuring the DHCP4 protocol\n");
+        goto Error;
+      }
     }
 
-    // If an IP Address has already been set and a different static IP address 
is requested then restart
-    // the Network service.
-    if (Status == EFI_ALREADY_STARTED) {
-      Status = gBS->LocateProtocol (&gEfiSimpleNetworkProtocolGuid, NULL, 
(VOID **)&Snp);
-      if (!EFI_ERROR (Status) && IPv4DevicePathNode->StaticIpAddress &&
-          (CompareMem (&Snp->Mode->CurrentAddress, 
&IPv4DevicePathNode->LocalIpAddress, sizeof(EFI_MAC_ADDRESS)) != 0))
-      {
-        Pxe->Stop (Pxe);
-        Status = Pxe->Start (Pxe, FALSE);
-        if (EFI_ERROR(Status)) {
-          break;
-        }
-        // After restarting the PXE protocol, we want to try again with our 
new IP Address
-        Status = EFI_ALREADY_STARTED;
+    //
+    // Start the DHCP configuration. This may have already been done thus do 
not leave in error
+    // if the return code is EFI_ALREADY_STARTED.
+    //
+    Status = Dhcp4->Start (Dhcp4, NULL);
+    if (EFI_ERROR (Status)) {
+      if (Status != EFI_ALREADY_STARTED) {
+        Print (L"DHCP configuration failed\n");
+        goto Error;
       }
+    } else {
+      Dhcp4ToStop = TRUE;
     }
-  } while (Status == EFI_ALREADY_STARTED);
 
-  if (EFI_ERROR(Status)) {
-    return Status;
+    Status = Dhcp4->GetModeData (Dhcp4, &Dhcp4Mode);
+    if (EFI_ERROR (Status)) {
+      goto Error;
+    }
+
+    if (Dhcp4Mode.State != Dhcp4Bound) {
+      Status = EFI_TIMEOUT;
+      Print (L"DHCP configuration failed\n");
+      goto Error;
+    }
   }
 
-  CopyMem (&ServerIp.v4, &IPv4DevicePathNode->RemoteIpAddress, sizeof 
(EFI_IPv4_ADDRESS));
+  //
+  // Configure the TFTP4 protocol
+  //
 
-  // Convert the Unicode PathName to Ascii
-  AsciiPathName = AllocatePool ((StrLen (FilePathDevicePath->PathName) + 1) * 
sizeof (CHAR8));
-  if (AsciiPathName == NULL) {
-    return EFI_OUT_OF_RESOURCES;
+  ZeroMem (&Mtftp4CfgData, sizeof (EFI_MTFTP4_CONFIG_DATA));
+  Mtftp4CfgData.UseDefaultSetting = FALSE;
+  Mtftp4CfgData.TimeoutValue      = 4;
+  Mtftp4CfgData.TryCount          = 6;
+
+  if (IPv4DevicePathNode->StaticIpAddress) {
+    CopyMem (&Mtftp4CfgData.StationIp , &IPv4DevicePathNode->LocalIpAddress, 
sizeof (EFI_IPv4_ADDRESS));
+    CopyMem (&Mtftp4CfgData.SubnetMask, &IPv4DevicePathNode->SubnetMask, 
sizeof (EFI_IPv4_ADDRESS));
+    CopyMem (&Mtftp4CfgData.GatewayIp , &IPv4DevicePathNode->GatewayIpAddress, 
sizeof (EFI_IPv4_ADDRESS));
+  } else {
+    CopyMem (&Mtftp4CfgData.StationIp , &Dhcp4Mode.ClientAddress, sizeof 
(EFI_IPv4_ADDRESS));
+    CopyMem (&Mtftp4CfgData.SubnetMask, &Dhcp4Mode.SubnetMask   , sizeof 
(EFI_IPv4_ADDRESS));
+    CopyMem (&Mtftp4CfgData.GatewayIp , &Dhcp4Mode.RouterAddress, sizeof 
(EFI_IPv4_ADDRESS));
   }
-  UnicodeStrToAsciiStr (FilePathDevicePath->PathName, AsciiPathName);
 
-  // Try to get the size (required the TFTP server to have "tsize" extension)
-  Status = Pxe->Mtftp (
-                  Pxe,
-                  EFI_PXE_BASE_CODE_TFTP_GET_FILE_SIZE,
-                  NULL,
-                  FALSE,
-                  &TftpBufferSize,
-                  NULL,
-                  &ServerIp,
-                  (UINT8*)AsciiPathName,
-                  NULL,
-                  FALSE
-                  );
-  // Pxe.Mtftp replies EFI_PROTOCOL_ERROR if tsize is not supported by the 
TFTP server
-  if (EFI_ERROR (Status) && (Status != EFI_PROTOCOL_ERROR)) {
-    if (Status == EFI_TFTP_ERROR) {
-      DEBUG((EFI_D_ERROR, "TFTP Error: Fail to get the size of the file\n"));
-    }
-    goto EXIT;
+  CopyMem (&Mtftp4CfgData.ServerIp  , &IPv4DevicePathNode->RemoteIpAddress, 
sizeof (EFI_IPv4_ADDRESS));
+
+  Status = Mtftp4->Configure (Mtftp4, &Mtftp4CfgData);
+  if (EFI_ERROR (Status)) {
+    Print (L"Error while configuring the MTFTP4 protocol\n");
+    goto Error;
   }
 
   //
-  // Two cases:
-  //   1) the file size is unknown (tsize extension not supported)
-  //   2) tsize returned the file size
+  // Convert the Unicode path of the file to Ascii
   //
-  if (Status == EFI_PROTOCOL_ERROR) {
-    for (TftpBufferSize = SIZE_8MB; TftpBufferSize <= FixedPcdGet32 
(PcdMaxTftpFileSize); TftpBufferSize += SIZE_8MB) {
-      // Allocate a buffer to hold the whole file.
-      Status = gBS->AllocatePages (
-                      Type,
-                      EfiBootServicesCode,
-                      EFI_SIZE_TO_PAGES (TftpBufferSize),
-                      Image
-                      );
-      if (EFI_ERROR (Status)) {
-        DEBUG ((EFI_D_ERROR, "Failed to allocate space for image: %r\n", 
Status));
-        goto EXIT;
-      }
 
-      TftpTransferSize = TftpBufferSize;
-      Status = Pxe->Mtftp (
-                      Pxe,
-                      EFI_PXE_BASE_CODE_TFTP_READ_FILE,
-                      (VOID *)(UINTN)*Image,
-                      FALSE,
-                      &TftpTransferSize,
-                      NULL,
-                      &ServerIp,
-                      (UINT8*)AsciiPathName,
-                      NULL,
-                      FALSE
-                      );
-      if (EFI_ERROR (Status)) {
-        gBS->FreePages (*Image, EFI_SIZE_TO_PAGES (TftpBufferSize));
-      } else {
-        *ImageSize = (UINTN)TftpBufferSize;
-        break;
-      }
-    }
+  FilePathDevicePathNode = (FILEPATH_DEVICE_PATH*)(IPv4DevicePathNode + 1);
+  AsciiFilePath = AllocatePool ((StrLen (FilePathDevicePathNode->PathName) + 
1) * sizeof (CHAR8));
+  if (AsciiFilePath == NULL) {
+    Status = EFI_OUT_OF_RESOURCES;
+    goto Error;
+  }
+  UnicodeStrToAsciiStr (FilePathDevicePathNode->PathName, AsciiFilePath);
+
+  //
+  // Try to get the size of the file in bytes from the server. If it fails,
+  // start with a 8MB buffer to download the file.
+  //
+  FileSize = 0;
+  if (Mtftp4GetFileSize (Mtftp4, AsciiFilePath, &FileSize) == EFI_SUCCESS) {
+    TftpBufferSize = FileSize;
   } else {
+    TftpBufferSize = SIZE_8MB;
+  }
+
+  TftpContext = AllocatePool (sizeof (BDS_TFTP_CONTEXT));
+  if (TftpContext == NULL) {
+    Status = EFI_OUT_OF_RESOURCES;
+    goto Error;
+  }
+  TftpContext->FileSize = FileSize;
+
+  for (; TftpBufferSize <= FixedPcdGet32 (PcdMaxTftpFileSize);
+         TftpBufferSize = (TftpBufferSize + SIZE_8MB) & (~(SIZE_8MB-1))) {
+    //
     // Allocate a buffer to hold the whole file.
+    //
     Status = gBS->AllocatePages (
                     Type,
                     EfiBootServicesCode,
@@ -900,31 +1189,85 @@
                     Image
                     );
     if (EFI_ERROR (Status)) {
-      DEBUG ((EFI_D_ERROR, "Failed to allocate space for kernel image: %r\n", 
Status));
-      goto EXIT;
+      Print (L"Failed to allocate space for image\n");
+      goto Error;
     }
 
-    Status = Pxe->Mtftp (
-                    Pxe,
-                    EFI_PXE_BASE_CODE_TFTP_READ_FILE,
-                    (VOID *)(UINTN)*Image,
-                    FALSE,
-                    &TftpBufferSize,
-                    NULL,
-                    &ServerIp,
-                    (UINT8*)AsciiPathName,
-                    NULL,
-                    FALSE
-                    );
+    TftpContext->DownloadedNbOfBytes   = 0;
+    TftpContext->LastReportedNbOfBytes = 0;
+
+    ZeroMem (&Mtftp4Token, sizeof (EFI_MTFTP4_TOKEN));
+    Mtftp4Token.Filename    = (UINT8*)AsciiFilePath;
+    Mtftp4Token.BufferSize  = TftpBufferSize;
+    Mtftp4Token.Buffer      = (VOID *)(UINTN)*Image;
+    Mtftp4Token.CheckPacket = Mtftp4CheckPacket;
+    Mtftp4Token.Context     = (VOID*)TftpContext;
+
+    Print (L"Downloading the file <%s> from the TFTP server\n", 
FilePathDevicePathNode->PathName);
+    Status = Mtftp4->ReadFile (Mtftp4, &Mtftp4Token);
+    Print (L"\n");
     if (EFI_ERROR (Status)) {
-      gBS->FreePages (*Image, EFI_SIZE_TO_PAGES (TftpBufferSize));
-    } else {
-      *ImageSize = (UINTN)TftpBufferSize;
+      if (Status == EFI_BUFFER_TOO_SMALL) {
+        Print (L"Downloading failed, file larger than expected.\n");
+        gBS->FreePages (*Image, EFI_SIZE_TO_PAGES (TftpBufferSize));
+        continue;
+      } else {
+        goto Error;
+      }
     }
+
+    *ImageSize = Mtftp4Token.BufferSize;
+    break;
   }
 
-EXIT:
-  FreePool (AsciiPathName);
+Error:
+  if (Dhcp4ChildHandle != NULL) {
+    if (Dhcp4 != NULL) {
+      if (Dhcp4ToStop) {
+        Dhcp4->Stop (Dhcp4);
+      }
+      gBS->CloseProtocol (
+             Dhcp4ChildHandle,
+             &gEfiDhcp4ProtocolGuid,
+             gImageHandle,
+             ControllerHandle
+            );
+    }
+    NetLibDestroyServiceChild (
+      ControllerHandle,
+      gImageHandle,
+      &gEfiDhcp4ServiceBindingProtocolGuid,
+      Dhcp4ChildHandle
+      );
+  }
+
+  if (Mtftp4ChildHandle != NULL) {
+    if (Mtftp4 != NULL) {
+      if (AsciiFilePath != NULL) {
+        FreePool (AsciiFilePath);
+      }
+      if (TftpContext != NULL) {
+        FreePool (TftpContext);
+      }
+      gBS->CloseProtocol (
+             Mtftp4ChildHandle,
+             &gEfiMtftp4ProtocolGuid,
+             gImageHandle,
+             ControllerHandle
+            );
+    }
+    NetLibDestroyServiceChild (
+      ControllerHandle,
+      gImageHandle,
+      &gEfiMtftp4ServiceBindingProtocolGuid,
+      Mtftp4ChildHandle
+      );
+  }
+
+  if (EFI_ERROR (Status)) {
+    Print (L"Failed to download the file - Error=%r\n", Status);
+  }
+
   return Status;
 }
 

Modified: trunk/edk2/ArmPkg/Library/BdsLib/BdsInternal.h
===================================================================
--- trunk/edk2/ArmPkg/Library/BdsLib/BdsInternal.h      2014-12-12 19:13:04 UTC 
(rev 16515)
+++ trunk/edk2/ArmPkg/Library/BdsLib/BdsInternal.h      2014-12-12 19:14:22 UTC 
(rev 16516)
@@ -71,6 +71,11 @@
   UINT64                      ResourceLength;
 } BDS_SYSTEM_MEMORY_RESOURCE;
 
+typedef struct {
+  UINT64  FileSize;
+  UINT64  DownloadedNbOfBytes;
+  UINT64  LastReportedNbOfBytes;
+} BDS_TFTP_CONTEXT;
 
 // BdsHelper.c
 EFI_STATUS

Modified: trunk/edk2/ArmPkg/Library/BdsLib/BdsLib.inf
===================================================================
--- trunk/edk2/ArmPkg/Library/BdsLib/BdsLib.inf 2014-12-12 19:13:04 UTC (rev 
16515)
+++ trunk/edk2/ArmPkg/Library/BdsLib/BdsLib.inf 2014-12-12 19:14:22 UTC (rev 
16516)
@@ -37,6 +37,7 @@
 
 [Packages]
   MdePkg/MdePkg.dec
+  MdeModulePkg/MdeModulePkg.dec
   EmbeddedPkg/EmbeddedPkg.dec
   ArmPkg/ArmPkg.dec
   ArmPlatformPkg/ArmPlatformPkg.dec
@@ -53,6 +54,7 @@
   SerialPortLib
   FdtLib
   TimerLib
+  NetLib
 
 [LibraryClasses.AARCH64]
   ArmGicLib
@@ -74,6 +76,10 @@
   gEfiUsbIoProtocolGuid
   gEfiLoadedImageProtocolGuid
   gEfiSimpleNetworkProtocolGuid
+  gEfiDhcp4ServiceBindingProtocolGuid
+  gEfiDhcp4ProtocolGuid
+  gEfiMtftp4ServiceBindingProtocolGuid
+  gEfiMtftp4ProtocolGuid
 
 [FeaturePcd]
   gArmTokenSpaceGuid.PcdArmLinuxSpinTable

Modified: trunk/edk2/ArmPlatformPkg/Bds/Bds.inf
===================================================================
--- trunk/edk2/ArmPlatformPkg/Bds/Bds.inf       2014-12-12 19:13:04 UTC (rev 
16515)
+++ trunk/edk2/ArmPlatformPkg/Bds/Bds.inf       2014-12-12 19:14:22 UTC (rev 
16516)
@@ -63,6 +63,8 @@
   gEfiDevicePathToTextProtocolGuid
   gEfiFirmwareVolumeBlockProtocolGuid
   gEfiFirmwareVolumeBlock2ProtocolGuid
+  gEfiDhcp4ServiceBindingProtocolGuid
+  gEfiMtftp4ServiceBindingProtocolGuid
 
 [Pcd]
   gArmPlatformTokenSpaceGuid.PcdFirmwareVendor

Modified: trunk/edk2/ArmPlatformPkg/Bds/BootOptionSupport.c
===================================================================
--- trunk/edk2/ArmPlatformPkg/Bds/BootOptionSupport.c   2014-12-12 19:13:04 UTC 
(rev 16515)
+++ trunk/edk2/ArmPlatformPkg/Bds/BootOptionSupport.c   2014-12-12 19:14:22 UTC 
(rev 16516)
@@ -22,6 +22,8 @@
 #include <Protocol/PxeBaseCode.h>
 #include <Protocol/SimpleFileSystem.h>
 #include <Protocol/SimpleNetwork.h>
+#include <Protocol/Dhcp4.h>
+#include <Protocol/Mtftp4.h>
 
 #include <Guid/FileSystemInfo.h>
 
@@ -866,49 +868,96 @@
   }
 }
 
+/**
+  Add to the list of boot devices the devices allowing a TFTP boot
+
+  @param[in]   BdsLoadOptionList  List of devices to boot from
+
+  @retval  EFI_SUCCESS            Update completed
+  @retval  EFI_OUT_OF_RESOURCES   Fail to perform the update due to lack of 
resource
+**/
 EFI_STATUS
 BdsLoadOptionTftpList (
   IN OUT LIST_ENTRY* BdsLoadOptionList
   )
 {
-  EFI_STATUS                        Status;
-  UINTN                             HandleCount;
-  EFI_HANDLE                        *HandleBuffer;
-  UINTN                             Index;
-  BDS_SUPPORTED_DEVICE              *SupportedDevice;
-  EFI_DEVICE_PATH_PROTOCOL*         DevicePathProtocol;
-  EFI_SIMPLE_NETWORK_PROTOCOL*      SimpleNet;
-  CHAR16                            
DeviceDescription[BOOT_DEVICE_DESCRIPTION_MAX];
-  EFI_MAC_ADDRESS                   *Mac;
+  EFI_STATUS                   Status;
+  UINTN                        HandleCount;
+  EFI_HANDLE                   *HandleBuffer;
+  EFI_HANDLE                   Handle;
+  UINTN                        Index;
+  EFI_DEVICE_PATH_PROTOCOL     *DevicePathProtocol;
+  VOID                         *Interface;
+  EFI_SIMPLE_NETWORK_PROTOCOL  *SimpleNetworkProtocol;
+  BDS_SUPPORTED_DEVICE         *SupportedDevice;
+  EFI_MAC_ADDRESS              *Mac;
 
-  // List all the PXE Protocols
-  Status = gBS->LocateHandleBuffer (ByProtocol, &gEfiPxeBaseCodeProtocolGuid, 
NULL, &HandleCount, &HandleBuffer);
+  //
+  // List all the handles on which the Simple Network Protocol is installed.
+  //
+  Status = gBS->LocateHandleBuffer (
+                  ByProtocol,
+                  &gEfiSimpleNetworkProtocolGuid,
+                  NULL,
+                  &HandleCount,
+                  &HandleBuffer
+                  );
   if (EFI_ERROR (Status)) {
     return Status;
   }
 
   for (Index = 0; Index < HandleCount; Index++) {
-    // We only select the handle WITH a Device Path AND the PXE Protocol AND 
the TFTP Protocol (the TFTP protocol is required to start PXE)
-    Status = gBS->HandleProtocol (HandleBuffer[Index], 
&gEfiDevicePathProtocolGuid, (VOID **)&DevicePathProtocol);
-    if (!EFI_ERROR(Status)) {
-      // Allocate BDS Supported Device structure
-      SupportedDevice = 
(BDS_SUPPORTED_DEVICE*)AllocatePool(sizeof(BDS_SUPPORTED_DEVICE));
+    Handle = HandleBuffer[Index];
+    //
+    // We select the handles that support :
+    // . the Device Path Protocol
+    // . the MTFTP4 Protocol
+    //
+    Status = gBS->HandleProtocol (
+                    Handle,
+                    &gEfiDevicePathProtocolGuid,
+                    (VOID **)&DevicePathProtocol
+                    );
+    if (EFI_ERROR (Status)) {
+      continue;
+    }
 
-      Status = gBS->LocateProtocol (&gEfiSimpleNetworkProtocolGuid, NULL, 
(VOID **)&SimpleNet);
-      if (!EFI_ERROR(Status)) {
-        Mac = &SimpleNet->Mode->CurrentAddress;
-        UnicodeSPrint (DeviceDescription,BOOT_DEVICE_DESCRIPTION_MAX,L"MAC 
Address: %02x:%02x:%02x:%02x:%02x:%02x", Mac->Addr[0],  Mac->Addr[1],  
Mac->Addr[2],  Mac->Addr[3],  Mac->Addr[4],  Mac->Addr[5]);
-      } else {
-        Status = GenerateDeviceDescriptionName (HandleBuffer[Index], 
DeviceDescription);
-        ASSERT_EFI_ERROR (Status);
-      }
-      UnicodeSPrint 
(SupportedDevice->Description,BOOT_DEVICE_DESCRIPTION_MAX,L"TFTP on 
%s",DeviceDescription);
+    Status = gBS->HandleProtocol (
+                    Handle,
+                    &gEfiMtftp4ServiceBindingProtocolGuid,
+                    &Interface
+                    );
+    if (EFI_ERROR (Status)) {
+      continue;
+    }
 
-      SupportedDevice->DevicePathProtocol = DevicePathProtocol;
-      SupportedDevice->Support = &BdsLoadOptionSupportList[BDS_DEVICE_TFTP];
+    Status = gBS->HandleProtocol (
+                    Handle,
+                    &gEfiSimpleNetworkProtocolGuid,
+                    (VOID **)&SimpleNetworkProtocol
+                    );
+    if (EFI_ERROR (Status)) {
+      continue;
+    }
 
-      InsertTailList (BdsLoadOptionList,&SupportedDevice->Link);
+    // Allocate BDS Supported Device structure
+    SupportedDevice = (BDS_SUPPORTED_DEVICE*)AllocatePool (sizeof 
(BDS_SUPPORTED_DEVICE));
+    if (SupportedDevice == NULL) {
+      continue;
     }
+
+    Mac = &SimpleNetworkProtocol->Mode->CurrentAddress;
+    UnicodeSPrint (
+      SupportedDevice->Description,
+      BOOT_DEVICE_DESCRIPTION_MAX,
+      L"TFTP on MAC Address: %02x:%02x:%02x:%02x:%02x:%02x",
+      Mac->Addr[0],  Mac->Addr[1],  Mac->Addr[2],  Mac->Addr[3],  
Mac->Addr[4],  Mac->Addr[5]
+      );
+
+    SupportedDevice->DevicePathProtocol = DevicePathProtocol;
+    SupportedDevice->Support = &BdsLoadOptionSupportList[BDS_DEVICE_TFTP];
+
+    InsertTailList (BdsLoadOptionList, &SupportedDevice->Link);
   }
 
   return EFI_SUCCESS;
@@ -920,38 +969,50 @@
   OUT EFI_DEVICE_PATH_PROTOCOL  **DevicePathNodes
   )
 {
-  EFI_STATUS    Status;
-  BOOLEAN       IsDHCP;
-  EFI_IP_ADDRESS  LocalIp;
-  EFI_IP_ADDRESS  RemoteIp;
-  IPv4_DEVICE_PATH*   IPv4DevicePathNode;
-  FILEPATH_DEVICE_PATH* FilePathDevicePath;
-  CHAR16      BootFilePath[BOOT_DEVICE_FILEPATH_MAX];
-  UINTN       BootFilePathSize;
+  EFI_STATUS            Status;
+  BOOLEAN               IsDHCP;
+  EFI_IP_ADDRESS        LocalIp;
+  EFI_IP_ADDRESS        SubnetMask;
+  EFI_IP_ADDRESS        GatewayIp;
+  EFI_IP_ADDRESS        RemoteIp;
+  IPv4_DEVICE_PATH      *IPv4DevicePathNode;
+  FILEPATH_DEVICE_PATH  *FilePathDevicePath;
+  CHAR16                BootFilePath[BOOT_DEVICE_FILEPATH_MAX];
+  UINTN                 BootFilePathSize;
 
-  Print(L"Get the IP address from DHCP: ");
+  Print (L"Get the IP address from DHCP: ");
   Status = GetHIInputBoolean (&IsDHCP);
-  if (EFI_ERROR(Status)) {
+  if (EFI_ERROR (Status)) {
     return EFI_ABORTED;
   }
 
   if (!IsDHCP) {
-    Print(L"Get the static IP address: ");
+    Print (L"Local static IP address: ");
     Status = GetHIInputIP (&LocalIp);
-    if (EFI_ERROR(Status)) {
+    if (EFI_ERROR (Status)) {
       return EFI_ABORTED;
     }
+    Print (L"Get the network mask: ");
+    Status = GetHIInputIP (&SubnetMask);
+    if (EFI_ERROR (Status)) {
+      return EFI_ABORTED;
+    }
+    Print (L"Get the gateway IP address: ");
+    Status = GetHIInputIP (&GatewayIp);
+    if (EFI_ERROR (Status)) {
+      return EFI_ABORTED;
+    }
   }
 
-  Print(L"Get the TFTP server IP address: ");
+  Print (L"Get the TFTP server IP address: ");
   Status = GetHIInputIP (&RemoteIp);
-  if (EFI_ERROR(Status)) {
+  if (EFI_ERROR (Status)) {
     return EFI_ABORTED;
   }
 
-  Print(L"File path of the %s : ", FileName);
+  Print (L"File path of the %s : ", FileName);
   Status = GetHIInputStr (BootFilePath, BOOT_DEVICE_FILEPATH_MAX);
-  if (EFI_ERROR(Status)) {
+  if (EFI_ERROR (Status)) {
     return EFI_ABORTED;
   }
 
@@ -967,7 +1028,13 @@
   IPv4DevicePathNode->Header.Type    = MESSAGING_DEVICE_PATH;
   IPv4DevicePathNode->Header.SubType = MSG_IPv4_DP;
   SetDevicePathNodeLength (&IPv4DevicePathNode->Header, 
sizeof(IPv4_DEVICE_PATH));
-  CopyMem (&IPv4DevicePathNode->LocalIpAddress, &LocalIp.v4, sizeof 
(EFI_IPv4_ADDRESS));
+
+  if (!IsDHCP) {
+    CopyMem (&IPv4DevicePathNode->LocalIpAddress, &LocalIp.v4, sizeof 
(EFI_IPv4_ADDRESS));
+    CopyMem (&IPv4DevicePathNode->SubnetMask, &SubnetMask.v4, sizeof 
(EFI_IPv4_ADDRESS));
+    CopyMem (&IPv4DevicePathNode->GatewayIpAddress, &GatewayIp.v4, sizeof 
(EFI_IPv4_ADDRESS));
+  }
+
   CopyMem (&IPv4DevicePathNode->RemoteIpAddress, &RemoteIp.v4, sizeof 
(EFI_IPv4_ADDRESS));
   IPv4DevicePathNode->LocalPort  = 0;
   IPv4DevicePathNode->RemotePort = 0;
@@ -1021,7 +1088,11 @@
   IPv4_DEVICE_PATH       Ipv4Node;
   BOOLEAN                IsDHCP;
   EFI_IP_ADDRESS         OldIp;
+  EFI_IP_ADDRESS         OldSubnetMask;
+  EFI_IP_ADDRESS         OldGatewayIp;
   EFI_IP_ADDRESS         LocalIp;
+  EFI_IP_ADDRESS         SubnetMask;
+  EFI_IP_ADDRESS         GatewayIp;
   EFI_IP_ADDRESS         RemoteIp;
   UINT8                 *FileNodePtr;
   CHAR16                 BootFilePath[BOOT_DEVICE_FILEPATH_MAX];
@@ -1074,9 +1145,7 @@
   if (!IsDHCP) {
     Print (L"Local static IP address: ");
     if (Ipv4Node.StaticIpAddress) {
-      // Copy local IPv4 address into IPv4 or IPv6 union
       CopyMem (&OldIp.v4, &Ipv4Node.LocalIpAddress, sizeof (EFI_IPv4_ADDRESS));
-
       Status = EditHIInputIP (&OldIp, &LocalIp);
     } else {
       Status = GetHIInputIP (&LocalIp);
@@ -1084,6 +1153,28 @@
     if (EFI_ERROR (Status)) {
       goto ErrorExit;
     }
+
+    Print (L"Get the network mask: ");
+    if (Ipv4Node.StaticIpAddress) {
+      CopyMem (&OldSubnetMask.v4, &Ipv4Node.SubnetMask, sizeof 
(EFI_IPv4_ADDRESS));
+      Status = EditHIInputIP (&OldSubnetMask, &SubnetMask);
+    } else {
+      Status = GetHIInputIP (&SubnetMask);
+    }
+    if (EFI_ERROR (Status)) {
+      goto ErrorExit;
+    }
+
+    Print (L"Get the gateway IP address: ");
+    if (Ipv4Node.StaticIpAddress) {
+      CopyMem (&OldGatewayIp.v4, &Ipv4Node.GatewayIpAddress, sizeof 
(EFI_IPv4_ADDRESS));
+      Status = EditHIInputIP (&OldGatewayIp, &GatewayIp);
+    } else {
+      Status = GetHIInputIP (&GatewayIp);
+    }
+    if (EFI_ERROR (Status)) {
+      goto ErrorExit;
+    }
   }
 
   Print (L"TFTP server IP address: ");
@@ -1126,12 +1217,18 @@
   //
   // Update the IPv4 node. IPv6 case not handled yet.
   //
-  if (IsDHCP == TRUE) {
+  if (IsDHCP) {
     Ipv4Node.StaticIpAddress = FALSE;
+    ZeroMem (&Ipv4Node.LocalIpAddress, sizeof (EFI_IPv4_ADDRESS));
+    ZeroMem (&Ipv4Node.SubnetMask, sizeof (EFI_IPv4_ADDRESS));
+    ZeroMem (&Ipv4Node.GatewayIpAddress, sizeof (EFI_IPv4_ADDRESS));
   } else {
     Ipv4Node.StaticIpAddress = TRUE;
+    CopyMem (&Ipv4Node.LocalIpAddress, &LocalIp.v4, sizeof (EFI_IPv4_ADDRESS));
+    CopyMem (&Ipv4Node.SubnetMask, &SubnetMask.v4, sizeof (EFI_IPv4_ADDRESS));
+    CopyMem (&Ipv4Node.GatewayIpAddress, &GatewayIp.v4, sizeof 
(EFI_IPv4_ADDRESS));
   }
-  CopyMem (&Ipv4Node.LocalIpAddress, &LocalIp.v4, sizeof (EFI_IPv4_ADDRESS));
+
   CopyMem (&Ipv4Node.RemoteIpAddress, &RemoteIp.v4, sizeof (EFI_IPv4_ADDRESS));
   CopyMem (Ipv4NodePtr, &Ipv4Node, sizeof (IPv4_DEVICE_PATH));
 


------------------------------------------------------------------------------
Download BIRT iHub F-Type - The Free Enterprise-Grade BIRT Server
from Actuate! Instantly Supercharge Your Business Reports and Dashboards
with Interactivity, Sharing, Native Excel Exports, App Integration & more
Get technology previously reserved for billion-dollar corporations, FREE
http://pubads.g.doubleclick.net/gampad/clk?id=164703151&iu=/4140/ostg.clktrk
_______________________________________________
edk2-commits mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/edk2-commits

Reply via email to