diff --git a/.gitignore b/.gitignore index 57420b246c..3aac82dd2d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ tests/tmp-nw tags Thumbs.db /tmp +external_binaries diff --git a/BUILD.gn b/BUILD.gn index 585666aed8..71d78b3c79 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -3,6 +3,10 @@ import("//build/toolchain/toolchain.gni") if (is_mac) { import("//build/util/branding.gni") chrome_framework_name = chrome_product_full_name + " Framework" + cflags = [ + "-F", + "../../content/nw/external_binaries", + ] } import("//build/util/version.gni") @@ -12,7 +16,30 @@ group("nwjs") { "//third_party/ffmpeg:ffmpeg" ] if (is_mac) { - deps += [ ":copy_ffmpeg" ] + deps += [ ":copy_ffmpeg", ":copy_external_binaries" ] + } +} + +static_library("nw_autoupdater") { + deps = [ + "//base", + ] + sources = [ + "src/browser/auto_updater.h", + "src/browser/auto_updater.cc", + "src/browser/auto_updater_mac.mm", + ] + if (is_mac) { + libs = [ + "Squirrel.framework", + "ReactiveCocoa.framework", + "Mantle.framework", + ] + cflags_objcc = [ + "-Wno-error=unused-command-line-argument", + "-fobjc-runtime=macosx-10.8", + "-fobjc-weak", + ] } } @@ -92,6 +119,8 @@ nw_chrome_browser_sources = [ "src/api/object_manager.h", "src/api/object_manager_factory.cc", "src/api/object_manager_factory.h", + "src/api/auto_updater/api_auto_updater.cc", + "src/api/auto_updater/api_auto_updater.h", "src/api/base/base.cc", "src/api/base/base.h", "src/api/menu/menu.cc", @@ -121,6 +150,7 @@ static_library("nw_browser") { "//third_party/zlib:minizip", "//skia", ":nw_base", + ":nw_autoupdater", "//content/nw/src/api:nw_api", "//content/nw/src/api:nw_api_registration", "//third_party/protobuf:protobuf_lite", @@ -256,6 +286,14 @@ if (is_mac) { "//third_party/ffmpeg:ffmpeg" ] } + copy("copy_external_binaries") { + sources = [ + "external_binaries/Squirrel.framework", + "external_binaries/ReactiveCocoa.framework", + "external_binaries/Mantle.framework", + ] + outputs = [ "$root_out_dir/$chrome_product_full_name.app/Contents/Versions/$chrome_version_full/{{source_file_part}}" ] + } } copy("copy_node") { diff --git a/nw.gypi b/nw.gypi index 35776afa6d..a794030d36 100644 --- a/nw.gypi +++ b/nw.gypi @@ -37,8 +37,46 @@ }, }, }, + 'conditions': [ + ['OS=="mac"', { + 'mac_framework_dirs': [ + '<(DEPTH)/../content/nw/external_binaries', + ], + }], + ], + }, 'targets': [ + { + 'target_name': 'nw_autoupdater', + 'type': 'static_library', + 'dependencies': [ + '<(DEPTH)/base/base.gyp:base', + ], + 'sources': [ + 'src/browser/auto_updater.h', + 'src/browser/auto_updater.cc', + 'src/browser/auto_updater_mac.mm', + ], + 'conditions': [ + ['OS == "mac"', { + 'link_settings': { + 'libraries': [ + 'external_binaries/Squirrel.framework', + 'external_binaries/ReactiveCocoa.framework', + 'external_binaries/Mantle.framework', + ], + }, + 'xcode_settings': { + 'OTHER_CFLAGS': [ + '-Wno-error=unused-command-line-argument', + '-fobjc-runtime=macosx-10.8', + '-fobjc-weak', + ], + }, + }], + ], + }, { 'target_name': 'nw_base', 'type': '<(component)', @@ -71,6 +109,7 @@ '<(DEPTH)/third_party/zlib/zlib.gyp:minizip', '<(DEPTH)/skia/skia.gyp:skia', 'nw_base', + 'nw_autoupdater', '<(DEPTH)/content/nw/src/api/api.gyp:nw_api', '<(DEPTH)/content/nw/src/api/api_registration.gyp:nw_api_registration', '<(DEPTH)/extensions/browser/api/api_registration.gyp:extensions_api_registration', @@ -107,6 +146,8 @@ 'src/api/object_manager.h', 'src/api/object_manager_factory.cc', 'src/api/object_manager_factory.h', + 'src/api/auto_updater/api_auto_updater.cc', + 'src/api/auto_updater/api_auto_updater.h', 'src/api/base/base.cc', 'src/api/base/base.h', 'src/api/menu/menu.cc', @@ -574,5 +615,10 @@ }, } ], + 'xcode_settings': { + 'LD_RUNPATH_SEARCH_PATHS': [ + '@loader_path/..', + ], + }, } diff --git a/src/api/_api_features.json b/src/api/_api_features.json index c68d9107d9..e78d755957 100644 --- a/src/api/_api_features.json +++ b/src/api/_api_features.json @@ -11,6 +11,10 @@ "channel": "stable", "contexts": ["blessed_extension"] }, + "nw.AutoUpdater": { + "channel": "stable", + "contexts": ["blessed_extension"] + }, "nw.Window": { "channel": "stable", "contexts": ["blessed_extension"] diff --git a/src/api/auto_updater/api_auto_updater.cc b/src/api/auto_updater/api_auto_updater.cc new file mode 100644 index 0000000000..b3e53d00d8 --- /dev/null +++ b/src/api/auto_updater/api_auto_updater.cc @@ -0,0 +1,127 @@ +// Copyright (c) 2016 Jefry Tedjokusumo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell co +// pies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in al +// l copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IM +// PLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNES +// S FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WH +// ETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "base/values.h" +#include "content/nw/src/api/auto_updater/api_auto_updater.h" +#include "content/nw/src/api/nw_auto_updater.h" +#include "content/nw/src/browser/auto_updater.h" +#include "extensions/browser/extensions_browser_client.h" + +using namespace extensions::nwapi::nw__auto_updater; +using namespace content; + +namespace extensions { + + static void DispatchEvent(events::HistogramValue histogram_value, + const std::string& event_name, + std::unique_ptr args) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + ExtensionsBrowserClient::Get()->BroadcastEventToRenderers( + histogram_value, event_name, std::move(args)); + } + + + class AutoUpdaterObserver : public auto_updater::Delegate { + + // An error happened. + void OnError(const std::string& error) override { + std::unique_ptr arguments (new base::ListValue()); + arguments->AppendString(error); + DispatchEvent(events::HistogramValue::UNKNOWN, + nwapi::nw__auto_updater::OnError::kEventName, + std::move(arguments)); + } + + // Checking to see if there is an update + void OnCheckingForUpdate() override { + std::unique_ptr arguments (new base::ListValue()); + DispatchEvent(events::HistogramValue::UNKNOWN, + nwapi::nw__auto_updater::OnCheckingForUpdate::kEventName, + std::move(arguments)); + } + + // There is an update available and it is being downloaded + void OnUpdateAvailable() override { + std::unique_ptr arguments (new base::ListValue()); + DispatchEvent(events::HistogramValue::UNKNOWN, + nwapi::nw__auto_updater::OnUpdateAvailable::kEventName, + std::move(arguments)); + } + + // There is no available update. + void OnUpdateNotAvailable() override { + std::unique_ptr arguments (new base::ListValue()); + DispatchEvent(events::HistogramValue::UNKNOWN, + nwapi::nw__auto_updater::OnUpdateNotAvailable::kEventName, + std::move(arguments)); + } + + // There is a new update which has been downloaded. + void OnUpdateDownloaded(const std::string& release_notes, + const std::string& release_name, + const base::Time& release_date, + const std::string& update_url) override { + std::unique_ptr arguments (new base::ListValue()); + arguments->AppendString(release_notes); + arguments->AppendString(release_name); + arguments->AppendDouble(release_date.ToJsTime()); + arguments->AppendString(update_url); + DispatchEvent(events::HistogramValue::UNKNOWN, + nwapi::nw__auto_updater::OnUpdateDownloaded::kEventName, + std::move(arguments)); + } + + public: + AutoUpdaterObserver() { + } + + ~AutoUpdaterObserver() override { + } + + }; + + AutoUpdaterObserver gAutoUpdateObserver; + NwAutoUpdaterNativeCallSyncFunction::NwAutoUpdaterNativeCallSyncFunction() {} + + bool NwAutoUpdaterNativeCallSyncFunction::RunNWSync(base::ListValue* response, std::string* error) { + if (!auto_updater::AutoUpdater::GetDelegate()) { + auto_updater::AutoUpdater::SetDelegate(&gAutoUpdateObserver); + } + + std::string method; + EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &method)); + + if (method == "SetFeedURL") { + auto_updater::AutoUpdater::HeaderMap headers; + std::string url; + EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &url)); + auto_updater::AutoUpdater::SetFeedURL(url, headers); + } else if (method == "GetFeedURL") { + std::string url = auto_updater::AutoUpdater::GetFeedURL(); + response->AppendString(url); + } else if (method == "QuitAndInstall") { + auto_updater::AutoUpdater::QuitAndInstall(); + } else if (method == "CheckForUpdates") { + auto_updater::AutoUpdater::CheckForUpdates(); + } + return true; + } + + +} // namespace extension diff --git a/src/api/auto_updater/api_auto_updater.h b/src/api/auto_updater/api_auto_updater.h new file mode 100644 index 0000000000..295eb2fb38 --- /dev/null +++ b/src/api/auto_updater/api_auto_updater.h @@ -0,0 +1,43 @@ +// Copyright (c) 2016 Jefry Tedjokusumo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell co +// pies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in al +// l copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IM +// PLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNES +// S FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WH +// ETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +#ifndef CONTENT_NW_SRC_API_AUTO_UPDATER_H_ +#define CONTENT_NW_SRC_API_AUTO_UPDATER_H_ + +#include "extensions/browser/extension_function.h" + +namespace extensions { + + class NwAutoUpdaterNativeCallSyncFunction: public NWSyncExtensionFunction { + public: + NwAutoUpdaterNativeCallSyncFunction(); + bool RunNWSync(base::ListValue* response, std::string* error) override; + + protected: + ~NwAutoUpdaterNativeCallSyncFunction() override {} + DECLARE_EXTENSION_FUNCTION("nw.AutoUpdater.NativeCallSync", UNKNOWN) + + private: + DISALLOW_COPY_AND_ASSIGN(NwAutoUpdaterNativeCallSyncFunction); + }; + +} // namespace extensions + +#endif //CONTENT_NW_SRC_API_AUTO_UPDATER_H_ diff --git a/src/api/dispatcher_bindings.cc b/src/api/dispatcher_bindings.cc index 44f7c58b3b..a965733ed4 100644 --- a/src/api/dispatcher_bindings.cc +++ b/src/api/dispatcher_bindings.cc @@ -218,6 +218,8 @@ DispatcherBindings::RequireNwGui(const v8::FunctionCallbackInfo& args NwGui, global, v8::String::NewFromUtf8(isolate, "shortcut.js"), IDR_NW_API_SHORTCUT_JS); RequireFromResource(args.This(), NwGui, global, v8::String::NewFromUtf8(isolate, "screen.js"), IDR_NW_API_SCREEN_JS); + RequireFromResource(args.This(), + NwGui, global, v8::String::NewFromUtf8(isolate, "auto_updater.js"), IDR_NW_API_AUTO_UPDATE_JS); g_context->Exit(); args.GetReturnValue().Set(handle_scope.Escape(NwGui)); diff --git a/src/api/dispatcher_host.cc b/src/api/dispatcher_host.cc index f5f25e7936..d827a8e012 100644 --- a/src/api/dispatcher_host.cc +++ b/src/api/dispatcher_host.cc @@ -28,6 +28,7 @@ #include "content/browser/web_contents/web_contents_impl.h" #include "content/nw/src/api/api_messages.h" #include "content/nw/src/api/app/app.h" +#include "content/nw/src/api/auto_updater/api_auto_updater.h" #include "content/nw/src/api/base/base.h" #include "content/nw/src/api/clipboard/clipboard.h" #include "content/nw/src/api/event/event.h" @@ -171,6 +172,8 @@ void DispatcherHost::OnAllocateObject(int object_id, objects_registry_.AddWithID(new Shortcut(object_id, weak_ptr_factory_.GetWeakPtr(), option), object_id); } else if (type == "Screen") { objects_registry_.AddWithID(new EventListener(object_id, weak_ptr_factory_.GetWeakPtr(), option), object_id); + } else if (type == "AutoUpdater") { + objects_registry_.AddWithID(new EventListener(object_id, weak_ptr_factory_.GetWeakPtr(), option), object_id); } else { LOG(ERROR) << "Allocate an object of unknown type: " << type; objects_registry_.AddWithID(new Base(object_id, weak_ptr_factory_.GetWeakPtr(), option), object_id); @@ -264,6 +267,9 @@ void DispatcherHost::OnCallStaticMethodSync( } else if (type == "Screen") { nwapi::Screen::Call(this, method, arguments, result); return; + } else if (type == "AutoUpdater") { + nwapi::AutoUpdater::Call(this, method, arguments, result); + return; } NOTREACHED() << "Calling unknown method " << method << " of class " << type; diff --git a/src/api/nw_auto_updater.idl b/src/api/nw_auto_updater.idl new file mode 100644 index 0000000000..ef57c25247 --- /dev/null +++ b/src/api/nw_auto_updater.idl @@ -0,0 +1,19 @@ +// Copyright 2016 The NW.js Authors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. + +// nw AutoUpdater API +[implemented_in="content/nw/src/api/auto_updater/api_auto_updater.h"] +namespace nw.AutoUpdater { + interface Events { + static void onError(DOMString msg); + static void onCheckingForUpdate(); + static void onUpdateAvailable(); + static void onUpdateNotAvailable(); + static void onUpdateDownloaded(DOMString releaseNotes, DOMString releaseName, double releaseDate, DOMString updateURL); + }; + + interface Functions { + static DOMString[] NativeCallSync(DOMString method, DOMString param); + }; +}; diff --git a/src/api/schemas.gni b/src/api/schemas.gni index 3e0d9ca639..0dd17568d3 100644 --- a/src/api/schemas.gni +++ b/src/api/schemas.gni @@ -12,6 +12,7 @@ assert(enable_extensions) schema_sources = [ "nw_object.idl", "nw_app.idl", + "nw_auto_updater.idl", "nw_window.idl", "nw_clipboard.idl", "nw_menu.idl", diff --git a/src/api/schemas.gypi b/src/api/schemas.gypi index 69a3c9af5e..34e5b40998 100644 --- a/src/api/schemas.gypi +++ b/src/api/schemas.gypi @@ -10,6 +10,7 @@ 'schema_files': [ 'nw_object.idl', 'nw_app.idl', + 'nw_auto_updater.idl', 'nw_window.idl', 'nw_clipboard.idl', 'nw_menu.idl', diff --git a/src/browser/auto_updater.cc b/src/browser/auto_updater.cc new file mode 100644 index 0000000000..e8fe995eac --- /dev/null +++ b/src/browser/auto_updater.cc @@ -0,0 +1,35 @@ +// Copyright (c) 2013 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "content/nw/src/browser/auto_updater.h" + +namespace auto_updater { + +Delegate* AutoUpdater::delegate_ = nullptr; + +Delegate* AutoUpdater::GetDelegate() { + return delegate_; +} + +void AutoUpdater::SetDelegate(Delegate* delegate) { + delegate_ = delegate; +} + +#if !defined(OS_MACOSX) || defined(MAS_BUILD) +std::string AutoUpdater::GetFeedURL() { + return ""; +} + +void AutoUpdater::SetFeedURL(const std::string& url, + const HeaderMap& requestHeaders) { +} + +void AutoUpdater::CheckForUpdates() { +} + +void AutoUpdater::QuitAndInstall() { +} +#endif + +} // namespace auto_updater diff --git a/src/browser/auto_updater.h b/src/browser/auto_updater.h new file mode 100644 index 0000000000..da846f5d28 --- /dev/null +++ b/src/browser/auto_updater.h @@ -0,0 +1,66 @@ +// Copyright (c) 2013 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef NW_BROWSER_AUTO_UPDATER_H_ +#define NW_BROWSER_AUTO_UPDATER_H_ + +#include +#include + +#include "base/macros.h" +#include "build/build_config.h" + +namespace base { +class Time; +} + +namespace auto_updater { + +class Delegate { + public: + // An error happened. + virtual void OnError(const std::string& error) {} + + // Checking to see if there is an update + virtual void OnCheckingForUpdate() {} + + // There is an update available and it is being downloaded + virtual void OnUpdateAvailable() {} + + // There is no available update. + virtual void OnUpdateNotAvailable() {} + + // There is a new update which has been downloaded. + virtual void OnUpdateDownloaded(const std::string& release_notes, + const std::string& release_name, + const base::Time& release_date, + const std::string& update_url) {} + + protected: + virtual ~Delegate() {} +}; + +class AutoUpdater { + public: + typedef std::map HeaderMap; + + // Gets/Sets the delegate. + static Delegate* GetDelegate(); + static void SetDelegate(Delegate* delegate); + + static std::string GetFeedURL(); + static void SetFeedURL(const std::string& url, + const HeaderMap& requestHeaders); + static void CheckForUpdates(); + static void QuitAndInstall(); + + private: + static Delegate* delegate_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(AutoUpdater); +}; + +} // namespace auto_updater + +#endif // NW_BROWSER_AUTO_UPDATER_H_ diff --git a/src/browser/auto_updater_mac.mm b/src/browser/auto_updater_mac.mm new file mode 100644 index 0000000000..d3f7eccaf9 --- /dev/null +++ b/src/browser/auto_updater_mac.mm @@ -0,0 +1,136 @@ +// Copyright (c) 2013 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "content/nw/src/browser/auto_updater.h" + +#import +#import +#import +#import + +#include "base/bind.h" +#include "base/time/time.h" +#include "base/strings/sys_string_conversions.h" + +namespace auto_updater { + +namespace { + +// The gloal SQRLUpdater object. +SQRLUpdater* g_updater = nil; + +} // namespace + +namespace { + +bool g_update_available = false; +std::string update_url_ = ""; + +} + +std::string AutoUpdater::GetFeedURL() { + return update_url_; +} + +// static +void AutoUpdater::SetFeedURL(const std::string& feed, + const HeaderMap& requestHeaders) { + Delegate* delegate = GetDelegate(); + if (!delegate) + return; + + update_url_ = feed; + + NSURL* url = [NSURL URLWithString:base::SysUTF8ToNSString(feed)]; + NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url]; + + for (const auto& it : requestHeaders) { + [urlRequest setValue:base::SysUTF8ToNSString(it.second) + forHTTPHeaderField:base::SysUTF8ToNSString(it.first)]; + } + + if (g_updater) + [g_updater release]; + + // Initialize the SQRLUpdater. + @try { + g_updater = [[SQRLUpdater alloc] initWithUpdateRequest:urlRequest]; + } @catch (NSException* error) { + delegate->OnError(base::SysNSStringToUTF8(error.reason)); + return; + } + + [[g_updater rac_valuesForKeyPath:@"state" observer:g_updater] + subscribeNext:^(NSNumber *stateNumber) { + int state = [stateNumber integerValue]; + // Dispatching the event on main thread. + dispatch_async(dispatch_get_main_queue(), ^{ + if (state == SQRLUpdaterStateCheckingForUpdate) + delegate->OnCheckingForUpdate(); + else if (state == SQRLUpdaterStateDownloadingUpdate) + delegate->OnUpdateAvailable(); + }); + }]; +} + +// static +void AutoUpdater::CheckForUpdates() { + Delegate* delegate = GetDelegate(); + if (!delegate) + return; + + [[[[g_updater.checkForUpdatesCommand + execute:nil] + // Send a `nil` after everything... + concat:[RACSignal return:nil]] + // But only take the first value. If an update is sent, we'll get that. + // Otherwise, we'll get our inserted `nil` value. + take:1] + subscribeNext:^(SQRLDownloadedUpdate *downloadedUpdate) { + if (downloadedUpdate) { + g_update_available = true; + SQRLUpdate* update = downloadedUpdate.update; + // There is a new update that has been downloaded. + delegate->OnUpdateDownloaded( + base::SysNSStringToUTF8(update.releaseNotes), + base::SysNSStringToUTF8(update.releaseName), + base::Time::FromDoubleT(update.releaseDate.timeIntervalSince1970), + base::SysNSStringToUTF8(update.updateURL.absoluteString)); + } else { + g_update_available = false; + // When the completed event is sent with no update, then we know there + // is no update available. + delegate->OnUpdateNotAvailable(); + } + } error:^(NSError *error) { + NSMutableString* failureString = + [NSMutableString stringWithString:error.localizedDescription]; + if (error.localizedFailureReason) { + [failureString appendString:@": "]; + [failureString appendString:error.localizedFailureReason]; + } + if (error.localizedRecoverySuggestion) { + if (![failureString hasSuffix:@"."]) + [failureString appendString:@"."]; + [failureString appendString:@" "]; + [failureString appendString:error.localizedRecoverySuggestion]; + } + delegate->OnError(base::SysNSStringToUTF8(failureString)); + }]; +} + +void AutoUpdater::QuitAndInstall() { + Delegate* delegate = AutoUpdater::GetDelegate(); + if (g_update_available) { + [[g_updater relaunchToInstallUpdate] subscribeError:^(NSError* error) { + if (delegate) + delegate->OnError(base::SysNSStringToUTF8(error.localizedDescription)); + }]; + } else { + if (delegate) + delegate->OnError("No update available, can't quit and install"); + } +} + +} // namespace auto_updater diff --git a/src/resources/api_nw_auto_updater.js b/src/resources/api_nw_auto_updater.js new file mode 100644 index 0000000000..8d7e8dd8ed --- /dev/null +++ b/src/resources/api_nw_auto_updater.js @@ -0,0 +1,258 @@ +// Copyright (c) 2016 Jefry Tedjokusumo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell co +// pies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in al +// l copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IM +// PLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNES +// S FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WH +// ETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +var AutoUpdater = null; +var EventEmitter = nw.require('events').EventEmitter; + +if (process.platform === 'win32') { + + const fs = nw.require('fs') + const path = nw.require('path') + const spawn = nw.require('child_process').spawn + + // i.e. my-app/app-0.1.13/ + const appFolder = path.dirname(process.execPath) + + // i.e. my-app/Update.exe + const updateExe = path.resolve(appFolder, '..', 'Update.exe') + const exeName = path.basename(process.execPath) + var spawnedArgs = [] + var spawnedProcess + + var isSameArgs = function (args) { + return (args.length === spawnedArgs.length) && args.every(function (e, i) { + return e === spawnedArgs[i] + }) + } + + // Spawn a command and invoke the callback when it completes with an error + // and the output from standard out. + var spawnUpdate = function (args, detached, callback) { + var error, errorEmitted, stderr, stdout + + try { + // Ensure we don't spawn multiple squirrel processes + // Process spawned, same args: Attach events to alread running process + // Process spawned, different args: Return with error + // No process spawned: Spawn new process + if (spawnedProcess && !isSameArgs(args)) { + return callback('AutoUpdater process with arguments ' + args + ' is already running') + } else if (!spawnedProcess) { + spawnedProcess = spawn(updateExe, args, { + detached: detached + }) + spawnedArgs = args || [] + } + } catch (error1) { + error = error1 + + // Shouldn't happen, but still guard it. + process.nextTick(function () { + return callback(error) + }) + return + } + stdout = '' + stderr = '' + spawnedProcess.stdout.on('data', function (data) { + stdout += data + }) + spawnedProcess.stderr.on('data', function (data) { + stderr += data + }) + errorEmitted = false + spawnedProcess.on('error', function (error) { + errorEmitted = true + callback(error) + }) + return spawnedProcess.on('exit', function (code, signal) { + spawnedProcess = undefined + spawnedArgs = [] + + // We may have already emitted an error. + if (errorEmitted) { + return + } + + // Process terminated with error. + if (code !== 0) { + return callback('Command failed: ' + (signal != null ? signal : code) + '\n' + stderr) + } + + // Success. + callback(null, stdout) + }) + } + + function Squirrel() { + } + // Start an instance of the installed app. + Squirrel.prototype.processStart = function () { + return spawnUpdate(['--processStartAndWait', exeName], true, function () {}) + } + + // Download the releases specified by the URL and write new results to stdout. + Squirrel.prototype.download = function (updateURL, callback) { + return spawnUpdate(['--download', updateURL], false, function (error, stdout) { + var json, ref, ref1, update + if (error != null) { + return callback(error) + } + try { + // Last line of output is the JSON details about the releases + json = stdout.trim().split('\n').pop() + update = (ref = JSON.parse(json)) != null ? (ref1 = ref.releasesToApply) != null ? typeof ref1.pop === 'function' ? ref1.pop() : void 0 : void 0 : void 0 + } catch (jsonError) { + return callback('Invalid result:\n' + stdout) + } + return callback(null, update) + }) + } + + // Update the application to the latest remote version specified by URL. + Squirrel.prototype.update = function (updateURL, callback) { + return spawnUpdate(['--update', updateURL], false, callback) + } + + // Is the Update.exe installed with the current application? + Squirrel.prototype.supported = function () { + try { + fs.accessSync(updateExe, fs.R_OK) + return true + } catch (error) { + return false + } + } + const squirrelUpdate = new Squirrel(); + + // === AutoUpdaterWin === + + var AutoUpdaterWin = new EventEmitter(); + + AutoUpdaterWin.GetFeedURL = function() { + return this.updateURL; + } + + AutoUpdaterWin.SetFeedURL = function(url) { + this.updateURL = url; + } + + AutoUpdaterWin.emitError = function(message) { + this.emit('error', message); + } + + AutoUpdaterWin.QuitAndInstall = function() { + if (!this.updateAvailable) { + return this.emitError('No update available, can\'t quit and install'); + } + squirrelUpdate.processStart(); + nw.App.quit(); + } + + AutoUpdaterWin.CheckForUpdates = function() { + if (!this.updateURL) { + return this.emitError('Update URL is not set') + } + if (!squirrelUpdate.supported()) { + return this.emitError('Can not find Squirrel') + } + this.emit('checking-for-update') + squirrelUpdate.download(this.updateURL, function(error, update) { + if (error != null) { + return AutoUpdaterWin.emitError(error) + } + if (update == null) { + return AutoUpdaterWin.emit('update-not-available') + } + AutoUpdaterWin.updateAvailable = true + AutoUpdaterWin.emit('update-available') + squirrelUpdate.update(AutoUpdaterWin.updateURL, function(error) { + if (error != null) { + return AutoUpdaterWin.emitError(error) + } + const releaseNotes = update + // Date is not available on Windows, so fake it. + const date = new Date() + AutoUpdaterWin.emit('update-downloaded', releaseNotes, date, AutoUpdaterWin.updateURL) + }) + }) + } + + AutoUpdater = AutoUpdaterWin; + +} else { + var nw_binding = require('binding').Binding.create('nw.AutoUpdater'); + var sendRequest = require('sendRequest'); + + var events = { + onError: 'error', + onCheckingForUpdate: 'checking-for-update', + onUpdateAvailable: 'update-available', + onUpdateNotAvailable: 'update-not-available', + onUpdateDownloaded: 'update-downloaded' + }; + + // Hook Sync API calls + nw_binding.registerCustomHook(function(bindingsAPI) { + var apiFunctions = bindingsAPI.apiFunctions; + ['NativeCallSync'].forEach(function(nwSyncAPIName) { + apiFunctions.setHandleRequest(nwSyncAPIName, function() { + return sendRequest.sendRequestSync(this.name, arguments, this.definition.parameters, {}); + }); + }); + }); + + var nwAutoUpdaterBinding = nw_binding.generate(); + + var AutoUpdaterNative = new EventEmitter(); + + AutoUpdaterNative.SetFeedURL = function (url) { + nwAutoUpdaterBinding.NativeCallSync("SetFeedURL", url); + } + + AutoUpdaterNative.GetFeedURL = function () { + return nwAutoUpdaterBinding.NativeCallSync("GetFeedURL", "")[0]; + } + + AutoUpdaterNative.CheckForUpdates = function () { + nwAutoUpdaterBinding.NativeCallSync("CheckForUpdates", ""); + } + + AutoUpdaterNative.QuitAndInstall = function () { + nwAutoUpdaterBinding.NativeCallSync("QuitAndInstall", ""); + } + + Object.keys(events).forEach(function(eventName) { + nwAutoUpdaterBinding[eventName].addListener(function() { + var args = Array.prototype.concat.apply([events[eventName]], arguments); + if(eventName == 'onUpdateDownloaded') { + args[3] = Date(args[3]); + } + AutoUpdaterNative.emit.apply(AutoUpdaterNative, args); + }); + }); + + AutoUpdater = AutoUpdaterNative; +} + +AutoUpdater.Init = function() { + //do nothing, compatibility with old api +} + +exports.binding = AutoUpdater; diff --git a/src/resources/nw_resources.grd b/src/resources/nw_resources.grd index 30f4dff82d..0179766310 100644 --- a/src/resources/nw_resources.grd +++ b/src/resources/nw_resources.grd @@ -50,6 +50,7 @@ + diff --git a/test/manual/autoupdate/autoupdate.sh b/test/manual/autoupdate/autoupdate.sh new file mode 100755 index 0000000000..a1f14c3388 --- /dev/null +++ b/test/manual/autoupdate/autoupdate.sh @@ -0,0 +1,33 @@ + +if [ "$#" -ne 2 ] || ! [ -d "$2" ]; then + echo "Usage: $0 IDENTITY DIRECTORY" >&2 + exit 1 +fi + +identity="$1" +rm -r nwjs.app +cp -r $2/nwjs.app . + +codesign --force --verify --verbose --sign "$identity" nwjs.dummy.app +zip -r nwjs.dummy.zip nwjs.dummy.app/ + +app="nwjs.app" +version=$(ls "$app"/Contents/Versions) + +echo "### signing frameworks" +codesign --force --verify --verbose --sign "$identity" "$app/Contents/Versions/$version/nwjs Framework.framework/Helpers/crashpad_handler" +codesign --force --verify --verbose --sign "$identity" "$app/Contents/Versions/$version/nwjs Framework.framework/libffmpeg.dylib" +codesign --force --verify --verbose --sign "$identity" "$app/Contents/Versions/$version/nwjs Framework.framework/libnode.dylib" +codesign --force --verify --verbose --sign "$identity" "$app/Contents/Versions/$version/nwjs Framework.framework/" +codesign --force --verify --verbose --sign "$identity" "$app/Contents/Versions/$version/nwjs Helper.app/" +codesign --force --verify --verbose --sign "$identity" "$app/Contents/Versions/$version/Mantle.framework/" +codesign --force --verify --verbose --sign "$identity" "$app/Contents/Versions/$version/ReactiveCocoa.framework/" +codesign --force --verify --verbose --sign "$identity" "$app/Contents/Versions/$version/Squirrel.framework/" + +echo "### signing app" +codesign --force --verify --verbose --sign "$identity" "$app" + +echo "### verifying signature" +codesign -vvv -d "$app" + +nwjs.app/Contents/MacOS/nwjs diff --git a/test/manual/autoupdate/index.html b/test/manual/autoupdate/index.html new file mode 100644 index 0000000000..482f1b49eb --- /dev/null +++ b/test/manual/autoupdate/index.html @@ -0,0 +1,57 @@ + + + + + + nwjs autoupdate + + + + +

+ + diff --git a/test/manual/autoupdate/nwjs.dummy.app/Contents/Info.plist b/test/manual/autoupdate/nwjs.dummy.app/Contents/Info.plist new file mode 100644 index 0000000000..fcfdc35fd8 --- /dev/null +++ b/test/manual/autoupdate/nwjs.dummy.app/Contents/Info.plist @@ -0,0 +1,50 @@ + + + + + BuildMachineOSBuild + 15G1108 + CFBundleDevelopmentRegion + en + CFBundleExecutable + nwjs + CFBundleIdentifier + io.nwjs.nwjs + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + nwjs + CFBundlePackageType + APPL + CFBundleShortVersionString + 999.0 + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 8C1002 + DTPlatformVersion + GM + DTSDKBuild + 16C58 + DTSDKName + macosx10.12 + DTXcode + 0821 + DTXcodeBuild + 8C1002 + LSMinimumSystemVersion + 10.11 + NSHumanReadableCopyright + Copyright © 2017 io.nwjs. All rights reserved. + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/test/manual/autoupdate/nwjs.dummy.app/Contents/MacOS/nwjs b/test/manual/autoupdate/nwjs.dummy.app/Contents/MacOS/nwjs new file mode 100755 index 0000000000..487b572e5e Binary files /dev/null and b/test/manual/autoupdate/nwjs.dummy.app/Contents/MacOS/nwjs differ diff --git a/test/manual/autoupdate/nwjs.dummy.app/Contents/PkgInfo b/test/manual/autoupdate/nwjs.dummy.app/Contents/PkgInfo new file mode 100644 index 0000000000..bd04210fb4 --- /dev/null +++ b/test/manual/autoupdate/nwjs.dummy.app/Contents/PkgInfo @@ -0,0 +1 @@ +APPL???? \ No newline at end of file diff --git a/test/manual/autoupdate/nwjs.dummy.app/Contents/Resources/Base.lproj/MainMenu.nib b/test/manual/autoupdate/nwjs.dummy.app/Contents/Resources/Base.lproj/MainMenu.nib new file mode 100644 index 0000000000..a7d0173ca3 Binary files /dev/null and b/test/manual/autoupdate/nwjs.dummy.app/Contents/Resources/Base.lproj/MainMenu.nib differ diff --git a/test/manual/autoupdate/nwjs.dummy.app/Contents/_CodeSignature/CodeResources b/test/manual/autoupdate/nwjs.dummy.app/Contents/_CodeSignature/CodeResources new file mode 100644 index 0000000000..8d8b6db480 --- /dev/null +++ b/test/manual/autoupdate/nwjs.dummy.app/Contents/_CodeSignature/CodeResources @@ -0,0 +1,129 @@ + + + + + files + + Resources/Base.lproj/MainMenu.nib + + hash + + LQ4qi4aOzondJ2SCCiWE8hK8IzQ= + + optional + + + + files2 + + Resources/Base.lproj/MainMenu.nib + + hash + + LQ4qi4aOzondJ2SCCiWE8hK8IzQ= + + hash2 + + R8+UXVFhR2c8A7NnqADgJJ6LeJ4mruHxEYVRqN88Dco= + + optional + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/test/manual/autoupdate/package.json b/test/manual/autoupdate/package.json new file mode 100644 index 0000000000..43684b8ad9 --- /dev/null +++ b/test/manual/autoupdate/package.json @@ -0,0 +1,4 @@ +{ + "name": "nwjs.autoupdate", + "main": "index.html" +} diff --git a/tools/package_binaries.py b/tools/package_binaries.py index 855980a8b6..4da840f038 100755 --- a/tools/package_binaries.py +++ b/tools/package_binaries.py @@ -9,6 +9,7 @@ import sys import tarfile import zipfile +import sys from hashlib import sha256 from subprocess import call @@ -353,15 +354,21 @@ def compress(from_dir, to_dir, fname, compress): _from = os.path.join(from_dir, fname) _to = os.path.join(to_dir, fname) if compress == 'zip': - z = zipfile.ZipFile(_to + '.zip', 'w', compression=zipfile.ZIP_DEFLATED) - if os.path.isdir(_from): - for root, dirs, files in os.walk(_from): - for f in files: - _path = os.path.join(root, f) - z.write(_path, _path.replace(from_dir+os.sep, '')) + if sys.platform.startswith('darwin'): + cwd = os.getcwd() + os.chdir(from_dir) + subprocess.check_output(['zip', '-r', '-y', _to+'.zip', fname], stderr=subprocess.STDOUT, env=os.environ) + os.chdir(cwd) else: - z.write(_from, fname) - z.close() + z = zipfile.ZipFile(_to + '.zip', 'w', compression=zipfile.ZIP_DEFLATED) + if os.path.isdir(_from): + for root, dirs, files in os.walk(_from): + for f in files: + _path = os.path.join(root, f) + z.write(_path, _path.replace(from_dir+os.sep, '')) + else: + z.write(_from, fname) + z.close() elif compress == 'tar.gz': # only for folders if not os.path.isdir(_from): print 'Will not create tar.gz for a single file: ' + _from @@ -424,13 +431,13 @@ def make_packages(targets): src = os.path.join(binaries_location, f) dest = os.path.join(folder, f) if os.path.isdir(src): # like nw.app - shutil.copytree(src, dest) + shutil.copytree(src, dest, True) else: shutil.copy(src, dest) compress(dist_dir, dist_dir, t['output'], t['compress']) # remove temp folders if (t.has_key('keep4test')) : - shutil.copytree(folder, nwfolder) + shutil.copytree(folder, nwfolder, True) shutil.rmtree(folder) else: