Skip to content

Commit 715e1ad

Browse files
joshklawik
andauthored
Only update the target firmware file with deltas that are smaller (#2308)
Instead of creating a new firmware file from scratch, this only updates/replaces files which have changed. This is an extension of #2307 --------- Co-authored-by: Lars Wikman <[email protected]>
1 parent aeccaac commit 715e1ad

File tree

4 files changed

+260
-73
lines changed

4 files changed

+260
-73
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ FROM ${RUNNER_IMAGE} AS app
154154

155155
RUN apt-get update -y && \
156156
apt-get upgrade -y && \
157-
apt-get install -y openssl ca-certificates locales bash xdelta3 && \
157+
apt-get install -y openssl ca-certificates locales bash xdelta3 zip unzip && \
158158
apt-get autoremove -y && \
159159
apt-get clean && \
160160
rm -rf /var/lib/apt/lists/*

lib/nerves_hub/firmwares/update_tool/fwup.ex

Lines changed: 102 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ defmodule NervesHub.Firmwares.UpdateTool.Fwup do
1616
@very_safe_version "1.13.0"
1717
@oldest_version Version.parse!("0.2.0")
1818

19+
# If a payload is smaller than this there is no point to do a delta, the overhead takes more space
20+
# in the best case
21+
@delta_overhead_limit 22
22+
1923
@impl NervesHub.Firmwares.UpdateTool
2024
def get_firmware_metadata_from_file(filepath) do
2125
with {:ok, firmware_metadata} <- FwupUtil.metadata(filepath),
@@ -61,7 +65,7 @@ defmodule NervesHub.Firmwares.UpdateTool.Fwup do
6165
{:ok, output}
6266

6367
{:error, reason} ->
64-
Logger.warning("Could not create a firmware delta: #{inspect(reason)}",
68+
Logger.warning("Firmware delta creation failed: #{inspect(reason)}",
6569
source_url: source_url,
6670
target_url: target_url
6771
)
@@ -158,7 +162,7 @@ defmodule NervesHub.Firmwares.UpdateTool.Fwup do
158162
{:ok, all_delta_files} <- delta_files(deltas) do
159163
Logger.info("Generating delta for files: #{Enum.join(all_delta_files, ", ")}")
160164

161-
_ =
165+
file_list =
162166
for absolute <- Path.wildcard(target_work_dir <> "/**"), not File.dir?(absolute) do
163167
path = Path.relative_to(absolute, target_work_dir)
164168

@@ -168,62 +172,107 @@ defmodule NervesHub.Firmwares.UpdateTool.Fwup do
168172
|> Path.dirname()
169173
|> File.mkdir_p!()
170174

171-
_ =
172-
case path do
173-
"meta." <> _ ->
174-
File.cp!(Path.join(target_work_dir, path), Path.join(output_work_dir, path))
175-
176-
"data/" <> subpath ->
177-
if subpath in all_delta_files do
178-
source_filepath = Path.join(source_work_dir, path)
179-
target_filepath = Path.join(target_work_dir, path)
180-
181-
case File.stat(source_filepath) do
182-
{:ok, %{size: f_source_size}} ->
183-
args = [
184-
"-A",
185-
"-S",
186-
"-f",
187-
"-s",
188-
source_filepath,
189-
target_filepath,
190-
output_path
191-
]
192-
193-
%{size: f_target_size} = File.stat!(target_filepath)
194-
195-
{_, 0} = System.cmd("xdelta3", args, stderr_to_stdout: true, env: [])
196-
%{size: f_delta_size} = File.stat!(output_path)
197-
198-
Logger.info(
199-
"Generated delta for #{path}, from #{Float.round(f_source_size / 1024 / 1024, 1)} MB to #{Float.round(f_target_size / 1024 / 1024, 1)} MB via delta of #{Float.round(f_delta_size / 1024 / 1024, 1)} MB"
200-
)
201-
202-
{:error, :enoent} ->
203-
File.cp!(target_filepath, output_path)
204-
end
205-
else
206-
File.cp!(Path.join(target_work_dir, path), Path.join(output_work_dir, path))
207-
end
208-
end
175+
maybe_generate_delta(
176+
path,
177+
source_work_dir,
178+
target_work_dir,
179+
output_work_dir,
180+
all_delta_files
181+
)
209182
end
183+
|> Enum.reject(&is_nil(&1))
184+
185+
{:ok, delta_zip_path} = Plug.Upload.random_file("#{source_uuid}_#{target_uuid}_delta.zip")
186+
_ = File.rm(delta_zip_path)
187+
_ = File.cp(target_path, delta_zip_path)
188+
189+
with {true, :changes_in_delta} <- {Enum.any?(file_list), :changes_in_delta},
190+
:ok <- update_changed_files(file_list, delta_zip_path, output_work_dir),
191+
{:ok, %{size: delta_size}} <- File.stat(delta_zip_path),
192+
{true, :delta_smaller} <- {delta_size < target_size, :delta_smaller} do
193+
{:ok,
194+
%{
195+
filepath: delta_zip_path,
196+
size: delta_size,
197+
source_size: source_size,
198+
target_size: target_size,
199+
tool: "fwup",
200+
tool_metadata: tool_metadata
201+
}}
202+
else
203+
{false, :changes_in_delta} -> {:error, :no_changes_in_delta}
204+
{false, :delta_smaller} -> {:error, :delta_larger_than_target}
205+
end
206+
end
207+
end
210208

211-
{:ok, delta_zip_path} = Plug.Upload.random_file("#{source_uuid}_#{target_uuid}_delta")
209+
defp update_changed_files(file_list, delta_zip_path, output_work_dir) do
210+
for file <- file_list do
211+
args = ["-9", delta_zip_path, String.replace_prefix(file, "#{output_work_dir}/", "")]
212+
{_output, 0} = System.cmd("zip", args, cd: output_work_dir)
213+
end
212214

213-
{:ok, _} =
214-
:zip.create(to_charlist(delta_zip_path), generate_file_list(output_work_dir), cwd: to_charlist(output_work_dir))
215+
:ok
216+
end
215217

216-
{:ok, %{size: size}} = File.stat(delta_zip_path)
218+
defp maybe_generate_delta("meta." <> _, _, _, _, _) do
219+
nil
220+
end
217221

218-
{:ok,
219-
%{
220-
filepath: delta_zip_path,
221-
size: size,
222-
source_size: source_size,
223-
target_size: target_size,
224-
tool: "fwup",
225-
tool_metadata: tool_metadata
226-
}}
222+
defp maybe_generate_delta(
223+
"data/" <> subpath = path,
224+
source_work_dir,
225+
target_work_dir,
226+
output_work_dir,
227+
all_delta_files
228+
) do
229+
output_path = Path.join(output_work_dir, path)
230+
target_filepath = Path.join(target_work_dir, path)
231+
232+
if subpath in all_delta_files do
233+
source_filepath = Path.join(source_work_dir, path)
234+
235+
case File.stat(source_filepath) do
236+
{:ok, %{size: f_source_size}} ->
237+
args = [
238+
"-A",
239+
"-S",
240+
"-f",
241+
"-s",
242+
source_filepath,
243+
target_filepath,
244+
output_path
245+
]
246+
247+
%{size: f_target_size} = File.stat!(target_filepath)
248+
249+
if f_target_size < @delta_overhead_limit do
250+
Logger.info("Skipping generating delta for #{path} it is under 22 bytes.")
251+
nil
252+
else
253+
{_, 0} = System.cmd("xdelta3", args, stderr_to_stdout: true, env: [])
254+
%{size: f_delta_size} = File.stat!(output_path)
255+
256+
if f_delta_size < f_target_size do
257+
Logger.info(
258+
"Generated delta for #{path}, from #{Float.round(f_source_size / 1024 / 1024, 1)} MB to #{Float.round(f_target_size / 1024 / 1024, 1)} MB via delta of #{Float.round(f_delta_size / 1024 / 1024, 1)} MB"
259+
)
260+
261+
output_path
262+
else
263+
Logger.info(
264+
"Skipping generated delta for #{path}, delta is larger: #{Float.round(f_source_size / 1024 / 1024, 1)} MB to #{Float.round(f_target_size / 1024 / 1024, 1)} MB via delta of #{Float.round(f_delta_size / 1024 / 1024, 1)} MB"
265+
)
266+
267+
nil
268+
end
269+
end
270+
271+
{:error, :enoent} ->
272+
nil
273+
end
274+
else
275+
nil
227276
end
228277
end
229278

@@ -239,25 +288,6 @@ defmodule NervesHub.Firmwares.UpdateTool.Fwup do
239288
end
240289
end
241290

242-
defp generate_file_list(workdir) do
243-
# firmware archive files order matters:
244-
# 1. meta.conf.ed25519 (optional)
245-
# 2. meta.conf
246-
# 3. other...
247-
[
248-
"meta.conf.*",
249-
"meta.conf",
250-
"data"
251-
]
252-
|> Enum.map(fn glob -> workdir |> Path.join(glob) |> Path.wildcard() end)
253-
|> List.flatten()
254-
|> Enum.map(fn file ->
255-
file
256-
|> String.replace_prefix("#{workdir}/", "")
257-
|> to_charlist()
258-
end)
259-
end
260-
261291
defp get_tool_metadata(meta_conf_path) do
262292
with {:ok, feature_usage} <- Confuse.Fwup.get_feature_usage(meta_conf_path) do
263293
tool_metadata =
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
define(ROOTFS_A_PART_OFFSET, 1024)
2+
define(ROOTFS_A_PART_COUNT, 1024)
3+
define(ROOTFS_B_PART_OFFSET, 2048)
4+
define(ROOTFS_B_PART_COUNT, 1024)
5+
6+
file-resource first {
7+
host-path = "${TEST_1}"
8+
}
9+
file-resource second {
10+
host-path = "${TEST_2}"
11+
}
12+
13+
task complete {
14+
on-init {
15+
raw_memset(${ROOTFS_B_PART_OFFSET}, ${ROOTFS_B_PART_COUNT}, 0)
16+
}
17+
on-resource first {
18+
raw_write(${ROOTFS_A_PART_OFFSET})
19+
}
20+
on-resource second {
21+
raw_write(${ROOTFS_A_PART_OFFSET})
22+
}
23+
}
24+
task upgrade {
25+
on-resource first {
26+
delta-source-raw-offset=${ROOTFS_A_PART_OFFSET}
27+
delta-source-raw-count=${ROOTFS_A_PART_COUNT}
28+
raw_write(${ROOTFS_B_PART_OFFSET})
29+
}
30+
on-resource second {
31+
delta-source-raw-offset=${ROOTFS_A_PART_OFFSET}
32+
delta-source-raw-count=${ROOTFS_A_PART_COUNT}
33+
raw_write(${ROOTFS_B_PART_OFFSET})
34+
}
35+
}

0 commit comments

Comments
 (0)