Skip to content

Commit

Permalink
ci: fix ubuntu-latest based emulator runs
Browse files Browse the repository at this point in the history
  • Loading branch information
larpon committed Sep 26, 2024
1 parent 0452e04 commit a28d3a5
Show file tree
Hide file tree
Showing 10 changed files with 517 additions and 87 deletions.
79 changes: 5 additions & 74 deletions .github/workflows/ci_emulator_run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,89 +76,20 @@ jobs:
uses: actions/cache@v4
with:
path: |
/Users/runner/.android/avd
/home/runner/.android/avd
/usr/local/lib/android/sdk/system-images/android-30
key: ${{ runner.os }}-android-emulator-${{ hashFiles('/Users/runner/.android/avd') }}
key: ${{ runner.os }}-android-emulator-${{ hashFiles('/home/runner/.android/avd') }}

- name: Prepare emulator
if: steps.cache-emulator.outputs.cache-hit != 'true'
run: |
export ANDROID_SDK_ROOT="/usr/local/lib/android/sdk"
echo yes | $ANDROID_SDK_ROOT/tools/bin/sdkmanager 'system-images;android-30;aosp_atd;x86_64'
echo no | $ANDROID_SDK_ROOT/tools/bin/avdmanager create avd --force --name test --abi aosp_atd/x86_64 --package 'system-images;android-30;aosp_atd;x86_64'
- name: Install and run V + V UI examples as APK and AAB
- name: Run `vab test-all`
run: |
$ANDROID_SDK_ROOT/emulator/emulator -avd test -wipe-data -no-metrics -no-snapshot -no-window -no-boot-anim -camera-back emulated -camera-front emulated &
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;'
# Test deployment of single file *after* build
echo "Testing vab deployment *after* build"
vab --package-id "io.v.ci.vab.apk.deploytest" --name "V DEPLOY TEST APK" v/examples/gg/bezier.v && vab v_deploy_test_apk.apk
sleep 1 # Experience tells us that the emulator likes to catch it's breath...
vab --package-id "io.v.ci.vab.aab.deploytest" --name "V DEPLOY TEST AAB" --package aab v/examples/gg/bezier.v && vab v_deploy_test_aab.aab
# 'flappylearning' can build but running is currently broken on Android
# Skip fireworks for now
declare -a v_examples=('2048' 'tetris' 'sokol/particles' 'sokol/drawing.v' 'sokol/freetype_raven.v' 'gg/bezier.v' 'gg/bezier_anim.v' 'gg/polygons.v' 'gg/raven_text_rendering.v' 'gg/rectangles.v' 'gg/stars.v' 'gg/worker_thread.v')
echo "Compiling V examples ${v_examples[@]}"
for example in "${v_examples[@]}"; do
#path_safe_name=$( echo "$example" | sed 's%/%-%' | sed 's%\.%-%' )
package_id=$( echo "$example" | sed 's%/%%' | sed 's%\.%%' )
package_id=$( echo "v$package_id" )
# APK
echo "Compiling apk from examples/$example ($package_id)"
vab --package-id "io.v.apk.$package_id" run v/examples/$example
sleep 1 # Experience tells us that the emulator likes to catch it's breath...
# AAB
echo "Compiling aab from examples/$example ($package_id)"
vab --package aab --package-id "io.v.aab.$package_id" run v/examples/$example
done
# Output test
echo "Testing if v/examples/tetris can run..."
vab -g --package-id "io.v.ci.vab.apk.examples.tetris" run v/examples/tetris
sleep 5 # give the emulator a little time to start the application...
adb -e logcat -d > /tmp/logcat.dump.txt
echo "Looking for traces of BDWGC"
cat /tmp/logcat.dump.txt | grep -q 'BDWGC : Grow'; if [ ! $? -eq 0 ]; then cat /tmp/logcat.dump.txt; fi
# V UI
echo "Installing V UI"
git clone --depth 1 https://github.com/vlang/ui
cd ui ; mkdir -p ~/.vmodules ; ln -s $(pwd) ~/.vmodules/ui ; cd ..
declare -a v_ui_examples=('rectangles.v')
echo "Compiling examples ${v_ui_examples[@]}"
for example in "${v_ui_examples[@]}"; do
package_id=$( echo "$example" | sed 's%/%%' | sed 's%\.%%' )
package_id=$( echo "v$package_id" )
# APK
echo "Compiling apk from ui/examples/$example ($package_id)"
vab --package-id "io.v.apk.ui.$package_id" run ui/examples/$example
sleep 1 # Experience tells us that the emulator likes to catch it's breath...
# AAB
echo "Compiling aab from ui/examples/$example ($package_id)"
vab --package aab --package-id "io.v.aab.ui.$package_id" run ui/examples/$example
done
# Output test
echo "Testing if ui/examples/calculator can run..."
vab -g --package-id "io.v.ui.ci.examples.calculator" run ui/examples/calculator.v
sleep 5 # give the emulator a little time to start the application...
adb -e logcat -d > /tmp/logcat.dump.txt
echo "Looking for traces of BDWGC"
cat /tmp/logcat.dump.txt | grep -q 'BDWGC : Grow'; if [ ! $? -eq 0 ]; then cat /tmp/logcat.dump.txt; fi
echo "Killing emulator"
adb -s emulator-5554 emu kill
vab test-all
macos-legacy-run-v-examples:
runs-on: macos-12
Expand Down
274 changes: 274 additions & 0 deletions android/emulator/emulator.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
// Copyright(C) 2019-2024 Lars Pontoppidan. All rights reserved.
// Use of this source code is governed by an MIT license file distributed with this software package
module emulator

