Skip to content

Commit c36d2a4

Browse files
author
pavelhov
authored
Merge pull request #40 from pavelhov/development
Promote macOS in-app updates to main
2 parents 85001c0 + 0e9a408 commit c36d2a4

54 files changed

Lines changed: 4740 additions & 83 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎THIRD_PARTY_NOTICES.md‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,145 @@ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPO
2525
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
2626
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
2727
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28+
29+
## Sparkle 2.9.6
30+
31+
Repository: <https://github.com/sparkle-project/Sparkle>
32+
33+
Pinned revision: ac2def288cbff5cfc7df3ffef6abdf45b72bcb0a.
34+
The macOS bundle includes the complete framework and its helpers.
35+
36+
Copyright (c) 2006-2013 Andy Matuschak.
37+
Copyright (c) 2009-2013 Elgato Systems GmbH.
38+
Copyright (c) 2011-2014 Kornel Lesiński.
39+
Copyright (c) 2015-2017 Mayur Pawashe.
40+
Copyright (c) 2014 C.W. Betts.
41+
Copyright (c) 2014 Petroules Corporation.
42+
Copyright (c) 2014 Big Nerd Ranch.
43+
All rights reserved.
44+
45+
Permission is hereby granted, free of charge, to any person obtaining a copy of
46+
this software and associated documentation files (the "Software"), to deal in
47+
the Software without restriction, including without limitation the rights to
48+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
49+
the Software, and to permit persons to whom the Software is furnished to do so,
50+
subject to the following conditions:
51+
52+
The above copyright notice and this permission notice shall be included in all
53+
copies or substantial portions of the Software.
54+
55+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
56+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
57+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
58+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
59+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
60+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
61+
62+
=================
63+
EXTERNAL LICENSES
64+
=================
65+
66+
bspatch.c and bsdiff.c, from bsdiff 4.3 <http://www.daemonology.net/bsdiff/>:
67+
68+
Copyright 2003-2005 Colin Percival
69+
All rights reserved
70+
71+
Redistribution and use in source and binary forms, with or without
72+
modification, are permitted providing that the following conditions
73+
are met:
74+
1. Redistributions of source code must retain the above copyright
75+
notice, this list of conditions and the following disclaimer.
76+
2. Redistributions in binary form must reproduce the above copyright
77+
notice, this list of conditions and the following disclaimer in the
78+
documentation and/or other materials provided with the distribution.
79+
80+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
81+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
82+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
83+
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
84+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
85+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
86+
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
87+
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
88+
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
89+
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
90+
POSSIBILITY OF SUCH DAMAGE.
91+
92+
--
93+
94+
sais.c and sais.h, from sais-lite (2010/08/07) <https://sites.google.com/site/yuta256/sais>:
95+
96+
The sais-lite copyright is as follows:
97+
98+
Copyright (c) 2008-2010 Yuta Mori All Rights Reserved.
99+
100+
Permission is hereby granted, free of charge, to any person
101+
obtaining a copy of this software and associated documentation
102+
files (the "Software"), to deal in the Software without
103+
restriction, including without limitation the rights to use,
104+
copy, modify, merge, publish, distribute, sublicense, and/or sell
105+
copies of the Software, and to permit persons to whom the
106+
Software is furnished to do so, subject to the following
107+
conditions:
108+
109+
The above copyright notice and this permission notice shall be
110+
included in all copies or substantial portions of the Software.
111+
112+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
113+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
114+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
115+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
116+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
117+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
118+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
119+
OTHER DEALINGS IN THE SOFTWARE.
120+
121+
--
122+
123+
Portable C implementation of Ed25519, from https://github.com/orlp/ed25519
124+
125+
Copyright (c) 2015 Orson Peters <orsonpeters@gmail.com>
126+
127+
This software is provided 'as-is', without any express or implied warranty. In no event will the
128+
authors be held liable for any damages arising from the use of this software.
129+
130+
Permission is granted to anyone to use this software for any purpose, including commercial
131+
applications, and to alter it and redistribute it freely, subject to the following restrictions:
132+
133+
1. The origin of this software must not be misrepresented; you must not claim that you wrote the
134+
original software. If you use this software in a product, an acknowledgment in the product
135+
documentation would be appreciated but is not required.
136+
137+
2. Altered source versions must be plainly marked as such, and must not be misrepresented as
138+
being the original software.
139+
140+
3. This notice may not be removed or altered from any source distribution.
141+
142+
--
143+
144+
SUSignatureVerifier.m:
145+
146+
Copyright (c) 2011 Mark Hamlin.
147+
148+
All rights reserved.
149+
150+
Redistribution and use in source and binary forms, with or without
151+
modification, are permitted providing that the following conditions
152+
are met:
153+
1. Redistributions of source code must retain the above copyright
154+
notice, this list of conditions and the following disclaimer.
155+
2. Redistributions in binary form must reproduce the above copyright
156+
notice, this list of conditions and the following disclaimer in the
157+
documentation and/or other materials provided with the distribution.
158+
159+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
160+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
161+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
162+
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
163+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
164+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
165+
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
166+
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
167+
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
168+
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
169+
POSSIBILITY OF SUCH DAMAGE.

