diff options
-rw-r--r-- | AUTHORS | 1 | ||||
-rw-r--r-- | cmake/Platform/Emscripten.cmake | 8 | ||||
-rwxr-xr-x | emcc | 3 | ||||
-rw-r--r-- | src/deps_info.json | 1 | ||||
-rw-r--r-- | src/library.js | 1 | ||||
-rw-r--r-- | src/library_browser.js | 33 | ||||
-rw-r--r-- | src/library_glfw.js | 7 | ||||
-rw-r--r-- | src/library_sdl.js | 2 | ||||
-rw-r--r-- | src/preamble.js | 2 | ||||
-rw-r--r-- | system/include/emscripten/emscripten.h | 24 | ||||
-rw-r--r-- | tests/cmake/target_library/CMakeLists.txt | 43 | ||||
-rw-r--r-- | tests/cmake/target_library/srcfile.cmake | 6 | ||||
-rw-r--r-- | tests/core/test_simd4.in | 39 | ||||
-rw-r--r-- | tests/core/test_simd4.out | 1 | ||||
-rw-r--r-- | tests/glfw.c | 4 | ||||
-rw-r--r-- | tests/sdl_image.c | 10 | ||||
-rw-r--r-- | tests/test_core.py | 9 | ||||
-rw-r--r-- | tests/test_other.py | 16 | ||||
-rwxr-xr-x | tools/ffdb.py | 276 | ||||
-rw-r--r-- | tools/js-optimizer.js | 42 | ||||
-rw-r--r-- | tools/shared.py | 7 | ||||
-rw-r--r-- | tools/system_libs.py | 8 | ||||
-rw-r--r-- | tools/test-js-optimizer-asm-last-output.js | 26 | ||||
-rw-r--r-- | tools/test-js-optimizer-asm-last.js | 32 |
24 files changed, 548 insertions, 53 deletions
@@ -143,4 +143,5 @@ a license to everyone to use it as detailed in LICENSE.) * Ryan Sturgell <ryan.sturgell@gmail.com> (copyright owned by Google, Inc.) * Jason Green <jason@transgaming.com> (copyright owned by TransGaming, Inc.) * Ningxin Hu <ningxin.hu@intel.com> (copyright owned by Intel) +* Nicolas Guillemot <nlguillemot@gmail.com> diff --git a/cmake/Platform/Emscripten.cmake b/cmake/Platform/Emscripten.cmake index f9d8c773..362a552d 100644 --- a/cmake/Platform/Emscripten.cmake +++ b/cmake/Platform/Emscripten.cmake @@ -98,6 +98,8 @@ set(CMAKE_SYSTEM_INCLUDE_PATH "${EMSCRIPTEN_ROOT_PATH}/system/include") #SET(CMAKE_FIND_LIBRARY_PREFIXES "") #SET(CMAKE_FIND_LIBRARY_SUFFIXES ".bc") +SET(CMAKE_C_USE_RESPONSE_FILE_FOR_LIBRARIES 1) +SET(CMAKE_CXX_USE_RESPONSE_FILE_FOR_LIBRARIES 1) SET(CMAKE_C_USE_RESPONSE_FILE_FOR_OBJECTS 1) SET(CMAKE_CXX_USE_RESPONSE_FILE_FOR_OBJECTS 1) SET(CMAKE_C_USE_RESPONSE_FILE_FOR_INCLUDES 1) @@ -107,10 +109,8 @@ set(CMAKE_C_RESPONSE_FILE_LINK_FLAG "@") set(CMAKE_CXX_RESPONSE_FILE_LINK_FLAG "@") # Specify the program to use when building static libraries. Force Emscripten-related command line options to clang. -set(CMAKE_CXX_ARCHIVE_CREATE "${CMAKE_AR} rc <TARGET> ${CMAKE_START_TEMP_FILE} <LINK_FLAGS> <OBJECTS>${CMAKE_END_TEMP_FILE}") -set(CMAKE_C_ARCHIVE_CREATE "${CMAKE_AR} rc <TARGET> ${CMAKE_START_TEMP_FILE} <LINK_FLAGS> <OBJECTS>${CMAKE_END_TEMP_FILE}") -set(CMAKE_CXX_ARCHIVE_APPEND "${CMAKE_AR} r <TARGET> ${CMAKE_START_TEMP_FILE} <LINK_FLAGS> <OBJECTS>${CMAKE_END_TEMP_FILE}") -set(CMAKE_C_ARCHIVE_APPEND "${CMAKE_AR} r <TARGET> ${CMAKE_START_TEMP_FILE} <LINK_FLAGS> <OBJECTS>${CMAKE_END_TEMP_FILE}") +set(CMAKE_C_CREATE_STATIC_LIBRARY "<CMAKE_AR> rc <TARGET> <LINK_FLAGS> <OBJECTS>") +set(CMAKE_CXX_CREATE_STATIC_LIBRARY "<CMAKE_AR> rc <TARGET> <LINK_FLAGS> <OBJECTS>") # Set a global EMSCRIPTEN variable that can be used in client CMakeLists.txt to detect when building using Emscripten. set(EMSCRIPTEN 1 CACHE BOOL "If true, we are targeting Emscripten output.") @@ -1347,6 +1347,7 @@ try: if shared.Settings.MAIN_MODULE: assert not shared.Settings.SIDE_MODULE shared.Settings.INCLUDE_FULL_LIBRARY = 1 + shared.Settings.EXPORT_ALL = 1 elif shared.Settings.SIDE_MODULE: assert not shared.Settings.MAIN_MODULE @@ -1375,6 +1376,8 @@ try: if shared.Settings.ASM_JS and shared.Settings.DLOPEN_SUPPORT: assert shared.Settings.DISABLE_EXCEPTION_CATCHING, 'no exceptions support with dlopen in asm yet' + assert not (bind and shared.Settings.NO_DYNAMIC_EXECUTION), 'NO_DYNAMIC_EXECUTION disallows embind' + if proxy_to_worker: shared.Settings.PROXY_TO_WORKER = 1 diff --git a/src/deps_info.json b/src/deps_info.json index 029a20e1..e0983064 100644 --- a/src/deps_info.json +++ b/src/deps_info.json @@ -3,6 +3,7 @@ "SDL_Init": ["malloc", "free"], "SDL_GL_GetProcAddress": ["emscripten_GetProcAddress"], "eglGetProcAddress": ["emscripten_GetProcAddress"], + "glfwGetProcAddress": ["emscripten_GetProcAddress"], "emscripten_GetProcAddress": ["strstr"] } diff --git a/src/library.js b/src/library.js index 120def05..c17952b3 100644 --- a/src/library.js +++ b/src/library.js @@ -3353,6 +3353,7 @@ LibraryManager.library = { return 0; } else { var size = Math.min(4095, absolute.path.length); // PATH_MAX - 1. + if (resolved_name === 0) resolved_name = _malloc(size+1); for (var i = 0; i < size; i++) { {{{ makeSetValue('resolved_name', 'i', 'absolute.path.charCodeAt(i)', 'i8') }}}; } diff --git a/src/library_browser.js b/src/library_browser.js index 4ef7c577..57ca5a24 100644 --- a/src/library_browser.js +++ b/src/library_browser.js @@ -1175,6 +1175,39 @@ mergeInto(LibraryManager.library, { var info = Browser.workers[id]; if (!info) return -1; return info.awaited; + }, + + emscripten_get_preloaded_image_data: function(path, w, h) { + if (typeof path === "number") { + path = Pointer_stringify(path); + } + + path = PATH.resolve(path); + + var canvas = Module["preloadedImages"][path]; + if (canvas) { + var ctx = canvas.getContext("2d"); + var image = ctx.getImageData(0, 0, canvas.width, canvas.height); + var buf = _malloc(canvas.width * canvas.height * 4); + + HEAPU8.set(image.data, buf); + + {{{ makeSetValue('w', '0', 'canvas.width', 'i32') }}}; + {{{ makeSetValue('h', '0', 'canvas.height', 'i32') }}}; + return buf; + } + + return 0; + }, + + emscripten_get_preloaded_image_data_from_FILE__deps: ['emscripten_get_preloaded_image_data'], + emscripten_get_preloaded_image_data_from_FILE: function(file, w, h) { + var stream = FS.getStreamFromPtr(file); + if (stream) { + return _emscripten_get_preloaded_image_data(stream.path, w, h); + } + + return 0; } }); diff --git a/src/library_glfw.js b/src/library_glfw.js index 6d539326..6dfea101 100644 --- a/src/library_glfw.js +++ b/src/library_glfw.js @@ -417,6 +417,9 @@ var LibraryGLFW = { glfwSetWindowSizeCallback: function(cbfun) { GLFW.resizeFunc = cbfun; + if (GLFW.resizeFunc) { + Runtime.dynCall('vii', GLFW.resizeFunc, [Module['canvas'].width, Module['canvas'].height]); + } }, glfwSetWindowCloseCallback: function(cbfun) { @@ -507,9 +510,9 @@ var LibraryGLFW = { return Module.ctx.getSupportedExtensions().indexOf(Pointer_stringify(extension)) > -1; }, - glfwGetProcAddress__deps: ['glfwGetProcAddress'], + glfwGetProcAddress__deps: ['emscripten_GetProcAddress'], glfwGetProcAddress: function(procname) { - return _getProcAddress(procname); + return _emscripten_GetProcAddress(procname); }, glfwGetGLVersion: function(major, minor, rev) { diff --git a/src/library_sdl.js b/src/library_sdl.js index a01b3c6c..7145a7ba 100644 --- a/src/library_sdl.js +++ b/src/library_sdl.js @@ -2601,7 +2601,7 @@ var LibrarySDL = { if (info && info.audio) { info.audio.pause(); } else { - Module.printErr('Mix_Pause: no sound found for channel: ' + channel); + //Module.printErr('Mix_Pause: no sound found for channel: ' + channel); } }, diff --git a/src/preamble.js b/src/preamble.js index 431a3c27..ae58e7e0 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -401,7 +401,7 @@ var cwrap, ccall; return ret; } - var sourceRegex = /^function\s\(([^)]*)\)\s*{\s*([^*]*?)[\s;]*(?:return\s*(.*?)[;\s]*)?}$/; + var sourceRegex = /^function\s*\(([^)]*)\)\s*{\s*([^*]*?)[\s;]*(?:return\s*(.*?)[;\s]*)?}$/; function parseJSFunc(jsfunc) { // Match the body and the return value of a javascript function source var parsed = jsfunc.toString().match(sourceRegex).slice(1); diff --git a/system/include/emscripten/emscripten.h b/system/include/emscripten/emscripten.h index 8a08aabb..b495c0f5 100644 --- a/system/include/emscripten/emscripten.h +++ b/system/include/emscripten/emscripten.h @@ -14,6 +14,8 @@ extern "C" { #endif +#include <stdio.h> + #if !__EMSCRIPTEN__ #include <SDL/SDL.h> /* for SDL_Delay in async_call */ #endif @@ -53,8 +55,8 @@ typedef double __attribute__((aligned(1))) emscripten_align1_double; * does a function call to reach it). It supports newlines, * * EM_ASM( - * window.alert('hai')); - * window.alert('bai')); + * window.alert('hai'); + * window.alert('bai'); * ) * * Notes: Double-quotes (") are not supported, but you can use @@ -515,6 +517,24 @@ int emscripten_get_compiler_setting(const char *name); */ void emscripten_debugger(); +/* + * Get preloaded image data and the size of the image. + * + * Returns pointer to loaded image or NULL. + * width/height of image are written to w/h if data is valid. + * Pointer should be free()'d + */ +char *emscripten_get_preloaded_image_data(const char *path, int *w, int *h); + +/* + * Get preloaded image data from a c FILE *. + * + * Returns pointer to loaded image or NULL. + * width/height of image are written to w/h if data is valid. + * Pointer should be free()'d + */ +char *emscripten_get_preloaded_image_data_from_FILE(FILE *file, int *w, int *h); + /* ===================================== */ /* Internal APIs. Be careful with these. */ diff --git a/tests/cmake/target_library/CMakeLists.txt b/tests/cmake/target_library/CMakeLists.txt new file mode 100644 index 00000000..c7023192 --- /dev/null +++ b/tests/cmake/target_library/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 2.8) + +project(test_cmake) + +option(BUILD_SHARED_LIBS "Build with shared libraries." OFF) + +if (CMAKE_BUILD_TYPE STREQUAL Debug) + SET(linkFlags "-g4") +else() # Either MinSizeRel, RelWithDebInfo or Release, all which run with optimizations enabled. + SET(linkFlags "-O2") +endif() + +set(MAX_SRC_FILE_INDEX 30) +SET(TEST_SRC_FILE_BASE_NAME "this_is_a_test_src_file_with_a_quite_lengthy_name_to_simulate_very_long_command_line_length_problems_on_windows_") + + +foreach(i RANGE ${MAX_SRC_FILE_INDEX}) + set (TEST_FUNCTION_NAME "FooBar_${i}") + configure_file("srcfile.cmake" "${TEST_SRC_FILE_BASE_NAME}${i}.c") + configure_file("srcfile.cmake" "${TEST_SRC_FILE_BASE_NAME}${i}.cpp") + list(APPEND TEST_SOURCES "${TEST_SRC_FILE_BASE_NAME}${i}.c" "${TEST_SRC_FILE_BASE_NAME}${i}.cpp") +endforeach() + +add_library(test_cmake ${TEST_SOURCES}) + +if (WIN32) + message(FATAL_ERROR "WIN32 should not be defined when cross-compiling!") +endif() + +if (APPLE) + message(FATAL_ERROR "APPLE should not be defined when cross-compiling!") +endif() + +if (NOT EMSCRIPTEN) + message(FATAL_ERROR "EMSCRIPTEN should be defined when cross-compiling!") +endif() + +if (NOT CMAKE_C_SIZEOF_DATA_PTR) + message(FATAL_ERROR "CMAKE_C_SIZEOF_DATA_PTR was not defined!") +endif() + +# GOTCHA: If your project has custom link flags, these must be set *before* calling any of the em_link_xxx functions! +set_target_properties(test_cmake PROPERTIES LINK_FLAGS "${linkFlags}") diff --git a/tests/cmake/target_library/srcfile.cmake b/tests/cmake/target_library/srcfile.cmake new file mode 100644 index 00000000..10e9e6f8 --- /dev/null +++ b/tests/cmake/target_library/srcfile.cmake @@ -0,0 +1,6 @@ +#include <stdio.h> + +void @TEST_FUNCTION_NAME@() +{ + printf("@TEST_FUNCTION_NAME@"); +} diff --git a/tests/core/test_simd4.in b/tests/core/test_simd4.in new file mode 100644 index 00000000..b597d8a3 --- /dev/null +++ b/tests/core/test_simd4.in @@ -0,0 +1,39 @@ +#include <stdio.h> +#include <xmmintrin.h> + +static __inline__ __m128 __attribute__((__always_inline__)) +_mm_load_ps(const float *__p) +{ + return *(__m128*)__p; +} + +float simdAverage(float *src, int len) { + __m128 sumx4 = _mm_setzero_ps(); + for (int i = 0; i < len; i += 4) { + __m128 v = _mm_load_ps(src); + sumx4 = _mm_add_ps(sumx4, v); + src += 4; + } + float sumx4_mem[4]; + float *sumx4_ptr = sumx4_mem; + _mm_store_ps(sumx4_ptr, sumx4); + return (sumx4_mem[0] + sumx4_mem[1] + + sumx4_mem[2] + sumx4_mem[3])/len; +} + +void initArray(float *src, int len) { + for (int i = 0; i < len; ++i) { + src[i] = 0.1 * i; + } +} + +int main() { + const int len = 100000; + float src[len]; + float result = 0.0; + + initArray(src, len); + + result = simdAverage(src, len); + printf("averagex4 result: %.1f\n", result); +}
\ No newline at end of file diff --git a/tests/core/test_simd4.out b/tests/core/test_simd4.out new file mode 100644 index 00000000..99449772 --- /dev/null +++ b/tests/core/test_simd4.out @@ -0,0 +1 @@ +averagex4 result: 4999.9
\ No newline at end of file diff --git a/tests/glfw.c b/tests/glfw.c index 79199d9a..cbdc81fe 100644 --- a/tests/glfw.c +++ b/tests/glfw.c @@ -376,6 +376,10 @@ void PullInfo(){ extension = "GL_EXT_framebuffer_object"; printf("'%s' extension is %s.\n", extension, glfwExtensionSupported(extension) ? "supported" : "not supported"); + extension = "glBindBuffer"; + void* proc_addr = glfwGetProcAddress(extension); + printf("'%s' extension proc address is %p.\n", extension, proc_addr); + printf("Sleeping 1 sec...\n"); glfwSleep(1); printf("...Done.\n"); diff --git a/tests/sdl_image.c b/tests/sdl_image.c index 523f8903..9639ea37 100644 --- a/tests/sdl_image.c +++ b/tests/sdl_image.c @@ -4,6 +4,7 @@ #include <assert.h> #include <emscripten.h> #include <unistd.h> +#include <stdlib.h> int testImage(SDL_Surface* screen, const char* fileName) { SDL_Surface *image = IMG_Load(fileName); @@ -18,7 +19,16 @@ int testImage(SDL_Surface* screen, const char* fileName) { int result = image->w; SDL_BlitSurface (image, NULL, screen, NULL); + + int w, h; + char *data = emscripten_get_preloaded_image_data(fileName, &w, &h); + + assert(data); + assert(w == image->w); + assert(h == image->h); + SDL_FreeSurface (image); + free(data); return result; } diff --git a/tests/test_core.py b/tests/test_core.py index bcb03830..b9057f4e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4892,6 +4892,15 @@ return malloc(size); self.do_run_from_file(src, output) + def test_simd4(self): + # test_simd4 is to test phi node handling of SIMD path + if Settings.ASM_JS: Settings.ASM_JS = 2 # does not validate + + test_path = path_from_root('tests', 'core', 'test_simd4') + src, output = (test_path + s for s in ('.in', '.out')) + + self.do_run_from_file(src, output) + def test_gcc_unmangler(self): if os.environ.get('EMCC_FAST_COMPILER') == '0': Settings.NAMED_GLOBALS = 1 # test coverage for this; fastcomp never names globals diff --git a/tests/test_other.py b/tests/test_other.py index 665832ee..39796b71 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -207,7 +207,8 @@ Options that are modified or new in %s include: (['-O2'], lambda generated: '// The Module object' not in generated, 'with opts, no comments in shell code'), (['-O2', '-g2'], lambda generated: '// The Module object' not in generated, 'with -g2, no comments in shell code'), (['-O2', '-g3'], lambda generated: '// The Module object' in generated, 'with -g3, yes comments in shell code'), - (['-O2', '-profiling'], lambda generated: '// The Module object' in generated, 'with -profiling, yes comments in shell code'), + (['-O2', '-profiling'], lambda generated: '// The Module object' in generated or os.environ.get('EMCC_FAST_COMPILER') == '0', 'with -profiling, yes comments in shell code (in fastcomp)'), + ]: print params, text self.clear() @@ -354,9 +355,10 @@ f.close() except KeyError: postbuild = None - cmake_cases = ['target_js', 'target_html'] - cmake_outputs = ['test_cmake.js', 'hello_world_gles.html'] - for i in range(0, 2): + cmake_cases = ['target_js', 'target_html', 'target_library', 'target_library'] + cmake_outputs = ['test_cmake.js', 'hello_world_gles.html', 'libtest_cmake.a', 'libtest_cmake.so'] + cmake_arguments = ['', '', '-DBUILD_SHARED_LIBS=OFF', '-DBUILD_SHARED_LIBS=ON'] + for i in range(0, len(cmake_cases)): for configuration in ['Debug', 'Release']: # CMake can be invoked in two ways, using 'emconfigure cmake', or by directly running 'cmake'. # Test both methods. @@ -374,11 +376,11 @@ f.close() if invoke_method == 'cmake': # Test invoking cmake directly. cmd = ['cmake', '-DCMAKE_TOOLCHAIN_FILE='+path_from_root('cmake', 'Platform', 'Emscripten.cmake'), - '-DCMAKE_BUILD_TYPE=' + configuration, '-G', generator, cmakelistsdir] + '-DCMAKE_BUILD_TYPE=' + configuration, cmake_arguments[i], '-G', generator, cmakelistsdir] else: # Test invoking via 'emconfigure cmake' - cmd = [emconfigure, 'cmake', '-DCMAKE_BUILD_TYPE=' + configuration, '-G', generator, cmakelistsdir] - + cmd = [emconfigure, 'cmake', '-DCMAKE_BUILD_TYPE=' + configuration, cmake_arguments[i], '-G', generator, cmakelistsdir] + ret = Popen(cmd, stdout=None if verbose_level >= 2 else PIPE, stderr=None if verbose_level >= 1 else PIPE).communicate() if len(ret) > 1 and ret[1] != None and len(ret[1].strip()) > 0: logging.error(ret[1]) # If there were any errors, print them directly to console for diagnostics. diff --git a/tools/ffdb.py b/tools/ffdb.py index c22fd9db..497f0162 100755 --- a/tools/ffdb.py +++ b/tools/ffdb.py @@ -1,7 +1,8 @@ #!/usr/bin/env python -import socket, json, sys, uuid, datetime, time, logging, cgi, zipfile, os, tempfile, atexit, subprocess +import socket, json, sys, uuid, datetime, time, logging, cgi, zipfile, os, tempfile, atexit, subprocess, re, base64, struct, imghdr +ADB = 'adb' # Path to the adb executable LOG_VERBOSE = False # Verbose printing enabled with --verbose HOST = 'localhost' # The remote host to connect to the B2G device PORT = 6000 # The port on the host on which the B2G device listens on @@ -18,7 +19,13 @@ def sizeof_fmt(num): return "%3.1f%s" % (num, 'TB') def zipdir(path, zipfilename): - zipf = zipfile.ZipFile(zipfilename, 'w') + try: + import zlib + zip_mode = zipfile.ZIP_DEFLATED + except: + zip_mode = zipfile.ZIP_STORED + + zipf = zipfile.ZipFile(zipfilename, 'w', zip_mode) files_to_compress = [] for root, dirs, files in os.walk(path): for file in files: @@ -29,9 +36,10 @@ def zipdir(path, zipfilename): (root, file) = tuple filename = os.path.join(root, file) filesize = os.path.getsize(filename) - print 'Compressing ' + str(n) + '/' + str(len(files_to_compress)) + ': "' + os.path.relpath(filename, path) + '" (' + sizeof_fmt(filesize) + ')...' + path_in_archive = os.path.relpath(filename, path) + print 'Compressing ' + str(n) + '/' + str(len(files_to_compress)) + ': "' + path_in_archive + '" (' + sizeof_fmt(filesize) + ')...' n += 1 - zipf.write(os.path.join(root, file)) + zipf.write(os.path.join(root, file), path_in_archive) zipf.close() print 'Done. ' @@ -46,16 +54,27 @@ def format_html(msg): # Prints a verbose log message to stdout channel. Only shown if run with --verbose. def logv(msg): if LOG_VERBOSE: - sys.stdout.write(format_html(msg)) + sys.stdout.write(format_html(msg) + '\n') sys.stdout.flush() # Reads data from the socket, and tries to parse what we have got so far as a JSON message. # The messages are of form "bytelength:{jsondict}", where bytelength tells how many bytes # there are in the data that comes after the colon. # Returns a JSON dictionary of the received message. -def read_b2g_response(): +def read_b2g_response(print_errors_to_console = True): global read_queue, b2g_socket - read_queue += b2g_socket.recv(65536*2) + try: + read_queue += b2g_socket.recv(65536*2) + except KeyboardInterrupt: + print ' Aborted by user' + sys.exit(1) + except Exception, e: + if e[0] == 57: # Socket is not connected + print 'Error! Failed to receive data from the device: socket is not connected!' + sys.exit(1) + else: + raise + payload = '' while ':' in read_queue: semicolon = read_queue.index(':') payload_len = int(read_queue[:semicolon]) @@ -66,10 +85,13 @@ def read_b2g_response(): read_queue = read_queue[semicolon+1+payload_len:] logv('Read a message of size ' + str(payload_len) + 'b from socket.') payload = json.loads(payload) + # Log received errors immediately to console + if print_errors_to_console and 'error' in payload: + print >> sys.stderr, 'Received error "' + payload['error'] + '"! Reason: ' + payload['message'] return payload # Sends a command to the B2G device and waits for the response and returns it as a JSON dict. -def send_b2g_cmd(to, cmd, data = {}): +def send_b2g_cmd(to, cmd, data = {}, print_errors_to_console = True): global b2g_socket msg = { 'to': to, 'type': cmd} msg = dict(msg.items() + data.items()) @@ -78,7 +100,7 @@ def send_b2g_cmd(to, cmd, data = {}): msg = str(len(msg))+':'+msg logv('Sending cmd:' + cmd + ' to:' + to) b2g_socket.sendall(msg) - return read_b2g_response() + return read_b2g_response(print_errors_to_console) def escape_bytes(b): return str(b) @@ -102,7 +124,16 @@ def send_b2g_data_chunk(to, data_blob): i += 1 message = '{"to":"'+to+'","type":"chunk","chunk":"' + ''.join(byte_str) + '"}' message = str(len(message)) + ':' + message + logv('{"to":"'+to+'","type":"chunk","chunk":"<data>"}') b2g_socket.sendall(message) + return read_b2g_response() + +def send_b2g_bulk_data(to, data_blob): + message = 'bulk ' + to + ' stream ' + str(len(data_blob)) + ':' + logv(message) + b2g_socket.sendall(message) + b2g_socket.sendall(data_blob) + # It seems that B2G doesn't send any response JSON back after a bulk transfer is finished, so no read_b2g_response() here. # Queries the device for a list of all installed apps. def b2g_get_appslist(): @@ -130,8 +161,60 @@ def print_applist(applist, running_app_manifests, print_removable): num_printed += 1 return num_printed +def adb_devices(): + try: + devices = subprocess.check_output([ADB, 'devices']) + devices = devices.strip().split('\n')[1:] + devices = map(lambda x: x.strip().split('\t'), devices) + return devices + except Exception, e: + return [] + +def b2g_get_prefs_filename(): + return subprocess.check_output([ADB, 'shell', 'echo', '-n', '/data/b2g/mozilla/*.default/prefs.js']) + +def b2g_get_prefs_data(): + return subprocess.check_output([ADB, 'shell', 'cat', '/data/b2g/mozilla/*.default/prefs.js']) + +def b2g_get_pref(sub): + prefs_data = b2g_get_prefs_data().split('\n') + # Filter to find all prefs that have the substring 'sub' in them. + r = re.compile('user_pref\w*\(\w*"([^"]*)"\w*,\w*([^\)]*)') + for line in prefs_data: + m = r.match(line) + if m and (sub is None or sub in m.group(1)): + print m.group(1) + ': ' + m.group(2).strip() + +def b2g_set_pref(pref, value): + prefs_data = b2g_get_prefs_data().split('\n') + # Remove any old value of this pref. + r = re.compile('user_pref\w*\(\w*"([^"]*)"\w*,\w*([^\)]*)') + new_prefs_data = [] + for line in prefs_data: + m = r.match(line) + if not m or m.group(1) != pref: + new_prefs_data += [line] + + if value != None: + print 'Setting pref "' + pref + '" = ' + value + new_prefs_data += ['user_pref("' + pref + '", ' + value + ');'] + else: + print 'Unsetting pref "' + pref + '"' + (oshandle, tempfilename) = tempfile.mkstemp(suffix='.js', prefix='ffdb_temp_') + os.write(oshandle, '\n'.join(new_prefs_data)); + + # Write the new pref + subprocess.check_output([ADB, 'shell', 'stop', 'b2g']) + subprocess.check_output([ADB, 'push', tempfilename, b2g_get_prefs_filename()]) + subprocess.check_output([ADB, 'shell', 'start', 'b2g']) + print 'Rebooting phone...' + + def delete_temp_file(): + os.remove(tempfilename) + atexit.register(delete_temp_file) + def main(): - global b2g_socket, webappsActorName + global b2g_socket, webappsActorName, HOST, PORT, VERBOSE, ADB if len(sys.argv) < 2 or '--help' in sys.argv or 'help' in sys.argv or '-v' in sys.argv: print '''Firefox OS Debug Bridge, a tool for automating FFOS device tasks from the command line. @@ -148,22 +231,74 @@ def main(): log <app> [--clear]: Starts a persistent log listener that reads web console messages from the given application. If --clear is passed, the message log for that application is cleared instead. navigate <url>: Opens the given web page in the B2G browser. + screenshot [filename.png]: Takes a screenshot of the current contents displayed on the device. If an optional + filename is specified, the screenshot is saved to that file. Otherwise the filename + will be autogenerated. + get <pref>: Fetches the value of the given developer pref option from the FFOS device and prints it to console. + set <pref> <value>: Writes the given pref option to the FFOS device and restarts the B2G process on it for the change to take effect. + unset <pref>: Removes the given pref option from the FFOS device and restarts the B2G process on it for the change to take effect. + + hide-prompt: Permanently removes the remote debugging connection dialog from showing up, and reboots the phone. This command is + provided for conveniency, and is the same as calling './ffdb.py set devtools.debugger.prompt-connection false' + restore-prompt: Restores the remote debugging connection dialog prompt to its default state. + + Options: Additionally, the following options may be passed to control FFDB execution: + + --host <hostname>: Specifies the target network address to connect to. Default: 'localhost'. + --port <number>: Specifies the network port to connect to. Default: 6000. + --verbose: Enables verbose printing, mostly useful for debugging. + --simulator: Signal that we will be connecting to a FFOS simulator and not a real device. In the above, whenever a command requires an <app> to be specified, either the human-readable name, localId or manifestURL of the application can be used.''' sys.exit(0) + connect_to_simulator = False + + options_with_value = ['--host', '--port'] + options = options_with_value + ['--verbose', '--simulator'] + # Process options + for i in range(0, len(sys.argv)): + if sys.argv[i] in options_with_value: + if i+1 >= sys.argv or sys.argv[i+1].startswith('-'): + print >> sys.stderr, "Missing value for option " + sys.argv[i] +'!' + sys.exit(1) + if sys.argv[i] == '--host': + HOST = sys.argv[i+1] + elif sys.argv[i] == '--port': + PORT = int(sys.argv[i+1]) + elif sys.argv[i] == '--verbose': + VERBOSE = True + elif sys.argv[i] == '--simulator': + connect_to_simulator = True + + # Clear the processed options so that parsing the commands below won't trip up on these. + if sys.argv[i] in options: sys.argv[i] = '' + if sys.argv[i] in options_with_value: sys.argv[i+1] = '' + + sys.argv = filter(lambda x: len(x) > 0, sys.argv) + + # Double-check that the device is found via adb: + if (HOST == 'localhost' or HOST == '127.0.0.1') and not connect_to_simulator: + devices = adb_devices() + if len(devices) == 0: + print 'Error! Failed to connect to B2G device debugger socket at address ' + HOST + ':' + str(PORT) + ' and no devices were detected via adb. Please double-check the following and try again: ' + print ' 1) The device is powered on and connected to the computer with an USB cable.' + print ' 2) ADB and DevTools debugging is enabled on the device. (Settings -> Developer -> Debugging via USB: "ADB and DevTools"' + print ' 3) The device is listed when you run "adb devices" on the command line.' + print ' 4) When launching ffdb, remember to acknowledge the "incoming debug connection" dialog if it pops up on the device.' + sys.exit(1) b2g_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: b2g_socket.connect((HOST, PORT)) except Exception, e: if e[0] == 61: # Connection refused - if HOST == 'localhost' or HOST == '127.0.0.1': - cmd = ['adb', 'forward', 'tcp:'+str(PORT), 'localfilesystem:/data/local/debugger-socket'] + if (HOST == 'localhost' or HOST == '127.0.0.1') and not connect_to_simulator: + cmd = [ADB, 'forward', 'tcp:'+str(PORT), 'localfilesystem:/data/local/debugger-socket'] print 'Connection to ' + HOST + ':' + str(PORT) + ' refused, attempting to forward device debugger-socket to local address by calling ' + str(cmd) + ':' else: - print 'Error! Failed to connect to B2G device debugger socket at address ' + HOST + ':' + str(PORT) + '!' + print 'Error! Failed to connect to B2G ' + ('simulator' if connect_to_simulator else 'device') + ' debugger socket at address ' + HOST + ':' + str(PORT) + '!' sys.exit(1) try: retcode = subprocess.check_call(cmd) @@ -187,6 +322,9 @@ def main(): logv('Connected. Handshake: ' + str(handshake)) data = send_b2g_cmd('root', 'listTabs') + if not 'deviceActor' in data: + print 'Error! Debugging connection was not available. Make sure that the "Remote debugging" developer option on the device is set to "ADB and Devtools".' + sys.exit(1) deviceActorName = data['deviceActor'] logv('deviceActor: ' + deviceActorName) webappsActorName = data['webappsActor'] @@ -241,28 +379,35 @@ def main(): print 'Uploading application package "' + target_app_path + '"...' print 'Size of compressed package: ' + sizeof_fmt(os.path.getsize(target_app_path)) + '.' - uploadResponse = send_b2g_cmd(webappsActorName, 'uploadPackage') - packageUploadActor = uploadResponse['actor'] app_file = open(target_app_path, 'rb') data = app_file.read() file_size = len(data) - chunk_size = 4*1024*1024 - i = 0 + + uploadResponse = send_b2g_cmd(webappsActorName, 'uploadPackage', { 'bulk': 'true'}, print_errors_to_console = False) # This may fail if on old device. start_time = time.time() - while i < file_size: - chunk = data[i:i+chunk_size] - - send_b2g_data_chunk(packageUploadActor, chunk) - i += chunk_size - bytes_uploaded = min(i, file_size) - cur_time = time.time() - secs_elapsed = cur_time - start_time - percentage_done = bytes_uploaded * 1.0 / file_size - total_time = secs_elapsed / percentage_done - time_left = total_time - secs_elapsed - print sizeof_fmt(bytes_uploaded) + " uploaded, {:5.1f} % |