import os
import vab.android.util
import vab.android.env
import time

enum ThreadStatus {
stopped
running
}

@[noinit]
pub struct Emulator {
pub:
name string @[required]
port u16
mut:
thread_ctrl thread
thread_status ThreadStatus = .stopped
thread_ctrl_recv chan int
exe string
options Options
}

@[params]
pub struct Config {
pub:
port u16 = 5554
}

@[params]
pub struct CameraOptions {
pub:
front string = 'emulated'
back string = 'emulated'
}

@[params]
pub struct SnapshotOptions {
pub:
name string
// TODO:
// -snapstorage <file> file that contains all state snapshots (default <datadir>/snapshots.img)
// -no-snapstorage do not mount a snapshot storage file (this disables all snapshot functionality)
// -snapshot <name> name of snapshot within storage file for auto-start and auto-save (default 'default-boot')
// -no-snapshot perform a full boot and do not do not auto-save, but qemu vmload and vmsave operate on snapstorage
// -no-snapshot-save do not auto-save to snapshot on exit: abandon changed state
// -no-snapshot-load do not auto-start from snapshot: perform a full boot
// -snapshot-list show a list of available snapshots
// -no-snapshot-update-time do not do try to correct snapshot time on restore
}

@[params]
pub struct Options {
pub:
verbosity int
wipe_data bool = true
avd string
await_boot bool = true // will wait for the device to boot
visible bool // show emulator window on desktop
metrics bool // send metrics to Google... default NO
snapshot SnapshotOptions
boot_anim bool
camera CameraOptions
}

// verbose prints `msg` to STDOUT if `Options.verbosity` level is >= `verbosity_level`.
pub fn (o &Options) verbose(verbosity_level int, msg string) {
if o.verbosity >= verbosity_level {
println(msg)
}
}

// validate validates the fields of `Options`.
pub fn (o &Options) validate() ! {
if o.avd == '' {
return error('${@MOD}.${@STRUCT}.${@FN}: No Android Virtual Device (avd) sat')
}
avdmanager := env.avdmanager()
avdmanager_list_cmd := [avdmanager, 'list', 'avd', '-c']
util.verbosity_print_cmd(avdmanager_list_cmd, o.verbosity)
avdmanager_list := util.run_or_error(avdmanager_list_cmd)!
avds := avdmanager_list.split('\n')
if o.avd !in avds {
return error('${@MOD}.${@STRUCT}.${@FN}: Android Virtual Device (avd) "${o.avd}" not found.')
}
}

// new returns a new `Emulator` instance.
pub fn new(config Config) !Emulator {
if !env.has_emulator() {
return error('${@MOD}.${@STRUCT}.${@FN}: the `emulator` needs to be installed in the Android SDK. Use `vab install emulator` to install it.')
}
if !env.has_avdmanager() {
// TODO: part of cmdline-tools, should be installed?!
return error('${@MOD}.${@STRUCT}.${@FN}: `avdmanager` could not be found in the Android SDK.')
}
emulator_exe := env.emulator()
// TODO: call adb to list devices and find a free port if other
// emulators are running. Give the user a notice if port has to
// be changed via util.vab_notice()
mut port := config.port
return Emulator{
name: 'emulator-${port}'
port: port
exe: emulator_exe
}
}

// start starts a new emulator process from a OS monitor thread.
// If `options.await_boot` is `true`, start will block until the
// emulator has fully booted, otherwise it will return immediately.
pub fn (mut e Emulator) start(options Options) ! {
options.validate()!
e.options = options
// start emulator in another process and monitor it in a thread.
e.thread_status = .running
e.thread_ctrl = spawn e.run_process(options)
e.options.verbose(2, 'Emulator thread started')

if e.options.await_boot {
e.wait_for_boot()!
}
}

