Skip to content

Commit

Permalink
image pull progress bar (#33)
Browse files Browse the repository at this point in the history
Add a progress bar that shows uenv image pull progress

Very useful for large images, which take a long enough to download.
  • Loading branch information
bcumming authored Jun 5, 2024
1 parent e196151 commit 05344d8
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 13 deletions.
1 change: 1 addition & 0 deletions install
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ run install -v -m644 -D -t "${destdir}/${lib_path}" ${script_path}/lib/datastore
run install -v -m644 -D -t "${destdir}/${lib_path}" ${script_path}/lib/jfrog.py
run install -v -m644 -D -t "${destdir}/${lib_path}" ${script_path}/lib/names.py
run install -v -m644 -D -t "${destdir}/${lib_path}" ${script_path}/lib/oras.py
run install -v -m644 -D -t "${destdir}/${lib_path}" ${script_path}/lib/progress.py
run install -v -m644 -D -t "${destdir}/${lib_path}" ${script_path}/lib/record.py
run install -v -m644 -D -t "${destdir}/${lib_path}" ${script_path}/lib/terminal.py

Expand Down
34 changes: 23 additions & 11 deletions lib/oras.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,33 @@ def find_oras() -> str:
return oras_file


def run_command(args):
def run_command(args, detach=False):
try:
command = [find_oras()] + args

terminal.info(f"calling oras: {' '.join(command)}")
result = subprocess.run(
command,
stdout=subprocess.PIPE, # Capture standard output
stderr=subprocess.PIPE, # Capture standard error
check=True, # Raise exception if command fails
encoding='utf-8' # Decode output from bytes to string
)

# Print standard output
terminal.info("Output:\n{result.stdout}")
if detach:
process = subprocess.Popen(
command,
stdout=subprocess.PIPE, # Capture standard output
stderr=subprocess.PIPE, # Capture standard error
encoding='utf-8' # Decode output from bytes to string
)
return process

else:
result = subprocess.run(
command,
stdout=subprocess.PIPE, # Capture standard output
stderr=subprocess.PIPE, # Capture standard error
check=True, # Raise exception if command fails
encoding='utf-8' # Decode output from bytes to string
)

# Print standard output
terminal.info("Output:\n{result.stdout}")

return None

except subprocess.CalledProcessError as e:
# Print error message along with captured standard error
Expand Down
35 changes: 35 additions & 0 deletions lib/progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import math
import sys
from terminal import colorize

def progress_bar(progress: float, width=25, msg="", stream=sys.stdout):
progress = min(1., max(0., progress))

fancy_bar = True
filled_slots = math.floor(width * progress)
lsep = colorize("[", "yellow")
rsep = colorize("]", "yellow")
if filled_slots==width:
if fancy_bar:
bar = colorize("≡"*width, "green")
else:
bar = colorize("="*width, "green")
else:
part_slot = (width * progress) - filled_slots
empty_slots = width - (filled_slots+1)
if fancy_bar:
part_width = math.floor(part_slot * 3)
part_char = [" ", "-", "="][part_width]
bar = colorize("≡"*filled_slots + part_char, "green") + " "*empty_slots
else:
part_width = math.floor(part_slot * 2)
part_char = [" ", "-"][part_width]
bar = colorize("="*filled_slots + part_char, "green") + " "*empty_slots

bar = lsep + bar + rsep

pc_str = f"{int(100*progress):3d}%"

# Use '\r' to return to the start of the line
stream.write('\r ' + bar + " " + pc_str + " " + msg)
stream.flush()
35 changes: 33 additions & 2 deletions uenv-image
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import pathlib
import re
import sys
import textwrap
import time

prefix = pathlib.Path(__file__).parent.resolve()
libpath = prefix / 'lib'
Expand All @@ -22,6 +23,7 @@ import datastore
import jfrog
import names
import oras
import progress
import record
import terminal
from terminal import colorize
Expand Down Expand Up @@ -444,7 +446,36 @@ if __name__ == "__main__":
if cache.database.get_record(t.sha256).is_empty:
terminal.stdout(f"downloading image {t.sha256} {image_size_string(t.size)}")
# download the image using oras
oras.run_command(["pull", "-o", image_path, source_address])
try:
# run the oras command in a separate process so that this process can
# draw a progress bar.
proc = oras.run_command(["pull", "-o", image_path, source_address], detach=True)
total_mb = t.size/(1024*1024)
while proc.poll() is None:
time.sleep(0.25)
sqfs_path = image_path + "/store.squashfs"
if os.path.exists(sqfs_path):
current_size = os.path.getsize(sqfs_path)
current_mb = current_size / (1024*1024)
p = current_mb/total_mb
msg = f"{int(current_mb)}/{int(total_mb)} MB"
progress.progress_bar(p, width=50, msg=msg)
# draw a final complete progress bar
progress.progress_bar(1.0, width=50, msg=f"{int(total_mb)}/{int(total_mb)} MB")
except KeyboardInterrupt:
proc.terminate()
terminal.stdout("")
terminal.error(f"image pull cancelled by user.")
except Exception as e:
proc.terminate()
terminal.stdout("")
terminal.error(f"image pull failed: {str(e)}")
sys.stdout.write("\n")
sys.stdout.flush()
if proc.returncode == 0:
terminal.info(f"oras download completed successfully\n{proc.stdout}")
else:
terminal.error(f"oras download failed {proc.stderr}")
else:
terminal.stdout(f"image {t.sha256} is already available locally")
# update all the tags associated with the image.
Expand All @@ -453,7 +484,7 @@ if __name__ == "__main__":
terminal.stdout(f"updating local reference {r.name}/{r.version}:{r.tag}")
cache.add_record(r)

terminal.stdout(f"image available at {image_path}/store.squashfs")
terminal.stdout(f"uenv {t.name}/{t.version}:{t.tag} downloaded")

sys.exit(0)

Expand Down

0 comments on commit 05344d8

Please sign in to comment.