mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 08:03:45 +08:00
Fix translocated launch on macOS.
This commit is contained in:
18
.github/workflows/canary.yml
vendored
18
.github/workflows/canary.yml
vendored
@@ -292,6 +292,9 @@ jobs:
|
||||
# windows-11-arm runner and the VS ARM64 build tools steps from
|
||||
# win.yml when the canary channels grow an arm feed.
|
||||
|
||||
outputs:
|
||||
signed: ${{ steps.sign.outputs.signed }}
|
||||
|
||||
env:
|
||||
PREPARE_PATH: "Telegram/build/prepare/prepare.py"
|
||||
|
||||
@@ -525,6 +528,7 @@ jobs:
|
||||
# --endpoint-url https://$R2_ACCOUNT_ID.r2.cloudflarestorage.com
|
||||
|
||||
- name: Sign binaries.
|
||||
id: sign
|
||||
shell: bash
|
||||
env:
|
||||
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
||||
@@ -541,6 +545,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "::warning::KeyLocker secrets absent, leaving binaries unsigned."
|
||||
echo "signed=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
# TODO(canary-infra): install the DigiCert KeyLocker tools once
|
||||
@@ -554,6 +559,7 @@ jobs:
|
||||
# the publish job can re-check just the portable's Telegram.exe.
|
||||
signtool verify /pa /all Telegram.exe
|
||||
signtool verify /pa /all Updater.exe
|
||||
echo "signed=true" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Azure login for update signing.
|
||||
if: needs.version.outputs.publish == 'true'
|
||||
@@ -637,6 +643,9 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
outputs:
|
||||
signed: ${{ steps.sign.outputs.signed }}
|
||||
|
||||
env:
|
||||
PREPARE_PATH: "Telegram/build/prepare/prepare.py"
|
||||
|
||||
@@ -790,6 +799,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Sign and notarize.
|
||||
id: sign
|
||||
env:
|
||||
CERTIFICATE_P12_B64: ${{ secrets.MACOS_CERTIFICATE_P12_B64 }}
|
||||
CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
@@ -807,6 +817,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "::warning::No signing certificate, leaving the apps unsigned."
|
||||
echo "signed=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
echo "$CERTIFICATE_P12_B64" | base64 -d > /tmp/certificate.p12
|
||||
@@ -834,6 +845,7 @@ jobs:
|
||||
spctl --assess --type execute --verbose=2 "$BUNDLE"
|
||||
rm notarize.zip
|
||||
done
|
||||
echo "signed=true" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Azure login for update signing.
|
||||
if: needs.version.outputs.publish == 'true'
|
||||
@@ -1226,7 +1238,7 @@ jobs:
|
||||
COMMIT: ${{ needs.version.outputs.commit }}
|
||||
VERSION_STR: ${{ needs.version.outputs.version_str }}
|
||||
PREVIOUS: ${{ needs.version.outputs.previous }}
|
||||
UNSIGNED: ${{ needs.version.outputs.unsigned }}
|
||||
SIGNED: ${{ needs.windows.outputs.signed }}
|
||||
KEYS_LOC: Telegram/Resources/update
|
||||
UPDATE_DIR: artifacts/update
|
||||
PORTABLE_DIR: artifacts/portable
|
||||
@@ -1315,7 +1327,7 @@ jobs:
|
||||
COMMIT: ${{ needs.version.outputs.commit }}
|
||||
VERSION_STR: ${{ needs.version.outputs.version_str }}
|
||||
PREVIOUS: ${{ needs.version.outputs.previous }}
|
||||
UNSIGNED: ${{ needs.version.outputs.unsigned }}
|
||||
SIGNED: ${{ needs.macos.outputs.signed }}
|
||||
KEYS_LOC: Telegram/Resources/update
|
||||
UPDATE_DIR: artifacts/update
|
||||
PORTABLE_DIR: artifacts/portable
|
||||
@@ -1390,7 +1402,7 @@ jobs:
|
||||
COMMIT: ${{ needs.version.outputs.commit }}
|
||||
VERSION_STR: ${{ needs.version.outputs.version_str }}
|
||||
PREVIOUS: ${{ needs.version.outputs.previous }}
|
||||
UNSIGNED: ${{ needs.version.outputs.unsigned }}
|
||||
SIGNED: true
|
||||
KEYS_LOC: Telegram/Resources/update
|
||||
UPDATE_DIR: artifacts/update
|
||||
PORTABLE_DIR: artifacts/portable
|
||||
|
||||
@@ -383,6 +383,12 @@ int Launcher::exec() {
|
||||
return psFixPrevious();
|
||||
}
|
||||
|
||||
// Before Logs::start(), which is where the working directory gets
|
||||
// chosen: a translocated bundle never sees its TelegramForcePortable.
|
||||
if (!Platform::CheckAppTranslocation()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Must be started before Platform is started.
|
||||
Logs::start();
|
||||
base::options::init(cWorkingDir() + "tdata/experimental_options.json");
|
||||
@@ -549,6 +555,7 @@ void Launcher::processArguments() {
|
||||
auto parseMap = std::map<QByteArray, KeyFormat> {
|
||||
{ "-debug" , KeyFormat::NoValues },
|
||||
{ "-testagent" , KeyFormat::NoValues },
|
||||
{ Platform::kUntranslocatedArgument, KeyFormat::NoValues },
|
||||
{ "-key" , KeyFormat::OneValue },
|
||||
{ "-autostart" , KeyFormat::NoValues },
|
||||
{ "-fixprevious" , KeyFormat::NoValues },
|
||||
|
||||
@@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
namespace Platform {
|
||||
|
||||
inline bool CheckAppTranslocation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void IgnoreApplicationActivationRightNow() {
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/history_widget.h"
|
||||
#include "core/crash_reports.h"
|
||||
#include "core/sandbox.h"
|
||||
#include "core/launcher.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "storage/localstorage.h"
|
||||
@@ -22,11 +23,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/platform/mac/base_utilities_mac.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
|
||||
#include <QtCore/QDirIterator>
|
||||
#include <QtGui/QDesktopServices>
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <dlfcn.h>
|
||||
#include <execinfo.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/xattr.h>
|
||||
|
||||
@@ -107,6 +111,154 @@ int psFixPrevious() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifndef OS_MAC_STORE
|
||||
namespace {
|
||||
|
||||
struct TranslocationState {
|
||||
bool translocated = false;
|
||||
QString original; // The bundle path macOS made the copy from.
|
||||
};
|
||||
|
||||
// Security.framework exports these since 10.12 without declaring them in
|
||||
// the public headers; the path check is the fallback when they are not
|
||||
// resolvable.
|
||||
using SecTranslocateIsTranslocatedURLFn = Boolean(*)(
|
||||
CFURLRef,
|
||||
bool*,
|
||||
CFErrorRef*);
|
||||
using SecTranslocateCreateOriginalPathForURLFn = CFURLRef(*)(
|
||||
CFURLRef,
|
||||
CFErrorRef*);
|
||||
|
||||
[[nodiscard]] TranslocationState CheckTranslocationState(
|
||||
const QString &bundle) {
|
||||
auto result = TranslocationState();
|
||||
NSURL *url = [NSURL fileURLWithPath:Platform::Q2NSString(bundle)];
|
||||
const auto security = dlopen(
|
||||
"/System/Library/Frameworks/Security.framework/Security",
|
||||
RTLD_LAZY);
|
||||
const auto isTranslocated = security
|
||||
? reinterpret_cast<SecTranslocateIsTranslocatedURLFn>(
|
||||
dlsym(security, "SecTranslocateIsTranslocatedURL"))
|
||||
: nullptr;
|
||||
const auto createOriginal = security
|
||||
? reinterpret_cast<SecTranslocateCreateOriginalPathForURLFn>(
|
||||
dlsym(security, "SecTranslocateCreateOriginalPathForURL"))
|
||||
: nullptr;
|
||||
|
||||
auto flag = false;
|
||||
if (isTranslocated && isTranslocated((CFURLRef)url, &flag, nullptr)) {
|
||||
result.translocated = flag;
|
||||
} else {
|
||||
result.translocated = bundle.contains(u"/AppTranslocation/"_q);
|
||||
}
|
||||
if (!result.translocated) {
|
||||
return result;
|
||||
}
|
||||
if (createOriginal) {
|
||||
if (const auto original = createOriginal((CFURLRef)url, nullptr)) {
|
||||
result.original = Platform::NS2QString([(NSURL*)original path]);
|
||||
CFRelease(original);
|
||||
}
|
||||
}
|
||||
if (result.original.isEmpty()) {
|
||||
// The translocated bundle lives on a read-only mount of the
|
||||
// original location, so the mount source names it.
|
||||
struct statfs info;
|
||||
if (statfs(QFile::encodeName(bundle).constData(), &info) == 0) {
|
||||
auto from = QFile::decodeName(info.f_mntfromname);
|
||||
const auto name = QFileInfo(bundle).fileName();
|
||||
if (!from.endsWith('/' + name)) {
|
||||
from += '/' + name;
|
||||
}
|
||||
result.original = from;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Every item of the bundle carries the attribute, and a single leftover
|
||||
// would translocate the relaunch again.
|
||||
[[nodiscard]] bool RemoveQuarantineRecursively(const QString &bundle) {
|
||||
constexpr auto kAttribute = "com.apple.quarantine";
|
||||
const auto remove = [&](const QString &path) {
|
||||
const auto local = QFile::encodeName(path);
|
||||
if (removexattr(local.constData(), kAttribute, XATTR_NOFOLLOW) == 0
|
||||
|| errno == ENOATTR) {
|
||||
return true;
|
||||
}
|
||||
LOG(("Translocation Error: removexattr failed for '%1': %2"
|
||||
).arg(path
|
||||
).arg(errno));
|
||||
return false;
|
||||
};
|
||||
if (!remove(bundle)) {
|
||||
return false;
|
||||
}
|
||||
auto iterator = QDirIterator(
|
||||
bundle,
|
||||
QDir::AllEntries
|
||||
| QDir::Hidden
|
||||
| QDir::System
|
||||
| QDir::NoDotAndDotDot,
|
||||
QDirIterator::Subdirectories);
|
||||
while (iterator.hasNext()) {
|
||||
if (!remove(iterator.next())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const auto local = QFile::encodeName(bundle);
|
||||
const auto left = getxattr(
|
||||
local.constData(),
|
||||
kAttribute,
|
||||
nullptr,
|
||||
0,
|
||||
0,
|
||||
XATTR_NOFOLLOW);
|
||||
return (left < 0) && (errno == ENOATTR);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool RelaunchUntranslocated(const QString &bundle) {
|
||||
NSDictionary *conf = @{
|
||||
NSWorkspaceLaunchConfigurationArguments: @[
|
||||
[NSString stringWithUTF8String:Platform::kUntranslocatedArgument]
|
||||
],
|
||||
};
|
||||
NSError *error = nil;
|
||||
const auto launched = [[NSWorkspace sharedWorkspace]
|
||||
launchApplicationAtURL:[NSURL fileURLWithPath:Platform::Q2NSString(bundle)]
|
||||
options:NSWorkspaceLaunchAsync | NSWorkspaceLaunchNewInstance
|
||||
configuration:conf
|
||||
error:&error];
|
||||
if (!launched) {
|
||||
LOG(("Translocation Error: relaunch failed: %1"
|
||||
).arg(Platform::NS2QString([error localizedDescription])));
|
||||
}
|
||||
return launched != nil;
|
||||
}
|
||||
|
||||
// Runs before Qt exists, so this is a bare AppKit alert. No localization
|
||||
// is loaded at this point either.
|
||||
void ShowTranslocationError() {
|
||||
NSApplication *app = [NSApplication sharedApplication];
|
||||
[app setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
[app activateIgnoringOtherApps:YES];
|
||||
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
alert.alertStyle = NSAlertStyleCritical;
|
||||
alert.messageText = @"Telegram Desktop can't start from here";
|
||||
alert.informativeText = @"macOS started this copy of Telegram Desktop "
|
||||
@"from a read-only temporary location (App Translocation), so it "
|
||||
@"can't use its own folder. Please reinstall the app and launch it "
|
||||
@"again.";
|
||||
[alert addButtonWithTitle:@"Quit"];
|
||||
[alert runModal];
|
||||
[alert release];
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif // !OS_MAC_STORE
|
||||
|
||||
namespace Platform {
|
||||
|
||||
void start() {
|
||||
@@ -117,6 +269,57 @@ void finish() {
|
||||
objc_finish();
|
||||
}
|
||||
|
||||
// macOS starts a quarantined bundle that was never moved by Finder from
|
||||
// a random read-only mount, so a portable build loses the
|
||||
// TelegramForcePortable folder it was shipped with and silently works on
|
||||
// the default installation's data. When the original location is known
|
||||
// and is the shipped portable layout, stripping the quarantine attribute
|
||||
// there is exactly what a Finder move would have done (Gatekeeper has
|
||||
// already assessed and allowed this launch), and the original can be
|
||||
// relaunched in place. Anything else is a hard stop: moving the parent
|
||||
// folder does not help and the user has to reinstall.
|
||||
bool CheckAppTranslocation() {
|
||||
#ifdef OS_MAC_STORE
|
||||
// Installed by the App Store: never quarantined, never translocated,
|
||||
// and the private SecTranslocate symbols must not appear in a store
|
||||
// binary.
|
||||
return true;
|
||||
#else // OS_MAC_STORE
|
||||
@autoreleasepool {
|
||||
|
||||
const auto bundle = cExeDir() + cExeName();
|
||||
if (cExeName().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
const auto state = CheckTranslocationState(bundle);
|
||||
if (!state.translocated) {
|
||||
return true;
|
||||
}
|
||||
LOG(("Translocation Info: running from '%1', original '%2'."
|
||||
).arg(bundle
|
||||
).arg(state.original));
|
||||
|
||||
const auto relaunched = Core::Launcher::Instance().arguments().contains(
|
||||
QString::fromLatin1(kUntranslocatedArgument));
|
||||
const auto portable = !state.original.isEmpty()
|
||||
&& QFileInfo(state.original + u"/Contents/Info.plist"_q).isFile()
|
||||
&& QDir(QFileInfo(state.original).path()
|
||||
+ u"/TelegramForcePortable"_q).exists();
|
||||
if (!relaunched
|
||||
&& portable
|
||||
&& RemoveQuarantineRecursively(state.original)
|
||||
&& RelaunchUntranslocated(state.original)) {
|
||||
LOG(("Translocation Info: relaunched from '%1'."
|
||||
).arg(state.original));
|
||||
return false;
|
||||
}
|
||||
ShowTranslocationError();
|
||||
return false;
|
||||
|
||||
}
|
||||
#endif // !OS_MAC_STORE
|
||||
}
|
||||
|
||||
QString SingleInstanceLocalServerName(const QString &hash) {
|
||||
#ifndef OS_MAC_STORE
|
||||
return u"/tmp/"_q + hash + '-' + cGUIDStr();
|
||||
|
||||
@@ -20,6 +20,16 @@ namespace Platform {
|
||||
void start();
|
||||
void finish();
|
||||
|
||||
// Passed to the instance relaunched from its original location after an
|
||||
// App Translocation fix, so a relaunch that is still translocated stops
|
||||
// instead of trying again.
|
||||
inline constexpr auto kUntranslocatedArgument = "-untranslocated";
|
||||
|
||||
// Returns false when startup must stop right away: the process was
|
||||
// started by macOS from a read-only translocated copy of the bundle and
|
||||
// either relaunched itself from the original location or told the user.
|
||||
[[nodiscard]] bool CheckAppTranslocation();
|
||||
|
||||
enum class PermissionStatus {
|
||||
Granted,
|
||||
CanRequest,
|
||||
|
||||
@@ -13,6 +13,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
namespace Platform {
|
||||
|
||||
inline bool CheckAppTranslocation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void IgnoreApplicationActivationRightNow() {
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
# BASE, COUNTER, COMMIT the version being published
|
||||
# VERSION_STR the display version (7.0.9) of the archives
|
||||
# PREVIOUS commit of the previous run, for the changelog
|
||||
# UNSIGNED "true" when platform signing was skipped
|
||||
# SIGNED "true" when the platform binaries carry their
|
||||
# Authenticode signature / notarization (Linux
|
||||
# has none to carry and always passes "true")
|
||||
# KEYS_LOC directory with manifest.min.json + manifest.sig
|
||||
# UPDATE_DIR, PORTABLE_DIR
|
||||
# downloaded artifacts of this platform
|
||||
@@ -109,10 +111,17 @@ if [ ! -f "$PORTABLE" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NOTE=""
|
||||
if [ "$SIGNED" != "true" ]; then
|
||||
case "$FIRST" in
|
||||
win64) NOTE="UNSIGNED test build: no Authenticode signature." ;;
|
||||
mac|armac) NOTE="UNSIGNED test build: not signed or notarized." ;;
|
||||
esac
|
||||
fi
|
||||
CAPTION=$({
|
||||
echo "Canary #$COUNTER · $COMMIT"
|
||||
if [ "$UNSIGNED" = "true" ]; then
|
||||
echo "UNSIGNED test build: no Authenticode / notarization."
|
||||
if [ -n "$NOTE" ]; then
|
||||
echo "$NOTE"
|
||||
fi
|
||||
echo ""
|
||||
if [ -n "$PREVIOUS" ] && git cat-file -e "$PREVIOUS^{commit}" 2>/dev/null \
|
||||
|
||||
Reference in New Issue
Block a user