// wait_for_boot blocks execution and waits for the emulator to boot.
// NOTE: this feature is unique to emulator devices.
pub fn (mut e Emulator) wait_for_boot() ! {
// wait for emulator or fail if emulator thread fails
adb_look_for_boot_complete_cmd := [
env.adb(),
'-s',
e.name,
'shell',
'getprop',
'dev.bootcomplete',
]
// mut signal := -1
e.options.verbose(2, 'Waiting for emulator to be fully booted...')
for {
$if debug {
eprintln('> ${@STRUCT}.${@FN}: adb boot check loop.\nRunning: ${adb_look_for_boot_complete_cmd.join(' ')}')
eprintln('> ${@STRUCT}.${@FN}: running "${adb_look_for_boot_complete_cmd.join(' ')}"...')
}
res := os.execute(adb_look_for_boot_complete_cmd.join(' '))
$if debug {
eprintln('> ${@STRUCT}.${@FN}: adb boot check exit: ${res.exit_code}')
}
if res.exit_code == 0 {
break
}
time.sleep(1000 * time.millisecond)
if e.thread_status != .running {
$if debug {
eprintln('> ${@STRUCT}.${@FN}: waiting for thread to close')
}
e.thread_ctrl.wait()
return error('${@MOD}.${@STRUCT}.${@FN}: emulator thread not running')
}
}
}

// stop stops the emulator thread and emulator child process if they are running.
pub fn (mut e Emulator) stop() {
e.options.verbose(1, 'Stopping emulator...')
if !e.thread_ctrl_recv.closed {
$if debug {
eprintln('> ${@STRUCT}.${@FN}: asking emulator to gracefully shutdown...')
}
e.thread_ctrl_recv <- 1 // ask for graceful exit
}
$if debug {
eprintln('> ${@STRUCT}.${@FN}: waiting for emulator thread exit...')
}
if e.thread_status != .running {
e.thread_ctrl.wait()
}
e.options.verbose(1, 'Emulator is now stopped')
}

// run_process runs the `emulator` and starts monitoring it.
// run_process can be started with `spawn` so it is non-blocking.
// If started with `spawn` action codes can be sent via the `thread_ctrl_recv` channel.
// NOTE: `e.thread_status` should be *read* everywhere else except in this function.
fn (mut e Emulator) run_process(options Options) {
mut emulator_args := [
'-avd',
e.options.avd,
'-port',
'${e.port}', // defines the adb device name. E.g.: "emulator-5554"
]
if e.options.wipe_data {
emulator_args << '-wipe-data'
}
if !e.options.metrics {
emulator_args << '-no-metrics'
}
if e.options.snapshot.name == '' {
emulator_args << '-no-snapshot'
}
if !e.options.visible {
emulator_args << '-no-window'
}
if !e.options.boot_anim {
emulator_args << '-no-boot-anim'
}
emulator_args << '-camera-front'
emulator_args << e.options.camera.front
emulator_args << '-camera-back'
emulator_args << e.options.camera.back

e.options.verbose(1, 'Starting emulator...')
if e.options.verbosity > 0 {
mut emulator_args_verbose := [e.exe]
emulator_args_verbose << emulator_args
util.verbosity_print_cmd(emulator_args_verbose, e.options.verbosity)
}
mut p := os.new_process(e.exe)
p.set_args(emulator_args)
p.set_redirect_stdio()
p.run()
mut action := 0
for {
if !p.is_alive() {
action = 2
e.options.verbose(3, 'Emulator process not alive anymore')
e.thread_status = .stopped
$if debug {
eprintln('> ${@STRUCT}.${@FN}: waiting for process; action: "${action}" status: "${p.status}"')
}
p.wait()
break
}
if e.thread_ctrl_recv.try_pop(mut action) == .success {
if action != 0 {
if action == 1 {
$if debug {
eprintln('> ${@STRUCT}.${@FN}: sending SIGTERM to process; action: "${action}" status: "${p.status}"')
}
p.signal_term()
$if debug {
eprintln('> ${@STRUCT}.${@FN}: waiting for process; action: "${action}" status: "${p.status}"')
}
p.wait()
}
e.options.verbose(2, 'Emulator process terminating on request')
$if debug {
eprintln('> ${@STRUCT}.${@FN}: breaking; action: "${action}" status: "${p.status}"')
}
break
}
}
$if debug {
eprintln('> ${@STRUCT}.${@FN}: thread reading; action: "${action}" status: "${p.status}"')
}
time.sleep(1000 * time.millisecond)
}
if action != 1 {
$if debug {
eprintln('> ${@STRUCT}.${@FN}: thread BAD status; action: "${action}" status: "${p.status}"')
}
} else {
$if debug {
eprintln('> ${@STRUCT}.${@FN}: thread OK status; action: "${action}" status: "${p.status}"')
}
}
e.thread_status = .stopped
e.thread_ctrl_recv.close()
p.close()
e.options.verbose(2, 'Exiting emulator thread')
}
Loading

0 comments on commit a28d3a5

Please sign in to comment.