‎app/Info.plist‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@
2222
<string>0.0.0</string>
2323
<key>CFBundleVersion</key>
2424
<string>0.0.0</string>
25+
<key>CodexCommanderUpdaterEnabled</key>
26+
<false/>
27+
<key>SUFeedURL</key>
28+
<string>https://github.com/pavelhov/CodexCommander/releases/latest/download/appcast.xml</string>
29+
<key>SURequireSignedFeed</key>
30+
<true/>
31+
<key>SUVerifyUpdateBeforeExtraction</key>
32+
<true/>
33+
<key>SUSignedFeedFailureExpirationInterval</key>
34+
<integer>0</integer>
35+
<key>SUAllowsAutomaticUpdates</key>
36+
<false/>
37+
<key>SUEnableAutomaticChecks</key>
38+
<true/>
2539
<key>LSUIElement</key>
2640
<true/>
2741
<key>LSMultipleInstancesProhibited</key>

‎app/Package.resolved‎

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎app/Package.swift‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ let package = Package(
1111
.executable(name: "UIProbe", targets: ["UIProbe"]),
1212
.executable(name: "IconProbe", targets: ["IconProbe"]),
1313
],
14+
dependencies: [
15+
// Sparkle 2.9.6; immutable revision also pins its binary artifact checksum.
16+
.package(url: "https://github.com/sparkle-project/Sparkle", revision: "ac2def288cbff5cfc7df3ffef6abdf45b72bcb0a"),
17+
],
1418
targets: [
1519
.target(name: "MenuBarCore", path: "Sources/MenuBarCore"),
1620
// AppKit views live in a library so both the app and the visual-QA probe can
1721
// build the same surface. An executable target cannot be imported.
18-
.target(name: "MenuBarUI", dependencies: ["MenuBarCore"], path: "Sources/MenuBarUI"),
22+
.target(name: "MenuBarUI", dependencies: ["MenuBarCore", .product(name: "Sparkle", package: "Sparkle")], path: "Sources/MenuBarUI"),
1923
.executableTarget(
2024
name: "MenuBarApp",
2125
dependencies: ["MenuBarCore", "MenuBarUI"],

‎app/Sources/MenuBarApp/main.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,6 @@ let app = NSApplication.shared
4242
// packaged bundle, and this covers `swift run` during development.
4343
app.setActivationPolicy(.accessory)
4444

45-
let delegate = AppDelegate()
45+
let delegate = MainActor.assumeIsolated { AppDelegate() }
4646
app.delegate = delegate
4747
app.run()

‎app/Sources/MenuBarCore/LifecycleHelper.swift‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ public enum LifecycleHelperDiscovery {
311311
}
312312
}
313313

314-
private final class BoundedOutput: @unchecked Sendable {
314+
final class BoundedOutput: @unchecked Sendable {
315315
private let lock = NSLock()
316316
private let limit: Int
317317
private var data = Data()
@@ -335,7 +335,7 @@ private final class BoundedOutput: @unchecked Sendable {
335335
}
336336
}
337337

338-
private final class TimeoutState: @unchecked Sendable {
338+
final class TimeoutState: @unchecked Sendable {
339339
private let lock = NSLock()
340340
private var fired = false
341341

@@ -455,7 +455,7 @@ public actor LifecycleHelper: LifecycleCommandRunning {
455455

456456
/// Preserve CodexCommander/Codex configuration while removing runtime preloads and an
457457
/// attacker-controlled PATH from this privileged fixed-action bridge.
458-
private nonisolated static func controlledEnvironment(for invocation: LifecycleInvocation) -> [String: String] {
458+
nonisolated static func controlledEnvironment(for invocation: LifecycleInvocation) -> [String: String] {
459459
var environment = ProcessInfo.processInfo.environment
460460
environment["PATH"] = "/usr/bin:/bin:/usr/sbin:/sbin"
461461
if let workingDirectory = invocation.workingDirectory {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import Foundation
2+
import Darwin
3+
4+
public enum MacOSUpdateAction: String, Codable, Sendable {
5+
case status, prepare, arm, cancel, reconcile
6+
case recordOff = "record-off"
7+
}
8+
public enum MacOSUpdateStatus: String, Codable, Sendable {
9+
case idle, prepared, armed, recovered, blocked
10+
case confirmationRequired = "confirmation-required"
11+
case finishRequired = "finish-required"
12+
}
13+
public struct MacOSUpdateCommand: Equatable, Sendable {
14+
public let action: MacOSUpdateAction
15+
public let transactionId: String?
16+
public let targetBuild: String?
17+
public let updateAnyway: Bool
18+
public init(_ action: MacOSUpdateAction, transactionId: String? = nil, targetBuild: String? = nil, updateAnyway: Bool = false) {
19+
self.action = action; self.transactionId = transactionId; self.targetBuild = targetBuild; self.updateAnyway = updateAnyway
20+
}
21+
public func arguments() throws -> [String] {
22+
switch action {
23+
case .status, .reconcile, .recordOff:
24+
guard transactionId == nil, targetBuild == nil, !updateAnyway else { throw LifecycleHelperError.invalidResponse }
25+
return [action.rawValue]
26+
case .arm, .cancel:
27+
guard let transactionId, UUID(uuidString: transactionId) != nil, targetBuild == nil, !updateAnyway else { throw LifecycleHelperError.invalidResponse }
28+
return [action.rawValue, transactionId]
29+
case .prepare:
30+
guard let transactionId, UUID(uuidString: transactionId) != nil, let targetBuild, Self.validBuild(targetBuild) else { throw LifecycleHelperError.invalidResponse }
31+
return [action.rawValue, transactionId, targetBuild] + (updateAnyway ? ["update-anyway"] : [])
32+
}
33+
}
34+
static func validBuild(_ value: String) -> Bool {
35+
value.range(of: "^[1-9][0-9]{0,19}$", options: .regularExpression) != nil
36+
}
37+
}
38+
public struct MacOSUpdateResult: Decodable, Equatable, Sendable {
39+
public let schemaVersion: Int
40+
public let action: MacOSUpdateAction
41+
public let status: MacOSUpdateStatus
42+
public let transactionId: String?
43+
public let targetBuild: String?
44+
public let active: Int
45+
public let errorCode: String?
46+
public let message: String
47+
public init(schemaVersion: Int = 1, action: MacOSUpdateAction, status: MacOSUpdateStatus, transactionId: String? = nil, targetBuild: String? = nil, active: Int = 0, errorCode: String? = nil, message: String = "Update lifecycle state verified.") {
48+
self.schemaVersion = schemaVersion; self.action = action; self.status = status
49+
self.transactionId = transactionId; self.targetBuild = targetBuild; self.active = active
50+
self.errorCode = errorCode; self.message = message
51+
}
52+
public static func decode(_ data: Data, command: MacOSUpdateCommand, exitCode: Int32) throws -> MacOSUpdateResult {
53+
guard data.count <= 2048,
54+
let object = try JSONSerialization.jsonObject(with: data) as? [String: Any],
55+
Set(object.keys) == Set(["schemaVersion", "action", "status", "transactionId", "targetBuild", "active", "errorCode", "message"])
56+
else { throw LifecycleHelperError.invalidResponse }
57+
let result = try JSONDecoder().decode(Self.self, from: data)
58+
guard result.schemaVersion == 1, result.action == command.action,
59+
result.active >= 0, result.active <= 1_000_000,
60+
result.message.utf8.count <= 240,
61+
result.transactionId.map({ UUID(uuidString: $0) != nil }) ?? true,
62+
result.targetBuild.map({ MacOSUpdateCommand.validBuild($0) }) ?? true,
63+
(result.transactionId == nil) == (result.targetBuild == nil),
64+
result.errorCode == nil || result.errorCode == "UPDATE_RECOVERY_REQUIRED",
65+
(result.status == .blocked ? exitCode == 1 : exitCode == 0),
66+
command.transactionId == nil || result.transactionId == command.transactionId || result.status == .recovered || result.status == .blocked,
67+
command.targetBuild == nil || result.targetBuild == command.targetBuild || result.status == .blocked,
68+
result.status != .confirmationRequired || (command.action == .prepare && result.active > 0 && result.transactionId != nil),
69+
![MacOSUpdateStatus.prepared, .armed, .finishRequired].contains(result.status) || result.transactionId != nil
70+
else { throw LifecycleHelperError.invalidResponse }
71+
return result
72+
}
73+
}
74+
public protocol MacOSUpdateCommandRunning: Sendable {
75+
func run(_ command: MacOSUpdateCommand) async throws -> MacOSUpdateResult
76+
}
77+
/// Fixed embedded-runtime bridge; no global-install discovery fallback is accepted.
78+
public actor MacOSUpdateHelper: MacOSUpdateCommandRunning {
79+
private let invocation: LifecycleInvocation?
80+
private let timeout: TimeInterval
81+
public init(invocation: LifecycleInvocation? = LifecycleHelperDiscovery.discover(), timeout: TimeInterval = LifecycleHelper.timeout) {
82+
self.invocation = invocation; self.timeout = timeout
83+
}
84+
public func run(_ command: MacOSUpdateCommand) async throws -> MacOSUpdateResult {
85+
guard let invocation, invocation.appOwnedRuntime else { throw LifecycleHelperError.unavailable }
86+
let arguments = try command.arguments()
87+
let timeout = self.timeout
88+
return try await withCheckedThrowingContinuation { continuation in
89+
DispatchQueue.global(qos: .userInitiated).async {
90+
let process = Process()
91+
let pipe = Pipe()
92+
let output = BoundedOutput(limit: LifecycleHelper.maximumOutputBytes)
93+
let timeoutState = TimeoutState()
94+
process.executableURL = invocation.executable
95+
process.arguments = invocation.prefixArguments + ["__macos-update"] + arguments
96+
process.currentDirectoryURL = invocation.workingDirectory
97+
process.environment = LifecycleHelper.controlledEnvironment(for: invocation)
98+
process.standardOutput = pipe
99+
process.standardError = FileHandle.nullDevice
100+
pipe.fileHandleForReading.readabilityHandler = { output.append($0.availableData) }
101+
let killWork = DispatchWorkItem {
102+
guard timeoutState.didFire, process.isRunning else { return }
103+
_ = Darwin.kill(process.processIdentifier, SIGKILL)
104+
}
105+
let timeoutWork = DispatchWorkItem {
106+
guard process.isRunning else { return }
107+
timeoutState.markFired(); process.terminate()
108+
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 1, execute: killWork)
109+
}
110+
do { try process.run() } catch {
111+
pipe.fileHandleForReading.readabilityHandler = nil
112+
continuation.resume(throwing: LifecycleHelperError.launchFailed); return
113+
}
114+
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + timeout, execute: timeoutWork)
115+
process.waitUntilExit(); timeoutWork.cancel(); killWork.cancel()
116+
pipe.fileHandleForReading.readabilityHandler = nil
117+
output.append(pipe.fileHandleForReading.readDataToEndOfFile())
118+
let (data, overflow) = output.snapshot()
119+
guard !overflow, process.terminationReason != .uncaughtSignal else {
120+
continuation.resume(throwing: timeoutState.didFire ? LifecycleHelperError.timedOut : LifecycleHelperError.invalidResponse); return
121+
}
122+
do { continuation.resume(returning: try MacOSUpdateResult.decode(data, command: command, exitCode: process.terminationStatus)) }
123+
catch { continuation.resume(throwing: LifecycleHelperError.invalidResponse) }
124+
}
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)