| #!/bin/bash | |
| # Copyright 2020 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| #### CATEGORY=Other | |
| ### Remotely run emulator | |
| ## usage: fx emu-remote [--no-build] [--stream [--no-emu] [--no-turn] [--no-open] [--display DPY] [--port PORT]] HOST [DIR] [-- ARGS] | |
| ## | |
| ## Connect to HOST, run a build using fx from DIR, fetch the artifacts and | |
| ## start the emulator. Alternatively, start the emulator on HOST, | |
| ## and open an WebRTC connection to it using local browser. | |
| ## | |
| ## --no-build do not build, just pull artifacts already present | |
| ## --stream stream output from remote emulator using WebRTC instead of fetching artifacts | |
| ## --no-emu only tunnel, do not start remote emulator | |
| ## --no-turn do not use turn configuration for remote emulator | |
| ## --no-open do not open https://web-femu.appspot.com, just run remote emulator | |
| ## --display DPY do not start remote virtual display, use DPY instead | |
| ## --port PORT port used on local machine to connect with remote emulator over HTTP (default: 8080) | |
| ## | |
| ## HOST the hostname to connect to | |
| ## DIR defaults to ~/fuchsia, the path to the FUCHSIA_DIR on HOST | |
| ## ARGS arguments to pass to emulator | |
| ## | |
| ## HOST and DIR are persisted in the file //.fx-remote-config and are reused as | |
| ## defaults in future invocations of any 'fx *-remote' tools. | |
| source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/lib/vars.sh || exit $? | |
| source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/lib/fx-remote.sh || exit $? | |
| fx-config-read | |
| build=true | |
| stream=false | |
| emu=true | |
| turn=true | |
| open=true | |
| display="xvfb-run" | |
| host="" | |
| dir="" | |
| local_port="8080" | |
| remote_port="8080" | |
| while [[ $# -ne 0 ]]; do | |
| case "$1" in | |
| --help|-h) | |
| fx-command-help | |
| exit 0 | |
| ;; | |
| --no-build) | |
| build=false | |
| ;; | |
| --stream) | |
| stream=true | |
| ;; | |
| --no-emu) | |
| emu=false | |
| ;; | |
| --no-turn) | |
| turn=false | |
| ;; | |
| --no-open) | |
| open=false | |
| ;; | |
| --display) | |
| shift | |
| display="DISPLAY=$1" | |
| ;; | |
| --port) | |
| shift | |
| local_port=$1 | |
| ;; | |
| --) | |
| shift | |
| break | |
| ;; | |
| *) | |
| if [[ -z "${host}" ]]; then | |
| host="$1" | |
| elif [[ -z "${dir}" ]]; then | |
| dir="$1" | |
| else | |
| fx-error "unexpected argument: '$1'" | |
| exit 1 | |
| fi | |
| ;; | |
| esac | |
| shift | |
| done | |
| if cached=( $(load_remote_info "$host") ); then | |
| host="${cached[0]}" | |
| dir="${cached[1]}" | |
| fi | |
| if [[ -z "${host}" ]]; then | |
| fx-error "HOST must be specified" | |
| fx-command-help | |
| exit 1 | |
| fi | |
| # Use a dedicated ControlPath so script can manage a connection separately from the user's. We | |
| # intentionally do not use %h/%p in the control path because there can only be one forwarding | |
| # session at a time (due to the local forward of 8083). | |
| ssh_base_args=( | |
| "${host}" | |
| -S "~/.ssh/control-fuchsia-fx-remote" | |
| -o ControlMaster=auto | |
| ) | |
| ssh_exit() { | |
| ssh "${ssh_base_args[@]}" -O exit > /dev/null 2>&1 | |
| wait # for ssh to exit | |
| } | |
| # If there is already control master then exit it. We can't be sure its to the right host and it | |
| # also could be stale. | |
| ssh_exit | |
| # When we exit the script, close the background ssh connection. | |
| trap_exit() { | |
| ssh_exit | |
| exit | |
| } | |
| trap trap_exit EXIT | |
| if [[ -z "${dir}" ]]; then | |
| echo ssh "${ssh_base_args[@]}" ls \~/fuchsia/.jiri_root/bin/fx | |
| if ssh "${ssh_base_args[@]}" ls \~/fuchsia/.jiri_root/bin/fx > /dev/null; then | |
| dir="~/fuchsia" | |
| else | |
| fx-error "failed to find ~/fuchsia on $host, please specify DIR" | |
| fx-command-help | |
| exit 1 | |
| fi | |
| fi | |
| emu_targets=(multiboot.bin fuchsia.zbi obj/build/images/fuchsia/fuchsia/fvm.blk) | |
| if "${build}"; then | |
| ssh "${ssh_base_args[@]}" "cd ${dir} && ./.jiri_root/bin/fx build ${emu_targets[@]}" || exit $? | |
| fi | |
| build_dir=$(ssh "${ssh_base_args[@]}" "cd ${dir} && ./.jiri_root/bin/fx get-build-dir") | |
| save_remote_info "$host" "$dir" | |
| # Fetch artifacts and run local emulator when streaming is disabled. | |
| if [[ "${stream}" == "false" ]]; then | |
| build_arch=$(ssh "${ssh_base_args[@]}" "cd ${dir} && ./.jiri_root/bin/fx exec sh -c 'echo \${FUCHSIA_ARCH}'") | |
| mkdir -p out/fetched | |
| printf -v files '%s,' "${emu_targets[@]}" | |
| rsync -z -P "${host}":"${build_dir}/{${files%,}}" out/fetched/ | |
| echo >&2 "Build images fetched into out/fetched" | |
| fx-command-exec emu \ | |
| -K out/fetched/multiboot.bin \ | |
| -z out/fetched/fuchsia.zbi \ | |
| -f out/fetched/fvm.blk \ | |
| -A "${build_arch}" "$@" | |
| fi | |
| # | |
| # The code below handles the case where output from a remote emulator | |
| # is streamed using WebRTC to a local browser instance. | |
| # | |
| # Check if the port given in argument "$1" is listened by other processes. | |
| # | |
| # Returns true if other process is already listening to this port; otherwise | |
| # returns false. | |
| is_port_used() { | |
| # Print all listening ports and check if $1 is listed. | |
| if [[ "$(uname -s)" == "Darwin" ]]; then | |
| # On macOS we parse output from `netstat`. | |
| netstat -anp tcp 2>/dev/null | | |
| awk '$1 ~ /tcp4/ && $6 == "LISTEN" { \ | |
| sub(/.*\./, "", $4); print $4 \ | |
| }' | |
| else | |
| # On Linux we parse output from `ss`. | |
| ss -lpn4 2>/dev/null | awk '$1 == "tcp" { \ | |
| if (match($5, /^[0-9.]+:[0-9]+$/)) { \ | |
| sub(/^.*:/, "", $5); print $5 \ | |
| } \ | |
| }' | |
| fi | grep -q "^$1$" | |
| } | |
| if is_port_used ${local_port}; then | |
| fx-error "Local port ${local_port} is not available. Please try specifying"\ | |
| "a different port using --port flag." | |
| exit 1 | |
| fi | |
| echo "Using remote ${host}:${dir}" | |
| wait_and_open() { | |
| timeout=10 | |
| waited=0 | |
| # Loop until connection is live or timeout expires. | |
| while true | |
| do | |
| # Check if we can connect to the forwarded port. | |
| if curl -sI "http://localhost:${local_port}" > /dev/null; then | |
| open_args=("https://web-femu.appspot.com/?port=${local_port}") | |
| # Launch Chrome browser tab for emulator feed. | |
| if [[ "$(uname -s)" == "Darwin" ]]; then | |
| exec open -a "/Applications/Google Chrome.app" "${open_args[@]}" | |
| else | |
| exec google-chrome "${open_args[@]}" | |
| fi | |
| fi | |
| # Continue waiting if we have not reached our timeout. | |
| if [ $waited -gt 0 ]; then | |
| if [ $waited -ge $timeout ]; then | |
| echo "No emulator after waiting $waited seconds, giving up." | |
| break | |
| fi | |
| echo "Waiting for emulator ($(($timeout-$waited)) seconds left).." | |
| fi | |
| sleep 1 | |
| waited=$(($waited+1)) | |
| done | |
| } | |
| ssh_args=( | |
| -6 # We want ipv6 binds for the port forwards. | |
| # Requests to the WebRTC service address locally go to the workstation. | |
| -L "${local_port}:localhost:${remote_port}" | |
| -o ExitOnForwardFailure=yes | |
| ) | |
| if "${emu}"; then | |
| # If the user requested emulator, then we'll check to see if there's a | |
| # emulator already running and kill it, this prevents most cases where | |
| # signal propagation not make it to "qemu-system". | |
| if ssh "${ssh_base_args[@]}" 'pgrep -u $USER goldfish-webrtc' > /dev/null; then | |
| ssh "${ssh_base_args[@]}" 'pkill -u $USER qemu-system; pkill -u $USER goldfish-webrtc' | |
| fi | |
| emu_args=(-x ${remote_port}) | |
| # Add turn configuration if enabled. | |
| if "${turn}"; then | |
| apikey="AIzaSyBl6TgfdN6FKAQ3nK2GvKNcKNjWLeRGVYQ" | |
| emu_args+=(-t "\"curl -s -X POST https://networktraversal.googleapis.com/v1alpha/iceconfig?key=${apikey}\"") | |
| fi | |
| # Starts emulator. | |
| ssh_args+=("cd ${dir} && ${display} ./.jiri_root/bin/fx emu ${emu_args[@]} $@") | |
| # Wait and open page for emulator when ready. | |
| if "${open}"; then | |
| wait_and_open & | |
| fi | |
| else | |
| # Starts nothing, just goes to sleep. | |
| ssh_args+=("-nNT") | |
| fi | |
| # Cleanup after ssh command ends: | |
| # - Terminate all child processes; | |
| # - Close port forwarding if we use ssh multiplexing. | |
| cleanup_processes_and_ports() { | |
| child_pids=$(jobs -p) | |
| if [[ -n "${child_pids}" ]]; then | |
| # Note: child_pids must be expanded to args here. | |
| kill ${child_pids} 2> /dev/null | |
| wait 2> /dev/null | |
| fi | |
| # Cancel port forwarding. | |
| if ssh -O check "${host}" > /dev/null 2>&1; then | |
| ssh -O cancel -L "${local_port}:localhost:${remote_port}" "${host}" | |
| fi | |
| } | |
| trap cleanup_processes_and_ports EXIT | |
| ssh "${ssh_base_args[@]}" "${ssh_args[@]}" | |