mirror of
https://github.com/libretro/parallel-gx1.git
synced 2026-11-07 04:26:01 -10:00
Remove the updaters
UpdaterCommon, WinUpdater, MacUpdater and UICommon's AutoUpdate only served the standalone Dolphin frontends; RetroArch updates the core. Also drop the AutoUpdate settings, the update-track build string and the u32 WindowsRegistry::ReadValue instantiation only WinUpdater used.
This commit is contained in:
1 parent
55857660c6
commit
dc7d0c21a0
28 files changed
+3
-2760
No files matched your search
+1
-2
@@ -83,8 +83,7 @@ SCMREV_STATUS := $(shell \
|
||||
'$(HASH)define SCM_DESC_STR "$(SCM_DESC)"' \
|
||||
'$(HASH)define SCM_BRANCH_STR "$(SCM_BRANCH)"' \
|
||||
'$(HASH)define SCM_COMMITS_AHEAD_MASTER $(SCM_AHEAD)' \
|
||||
'$(HASH)define SCM_DISTRIBUTOR_STR "None"' \
|
||||
'$(HASH)define SCM_UPDATE_TRACK_STR ""' > $(SCMREV_H).tmp; \
|
||||
'$(HASH)define SCM_DISTRIBUTOR_STR "None"' > $(SCMREV_H).tmp; \
|
||||
cmp -s $(SCMREV_H).tmp $(SCMREV_H) || mv -f $(SCMREV_H).tmp $(SCMREV_H); \
|
||||
rm -f $(SCMREV_H).tmp)
|
||||
|
||||
|
||||
@@ -71,12 +71,6 @@ const std::string& GetScmDistributorStr()
|
||||
return scm_distributor_str;
|
||||
}
|
||||
|
||||
const std::string& GetScmUpdateTrackStr()
|
||||
{
|
||||
static const std::string scm_update_track_str = SCM_UPDATE_TRACK_STR;
|
||||
return scm_update_track_str;
|
||||
}
|
||||
|
||||
const std::string& GetNetplayDolphinVer()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -14,7 +14,6 @@ const std::string& GetScmRevStr();
|
||||
const std::string& GetScmRevGitStr();
|
||||
const std::string& GetUserAgentStr();
|
||||
const std::string& GetScmDistributorStr();
|
||||
const std::string& GetScmUpdateTrackStr();
|
||||
const std::string& GetNetplayDolphinVer();
|
||||
int GetScmCommitsAheadMaster();
|
||||
} // namespace Common
|
||||
@@ -46,9 +46,8 @@ bool ReadValue(std::string* value, const std::string& subkey, const std::string&
|
||||
return true;
|
||||
}
|
||||
|
||||
// These explicit instantiations are needed to prevent linker errors when calling
|
||||
// ReadValue in WinUpdater/Platform.cpp (for u32) and ArmCPUDetect.cpp (for u64)
|
||||
template bool ReadValue(u32* value, const std::string& subkey, const std::string& name);
|
||||
// This explicit instantiation is needed to prevent linker errors when calling
|
||||
// ReadValue in ArmCPUDetect.cpp (for u64)
|
||||
template bool ReadValue(u64* value, const std::string& subkey, const std::string& name);
|
||||
|
||||
OSVERSIONINFOW GetOSVersion()
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Indicate the minimum OS version required for the binary to run properly.
|
||||
// Updater will fail the update if the user does not meet this requirement.
|
||||
OSMinimumVersionWin10=10.0.18362.0
|
||||
OSMinimumVersionWin11=10.0.22000.0
|
||||
|
||||
// This is the runtime which was compiled against - providing a way for Updater to detect if update
|
||||
// is needed before executing this binary. Note that, annoyingly, the version in environment
|
||||
// variables does not match the "real" version. Consider:
|
||||
// VersionInfo : File: C:\Program Files\Microsoft Visual Studio\18\Community\VC\Redist\MSVC\14.51.36231\vc_redist.x64.exe
|
||||
// InternalName: setup
|
||||
// OriginalFilename: VC_redist.x64.exe
|
||||
// FileVersion: 14.51.36247.0
|
||||
// FileDescription: Microsoft Visual C++ v14 Redistributable (x64) - 14.51.36247
|
||||
// Product: Microsoft Visual C++ v14 Redistributable (x64) - 14.51.36247
|
||||
// ProductVersion: 14.51.36247.0
|
||||
// Whereas the environment variables look like:
|
||||
// VCToolsInstallDir=C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.51.36231\
|
||||
// VCToolsRedistDir=C:\Program Files\Microsoft Visual Studio\18\Community\VC\Redist\MSVC\14.51.36231\
|
||||
// VCToolsVersion=14.51.36231
|
||||
// We're really looking for "14.51.36247.00" (because that's what will appear in the registry once
|
||||
// installed), NOT the other values!
|
||||
VCToolsVersion=${VC_TOOLS_VERSION}
|
||||
VCToolsUpdateURL=https://aka.ms/vc14/vc_redist.x64.exe
|
||||
@@ -3,4 +3,3 @@
|
||||
#define SCM_BRANCH_STR "${DOLPHIN_WC_BRANCH}"
|
||||
#define SCM_COMMITS_AHEAD_MASTER ${DOLPHIN_WC_COMMITS_AHEAD_MASTER}
|
||||
#define SCM_DISTRIBUTOR_STR "${DISTRIBUTOR}"
|
||||
#define SCM_UPDATE_TRACK_STR "${DOLPHIN_DEFAULT_UPDATE_TRACK}"
|
||||
@@ -509,13 +509,6 @@ const Info<bool> MAIN_FIFOPLAYER_LOOP_REPLAY{{System::Main, "FifoPlayer", "LoopR
|
||||
const Info<bool> MAIN_FIFOPLAYER_EARLY_MEMORY_UPDATES{
|
||||
{System::Main, "FifoPlayer", "EarlyMemoryUpdates"}, false};
|
||||
|
||||
// Main.AutoUpdate
|
||||
|
||||
const Info<std::string> MAIN_AUTOUPDATE_UPDATE_TRACK{{System::Main, "AutoUpdate", "UpdateTrack"},
|
||||
Common::GetScmUpdateTrackStr()};
|
||||
const Info<std::string> MAIN_AUTOUPDATE_HASH_OVERRIDE{{System::Main, "AutoUpdate", "HashOverride"},
|
||||
""};
|
||||
|
||||
// Main.Movie
|
||||
|
||||
const Info<bool> MAIN_MOVIE_PAUSE_MOVIE{{System::Main, "Movie", "PauseMovie"}, false};
|
||||
|
||||
@@ -323,11 +323,6 @@ extern const Info<bool> MAIN_GAMELIST_COLUMN_TAGS;
|
||||
extern const Info<bool> MAIN_FIFOPLAYER_LOOP_REPLAY;
|
||||
extern const Info<bool> MAIN_FIFOPLAYER_EARLY_MEMORY_UPDATES;
|
||||
|
||||
// Main.AutoUpdate
|
||||
|
||||
extern const Info<std::string> MAIN_AUTOUPDATE_UPDATE_TRACK;
|
||||
extern const Info<std::string> MAIN_AUTOUPDATE_HASH_OVERRIDE;
|
||||
|
||||
// Main.Movie
|
||||
|
||||
extern const Info<bool> MAIN_MOVIE_PAUSE_MOVIE;
|
||||
|
||||
@@ -290,9 +290,6 @@ void DolphinAnalytics::MakeBaseBuilder()
|
||||
builder.AddData("version-branch", Common::GetScmBranchStr());
|
||||
builder.AddData("version-dist", Common::GetScmDistributorStr());
|
||||
|
||||
// Auto-Update information.
|
||||
builder.AddData("update-track", Config::Get(Config::MAIN_AUTOUPDATE_UPDATE_TRACK));
|
||||
|
||||
// CPU information.
|
||||
builder.AddData("cpu-summary", cpu_info.Summarize());
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2019 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||
|
||||
@end
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright 2019 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#import "AppDelegate.h"
|
||||
|
||||
#include "UpdaterCommon/UpdaterCommon.h"
|
||||
|
||||
#include <Cocoa/Cocoa.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
@interface AppDelegate ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification*)aNotification
|
||||
{
|
||||
NSArray* arguments = [[NSProcessInfo processInfo] arguments];
|
||||
|
||||
__block std::vector<std::string> args;
|
||||
[arguments
|
||||
enumerateObjectsUsingBlock:^(NSString* _Nonnull obj, NSUInteger idx, BOOL* _Nonnull stop) {
|
||||
args.push_back(std::string([obj UTF8String]));
|
||||
}];
|
||||
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
|
||||
dispatch_async(queue, ^{
|
||||
RunUpdater(args);
|
||||
[NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification*)aNotification
|
||||
{
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Dolphin Updater</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Dolphin.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.dolphin-emu.updater</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Dolphin Updater</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Licensed under GPL version 2 or later (GPLv2+)</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,197 +0,0 @@
|
||||
// Copyright 2019 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "MacUpdater/ViewController.h"
|
||||
|
||||
#include "UpdaterCommon/Platform.h"
|
||||
#include "UpdaterCommon/UI.h"
|
||||
#include "UpdaterCommon/UpdaterCommon.h"
|
||||
|
||||
#include <Cocoa/Cocoa.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
// When we call from the main thread, we are not allowed to use
|
||||
// dispatch_sync(dispatch_get_main_queue() as it will cause crashes) To prevent this check if we're
|
||||
// already on the main thread first
|
||||
void run_on_main(std::function<void()> fnc)
|
||||
{
|
||||
if (![NSThread isMainThread])
|
||||
{
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
fnc();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
fnc();
|
||||
}
|
||||
}
|
||||
|
||||
NSWindow* GetWindow()
|
||||
{
|
||||
return [[[NSApplication sharedApplication] windows] objectAtIndex:0];
|
||||
}
|
||||
|
||||
ViewController* GetView()
|
||||
{
|
||||
return (ViewController*)GetWindow().contentViewController;
|
||||
}
|
||||
|
||||
void UI::Error(const std::string& text)
|
||||
{
|
||||
run_on_main([&] {
|
||||
NSAlert* alert = [[[NSAlert alloc] init] autorelease];
|
||||
|
||||
[alert setMessageText:@"Fatal error"];
|
||||
[alert setInformativeText:[NSString stringWithCString:text.c_str()
|
||||
encoding:NSUTF8StringEncoding]];
|
||||
[alert setAlertStyle:NSAlertStyleCritical];
|
||||
|
||||
[alert beginSheetModalForWindow:GetWindow()
|
||||
completionHandler:^(NSModalResponse) {
|
||||
[NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0];
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
void UI::SetVisible(bool visible)
|
||||
{
|
||||
run_on_main([&] {
|
||||
if (visible)
|
||||
{
|
||||
[NSApp unhide:nil];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
}
|
||||
else
|
||||
{
|
||||
[NSApp hide:nil];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void UI::SetDescription(const std::string& text)
|
||||
{
|
||||
run_on_main([&] {
|
||||
[GetView() SetDescription:[NSString stringWithCString:text.c_str()
|
||||
encoding:NSUTF8StringEncoding]];
|
||||
});
|
||||
}
|
||||
|
||||
void UI::SetTotalMarquee(bool marquee)
|
||||
{
|
||||
run_on_main([marquee] { [GetView() SetTotalMarquee:marquee]; });
|
||||
}
|
||||
|
||||
void UI::SetCurrentMarquee(bool marquee)
|
||||
{
|
||||
run_on_main([&] { [GetView() SetCurrentMarquee:marquee]; });
|
||||
}
|
||||
|
||||
void UI::ResetTotalProgress()
|
||||
{
|
||||
run_on_main([] { SetTotalProgress(0, 1); });
|
||||
}
|
||||
|
||||
void UI::ResetCurrentProgress()
|
||||
{
|
||||
run_on_main([] { SetCurrentProgress(0, 1); });
|
||||
}
|
||||
|
||||
void UI::SetCurrentProgress(int current, int total)
|
||||
{
|
||||
run_on_main([&] { [GetView() SetCurrentProgress:(double)current total:(double)total]; });
|
||||
}
|
||||
|
||||
void UI::SetTotalProgress(int current, int total)
|
||||
{
|
||||
run_on_main([&] { [GetView() SetTotalProgress:(double)current total:(double)total]; });
|
||||
}
|
||||
|
||||
void UI::Sleep(int seconds)
|
||||
{
|
||||
[NSThread sleepForTimeInterval:static_cast<float>(seconds)];
|
||||
}
|
||||
|
||||
void UI::WaitForPID(u32 pid)
|
||||
{
|
||||
for (int res = kill(pid, 0); res == 0 || (res < 0 && errno == EPERM); res = kill(pid, 0))
|
||||
{
|
||||
UI::Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
void UI::LaunchApplication(std::string path)
|
||||
{
|
||||
[[NSWorkspace sharedWorkspace]
|
||||
launchApplication:[NSString stringWithCString:path.c_str()
|
||||
encoding:[NSString defaultCStringEncoding]]];
|
||||
}
|
||||
|
||||
void UI::Stop()
|
||||
{
|
||||
run_on_main([] { [NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0]; });
|
||||
}
|
||||
|
||||
// Stub. Only needed on Windows
|
||||
void UI::Init()
|
||||
{
|
||||
}
|
||||
|
||||
// test-updater.py only works on Windows.
|
||||
bool UI::IsTestMode()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Platform::VersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
|
||||
const std::string& install_base_path, const std::string& temp_dir)
|
||||
{
|
||||
const auto op_it = std::ranges::find(to_update, "Dolphin.app/Contents/Info.plist",
|
||||
&TodoList::UpdateOp::filename);
|
||||
if (op_it == to_update.cend())
|
||||
return true;
|
||||
|
||||
const auto op = *op_it;
|
||||
std::string plist_path = temp_dir + "/" + HexEncode(op.new_hash.data(), op.new_hash.size());
|
||||
|
||||
NSData* data = [NSData dataWithContentsOfFile:[NSString stringWithCString:plist_path.c_str()]];
|
||||
if (!data)
|
||||
{
|
||||
LogToFile("Failed to read %s, skipping platform version check.\n", plist_path.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
NSError* error = nil;
|
||||
NSDictionary* info_dict =
|
||||
[NSPropertyListSerialization propertyListWithData:data
|
||||
options:NSPropertyListImmutable
|
||||
format:nil
|
||||
error:&error];
|
||||
if (error)
|
||||
{
|
||||
LogToFile("Failed to parse %s, skipping platform version check.\n", plist_path.c_str());
|
||||
return true;
|
||||
}
|
||||
NSString* min_version_str = info_dict[@"LSMinimumSystemVersion"];
|
||||
if (!min_version_str)
|
||||
{
|
||||
LogToFile("LSMinimumSystemVersion key missing, skipping platform version check.\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
NSArray* components = [min_version_str componentsSeparatedByString:@"."];
|
||||
NSOperatingSystemVersion next_version{
|
||||
[components[0] integerValue], [components[1] integerValue], [components[2] integerValue]};
|
||||
|
||||
LogToFile("Platform version check: next_version=%ld.%ld.%ld\n", (long)next_version.majorVersion,
|
||||
(long)next_version.minorVersion, (long)next_version.patchVersion);
|
||||
|
||||
if (![[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:next_version])
|
||||
{
|
||||
UI::Error("Please update macOS in order to update Dolphin.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<deployment version="101000" identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="Updater" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Updater" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About Updater" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Quit Updater" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="0.0"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="Dolphin Updater" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" titlebarAppearsTransparent="YES" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="250"/>
|
||||
</scene>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" sceneMemberID="viewController">
|
||||
<view key="view" wantsLayer="YES" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="133"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<progressIndicator identifier="progressTotal" wantsLayer="YES" fixedFrame="YES" maxValue="100" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="oLS-AW-V72">
|
||||
<rect key="frame" x="20" y="94" width="440" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
</progressIndicator>
|
||||
<progressIndicator identifier="progressCurrent" wantsLayer="YES" fixedFrame="YES" maxValue="100" indeterminate="YES" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="mni-rL-166">
|
||||
<rect key="frame" x="20" y="52" width="440" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
</progressIndicator>
|
||||
<textField identifier="labelProgress" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="r87-pn-Exw" userLabel="Label">
|
||||
<rect key="frame" x="18" y="7" width="444" height="38"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<textFieldCell key="cell" truncatesLastVisibleLine="YES" title="Initializing..." id="FBK-Tz-5cA">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<action selector="progressLabel:" target="XfG-lQ-9wD" id="PDs-H5-idS"/>
|
||||
</connections>
|
||||
</textField>
|
||||
</subviews>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="foo" destination="r87-pn-Exw" id="i76-Sg-K3o"/>
|
||||
<outlet property="labelProgress" destination="r87-pn-Exw" id="v6b-r7-3lR"/>
|
||||
<outlet property="progressCurrent" destination="mni-rL-166" id="A3C-wp-83j"/>
|
||||
<outlet property="progressTotal" destination="oLS-AW-V72" id="pwo-CZ-XyN"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="653.5"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright 2019 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface ViewController : NSViewController
|
||||
|
||||
@property(assign) IBOutlet NSProgressIndicator* progressCurrent;
|
||||
@property(assign) IBOutlet NSProgressIndicator* progressTotal;
|
||||
@property(assign) IBOutlet NSTextField* labelProgress;
|
||||
|
||||
- (void)SetDescription:(NSString*)string;
|
||||
|
||||
- (void)SetTotalMarquee:(bool)marquee;
|
||||
- (void)SetCurrentMarquee:(bool)marquee;
|
||||
|
||||
- (void)SetTotalProgress:(double)current total:(double)total;
|
||||
- (void)SetCurrentProgress:(double)current total:(double)total;
|
||||
|
||||
@end
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright 2019 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#import "ViewController.h"
|
||||
|
||||
@implementation ViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
NSWindow* window = [[self view] window];
|
||||
|
||||
[window setLevel:kCGMainMenuWindowLevel - 1];
|
||||
[window setCollectionBehavior:NSWindowCollectionBehaviorStationary |
|
||||
NSWindowCollectionBehaviorCanJoinAllSpaces |
|
||||
NSWindowCollectionBehaviorFullScreenAuxiliary];
|
||||
}
|
||||
|
||||
- (void)SetDescription:(NSString*)string
|
||||
{
|
||||
[_labelProgress setStringValue:string];
|
||||
}
|
||||
|
||||
- (void)SetTotalMarquee:(bool)marquee
|
||||
{
|
||||
[_progressTotal setIndeterminate:marquee];
|
||||
}
|
||||
|
||||
- (void)SetCurrentMarquee:(bool)marquee
|
||||
{
|
||||
[_progressCurrent setIndeterminate:marquee];
|
||||
}
|
||||
|
||||
- (void)SetTotalProgress:(double)current total:(double)total
|
||||
{
|
||||
[_progressTotal setMaxValue:total];
|
||||
[_progressTotal setDoubleValue:current];
|
||||
}
|
||||
|
||||
- (void)SetCurrentProgress:(double)current total:(double)total
|
||||
{
|
||||
[_progressCurrent setMaxValue:total];
|
||||
[_progressCurrent setDoubleValue:current];
|
||||
}
|
||||
|
||||
- (void)setRepresentedObject:(id)representedObject
|
||||
{
|
||||
[super setRepresentedObject:representedObject];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright 2019 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
int main(int argc, const char** argv)
|
||||
{
|
||||
if (argc == 1)
|
||||
{
|
||||
NSAlert* alert = [[[NSAlert alloc] init] autorelease];
|
||||
|
||||
[alert setMessageText:@"This updater is not meant to be launched directly."];
|
||||
[alert setAlertStyle:NSAlertStyleWarning];
|
||||
[alert setInformativeText:@"Configure Auto-Update in Dolphin's settings instead."];
|
||||
[alert runModal];
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return NSApplicationMain(argc, argv);
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
// Copyright 2018 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "UICommon/AutoUpdate.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <picojson.h>
|
||||
|
||||
#include "Common/HttpRequest.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/MsgHandler.h"
|
||||
#include "Common/ScopeGuard.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Common/Version.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
#define OS_SUPPORTS_UPDATER
|
||||
#include "Common/CommonFuncs.h"
|
||||
#include "Common/CommonPaths.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#endif
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
std::atomic_bool s_check_in_progress = false;
|
||||
bool s_update_triggered = false;
|
||||
|
||||
#ifdef __APPLE__
|
||||
const char UPDATER_CONTENT_PATH[] = "/Contents/MacOS/Dolphin Updater";
|
||||
#endif
|
||||
|
||||
#ifdef OS_SUPPORTS_UPDATER
|
||||
|
||||
const char UPDATER_LOG_FILE[] = "Updater.log";
|
||||
|
||||
std::string UpdaterPath(bool relocated = false)
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
if (relocated)
|
||||
return File::GetExeDirectory() + DIR_SEP + ".Dolphin Updater.2.app";
|
||||
else
|
||||
return File::GetBundleDirectory() + DIR_SEP + "Contents/Helpers/Dolphin Updater.app";
|
||||
#else
|
||||
return File::GetExeDirectory() + DIR_SEP + "Updater.exe";
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string MakeUpdaterCommandLine(const std::map<std::string, std::string>& flags)
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
std::string cmdline = "\"" + UpdaterPath(true) + UPDATER_CONTENT_PATH + "\"";
|
||||
#else
|
||||
std::string cmdline = UpdaterPath();
|
||||
#endif
|
||||
|
||||
cmdline += " ";
|
||||
|
||||
for (const auto& pair : flags)
|
||||
{
|
||||
std::string value = "--" + pair.first + "=" + pair.second;
|
||||
value = ReplaceAll(value, "\"", "\\\""); // Escape double quotes.
|
||||
value = "\"" + value + "\" ";
|
||||
cmdline += value;
|
||||
}
|
||||
return cmdline;
|
||||
}
|
||||
|
||||
void CleanupFromPreviousUpdate()
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
// Remove the relocated updater file.
|
||||
File::DeleteDirRecursively(UpdaterPath(true));
|
||||
|
||||
// Remove the old (non-embedded) updater app bundle.
|
||||
// While the update process will delete the files within the old bundle after updating to a
|
||||
// version with an embedded updater, it won't delete the folder structure of the bundle, so
|
||||
// we should clean those leftovers up.
|
||||
File::DeleteDirRecursively(File::GetExeDirectory() + DIR_SEP + "Dolphin Updater.app");
|
||||
#endif
|
||||
|
||||
// Updater.log was moved from GetExeDirectory() to GetUserPath(D_LOGS_IDX) in 5.0-14529.
|
||||
File::Delete(File::GetExeDirectory() + DIR_SEP + "Updater.log",
|
||||
File::IfAbsentBehavior::NoConsoleWarning);
|
||||
}
|
||||
#endif
|
||||
|
||||
// This ignores i18n because most of the text in there (change descriptions) is only going to be
|
||||
// written in english anyway.
|
||||
std::string GenerateChangelog(const picojson::array& versions)
|
||||
{
|
||||
std::string changelog;
|
||||
for (const auto& ver : versions)
|
||||
{
|
||||
if (!ver.is<picojson::object>())
|
||||
continue;
|
||||
picojson::object ver_obj = ver.get<picojson::object>();
|
||||
|
||||
if (ver_obj["changelog_html"].is<picojson::null>())
|
||||
{
|
||||
if (!changelog.empty())
|
||||
changelog += "<div style=\"margin-top: 0.4em;\"></div>"; // Vertical spacing.
|
||||
|
||||
// Try to link to the PR if we have this info. Otherwise just show shortrev.
|
||||
if (ver_obj["pr_url"].is<std::string>())
|
||||
{
|
||||
changelog += "<a href=\"" + ver_obj["pr_url"].get<std::string>() + "\">" +
|
||||
ver_obj["shortrev"].get<std::string>() + "</a>";
|
||||
}
|
||||
else
|
||||
{
|
||||
changelog += ver_obj["shortrev"].get<std::string>();
|
||||
}
|
||||
const std::string escaped_description =
|
||||
Common::GetEscapedHtml(ver_obj["short_descr"].get<std::string>());
|
||||
changelog += " by <a href = \"" + ver_obj["author_url"].get<std::string>() + "\">" +
|
||||
ver_obj["author"].get<std::string>() + "</a> — " + escaped_description;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!changelog.empty())
|
||||
changelog += "<hr>";
|
||||
changelog += "<b>Dolphin " + ver_obj["shortrev"].get<std::string>() + "</b>";
|
||||
changelog += "<p>" + ver_obj["changelog_html"].get<std::string>() + "</p>";
|
||||
}
|
||||
}
|
||||
return changelog;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool AutoUpdateChecker::SystemSupportsAutoUpdates()
|
||||
{
|
||||
#if defined(AUTOUPDATE) && defined(OS_SUPPORTS_UPDATER)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static std::string GetPlatformID()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
#if defined(_M_ARM_64)
|
||||
return "win-arm64";
|
||||
#else
|
||||
return "win";
|
||||
#endif
|
||||
#elif defined(__APPLE__)
|
||||
#if defined(MACOS_UNIVERSAL_BUILD)
|
||||
return "macos-universal";
|
||||
#else
|
||||
return "macos";
|
||||
#endif
|
||||
#else
|
||||
return "unknown";
|
||||
#endif
|
||||
}
|
||||
|
||||
static std::string GetUpdateServerUrl()
|
||||
{
|
||||
auto server_url = std::getenv("DOLPHIN_UPDATE_SERVER_URL");
|
||||
if (server_url)
|
||||
return server_url;
|
||||
return "https://dolphin-emu.org";
|
||||
}
|
||||
|
||||
static u32 GetOwnProcessId()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return GetCurrentProcessId();
|
||||
#else
|
||||
return getpid();
|
||||
#endif
|
||||
}
|
||||
|
||||
void AutoUpdateChecker::CheckForUpdate(std::string_view update_track,
|
||||
std::string_view hash_override, const CheckType check_type)
|
||||
{
|
||||
bool expected_check_in_progress = false;
|
||||
if (!s_check_in_progress.compare_exchange_strong(expected_check_in_progress, true))
|
||||
return;
|
||||
|
||||
Common::ScopeGuard guard([]() { s_check_in_progress.store(false); });
|
||||
|
||||
if (s_update_triggered)
|
||||
{
|
||||
if (check_type == CheckType::Manual)
|
||||
SuccessAlertFmtT("A Dolphin update is already scheduled for the next time it closes.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't bother checking if updates are not supported or not enabled.
|
||||
if (!SystemSupportsAutoUpdates() || update_track.empty())
|
||||
return;
|
||||
|
||||
#ifdef OS_SUPPORTS_UPDATER
|
||||
CleanupFromPreviousUpdate();
|
||||
#endif
|
||||
|
||||
std::string_view version_hash = hash_override.empty() ? Common::GetScmRevGitStr() : hash_override;
|
||||
std::string url = fmt::format("{}/update/check/v1/{}/{}/{}", GetUpdateServerUrl(), update_track,
|
||||
version_hash, GetPlatformID());
|
||||
|
||||
const bool is_manual_check = check_type == CheckType::Manual;
|
||||
|
||||
Common::HttpRequest req{std::chrono::seconds{10}};
|
||||
auto resp = req.Get(url);
|
||||
if (!resp)
|
||||
{
|
||||
if (is_manual_check)
|
||||
CriticalAlertFmtT("Unable to contact update server.");
|
||||
return;
|
||||
}
|
||||
const std::string contents(reinterpret_cast<char*>(resp->data()), resp->size());
|
||||
INFO_LOG_FMT(COMMON, "Auto-update JSON response: {}", contents);
|
||||
|
||||
picojson::value json;
|
||||
const std::string err = picojson::parse(json, contents);
|
||||
if (!err.empty())
|
||||
{
|
||||
CriticalAlertFmtT("Invalid JSON received from auto-update service : {0}", err);
|
||||
return;
|
||||
}
|
||||
picojson::object obj = json.get<picojson::object>();
|
||||
|
||||
if (obj["status"].get<std::string>() != "outdated")
|
||||
{
|
||||
if (is_manual_check)
|
||||
SuccessAlertFmtT("You are running the latest version available on this update track.");
|
||||
INFO_LOG_FMT(COMMON, "Auto-update status: we are up to date.");
|
||||
return;
|
||||
}
|
||||
|
||||
NewVersionInformation nvi;
|
||||
nvi.this_manifest_url = obj["old"].get<picojson::object>()["manifest"].get<std::string>();
|
||||
nvi.next_manifest_url = obj["new"].get<picojson::object>()["manifest"].get<std::string>();
|
||||
nvi.content_store_url = obj["content-store"].get<std::string>();
|
||||
nvi.new_shortrev = obj["new"].get<picojson::object>()["name"].get<std::string>();
|
||||
nvi.new_hash = obj["new"].get<picojson::object>()["hash"].get<std::string>();
|
||||
|
||||
// TODO: generate the HTML changelog from the JSON information.
|
||||
nvi.changelog_html = GenerateChangelog(obj["changelog"].get<picojson::array>());
|
||||
|
||||
if (std::getenv("DOLPHIN_UPDATE_TEST_DONE"))
|
||||
{
|
||||
// We are at end of updater test flow, send a message to server, which will kill us.
|
||||
req.Get(fmt::format("{}/update-test-done/{}", GetUpdateServerUrl(), GetOwnProcessId()));
|
||||
}
|
||||
else
|
||||
{
|
||||
OnUpdateAvailable(nvi);
|
||||
}
|
||||
}
|
||||
|
||||
void AutoUpdateChecker::TriggerUpdate(const AutoUpdateChecker::NewVersionInformation& info,
|
||||
const AutoUpdateChecker::RestartMode restart_mode)
|
||||
{
|
||||
// Check to make sure we don't already have an update triggered
|
||||
if (s_update_triggered)
|
||||
{
|
||||
WARN_LOG_FMT(COMMON, "Auto-update: received a redundant trigger request, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef OS_SUPPORTS_UPDATER
|
||||
std::map<std::string, std::string> updater_flags;
|
||||
updater_flags["this-manifest-url"] = info.this_manifest_url;
|
||||
updater_flags["next-manifest-url"] = info.next_manifest_url;
|
||||
updater_flags["content-store-url"] = info.content_store_url;
|
||||
updater_flags["parent-pid"] = std::to_string(GetOwnProcessId());
|
||||
updater_flags["install-base-path"] = File::GetExeDirectory();
|
||||
updater_flags["log-file"] = File::GetUserPath(D_LOGS_IDX) + UPDATER_LOG_FILE;
|
||||
|
||||
if (restart_mode == RestartMode::RESTART_AFTER_UPDATE)
|
||||
updater_flags["binary-to-restart"] = File::GetExePath();
|
||||
|
||||
#ifdef __APPLE__
|
||||
// Copy the updater so it can update itself if needed.
|
||||
const std::string reloc_updater_path = UpdaterPath(true);
|
||||
if (!File::Copy(UpdaterPath(), reloc_updater_path))
|
||||
{
|
||||
CriticalAlertFmtT("Unable to create updater copy.");
|
||||
return;
|
||||
}
|
||||
if (chmod((reloc_updater_path + UPDATER_CONTENT_PATH).c_str(), 0700) != 0)
|
||||
{
|
||||
CriticalAlertFmtT("Unable to set permissions on updater copy.");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Run the updater!
|
||||
std::string command_line = MakeUpdaterCommandLine(updater_flags);
|
||||
INFO_LOG_FMT(COMMON, "Updater command line: {}", command_line);
|
||||
|
||||
#ifdef _WIN32
|
||||
STARTUPINFO sinfo{.cb = sizeof(sinfo)};
|
||||
sinfo.dwFlags = STARTF_FORCEOFFFEEDBACK; // No hourglass cursor after starting the process.
|
||||
PROCESS_INFORMATION pinfo;
|
||||
if (CreateProcessW(UTF8ToWString(UpdaterPath()).c_str(), UTF8ToWString(command_line).data(),
|
||||
nullptr, nullptr, FALSE, 0, nullptr, nullptr, &sinfo, &pinfo))
|
||||
{
|
||||
CloseHandle(pinfo.hThread);
|
||||
CloseHandle(pinfo.hProcess);
|
||||
s_update_triggered = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string error = Common::GetLastErrorString();
|
||||
CriticalAlertFmtT("Could not start updater process: {0}", error);
|
||||
}
|
||||
#else
|
||||
if (popen(command_line.c_str(), "r") == nullptr)
|
||||
{
|
||||
const std::string error = Common::LastStrerrorString();
|
||||
CriticalAlertFmtT("Could not start updater process: {0}", error);
|
||||
}
|
||||
else
|
||||
{
|
||||
s_update_triggered = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright 2018 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
|
||||
// This class defines all the logic for Dolphin auto-update checking. UI-specific elements have to
|
||||
// be defined in a backend specific subclass.
|
||||
class AutoUpdateChecker
|
||||
{
|
||||
public:
|
||||
enum class CheckType
|
||||
{
|
||||
Automatic,
|
||||
Manual,
|
||||
};
|
||||
// Initiates a check for updates in the background. Calls the OnUpdateAvailable callback if an
|
||||
// update is available, does "nothing" otherwise.
|
||||
void CheckForUpdate(std::string_view update_track, std::string_view hash_override,
|
||||
CheckType check_type);
|
||||
|
||||
static bool SystemSupportsAutoUpdates();
|
||||
|
||||
struct NewVersionInformation
|
||||
{
|
||||
// Name (5.0-1234) and revision hash of the new version.
|
||||
std::string new_shortrev;
|
||||
std::string new_hash;
|
||||
|
||||
// The full changelog in HTML format.
|
||||
std::string changelog_html;
|
||||
|
||||
// Internals, to be passed to the updater binary.
|
||||
std::string this_manifest_url;
|
||||
std::string next_manifest_url;
|
||||
std::string content_store_url;
|
||||
};
|
||||
|
||||
// Starts the updater process, which will wait in the background until the current process exits.
|
||||
enum class RestartMode
|
||||
{
|
||||
NO_RESTART_AFTER_UPDATE = 0,
|
||||
RESTART_AFTER_UPDATE,
|
||||
};
|
||||
void TriggerUpdate(const NewVersionInformation& info, RestartMode restart_mode);
|
||||
|
||||
protected:
|
||||
virtual void OnUpdateAvailable(const NewVersionInformation& info) = 0;
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright 2018 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/StringUtil.h"
|
||||
|
||||
#include "UpdaterCommon/UpdaterCommon.h"
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
bool VersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
|
||||
const std::string& install_base_path, const std::string& temp_dir);
|
||||
} // namespace Platform
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright 2018 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
namespace UI
|
||||
{
|
||||
void Error(const std::string& text);
|
||||
|
||||
void SetDescription(const std::string& text);
|
||||
|
||||
void SetTotalMarquee(bool marquee);
|
||||
void ResetTotalProgress();
|
||||
void SetTotalProgress(int current, int total);
|
||||
|
||||
void SetCurrentMarquee(bool marquee);
|
||||
void ResetCurrentProgress();
|
||||
void SetCurrentProgress(int current, int total);
|
||||
|
||||
void SetVisible(bool visible);
|
||||
|
||||
void Stop();
|
||||
|
||||
void Init();
|
||||
void Sleep(int seconds);
|
||||
void WaitForPID(u32 pid);
|
||||
void LaunchApplication(std::string path);
|
||||
|
||||
bool IsTestMode();
|
||||
} // namespace UI
|
||||
@@ -1,794 +0,0 @@
|
||||
// Copyright 2019 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "UpdaterCommon/UpdaterCommon.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include <OptionParser.h>
|
||||
#include <ed25519.h>
|
||||
#include <fmt/format.h>
|
||||
#include <mbedtls/base64.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include <zlib.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "Common/CommonFuncs.h"
|
||||
#endif
|
||||
#include "Common/CommonPaths.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/HttpRequest.h"
|
||||
#include "Common/IOFile.h"
|
||||
#include "Common/ScopeGuard.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "UpdaterCommon/Platform.h"
|
||||
#include "UpdaterCommon/UI.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <filesystem>
|
||||
#endif
|
||||
|
||||
|
||||
// Public key used to verify update manifests.
|
||||
const std::array<u8, 32> UPDATE_PUB_KEY = {
|
||||
0x2a, 0xb3, 0xd1, 0xdc, 0x6e, 0xf5, 0x07, 0xf6, 0xa0, 0x6c, 0x7c, 0x54, 0xdf, 0x54, 0xf4, 0x42,
|
||||
0x80, 0xa6, 0x28, 0x8b, 0x6d, 0x70, 0x14, 0xb5, 0x4c, 0x34, 0x95, 0x20, 0x4d, 0xd4, 0xd3, 0x5d};
|
||||
// The private key for UPDATE_PUB_KEY_TEST is in Tools/test-updater.py
|
||||
const std::array<u8, 32> UPDATE_PUB_KEY_TEST = {
|
||||
0x0c, 0x5f, 0xdc, 0xd1, 0x15, 0x71, 0xfb, 0x86, 0x4f, 0x9e, 0x6d, 0xe6, 0x65, 0x39, 0x43, 0xe1,
|
||||
0x9e, 0xe0, 0x9b, 0x28, 0xc9, 0x1a, 0x60, 0xb7, 0x67, 0x1c, 0xf3, 0xf6, 0xca, 0x1b, 0xdd, 0x1a};
|
||||
|
||||
// Where to log updater output.
|
||||
static File::IOFile log_file;
|
||||
|
||||
void LogToFile(const char* fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
|
||||
log_file.WriteString(StringFromFormatV(fmt, args));
|
||||
log_file.Flush();
|
||||
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
static bool ProgressCallback(s64 total, s64 now, s64, s64)
|
||||
{
|
||||
UI::SetCurrentProgress(static_cast<int>(now), static_cast<int>(total));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string HexEncode(const u8* buffer, size_t size)
|
||||
{
|
||||
std::string out(size * 2, '\0');
|
||||
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
{
|
||||
out[2 * i] = "0123456789abcdef"[buffer[i] >> 4];
|
||||
out[2 * i + 1] = "0123456789abcdef"[buffer[i] & 0xF];
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool HexDecode(const std::string& hex, u8* buffer, size_t size)
|
||||
{
|
||||
if (hex.size() != size * 2)
|
||||
return false;
|
||||
|
||||
auto DecodeNibble = [](char c) -> std::optional<u8> {
|
||||
if (c >= '0' && c <= '9')
|
||||
return static_cast<u8>(c - '0');
|
||||
else if (c >= 'a' && c <= 'f')
|
||||
return static_cast<u8>(c - 'a' + 10);
|
||||
else if (c >= 'A' && c <= 'F')
|
||||
return static_cast<u8>(c - 'A' + 10);
|
||||
else
|
||||
return {};
|
||||
};
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
{
|
||||
std::optional<u8> high = DecodeNibble(hex[2 * i]);
|
||||
std::optional<u8> low = DecodeNibble(hex[2 * i + 1]);
|
||||
|
||||
if (!high || !low)
|
||||
return false;
|
||||
|
||||
buffer[i] = (*high << 4) | *low;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::optional<std::string> GzipInflate(const std::string& data)
|
||||
{
|
||||
z_stream zstrm;
|
||||
zstrm.zalloc = nullptr;
|
||||
zstrm.zfree = nullptr;
|
||||
zstrm.opaque = nullptr;
|
||||
zstrm.avail_in = static_cast<u32>(data.size());
|
||||
zstrm.next_in = reinterpret_cast<u8*>(const_cast<char*>(data.data()));
|
||||
|
||||
// 16 + MAX_WBITS means gzip. Don't ask me.
|
||||
inflateInit2(&zstrm, 16 + MAX_WBITS);
|
||||
|
||||
std::string out;
|
||||
const size_t buf_len = 20 * 1024 * 1024;
|
||||
auto buffer = std::make_unique<char[]>(buf_len);
|
||||
int ret;
|
||||
|
||||
do
|
||||
{
|
||||
zstrm.avail_out = buf_len;
|
||||
zstrm.next_out = reinterpret_cast<u8*>(buffer.get());
|
||||
|
||||
ret = inflate(&zstrm, 0);
|
||||
out.append(buffer.get(), buf_len - zstrm.avail_out);
|
||||
} while (ret == Z_OK);
|
||||
|
||||
inflateEnd(&zstrm);
|
||||
|
||||
if (ret != Z_STREAM_END)
|
||||
{
|
||||
LogToFile("Could not read the data as gzip: error %d.\n", ret);
|
||||
return {};
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Manifest::Hash ComputeHash(const std::string& contents)
|
||||
{
|
||||
std::array<u8, 32> full;
|
||||
mbedtls_sha256_ret(reinterpret_cast<const u8*>(contents.data()), contents.size(), full.data(),
|
||||
false);
|
||||
|
||||
Manifest::Hash out;
|
||||
std::copy_n(full.begin(), 16, out.begin());
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool VerifySignature(const std::string& data, const std::string& b64_signature)
|
||||
{
|
||||
u8 signature[64]; // ed25519 sig size.
|
||||
size_t sig_size;
|
||||
|
||||
if (mbedtls_base64_decode(signature, sizeof(signature), &sig_size,
|
||||
reinterpret_cast<const u8*>(b64_signature.data()),
|
||||
b64_signature.size()) ||
|
||||
sig_size != sizeof(signature))
|
||||
{
|
||||
LogToFile("Invalid base64: %s\n", b64_signature.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& pub_key = UI::IsTestMode() ? UPDATE_PUB_KEY_TEST : UPDATE_PUB_KEY;
|
||||
return ed25519_verify(signature, reinterpret_cast<const u8*>(data.data()), data.size(),
|
||||
pub_key.data());
|
||||
}
|
||||
|
||||
static void FlushLog()
|
||||
{
|
||||
log_file.Flush();
|
||||
log_file.Close();
|
||||
}
|
||||
|
||||
void TodoList::Log() const
|
||||
{
|
||||
if (to_update.size())
|
||||
{
|
||||
LogToFile("Updating:\n");
|
||||
for (const auto& op : to_update)
|
||||
{
|
||||
std::string old_desc =
|
||||
op.old_hash ? HexEncode(op.old_hash->data(), op.old_hash->size()) : "(new)";
|
||||
LogToFile(" - %s: %s -> %s\n", op.filename.c_str(), old_desc.c_str(),
|
||||
HexEncode(op.new_hash.data(), op.new_hash.size()).c_str());
|
||||
}
|
||||
}
|
||||
if (to_delete.size())
|
||||
{
|
||||
LogToFile("Deleting:\n");
|
||||
for (const auto& op : to_delete)
|
||||
{
|
||||
LogToFile(" - %s (%s)\n", op.filename.c_str(),
|
||||
HexEncode(op.old_hash.data(), op.old_hash.size()).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool DownloadContent(std::span<const TodoList::DownloadOp> to_download,
|
||||
const std::string& content_base_url, const std::string& temp_path)
|
||||
{
|
||||
Common::HttpRequest req(std::chrono::seconds(30), ProgressCallback);
|
||||
|
||||
UI::SetTotalMarquee(false);
|
||||
|
||||
for (size_t i = 0; i < to_download.size(); i++)
|
||||
{
|
||||
UI::SetTotalProgress(static_cast<int>(i + 1), static_cast<int>(to_download.size()));
|
||||
|
||||
auto& download = to_download[i];
|
||||
|
||||
std::string hash_filename = HexEncode(download.hash.data(), download.hash.size());
|
||||
|
||||
// File already exists, skipping
|
||||
if (File::Exists(temp_path + DIR_SEP + hash_filename))
|
||||
continue;
|
||||
|
||||
UI::SetDescription(fmt::format("Downloading {}... (File {} of {})", download.filename, i + 1,
|
||||
to_download.size()));
|
||||
UI::SetCurrentMarquee(false);
|
||||
|
||||
// Add slashes where needed.
|
||||
std::string content_store_path = hash_filename;
|
||||
content_store_path.insert(4, "/");
|
||||
content_store_path.insert(2, "/");
|
||||
|
||||
std::string url = content_base_url + content_store_path;
|
||||
LogToFile("Downloading %s ...\n", url.c_str());
|
||||
|
||||
auto resp = req.Get(url);
|
||||
if (!resp)
|
||||
return false;
|
||||
|
||||
UI::SetCurrentMarquee(true);
|
||||
UI::SetDescription("Verifying " + download.filename + "...");
|
||||
|
||||
std::string contents(reinterpret_cast<char*>(resp->data()), resp->size());
|
||||
std::optional<std::string> maybe_decompressed = GzipInflate(contents);
|
||||
if (!maybe_decompressed)
|
||||
return false;
|
||||
const std::string decompressed = std::move(*maybe_decompressed);
|
||||
|
||||
// Check that the downloaded contents have the right hash.
|
||||
Manifest::Hash contents_hash = ComputeHash(decompressed);
|
||||
if (contents_hash != download.hash)
|
||||
{
|
||||
LogToFile("Wrong hash on downloaded content %s.\n", url.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string out = temp_path + DIR_SEP + hash_filename;
|
||||
if (!File::WriteStringToFile(out, decompressed))
|
||||
{
|
||||
LogToFile("Could not write cache file %s.\n", out.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool PlatformVersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
|
||||
const std::string& install_base_path, const std::string& temp_dir)
|
||||
{
|
||||
UI::SetDescription("Checking platform...");
|
||||
return Platform::VersionCheck(to_update, install_base_path, temp_dir);
|
||||
}
|
||||
|
||||
static TodoList ComputeActionsToDo(Manifest this_manifest, Manifest next_manifest)
|
||||
{
|
||||
TodoList todo;
|
||||
|
||||
// Delete if present in this manifest but not in next manifest.
|
||||
for (const auto& entry : this_manifest.entries)
|
||||
{
|
||||
if (!next_manifest.entries.contains(entry.first))
|
||||
{
|
||||
TodoList::DeleteOp del;
|
||||
del.filename = entry.first;
|
||||
del.old_hash = entry.second;
|
||||
todo.to_delete.push_back(std::move(del));
|
||||
}
|
||||
}
|
||||
|
||||
// Download and update if present in next manifest with different hash from this manifest.
|
||||
for (const auto& entry : next_manifest.entries)
|
||||
{
|
||||
std::optional<Manifest::Hash> old_hash;
|
||||
|
||||
const auto& old_entry = this_manifest.entries.find(entry.first);
|
||||
if (old_entry != this_manifest.entries.end())
|
||||
old_hash = old_entry->second;
|
||||
|
||||
if (!old_hash || *old_hash != entry.second)
|
||||
{
|
||||
TodoList::DownloadOp download;
|
||||
download.filename = entry.first;
|
||||
download.hash = entry.second;
|
||||
|
||||
todo.to_download.push_back(std::move(download));
|
||||
|
||||
TodoList::UpdateOp update;
|
||||
update.filename = entry.first;
|
||||
update.old_hash = old_hash;
|
||||
update.new_hash = entry.second;
|
||||
todo.to_update.push_back(std::move(update));
|
||||
}
|
||||
}
|
||||
|
||||
return todo;
|
||||
}
|
||||
|
||||
static void CleanUpTempDir(const std::string& temp_dir, const TodoList& todo)
|
||||
{
|
||||
// This is best-effort cleanup, we ignore most errors.
|
||||
for (const auto& download : todo.to_download)
|
||||
File::Delete(temp_dir + DIR_SEP + HexEncode(download.hash.data(), download.hash.size()));
|
||||
File::DeleteDir(temp_dir);
|
||||
}
|
||||
|
||||
static bool BackupFile(const std::string& path)
|
||||
{
|
||||
std::string backup_path = path + ".bak";
|
||||
LogToFile("Backing up existing %s to .bak.\n", path.c_str());
|
||||
if (!File::Rename(path, backup_path))
|
||||
{
|
||||
LogToFile("Could not rename %s to %s for backup.\n", path.c_str(), backup_path.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool DeleteObsoleteFiles(std::span<const TodoList::DeleteOp> to_delete,
|
||||
const std::string& install_base_path)
|
||||
{
|
||||
for (const auto& op : to_delete)
|
||||
{
|
||||
std::string path = install_base_path + DIR_SEP + op.filename;
|
||||
|
||||
if (!File::Exists(path))
|
||||
{
|
||||
LogToFile("File %s is already missing.\n", op.filename.c_str());
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string contents;
|
||||
if (!File::ReadFileToString(path, contents))
|
||||
{
|
||||
LogToFile("Could not read file planned for deletion: %s.\n", op.filename.c_str());
|
||||
return false;
|
||||
}
|
||||
Manifest::Hash contents_hash = ComputeHash(contents);
|
||||
if (contents_hash != op.old_hash)
|
||||
{
|
||||
if (!BackupFile(path))
|
||||
return false;
|
||||
}
|
||||
|
||||
File::Delete(path);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool UpdateFiles(std::span<const TodoList::UpdateOp> to_update,
|
||||
const std::string& install_base_path, const std::string& temp_path)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
const auto self_path = std::filesystem::path(Common::GetModuleName(nullptr).value());
|
||||
const auto self_filename = self_path.filename();
|
||||
#endif
|
||||
|
||||
for (const auto& op : to_update)
|
||||
{
|
||||
std::string path = install_base_path + DIR_SEP + op.filename;
|
||||
if (!File::CreateFullPath(path))
|
||||
{
|
||||
LogToFile("Could not create directory structure for %s.\n", op.filename.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
// TODO: A new updater protocol version is required to properly mark executable files. For
|
||||
// now, copy executable bits from existing files. This will break for newly added executables.
|
||||
std::optional<mode_t> permission;
|
||||
#endif
|
||||
|
||||
if (File::Exists(path))
|
||||
{
|
||||
#ifndef _WIN32
|
||||
struct stat file_stats;
|
||||
|
||||
if (lstat(path.c_str(), &file_stats) != 0)
|
||||
continue;
|
||||
|
||||
if (S_ISLNK(file_stats.st_mode))
|
||||
{
|
||||
LogToFile("%s is symlink, skipping\n", path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
permission = file_stats.st_mode;
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
// If incoming file would overwrite the currently executing file, rename ourself to allow the
|
||||
// overwrite to complete. Renaming ourself while executing is fine, but deleting ourself is
|
||||
// rather tricky. The best way to handle that would be to execute the newly-placed Updater.exe
|
||||
// after entire update has completed, and have it delete our relocated executable. For now we
|
||||
// just let the relocated file hang around.
|
||||
// It is enough to match based on filename, don't need File/VolumeId etc.
|
||||
const bool is_self = op.filename == self_filename;
|
||||
#else
|
||||
// On other platforms, the renaming is handled by Dolphin before running the Updater.
|
||||
const bool is_self = false;
|
||||
#endif
|
||||
|
||||
std::string contents;
|
||||
if (!File::ReadFileToString(path, contents))
|
||||
{
|
||||
LogToFile("Could not read existing file %s.\n", op.filename.c_str());
|
||||
return false;
|
||||
}
|
||||
Manifest::Hash contents_hash = ComputeHash(contents);
|
||||
if (contents_hash == op.new_hash)
|
||||
{
|
||||
LogToFile("File %s was already up to date. Partial update?\n", op.filename.c_str());
|
||||
continue;
|
||||
}
|
||||
else if (!op.old_hash || contents_hash != *op.old_hash || is_self)
|
||||
{
|
||||
if (!BackupFile(path))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can safely move the new contents to the location.
|
||||
std::string content_filename = HexEncode(op.new_hash.data(), op.new_hash.size());
|
||||
LogToFile("Updating file %s from content %s...\n", op.filename.c_str(),
|
||||
content_filename.c_str());
|
||||
#ifdef __APPLE__
|
||||
// macOS caches the code signature of Mach-O executables when they're first loaded.
|
||||
// Unfortunately, there is a quirk in the kernel with how it handles the cache: if the file is
|
||||
// simply overwritten, the cache isn't invalidated and the old code signature is used to verify
|
||||
// the new file. This causes macOS to kill the process with a code signing error. To workaround
|
||||
// this, we use File::Rename() instead of File::CopyRegularFile(). However, this also means that
|
||||
// if two files have the same hash, the first file will succeed, but the second file will fail
|
||||
// because the source file no longer exists. To deal with this, we copy the content file to a
|
||||
// temporary file and then rename the temporary file to the destination path.
|
||||
const std::string temporary_file = temp_path + DIR_SEP + "temporary_file";
|
||||
if (!File::CopyRegularFile(temp_path + DIR_SEP + content_filename, temporary_file))
|
||||
{
|
||||
LogToFile("Could not copy %s to %s.\n", content_filename.c_str(), temporary_file.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File::Rename(temporary_file, path))
|
||||
#else
|
||||
if (!File::CopyRegularFile(temp_path + DIR_SEP + content_filename, path))
|
||||
#endif
|
||||
{
|
||||
LogToFile("Could not update file %s.\n", op.filename.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
if (permission.has_value() && chmod(path.c_str(), permission.value()) != 0)
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool PerformUpdate(const TodoList& todo, const std::string& install_base_path,
|
||||
const std::string& content_base_url, const std::string& temp_path)
|
||||
{
|
||||
LogToFile("Starting download step...\n");
|
||||
if (!DownloadContent(todo.to_download, content_base_url, temp_path))
|
||||
return false;
|
||||
LogToFile("Download step completed.\n");
|
||||
|
||||
LogToFile("Starting platform version check step...\n");
|
||||
if (!PlatformVersionCheck(todo.to_update, install_base_path, temp_path))
|
||||
return false;
|
||||
LogToFile("Platform version check step completed.\n");
|
||||
|
||||
LogToFile("Starting update step...\n");
|
||||
if (!UpdateFiles(todo.to_update, install_base_path, temp_path))
|
||||
return false;
|
||||
LogToFile("Update step completed.\n");
|
||||
|
||||
LogToFile("Starting deletion step...\n");
|
||||
if (!DeleteObsoleteFiles(todo.to_delete, install_base_path))
|
||||
return false;
|
||||
LogToFile("Deletion step completed.\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void FatalError(const std::string& message)
|
||||
{
|
||||
LogToFile("%s\n", message.c_str());
|
||||
|
||||
UI::SetVisible(true);
|
||||
UI::Error(message);
|
||||
}
|
||||
|
||||
static std::optional<Manifest> ParseManifest(const std::string& manifest)
|
||||
{
|
||||
Manifest parsed;
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < manifest.size())
|
||||
{
|
||||
size_t filename_end_pos = manifest.find('\t', pos);
|
||||
if (filename_end_pos == std::string::npos)
|
||||
{
|
||||
LogToFile("Manifest entry %zu: could not find filename end.\n", parsed.entries.size());
|
||||
return {};
|
||||
}
|
||||
size_t hash_end_pos = manifest.find('\n', filename_end_pos);
|
||||
if (hash_end_pos == std::string::npos)
|
||||
{
|
||||
LogToFile("Manifest entry %zu: could not find hash end.\n", parsed.entries.size());
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string filename = manifest.substr(pos, filename_end_pos - pos);
|
||||
std::string hash = manifest.substr(filename_end_pos + 1, hash_end_pos - filename_end_pos - 1);
|
||||
if (hash.size() != 32)
|
||||
{
|
||||
LogToFile("Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(), hash.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
Manifest::Hash decoded_hash;
|
||||
if (!HexDecode(hash, decoded_hash.data(), decoded_hash.size()))
|
||||
{
|
||||
LogToFile("Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(), hash.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
parsed.entries[filename] = decoded_hash;
|
||||
pos = hash_end_pos + 1;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Not showing a progress bar here because this part is just too quick
|
||||
static std::optional<Manifest> FetchAndParseManifest(const std::string& url)
|
||||
{
|
||||
Common::HttpRequest http;
|
||||
|
||||
Common::HttpRequest::Response resp = http.Get(url);
|
||||
if (!resp)
|
||||
{
|
||||
LogToFile("Manifest download failed.\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string contents(reinterpret_cast<char*>(resp->data()), resp->size());
|
||||
std::optional<std::string> maybe_decompressed = GzipInflate(contents);
|
||||
if (!maybe_decompressed)
|
||||
return {};
|
||||
std::string decompressed = std::move(*maybe_decompressed);
|
||||
|
||||
// Split into manifest and signature.
|
||||
size_t boundary = decompressed.rfind("\n\n");
|
||||
if (boundary == std::string::npos)
|
||||
{
|
||||
LogToFile("No signature was found in manifest.\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string signature_block = decompressed.substr(boundary + 2); // 2 for "\n\n".
|
||||
decompressed.resize(boundary + 1); // 1 to keep the final "\n".
|
||||
|
||||
std::vector<std::string> signatures = SplitString(signature_block, '\n');
|
||||
bool found_valid_signature = false;
|
||||
for (const auto& signature : signatures)
|
||||
{
|
||||
if (VerifySignature(decompressed, signature))
|
||||
{
|
||||
found_valid_signature = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found_valid_signature)
|
||||
{
|
||||
LogToFile("Could not verify signature of the manifest.\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
return ParseManifest(decompressed);
|
||||
}
|
||||
|
||||
struct Options
|
||||
{
|
||||
std::string this_manifest_url;
|
||||
std::string next_manifest_url;
|
||||
std::string content_store_url;
|
||||
std::string install_base_path;
|
||||
std::optional<std::string> binary_to_restart;
|
||||
std::optional<u32> parent_pid;
|
||||
std::optional<std::string> log_file;
|
||||
};
|
||||
|
||||
static std::optional<Options> ParseCommandLine(std::vector<std::string>& args)
|
||||
{
|
||||
using optparse::OptionParser;
|
||||
|
||||
OptionParser parser =
|
||||
OptionParser().prog("Dolphin Updater").description("Dolphin Updater binary");
|
||||
|
||||
parser.add_option("--this-manifest-url")
|
||||
.dest("this-manifest-url")
|
||||
.help("URL to the update manifest for the currently installed version.")
|
||||
.metavar("URL");
|
||||
parser.add_option("--next-manifest-url")
|
||||
.dest("next-manifest-url")
|
||||
.help("URL to the update manifest for the to-be-installed version.")
|
||||
.metavar("URL");
|
||||
parser.add_option("--content-store-url")
|
||||
.dest("content-store-url")
|
||||
.help("Base URL of the content store where files to download are stored.")
|
||||
.metavar("URL");
|
||||
parser.add_option("--install-base-path")
|
||||
.dest("install-base-path")
|
||||
.help("Base path of the Dolphin install to be updated.")
|
||||
.metavar("PATH");
|
||||
parser.add_option("--binary-to-restart")
|
||||
.dest("binary-to-restart")
|
||||
.help("Binary to restart after the update is over.")
|
||||
.metavar("PATH");
|
||||
parser.add_option("--log-file")
|
||||
.dest("log-file")
|
||||
.help("File where to log updater debug output.")
|
||||
.metavar("PATH");
|
||||
parser.add_option("--parent-pid")
|
||||
.dest("parent-pid")
|
||||
.type("int")
|
||||
.help("(optional) PID of the parent process. The updater will wait for this process to "
|
||||
"complete before proceeding.")
|
||||
.metavar("PID");
|
||||
|
||||
optparse::Values options = parser.parse_args(args);
|
||||
|
||||
Options opts;
|
||||
|
||||
// Required arguments.
|
||||
std::vector<std::string> required{"this-manifest-url", "next-manifest-url", "content-store-url",
|
||||
"install-base-path"};
|
||||
for (const auto& req : required)
|
||||
{
|
||||
if (!options.is_set(req))
|
||||
{
|
||||
parser.print_help();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
opts.this_manifest_url = options["this-manifest-url"];
|
||||
opts.next_manifest_url = options["next-manifest-url"];
|
||||
opts.content_store_url = options["content-store-url"];
|
||||
opts.install_base_path = options["install-base-path"];
|
||||
|
||||
// Optional arguments.
|
||||
if (options.is_set("binary-to-restart"))
|
||||
opts.binary_to_restart = options["binary-to-restart"];
|
||||
if (options.is_set("parent-pid"))
|
||||
opts.parent_pid = static_cast<u32>(options.get("parent-pid"));
|
||||
if (options.is_set("log-file"))
|
||||
opts.log_file = options["log-file"];
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
bool RunUpdater(std::vector<std::string> args)
|
||||
{
|
||||
std::optional<Options> maybe_opts = ParseCommandLine(args);
|
||||
|
||||
if (!maybe_opts)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UI::Init();
|
||||
UI::SetVisible(false);
|
||||
|
||||
Common::ScopeGuard ui_guard{[] { UI::Stop(); }};
|
||||
Options opts = std::move(*maybe_opts);
|
||||
|
||||
if (opts.log_file)
|
||||
{
|
||||
if (!log_file.Open(opts.log_file.value(), "w"))
|
||||
log_file.SetHandle(stderr);
|
||||
else
|
||||
atexit(FlushLog);
|
||||
}
|
||||
|
||||
LogToFile("Updating from: %s\n", opts.this_manifest_url.c_str());
|
||||
LogToFile("Updating to: %s\n", opts.next_manifest_url.c_str());
|
||||
LogToFile("Install path: %s\n", opts.install_base_path.c_str());
|
||||
|
||||
if (!File::IsDirectory(opts.install_base_path))
|
||||
{
|
||||
FatalError("Cannot find install base path, or not a directory.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts.parent_pid)
|
||||
{
|
||||
LogToFile("Waiting for parent PID %d to complete...\n", *opts.parent_pid);
|
||||
|
||||
auto pid = opts.parent_pid.value();
|
||||
|
||||
UI::WaitForPID(static_cast<u32>(pid));
|
||||
|
||||
LogToFile("Completed! Proceeding with update.\n");
|
||||
}
|
||||
|
||||
UI::SetVisible(true);
|
||||
|
||||
UI::SetDescription("Fetching and parsing manifests...");
|
||||
|
||||
Manifest this_manifest, next_manifest;
|
||||
{
|
||||
std::optional<Manifest> maybe_manifest = FetchAndParseManifest(opts.this_manifest_url);
|
||||
if (!maybe_manifest)
|
||||
{
|
||||
FatalError("Could not fetch current manifest. Aborting.");
|
||||
return false;
|
||||
}
|
||||
this_manifest = std::move(*maybe_manifest);
|
||||
|
||||
maybe_manifest = FetchAndParseManifest(opts.next_manifest_url);
|
||||
if (!maybe_manifest)
|
||||
{
|
||||
FatalError("Could not fetch next manifest. Aborting.");
|
||||
return false;
|
||||
}
|
||||
next_manifest = std::move(*maybe_manifest);
|
||||
}
|
||||
|
||||
UI::SetDescription("Computing what to do...");
|
||||
|
||||
TodoList todo = ComputeActionsToDo(this_manifest, next_manifest);
|
||||
todo.Log();
|
||||
|
||||
std::string temp_dir = File::CreateTempDir();
|
||||
if (temp_dir.empty())
|
||||
{
|
||||
FatalError("Could not create temporary directory. Aborting.");
|
||||
return false;
|
||||
}
|
||||
|
||||
UI::SetDescription("Performing Update...");
|
||||
|
||||
bool ok = PerformUpdate(todo, opts.install_base_path, opts.content_store_url, temp_dir);
|
||||
CleanUpTempDir(temp_dir, todo);
|
||||
if (!ok)
|
||||
{
|
||||
FatalError("Failed to apply the update.");
|
||||
return false;
|
||||
}
|
||||
|
||||
UI::ResetCurrentProgress();
|
||||
UI::ResetTotalProgress();
|
||||
UI::SetCurrentMarquee(false);
|
||||
UI::SetTotalMarquee(false);
|
||||
UI::SetCurrentProgress(1, 1);
|
||||
UI::SetTotalProgress(1, 1);
|
||||
UI::SetDescription("Done!");
|
||||
|
||||
// Let the user process that we are done.
|
||||
UI::Sleep(1);
|
||||
|
||||
if (opts.binary_to_restart)
|
||||
{
|
||||
UI::LaunchApplication(opts.binary_to_restart.value());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright 2019 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
|
||||
struct Manifest
|
||||
{
|
||||
using Filename = std::string;
|
||||
using Hash = std::array<u8, 16>;
|
||||
std::map<Filename, Hash> entries;
|
||||
};
|
||||
|
||||
// Represent the operations to be performed by the updater.
|
||||
struct TodoList
|
||||
{
|
||||
struct DownloadOp
|
||||
{
|
||||
Manifest::Filename filename;
|
||||
Manifest::Hash hash{};
|
||||
};
|
||||
std::vector<DownloadOp> to_download;
|
||||
|
||||
struct UpdateOp
|
||||
{
|
||||
Manifest::Filename filename;
|
||||
std::optional<Manifest::Hash> old_hash;
|
||||
Manifest::Hash new_hash{};
|
||||
};
|
||||
std::vector<UpdateOp> to_update;
|
||||
|
||||
struct DeleteOp
|
||||
{
|
||||
Manifest::Filename filename;
|
||||
Manifest::Hash old_hash{};
|
||||
};
|
||||
std::vector<DeleteOp> to_delete;
|
||||
|
||||
void Log() const;
|
||||
};
|
||||
|
||||
void LogToFile(const char* fmt, ...);
|
||||
std::string HexEncode(const u8* buffer, size_t size);
|
||||
Manifest::Hash ComputeHash(const std::string& contents);
|
||||
bool RunUpdater(std::vector<std::string> args);
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright 2018 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <windows.h>
|
||||
#include <ShlObj.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/CommonFuncs.h"
|
||||
#include "Common/StringUtil.h"
|
||||
|
||||
#include "UpdaterCommon/UI.h"
|
||||
#include "UpdaterCommon/UpdaterCommon.h"
|
||||
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
|
||||
{
|
||||
if (lstrlenW(pCmdLine) == 0)
|
||||
{
|
||||
MessageBoxW(nullptr,
|
||||
L"This updater is not meant to be launched directly. Configure Auto-Update in "
|
||||
"Dolphin's settings instead.",
|
||||
L"Error", MB_ICONERROR);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test for write permissions
|
||||
bool need_admin = false;
|
||||
|
||||
auto path = Common::GetModuleName(hInstance);
|
||||
if (!path)
|
||||
{
|
||||
UI::Error("Failed to get updater filename.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto test_fh = ::CreateFileW(
|
||||
(std::filesystem::path(*path).parent_path() / "directory_writable_check.tmp").c_str(),
|
||||
GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, nullptr);
|
||||
|
||||
if (test_fh == INVALID_HANDLE_VALUE)
|
||||
need_admin = true;
|
||||
else
|
||||
CloseHandle(test_fh);
|
||||
|
||||
if (need_admin)
|
||||
{
|
||||
if (IsUserAnAdmin())
|
||||
{
|
||||
MessageBox(nullptr, L"Failed to write to directory despite administrator privileges.",
|
||||
L"Error", MB_ICONERROR);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Relaunch the updater as administrator
|
||||
ShellExecuteW(nullptr, L"runas", path->c_str(), pCmdLine, nullptr, SW_SHOW);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<std::string> args = Common::CommandLineToUtf8Argv(pCmdLine);
|
||||
|
||||
return RunUpdater(args) ? 0 : 1;
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
#include <windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
#include "Common/CommonPaths.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/HttpRequest.h"
|
||||
#include "Common/IOFile.h"
|
||||
#include "Common/ScopeGuard.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Common/WindowsRegistry.h"
|
||||
|
||||
#include "UpdaterCommon/Platform.h"
|
||||
#include "UpdaterCommon/UI.h"
|
||||
#include "UpdaterCommon/UpdaterCommon.h"
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
struct BuildVersion
|
||||
{
|
||||
u32 major{};
|
||||
u32 minor{};
|
||||
u32 build{};
|
||||
auto operator<=>(BuildVersion const& rhs) const = default;
|
||||
static std::optional<BuildVersion> from_string(const std::string& str)
|
||||
{
|
||||
auto components = SplitString(str, '.');
|
||||
// Allow variable number of components (truncating after "build"), but not
|
||||
// empty.
|
||||
if (components.size() == 0)
|
||||
return {};
|
||||
BuildVersion version;
|
||||
if (!TryParse(components[0], &version.major, 10))
|
||||
return {};
|
||||
if (components.size() > 1 && !TryParse(components[1], &version.minor, 10))
|
||||
return {};
|
||||
if (components.size() > 2 && !TryParse(components[2], &version.build, 10))
|
||||
return {};
|
||||
return version;
|
||||
}
|
||||
};
|
||||
|
||||
enum class VersionCheckStatus
|
||||
{
|
||||
NothingToDo,
|
||||
UpdateOptional,
|
||||
UpdateRequired,
|
||||
};
|
||||
|
||||
struct VersionCheckResult
|
||||
{
|
||||
VersionCheckStatus status{VersionCheckStatus::NothingToDo};
|
||||
std::optional<BuildVersion> current_version{};
|
||||
std::optional<BuildVersion> target_version{};
|
||||
};
|
||||
|
||||
class BuildInfo
|
||||
{
|
||||
using Map = std::map<std::string, std::string>;
|
||||
|
||||
public:
|
||||
BuildInfo() = default;
|
||||
BuildInfo(const std::string& content)
|
||||
{
|
||||
map = {{"OSMinimumVersionWin10", ""},
|
||||
{"OSMinimumVersionWin11", ""},
|
||||
{"VCToolsVersion", ""},
|
||||
{"VCToolsUpdateURL", ""}};
|
||||
Parse(content);
|
||||
}
|
||||
|
||||
std::optional<std::string> GetString(const std::string& name) const
|
||||
{
|
||||
auto it = map.find(name);
|
||||
if (it == map.end() || it->second.size() == 0)
|
||||
return {};
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::optional<BuildVersion> GetVersion(const std::string& name) const
|
||||
{
|
||||
auto str = GetString(name);
|
||||
if (!str.has_value())
|
||||
return {};
|
||||
return BuildVersion::from_string(str.value());
|
||||
}
|
||||
|
||||
private:
|
||||
void Parse(const std::string& content)
|
||||
{
|
||||
std::stringstream content_stream(content);
|
||||
std::string line;
|
||||
while (std::getline(content_stream, line))
|
||||
{
|
||||
if (line.starts_with("//"))
|
||||
continue;
|
||||
const size_t equals_index = line.find('=');
|
||||
if (equals_index == line.npos)
|
||||
continue;
|
||||
auto key = line.substr(0, equals_index);
|
||||
auto key_it = map.find(key);
|
||||
if (key_it == map.end())
|
||||
continue;
|
||||
auto val_start = equals_index + 1;
|
||||
auto eol = line.find('\r', val_start);
|
||||
auto val_size = (eol == line.npos) ? line.npos : eol - val_start;
|
||||
key_it->second = line.substr(val_start, val_size);
|
||||
}
|
||||
}
|
||||
Map map;
|
||||
};
|
||||
|
||||
struct BuildInfos
|
||||
{
|
||||
BuildInfo current;
|
||||
BuildInfo next;
|
||||
};
|
||||
|
||||
// This default value should be kept in sync with the value of VCToolsUpdateURL in
|
||||
// build_info.txt.in
|
||||
static const char* VCToolsUpdateURLDefault = "https://aka.ms/vc14/vc_redist.x64.exe";
|
||||
#define VC_RUNTIME_REGKEY R"(SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\)"
|
||||
|
||||
static const char* VCRuntimeRegistrySubkey()
|
||||
{
|
||||
return VC_RUNTIME_REGKEY
|
||||
#ifdef _M_X86_64
|
||||
"x64";
|
||||
#elif _M_ARM_64
|
||||
"arm64";
|
||||
#else
|
||||
#error unsupported architecture
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool ReadVCRuntimeVersionField(u32* value, const char* name)
|
||||
{
|
||||
return WindowsRegistry::ReadValue(value, VCRuntimeRegistrySubkey(), name);
|
||||
}
|
||||
|
||||
static std::optional<BuildVersion> GetInstalledVCRuntimeVersion()
|
||||
{
|
||||
u32 installed;
|
||||
if (!ReadVCRuntimeVersionField(&installed, "Installed") || !installed)
|
||||
return {};
|
||||
BuildVersion version;
|
||||
if (!ReadVCRuntimeVersionField(&version.major, "Major") ||
|
||||
!ReadVCRuntimeVersionField(&version.minor, "Minor") ||
|
||||
!ReadVCRuntimeVersionField(&version.build, "Bld"))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
static VersionCheckResult VCRuntimeVersionCheck(const BuildInfos& build_infos)
|
||||
{
|
||||
VersionCheckResult result;
|
||||
result.current_version = GetInstalledVCRuntimeVersion();
|
||||
result.target_version = build_infos.next.GetVersion("VCToolsVersion");
|
||||
|
||||
auto existing_version = build_infos.current.GetVersion("VCToolsVersion");
|
||||
|
||||
if (!result.target_version.has_value())
|
||||
result.status = VersionCheckStatus::UpdateOptional;
|
||||
else if (!result.current_version.has_value() || result.current_version < result.target_version)
|
||||
result.status = VersionCheckStatus::UpdateRequired;
|
||||
|
||||
// See if the current build was already running on acceptable version of the runtime. This could
|
||||
// happen if the user manually copied the redist DLLs and got Dolphin running that way.
|
||||
if (existing_version.has_value() && existing_version >= result.target_version)
|
||||
result.status = VersionCheckStatus::NothingToDo;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool VCRuntimeUpdate(const BuildInfo& build_info)
|
||||
{
|
||||
UI::SetDescription("Updating VC++ Redist, please wait...");
|
||||
|
||||
Common::HttpRequest req;
|
||||
req.FollowRedirects(10);
|
||||
auto resp = req.Get(build_info.GetString("VCToolsUpdateURL").value_or(VCToolsUpdateURLDefault));
|
||||
if (!resp)
|
||||
return false;
|
||||
|
||||
// Write it to current working directory.
|
||||
auto redist_path = std::filesystem::current_path() / L"vc_redist.x64.exe";
|
||||
auto redist_path_u8 = WStringToUTF8(redist_path.wstring());
|
||||
File::IOFile redist_file;
|
||||
redist_file.Open(redist_path_u8, "wb");
|
||||
if (!redist_file.WriteBytes(resp->data(), resp->size()))
|
||||
return false;
|
||||
redist_file.Close();
|
||||
|
||||
Common::ScopeGuard redist_deleter([&] { File::Delete(redist_path_u8); });
|
||||
|
||||
// The installer also supports /passive and /quiet. We normally pass neither (the
|
||||
// exception being test automation) to allow the user to see and interact with the installer.
|
||||
std::wstring cmdline = redist_path.filename().wstring() + L" /install /norestart";
|
||||
if (UI::IsTestMode())
|
||||
cmdline += L" /passive /quiet";
|
||||
|
||||
STARTUPINFOW startup_info{.cb = sizeof(startup_info)};
|
||||
PROCESS_INFORMATION process_info;
|
||||
if (!CreateProcessW(redist_path.c_str(), cmdline.data(), nullptr, nullptr, TRUE, 0, nullptr,
|
||||
nullptr, &startup_info, &process_info))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
CloseHandle(process_info.hThread);
|
||||
|
||||
// Wait for it to run
|
||||
WaitForSingleObject(process_info.hProcess, INFINITE);
|
||||
DWORD exit_code;
|
||||
bool has_exit_code = GetExitCodeProcess(process_info.hProcess, &exit_code);
|
||||
CloseHandle(process_info.hProcess);
|
||||
// NOTE: Some nonzero exit codes can still be considered success (e.g. if installation was
|
||||
// bypassed because the same version already installed).
|
||||
return has_exit_code &&
|
||||
(exit_code == ERROR_SUCCESS || exit_code == ERROR_SUCCESS_REBOOT_REQUIRED);
|
||||
}
|
||||
|
||||
static BuildVersion CurrentOSVersion()
|
||||
{
|
||||
OSVERSIONINFOW info = WindowsRegistry::GetOSVersion();
|
||||
return {.major = info.dwMajorVersion, .minor = info.dwMinorVersion, .build = info.dwBuildNumber};
|
||||
}
|
||||
|
||||
static VersionCheckResult OSVersionCheck(const BuildInfo& build_info)
|
||||
{
|
||||
VersionCheckResult result;
|
||||
result.current_version = CurrentOSVersion();
|
||||
|
||||
constexpr BuildVersion WIN11_BASE{10, 0, 22000};
|
||||
const char* version_name =
|
||||
(result.current_version >= WIN11_BASE) ? "OSMinimumVersionWin11" : "OSMinimumVersionWin10";
|
||||
result.target_version = build_info.GetVersion(version_name);
|
||||
|
||||
if (!result.target_version.has_value() || result.current_version >= result.target_version)
|
||||
result.status = VersionCheckStatus::NothingToDo;
|
||||
else
|
||||
result.status = VersionCheckStatus::UpdateRequired;
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::optional<BuildInfos> InitBuildInfos(const std::vector<TodoList::UpdateOp>& to_update,
|
||||
const std::string& install_base_path,
|
||||
const std::string& temp_dir)
|
||||
{
|
||||
const auto op_it = std::ranges::find(to_update, "build_info.txt", &TodoList::UpdateOp::filename);
|
||||
if (op_it == to_update.cend())
|
||||
return {};
|
||||
|
||||
const auto op = *op_it;
|
||||
std::string build_info_path =
|
||||
temp_dir + DIR_SEP + HexEncode(op.new_hash.data(), op.new_hash.size());
|
||||
std::string build_info_content;
|
||||
if (!File::ReadFileToString(build_info_path, build_info_content) ||
|
||||
op.new_hash != ComputeHash(build_info_content))
|
||||
{
|
||||
LogToFile("Failed to read %s\n.", build_info_path.c_str());
|
||||
return {};
|
||||
}
|
||||
BuildInfos build_infos;
|
||||
build_infos.next = Platform::BuildInfo(build_info_content);
|
||||
|
||||
build_info_path = install_base_path + DIR_SEP + "build_info.txt";
|
||||
build_infos.current = Platform::BuildInfo();
|
||||
if (File::ReadFileToString(build_info_path, build_info_content))
|
||||
{
|
||||
if (op.old_hash != ComputeHash(build_info_content))
|
||||
LogToFile("Using modified existing BuildInfo %s.\n", build_info_path.c_str());
|
||||
build_infos.current = Platform::BuildInfo(build_info_content);
|
||||
}
|
||||
return build_infos;
|
||||
}
|
||||
|
||||
static bool CheckBuildInfo(const BuildInfos& build_infos)
|
||||
{
|
||||
// The existing BuildInfo may have been modified. Be careful not to overly trust its contents!
|
||||
|
||||
// If the binary requires more recent OS, inform the user.
|
||||
auto os_check = OSVersionCheck(build_infos.next);
|
||||
if (os_check.status == VersionCheckStatus::UpdateRequired)
|
||||
{
|
||||
UI::Error("Please update Windows in order to update Dolphin.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if application being launched needs more recent version of VC Redist. If so, download
|
||||
// latest updater and execute it.
|
||||
auto vc_check = VCRuntimeVersionCheck(build_infos);
|
||||
const auto is_test_mode = UI::IsTestMode();
|
||||
if (vc_check.status != VersionCheckStatus::NothingToDo || is_test_mode)
|
||||
{
|
||||
auto update_ok = VCRuntimeUpdate(build_infos.next);
|
||||
if (!update_ok && is_test_mode)
|
||||
{
|
||||
// For now, only check return value when test automation is running.
|
||||
// The vc_redist exe may return other non-zero status that we don't check for, yet.
|
||||
return false;
|
||||
}
|
||||
vc_check = VCRuntimeVersionCheck(build_infos);
|
||||
if (vc_check.status == VersionCheckStatus::UpdateRequired)
|
||||
{
|
||||
// The update is required and the install failed for some reason.
|
||||
UI::Error("Please update VC++ Runtime in order to update Dolphin.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VersionCheck(const std::vector<TodoList::UpdateOp>& to_update,
|
||||
const std::string& install_base_path, const std::string& temp_dir)
|
||||
{
|
||||
auto build_infos = InitBuildInfos(to_update, install_base_path, temp_dir);
|
||||
// If there's no build info, it means the check should be skipped.
|
||||
if (!build_infos.has_value())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return CheckBuildInfo(build_infos.value());
|
||||
}
|
||||
|
||||
} // namespace Platform
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="amd64"
|
||||
name="DolphinTeam.DolphinEmuUpdater"
|
||||
type="win32"
|
||||
/>
|
||||
<description>Dolphin updater</description>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
@@ -1,308 +0,0 @@
|
||||
// Copyright 2018 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "UpdaterCommon/UI.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <windows.h>
|
||||
#include <commctrl.h>
|
||||
#include <shellapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <shobjidl.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include "Common/Event.h"
|
||||
#include "Common/ScopeGuard.h"
|
||||
#include "Common/StringUtil.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
HWND window_handle = nullptr;
|
||||
HWND label_handle = nullptr;
|
||||
HWND total_progressbar_handle = nullptr;
|
||||
HWND current_progressbar_handle = nullptr;
|
||||
Microsoft::WRL::ComPtr<ITaskbarList3> taskbar_list;
|
||||
|
||||
std::thread ui_thread;
|
||||
Common::Event window_created_event;
|
||||
|
||||
int GetWindowHeight(HWND hwnd)
|
||||
{
|
||||
RECT rect;
|
||||
GetWindowRect(hwnd, &rect);
|
||||
|
||||
return rect.bottom - rect.top;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (uMsg)
|
||||
{
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProc(hwnd, uMsg, wParam, lParam);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
constexpr int PROGRESSBAR_FLAGS = WS_VISIBLE | WS_CHILD | PBS_SMOOTH | PBS_SMOOTHREVERSE;
|
||||
constexpr int WINDOW_FLAGS = WS_CLIPCHILDREN;
|
||||
constexpr int PADDING_HEIGHT = 5;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
static bool InitWindow()
|
||||
{
|
||||
InitCommonControls();
|
||||
|
||||
// Notify main thread we're done creating the window when we return
|
||||
Common::ScopeGuard ui_guard{[] { window_created_event.Set(); }};
|
||||
|
||||
WNDCLASS wndcl = {};
|
||||
wndcl.lpfnWndProc = WindowProc;
|
||||
wndcl.hbrBackground = GetSysColorBrush(COLOR_MENU);
|
||||
wndcl.lpszClassName = L"UPDATER";
|
||||
|
||||
if (!RegisterClass(&wndcl))
|
||||
return false;
|
||||
|
||||
window_handle =
|
||||
CreateWindow(L"UPDATER", L"Dolphin Updater", WINDOW_FLAGS, CW_USEDEFAULT, CW_USEDEFAULT, 500,
|
||||
100, nullptr, nullptr, GetModuleHandle(nullptr), 0);
|
||||
|
||||
if (!window_handle)
|
||||
return false;
|
||||
|
||||
if (SUCCEEDED(CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(&taskbar_list))))
|
||||
{
|
||||
if (FAILED(taskbar_list->HrInit()))
|
||||
{
|
||||
taskbar_list.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
int y = PADDING_HEIGHT;
|
||||
|
||||
label_handle = CreateWindow(L"STATIC", nullptr, WS_VISIBLE | WS_CHILD, 5, y, 500, 25,
|
||||
window_handle, nullptr, nullptr, 0);
|
||||
|
||||
if (!label_handle)
|
||||
return false;
|
||||
|
||||
// Get the default system font
|
||||
NONCLIENTMETRICS metrics = {};
|
||||
metrics.cbSize = sizeof(NONCLIENTMETRICS);
|
||||
|
||||
if (!SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(metrics), &metrics, 0))
|
||||
return false;
|
||||
|
||||
SendMessage(label_handle, WM_SETFONT,
|
||||
reinterpret_cast<WPARAM>(CreateFontIndirect(&metrics.lfMessageFont)), 0);
|
||||
|
||||
y += GetWindowHeight(label_handle) + PADDING_HEIGHT;
|
||||
|
||||
total_progressbar_handle = CreateWindow(PROGRESS_CLASS, nullptr, PROGRESSBAR_FLAGS, 5, y, 470, 25,
|
||||
window_handle, nullptr, nullptr, 0);
|
||||
|
||||
y += GetWindowHeight(total_progressbar_handle) + PADDING_HEIGHT;
|
||||
|
||||
if (!total_progressbar_handle)
|
||||
return false;
|
||||
|
||||
current_progressbar_handle = CreateWindow(PROGRESS_CLASS, nullptr, PROGRESSBAR_FLAGS, 5, y, 470,
|
||||
25, window_handle, nullptr, nullptr, 0);
|
||||
|
||||
y += GetWindowHeight(current_progressbar_handle) + PADDING_HEIGHT;
|
||||
|
||||
if (!current_progressbar_handle)
|
||||
return false;
|
||||
|
||||
RECT rect;
|
||||
GetWindowRect(window_handle, &rect);
|
||||
|
||||
// Account for the title bar
|
||||
y += GetSystemMetrics(SM_CYFRAME) + GetSystemMetrics(SM_CYCAPTION) +
|
||||
GetSystemMetrics(SM_CXPADDEDBORDER);
|
||||
// ...and window border
|
||||
y += GetSystemMetrics(SM_CYBORDER);
|
||||
|
||||
// Add some padding for good measure
|
||||
y += PADDING_HEIGHT * 3;
|
||||
|
||||
SetWindowPos(window_handle, HWND_TOPMOST, 0, 0, rect.right - rect.left, y, SWP_NOMOVE);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetTotalMarquee(bool marquee)
|
||||
{
|
||||
SetWindowLong(total_progressbar_handle, GWL_STYLE,
|
||||
PROGRESSBAR_FLAGS | (marquee ? PBS_MARQUEE : 0));
|
||||
SendMessage(total_progressbar_handle, PBM_SETMARQUEE, marquee, 0);
|
||||
if (taskbar_list)
|
||||
{
|
||||
taskbar_list->SetProgressState(window_handle, marquee ? TBPF_INDETERMINATE : TBPF_NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
void ResetTotalProgress()
|
||||
{
|
||||
SendMessage(total_progressbar_handle, PBM_SETPOS, 0, 0);
|
||||
SetCurrentMarquee(true);
|
||||
}
|
||||
|
||||
void SetTotalProgress(int current, int total)
|
||||
{
|
||||
SendMessage(total_progressbar_handle, PBM_SETRANGE32, 0, total);
|
||||
SendMessage(total_progressbar_handle, PBM_SETPOS, current, 0);
|
||||
if (taskbar_list)
|
||||
{
|
||||
taskbar_list->SetProgressValue(window_handle, current, total);
|
||||
}
|
||||
}
|
||||
|
||||
void SetCurrentMarquee(bool marquee)
|
||||
{
|
||||
SetWindowLong(current_progressbar_handle, GWL_STYLE,
|
||||
PROGRESSBAR_FLAGS | (marquee ? PBS_MARQUEE : 0));
|
||||
SendMessage(current_progressbar_handle, PBM_SETMARQUEE, marquee, 0);
|
||||
}
|
||||
|
||||
void ResetCurrentProgress()
|
||||
{
|
||||
SendMessage(current_progressbar_handle, PBM_SETPOS, 0, 0);
|
||||
SetCurrentMarquee(true);
|
||||
}
|
||||
|
||||
void Error(const std::string& text)
|
||||
{
|
||||
auto message = L"A fatal error occurred and the updater cannot continue:\n " +
|
||||
UTF8ToWString(text) + L"\n" +
|
||||
L"If the issue persists, please manually download the latest version from "
|
||||
L"dolphin-emu.org/download and extract it overtop your existing installation.\n" +
|
||||
L"Also consider filing a bug at bugs.dolphin-emu.org/projects/emulator";
|
||||
|
||||
MessageBox(nullptr, message.c_str(), L"Error", MB_ICONERROR);
|
||||
|
||||
if (taskbar_list)
|
||||
{
|
||||
taskbar_list->SetProgressState(window_handle, TBPF_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
void SetCurrentProgress(int current, int total)
|
||||
{
|
||||
SendMessage(current_progressbar_handle, PBM_SETRANGE32, 0, total);
|
||||
SendMessage(current_progressbar_handle, PBM_SETPOS, current, 0);
|
||||
}
|
||||
|
||||
void SetDescription(const std::string& text)
|
||||
{
|
||||
SetWindowText(label_handle, UTF8ToWString(text).c_str());
|
||||
}
|
||||
|
||||
static void MessageLoop()
|
||||
{
|
||||
HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
|
||||
Common::ScopeGuard ui_guard{[result] {
|
||||
taskbar_list.Reset();
|
||||
if (SUCCEEDED(result))
|
||||
CoUninitialize();
|
||||
}};
|
||||
|
||||
if (!InitWindow())
|
||||
{
|
||||
MessageBox(nullptr, L"Window init failed!", L"", MB_ICONERROR);
|
||||
// Destroying the parent (if exists) destroys all children windows
|
||||
if (window_handle)
|
||||
{
|
||||
DestroyWindow(window_handle);
|
||||
window_handle = nullptr;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
SetTotalMarquee(true);
|
||||
SetCurrentMarquee(true);
|
||||
|
||||
MSG msg;
|
||||
while (GetMessage(&msg, nullptr, 0, 0))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
void Init()
|
||||
{
|
||||
ui_thread = std::thread(MessageLoop);
|
||||
|
||||
// Wait for UI thread to finish creating the window (or at least attempting to)
|
||||
window_created_event.Wait();
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
if (window_handle)
|
||||
PostMessage(window_handle, WM_CLOSE, 0, 0);
|
||||
|
||||
ui_thread.join();
|
||||
}
|
||||
|
||||
bool IsTestMode()
|
||||
{
|
||||
return std::getenv("DOLPHIN_UPDATE_SERVER_URL") != nullptr;
|
||||
}
|
||||
|
||||
void LaunchApplication(std::string path)
|
||||
{
|
||||
const auto wpath = UTF8ToWString(path);
|
||||
if (IsUserAnAdmin())
|
||||
{
|
||||
// Indirectly start the application via explorer. This effectively drops admin privileges
|
||||
// because explorer is running as current user.
|
||||
ShellExecuteW(nullptr, nullptr, L"explorer.exe", wpath.c_str(), nullptr, SW_SHOW);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::wstring cmdline = L"\"" + wpath + L"\"";
|
||||
STARTUPINFOW startup_info{.cb = sizeof(startup_info)};
|
||||
PROCESS_INFORMATION process_info;
|
||||
if (IsTestMode())
|
||||
SetEnvironmentVariableA("DOLPHIN_UPDATE_TEST_DONE", "1");
|
||||
if (CreateProcessW(wpath.c_str(), cmdline.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr,
|
||||
&startup_info, &process_info))
|
||||
{
|
||||
CloseHandle(process_info.hThread);
|
||||
CloseHandle(process_info.hProcess);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Sleep(int sleep)
|
||||
{
|
||||
::Sleep(sleep * 1000);
|
||||
}
|
||||
|
||||
void WaitForPID(u32 pid)
|
||||
{
|
||||
HANDLE parent_handle = OpenProcess(SYNCHRONIZE, FALSE, static_cast<DWORD>(pid));
|
||||
if (parent_handle)
|
||||
{
|
||||
WaitForSingleObject(parent_handle, INFINITE);
|
||||
CloseHandle(parent_handle);
|
||||
}
|
||||
}
|
||||
|
||||
void SetVisible(bool visible)
|
||||
{
|
||||
ShowWindow(window_handle, visible ? SW_SHOW : SW_HIDE);
|
||||
}
|
||||
|
||||
} // namespace UI
|
||||
@@ -1,201 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# requirements: pycryptodome
|
||||
from Crypto.PublicKey import ECC
|
||||
from Crypto.Signature import eddsa
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
import base64
|
||||
import configparser
|
||||
import gzip
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socketserver
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
UPDATE_KEY_TEST = ECC.construct(
|
||||
curve="Ed25519",
|
||||
seed=bytes.fromhex(
|
||||
"543a581db60008bbb978a464e136d686dbc9d594119e928b5276bece3d583d81"
|
||||
),
|
||||
)
|
||||
|
||||
HTTP_SERVER_ADDR = ("localhost", 8042)
|
||||
DOLPHIN_UPDATE_SERVER_URL = f"http://{HTTP_SERVER_ADDR[0]}:{HTTP_SERVER_ADDR[1]}"
|
||||
|
||||
|
||||
class Manifest:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
self.entries = {}
|
||||
for p in self.path.glob("**/*.*"):
|
||||
if not p.is_file():
|
||||
continue
|
||||
digest = sha256(p.read_bytes()).digest()[:0x10].hex()
|
||||
self.entries[digest] = p.relative_to(self.path).as_posix()
|
||||
|
||||
def get_signed(self):
|
||||
manifest = "".join(
|
||||
f"{name}\t{digest}\n" for digest, name in self.entries.items()
|
||||
)
|
||||
manifest = manifest.encode("utf-8")
|
||||
sig = eddsa.new(UPDATE_KEY_TEST, "rfc8032").sign(manifest)
|
||||
manifest += b"\n" + base64.b64encode(sig) + b"\n"
|
||||
return gzip.compress(manifest)
|
||||
|
||||
def get_path(self, digest):
|
||||
return self.path.joinpath(self.entries.get(digest))
|
||||
|
||||
|
||||
class HTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path.startswith("/update/check/v1/updater-test"):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(
|
||||
bytes(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "outdated",
|
||||
"content-store": DOLPHIN_UPDATE_SERVER_URL + "/content/",
|
||||
"changelog": [],
|
||||
"old": {"manifest": DOLPHIN_UPDATE_SERVER_URL + "/old"},
|
||||
"new": {
|
||||
"manifest": DOLPHIN_UPDATE_SERVER_URL + "/new",
|
||||
"name": "updater-test",
|
||||
"hash": bytes(range(32)).hex(),
|
||||
},
|
||||
}
|
||||
),
|
||||
"utf-8",
|
||||
)
|
||||
)
|
||||
elif self.path == "/old":
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(self.current.get_signed())
|
||||
elif self.path == "/new":
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(self.next.get_signed())
|
||||
elif self.path.startswith("/content/"):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
digest = "".join(self.path[len("/content/") :].split("/"))
|
||||
path = self.next.get_path(digest)
|
||||
self.wfile.write(gzip.compress(path.read_bytes()))
|
||||
elif self.path.startswith("/update-test-done/"):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
HTTPRequestHandler.dolphin_pid = int(self.path[len("/update-test-done/") :])
|
||||
self.done.set()
|
||||
|
||||
|
||||
def http_server():
|
||||
with socketserver.TCPServer(HTTP_SERVER_ADDR, HTTPRequestHandler) as httpd:
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
def create_entries_in_ini(ini_path: Path, entries: dict):
|
||||
config = configparser.ConfigParser()
|
||||
if ini_path.exists():
|
||||
config.read(ini_path)
|
||||
else:
|
||||
ini_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for section, options in entries.items():
|
||||
if not config.has_section(section):
|
||||
config.add_section(section)
|
||||
for option, value in options.items():
|
||||
config.set(section, option, value)
|
||||
|
||||
with ini_path.open("w") as f:
|
||||
config.write(f)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
dolphin_bin_path = Path(sys.argv[1])
|
||||
|
||||
threading.Thread(target=http_server, daemon=True).start()
|
||||
|
||||
with tempfile.TemporaryDirectory(suffix=" ¿ 🐬") as tmp_dir:
|
||||
tmp_dir = Path(tmp_dir)
|
||||
|
||||
tmp_dolphin = tmp_dir.joinpath("dolphin")
|
||||
print(f"install to {tmp_dolphin}")
|
||||
shutil.copytree(dolphin_bin_path.parent, tmp_dolphin)
|
||||
tmp_dolphin.joinpath("portable.txt").touch()
|
||||
create_entries_in_ini(
|
||||
tmp_dolphin.joinpath("User/Config/Dolphin.ini"),
|
||||
{
|
||||
"Analytics": {"Enabled": "False", "PermissionAsked": "True"},
|
||||
"AutoUpdate": {"UpdateTrack": "updater-test"},
|
||||
},
|
||||
)
|
||||
|
||||
tmp_dolphin_next = tmp_dir.joinpath("dolphin_next")
|
||||
print(f"install next to {tmp_dolphin_next}")
|
||||
# XXX copies from just-created dir so Dolphin.ini is kept
|
||||
shutil.copytree(tmp_dolphin, tmp_dolphin_next)
|
||||
tmp_dolphin_next.joinpath("updater-test-file").write_text("test")
|
||||
tmp_dolphin_next.joinpath("updater-test-filἑ").write_text("test")
|
||||
with tmp_dolphin_next.joinpath("build_info.txt").open("a") as f:
|
||||
print("test", file=f)
|
||||
for ext in ("exe", "dll"):
|
||||
for path in tmp_dolphin_next.glob("**/*." + ext):
|
||||
data = bytearray(path.read_bytes())
|
||||
richpos = data[:0x200].find(b"Rich")
|
||||
if richpos < 0:
|
||||
continue
|
||||
data[richpos : richpos + 4] = b"DOLP"
|
||||
path.write_bytes(data)
|
||||
|
||||
HTTPRequestHandler.current = Manifest(tmp_dolphin)
|
||||
HTTPRequestHandler.next = Manifest(tmp_dolphin_next)
|
||||
HTTPRequestHandler.done = threading.Event()
|
||||
|
||||
tmp_env = os.environ
|
||||
tmp_env.update({"DOLPHIN_UPDATE_SERVER_URL": DOLPHIN_UPDATE_SERVER_URL})
|
||||
tmp_dolphin_bin = tmp_dolphin.joinpath(dolphin_bin_path.name)
|
||||
result = subprocess.run(tmp_dolphin_bin, env=tmp_env)
|
||||
assert result.returncode == 0
|
||||
|
||||
assert HTTPRequestHandler.done.wait(60 * 2)
|
||||
# works fine but raises exceptions...
|
||||
try:
|
||||
os.kill(HTTPRequestHandler.dolphin_pid, 0)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
os.waitpid(HTTPRequestHandler.dolphin_pid, 0)
|
||||
except:
|
||||
pass
|
||||
|
||||
failed = False
|
||||
for path in tmp_dolphin_next.glob("**/*.*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
path_rel = path.relative_to(tmp_dolphin_next)
|
||||
if path_rel.parts[0] == "User":
|
||||
continue
|
||||
new_path = tmp_dolphin.joinpath(path_rel)
|
||||
if not new_path.exists():
|
||||
print(f"missing: {new_path}")
|
||||
failed = True
|
||||
continue
|
||||
if (
|
||||
sha256(new_path.read_bytes()).digest()
|
||||
!= sha256(path.read_bytes()).digest()
|
||||
):
|
||||
print(f"bad digest: {new_path} {path}")
|
||||
failed = True
|
||||
continue
|
||||
assert not failed
|
||||
|
||||
print(tmp_dolphin.joinpath("User/Logs/Updater.log").read_text())
|
||||
# while True: time.sleep(1)
|
||||
Reference in new issue
Block a user