afterpresets 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/native-runtime/CMakeLists.txt +82 -0
- package/native-runtime/README.md +40 -0
- package/native-runtime/THIRD_PARTY_NOTICES.md +14 -0
- package/native-runtime/include/ap_native.h +223 -0
- package/native-runtime/src/apply.cpp +199 -0
- package/native-runtime/src/crypto.cpp +225 -0
- package/native-runtime/src/crypto.hpp +51 -0
- package/native-runtime/src/dialog.cpp +62 -0
- package/native-runtime/src/dialog.hpp +35 -0
- package/native-runtime/src/engine.cpp +909 -0
- package/native-runtime/src/engine.hpp +232 -0
- package/native-runtime/src/fsutil.cpp +291 -0
- package/native-runtime/src/fsutil.hpp +57 -0
- package/native-runtime/src/json.cpp +326 -0
- package/native-runtime/src/json.hpp +70 -0
- package/native-runtime/src/platform.hpp +83 -0
- package/native-runtime/src/platform_common.cpp +60 -0
- package/native-runtime/src/platform_mac.mm +314 -0
- package/native-runtime/src/platform_win.cpp +277 -0
- package/native-runtime/src/runtime.cpp +473 -0
- package/native-runtime/src/util.cpp +169 -0
- package/native-runtime/src/util.hpp +32 -0
- package/native-runtime/src/zip.cpp +243 -0
- package/native-runtime/src/zip.hpp +58 -0
- package/native-runtime/third_party/monocypher/LICENCE.md +167 -0
- package/native-runtime/third_party/monocypher/monocypher-ed25519.c +500 -0
- package/native-runtime/third_party/monocypher/monocypher-ed25519.h +140 -0
- package/native-runtime/third_party/monocypher/monocypher.c +2956 -0
- package/native-runtime/third_party/monocypher/monocypher.h +321 -0
- package/native-runtime/third_party/tinf/LICENSE +22 -0
- package/native-runtime/third_party/tinf/crc32.c +57 -0
- package/native-runtime/third_party/tinf/tinf.h +140 -0
- package/native-runtime/third_party/tinf/tinflate.c +636 -0
- package/native-runtime/vendor-files.json +39 -0
- package/package.json +6 -3
- package/runtime/docs/cep/host-after-effects.md +57 -0
- package/runtime/docs/cep/host-premiere.md +84 -0
- package/runtime/docs/{host.md → cep/host.md} +42 -16
- package/runtime/docs/{README.md → common/README.md} +10 -8
- package/runtime/docs/{assets.md → common/assets.md} +16 -2
- package/runtime/docs/{licensing.md → common/licensing.md} +1 -1
- package/runtime/docs/{panels.md → common/panels.md} +5 -5
- package/runtime/docs/{storage.md → common/storage.md} +5 -5
- package/runtime/docs/{updates.md → common/updates.md} +1 -1
- package/runtime/docs/native/README.md +44 -0
- package/runtime/docs/native/debugging.md +140 -0
- package/runtime/docs/native/rules.md +402 -0
- package/runtime/docs/native/testing.md +154 -0
- package/runtime/package.json +1 -1
- package/runtime/scripts/ae-sdk-docs/config.js +102 -0
- package/runtime/scripts/ae-sdk-docs/convert.js +430 -0
- package/runtime/scripts/ae-sdk-docs/index.js +314 -0
- package/runtime/scripts/ae-sdk-docs/source.js +205 -0
- package/runtime/scripts/ae-sdk-docs/split.js +170 -0
- package/runtime/scripts/ae-sdk-docs.js +72 -0
- package/runtime/src/host.js +1 -1
- package/runtime/src/updates.js +1 -1
- package/src/api/error-messages.js +1 -1
- package/src/build/build.js +7 -7
- package/src/build/html.js +1 -1
- package/src/build/jsx.js +2 -2
- package/src/build/runtime.js +3 -3
- package/src/check/check.js +3 -3
- package/src/check/native.js +14 -0
- package/src/check/pricing.js +1 -1
- package/src/check/types.js +2 -2
- package/src/cli.js +5 -5
- package/src/commands/add.js +8 -3
- package/src/commands/build.js +8 -2
- package/src/commands/check.js +11 -3
- package/src/commands/dev.js +9 -2
- package/src/commands/docs.js +5 -1
- package/src/commands/init.js +15 -3
- package/src/commands/push.js +5 -2
- package/src/commands/sdk.js +14 -0
- package/src/commands/testers.js +1 -1
- package/src/commands/upgrade.js +2 -2
- package/src/commands/whoami.js +1 -1
- package/src/config.js +1 -1
- package/src/dev/bridge.js +8 -0
- package/src/dev/cep-link.js +1 -1
- package/src/dev/debug-file.js +5 -5
- package/src/dev/native.js +3 -0
- package/src/dev/session.js +4 -3
- package/src/mcp/extendscript.js +10 -7
- package/src/mcp/host-target.js +1 -0
- package/src/mcp/panels.js +1 -1
- package/src/mcp/png.js +1 -0
- package/src/mcp/server.js +2 -2
- package/src/mcp/tools/capture.js +3 -0
- package/src/mcp/tools/host.js +1 -1
- package/src/mcp/tools/native.js +6 -0
- package/src/mcp/tools/panel.js +4 -4
- package/src/mcp/tools/project.js +47 -31
- package/src/mcp/tools/releases.js +23 -20
- package/src/native/build.js +27 -0
- package/src/native/cmake/CMakeLists.txt +88 -0
- package/src/native/cmake/zig-windows.cmake +19 -0
- package/src/native/effect.js +3 -0
- package/src/native/install.js +8 -0
- package/src/native/installers.js +4 -0
- package/src/native/pipl.js +20 -0
- package/src/native/restart.js +2 -0
- package/src/native/sdk.js +8 -0
- package/src/project/add-panel.js +1 -1
- package/src/project/docs.js +3 -1
- package/src/project/manifest.js +3 -2
- package/src/project/project.js +2 -2
- package/src/project/template.js +5 -1
- package/src/prompt.js +1 -1
- package/src/release/installers.js +4 -4
- package/src/release/native.js +3 -0
- package/src/release/package.js +1 -1
- package/src/release/publish.js +1 -1
- package/src/system/run-command.js +1 -1
- package/src/targets.js +1 -0
- package/src/user-config.js +2 -2
- package/template/cep/AGENTS.md +104 -0
- package/template/{README.md → cep/README.md} +16 -11
- package/template/cep/docs/code_guide.md +67 -0
- package/template/cep/docs/initial_setup.md +115 -0
- package/template/cep/docs/release_checklist.md +24 -0
- package/template/{src → cep/src}/features/home/home.html +4 -4
- package/template/{src → cep/src}/features/home/home.js +10 -11
- package/template/{src → cep/src}/shared/lib/host-theme.js +3 -3
- package/template/{src → cep/src}/shared/lib/view.js +1 -1
- package/template/{src → cep/src}/shared/ui/tokens.css +1 -1
- package/template/hosts/AGENTS.two-apps.md +12 -0
- package/template/hosts/aeft/AGENTS.host.md +10 -0
- package/template/hosts/aeft/jsx/aeft/home.jsx +16 -0
- package/template/hosts/ppro/AGENTS.host.md +15 -0
- package/template/hosts/ppro/jsx/ppro/home.jsx +20 -0
- package/template/native/.mcp.json +8 -0
- package/template/native/AGENTS.md +105 -0
- package/template/native/README.md +71 -0
- package/template/native/docs/code_guide.md +72 -0
- package/template/native/docs/initial_setup.md +179 -0
- package/template/native/docs/release_checklist.md +24 -0
- package/template/native/effect.json +16 -0
- package/template/native/gitignore +8 -0
- package/template/native/package.json +14 -0
- package/template/native/src/Effect.cpp +224 -0
- package/template/native/src/License.cpp +95 -0
- package/template/native/src/License.h +29 -0
- package/template/native/src/Render.h +68 -0
- package/vendor/afterpresets-additions/AfterEffects.d.ts +291 -0
- package/vendor/afterpresets-additions/Premiere.d.ts +179 -0
- package/vendor/types-for-adobe/Premiere/24.0/index.d.ts +2567 -0
- package/vendor/types-for-adobe/README.md +8 -4
- package/template/AGENTS.md +0 -168
- package/template/docs/initial_setup.md +0 -69
- package/template/jsx/home.jsx +0 -17
- /package/runtime/docs/{boot.md → common/boot.md} +0 -0
- /package/template/{.mcp.json → cep/.mcp.json} +0 -0
- /package/template/{gitignore → cep/gitignore} +0 -0
- /package/template/{package.json → cep/package.json} +0 -0
- /package/template/{src → cep/src}/features/home/home.css +0 -0
- /package/template/{src → cep/src}/features/license-screen/license-screen.css +0 -0
- /package/template/{src → cep/src}/features/license-screen/license-screen.html +0 -0
- /package/template/{src → cep/src}/features/license-screen/license-screen.js +0 -0
- /package/template/{src → cep/src}/features/update-screen/update-screen.css +0 -0
- /package/template/{src → cep/src}/features/update-screen/update-screen.html +0 -0
- /package/template/{src → cep/src}/features/update-screen/update-screen.js +0 -0
- /package/template/{src → cep/src}/panels/main/index.html +0 -0
- /package/template/{src → cep/src}/panels/main/main.js +0 -0
- /package/template/{src → cep/src}/shared/lib/dom.js +0 -0
- /package/template/{src → cep/src}/shared/lib/markdown.js +0 -0
- /package/template/{src → cep/src}/shared/ui/base.css +0 -0
- /package/template/{src → cep/src}/shared/ui/button.css +0 -0
- /package/template/{src → cep/src}/shared/ui/card.css +0 -0
- /package/template/{src → cep/src}/shared/ui/field.css +0 -0
- /package/template/{src → cep/src}/shared/ui/index.css +0 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// C API, worker thread and log for ap_native. See include/ap_native.h.
|
|
2
|
+
#include <algorithm>
|
|
3
|
+
#include <chrono>
|
|
4
|
+
#include <condition_variable>
|
|
5
|
+
#include <cstring>
|
|
6
|
+
#include <deque>
|
|
7
|
+
#include <mutex>
|
|
8
|
+
#include <regex>
|
|
9
|
+
#include <thread>
|
|
10
|
+
|
|
11
|
+
#ifdef __APPLE__
|
|
12
|
+
#include <pthread.h>
|
|
13
|
+
#endif
|
|
14
|
+
|
|
15
|
+
#include "ap_native.h"
|
|
16
|
+
#include "dialog.hpp"
|
|
17
|
+
#include "engine.hpp"
|
|
18
|
+
#include "fsutil.hpp"
|
|
19
|
+
#include "util.hpp"
|
|
20
|
+
|
|
21
|
+
namespace apn {
|
|
22
|
+
|
|
23
|
+
// ------------------------------------------------------------------ log
|
|
24
|
+
//
|
|
25
|
+
// Size-capped, rotating, thread-safe. ap_log never blocks: it appends to an
|
|
26
|
+
// in-memory queue with try_lock (render threads) and drops the line if the
|
|
27
|
+
// queue is busy or full; the worker thread writes the file. Tokens (apl_...)
|
|
28
|
+
// and URL query strings (signed download URLs) are scrubbed before writing.
|
|
29
|
+
class Logger {
|
|
30
|
+
public:
|
|
31
|
+
static constexpr size_t MAX_QUEUE = 512;
|
|
32
|
+
static constexpr uint64_t MAX_BYTES = 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
void open(const std::string &path) { path_ = path; }
|
|
35
|
+
const std::string &path() const { return path_; }
|
|
36
|
+
|
|
37
|
+
void push(int level, const std::string &message, int64_t now_ms, bool may_block) {
|
|
38
|
+
if (path_.empty()) return;
|
|
39
|
+
std::unique_lock<std::mutex> l(mu_, std::defer_lock);
|
|
40
|
+
if (may_block) l.lock();
|
|
41
|
+
else if (!l.try_lock()) { dropped_.fetch_add(1); return; }
|
|
42
|
+
if (queue_.size() >= MAX_QUEUE) { dropped_.fetch_add(1); return; }
|
|
43
|
+
static const char *names[] = {"debug", "info", "warn", "error"};
|
|
44
|
+
queue_.push_back(format_iso8601(now_ms) + " [" + names[std::clamp(level, 0, 3)] + "] " + message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
bool pending() {
|
|
48
|
+
std::lock_guard<std::mutex> l(mu_);
|
|
49
|
+
return !queue_.empty();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
void flush() {
|
|
53
|
+
std::deque<std::string> lines;
|
|
54
|
+
{
|
|
55
|
+
std::lock_guard<std::mutex> l(mu_);
|
|
56
|
+
lines.swap(queue_);
|
|
57
|
+
}
|
|
58
|
+
unsigned dropped = dropped_.exchange(0);
|
|
59
|
+
if (lines.empty() && !dropped) return;
|
|
60
|
+
std::string text;
|
|
61
|
+
for (auto &line : lines) text += scrub(line) + "\n";
|
|
62
|
+
if (dropped) text += "(" + std::to_string(dropped) + " log lines dropped)\n";
|
|
63
|
+
fsu::mkdirs(fsu::parent(path_));
|
|
64
|
+
auto st = fsu::stamp(path_);
|
|
65
|
+
if (st && st->size > MAX_BYTES) {
|
|
66
|
+
// Over the cap: keep one previous file.
|
|
67
|
+
fsu::remove_file(path_ + ".1");
|
|
68
|
+
fsu::rename(path_, path_ + ".1");
|
|
69
|
+
}
|
|
70
|
+
FILE *f = std::fopen(path_.c_str(), "ab");
|
|
71
|
+
if (!f) return;
|
|
72
|
+
std::fwrite(text.data(), 1, text.size(), f);
|
|
73
|
+
std::fclose(f);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static std::string scrub(const std::string &line) {
|
|
77
|
+
static const std::regex token("apl_[A-Za-z0-9_-]{8,}");
|
|
78
|
+
static const std::regex query("(https?://[^\\s?#]+)\\?[^\\s]*");
|
|
79
|
+
return std::regex_replace(std::regex_replace(line, token, "apl_***"), query, "$1?***");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private:
|
|
83
|
+
std::string path_;
|
|
84
|
+
std::mutex mu_;
|
|
85
|
+
std::deque<std::string> queue_;
|
|
86
|
+
std::atomic<unsigned> dropped_{0};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
std::string default_log_dir(const std::string &data_root_override) {
|
|
90
|
+
if (!data_root_override.empty()) return fsu::join(fsu::join(data_root_override, "AfterPresets"), "Logs");
|
|
91
|
+
#ifdef _WIN32
|
|
92
|
+
std::string root = platform_data_root(); // %LOCALAPPDATA%
|
|
93
|
+
return root.empty() ? "" : fsu::join(fsu::join(root, "AfterPresets"), "Logs");
|
|
94
|
+
#else
|
|
95
|
+
const char *home = getenv("HOME");
|
|
96
|
+
return home && *home ? std::string(home) + "/Library/Logs/AfterPresets" : "";
|
|
97
|
+
#endif
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ------------------------------------------------------------------ worker
|
|
101
|
+
|
|
102
|
+
struct Cmd {
|
|
103
|
+
enum Kind { Validate, ValidateBehind, SignIn, CancelSignIn, OpenPage, Retry, SignOut, CheckUpdate, Download,
|
|
104
|
+
MarkApplied } kind;
|
|
105
|
+
std::string arg;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
struct Shared {
|
|
109
|
+
Settings settings;
|
|
110
|
+
Env env;
|
|
111
|
+
std::unique_ptr<Engine> engine;
|
|
112
|
+
Logger logger;
|
|
113
|
+
|
|
114
|
+
std::atomic<int> unlocked{0};
|
|
115
|
+
std::atomic<int> status{(int)Status::Checking};
|
|
116
|
+
std::atomic<uint64_t> generation{0};
|
|
117
|
+
std::mutex view_mu;
|
|
118
|
+
LicenseView license;
|
|
119
|
+
UpdateView update;
|
|
120
|
+
std::string notice;
|
|
121
|
+
|
|
122
|
+
std::mutex q_mu;
|
|
123
|
+
std::condition_variable cv; // work arrived / stop
|
|
124
|
+
std::condition_variable idle_cv; // worker went idle / finished
|
|
125
|
+
std::deque<Cmd> queue;
|
|
126
|
+
bool stop = false;
|
|
127
|
+
bool busy = false;
|
|
128
|
+
bool finished = false;
|
|
129
|
+
std::atomic<bool> cancel{false};
|
|
130
|
+
|
|
131
|
+
void (*on_change)(void *) = nullptr;
|
|
132
|
+
void *user = nullptr;
|
|
133
|
+
|
|
134
|
+
void publish() {
|
|
135
|
+
{
|
|
136
|
+
std::lock_guard<std::mutex> l(view_mu);
|
|
137
|
+
license = engine->license();
|
|
138
|
+
update = engine->update();
|
|
139
|
+
notice = engine->notice();
|
|
140
|
+
}
|
|
141
|
+
status.store((int)engine->license().status);
|
|
142
|
+
unlocked.store(engine->unlocked() ? 1 : 0);
|
|
143
|
+
generation.fetch_add(1);
|
|
144
|
+
if (on_change) on_change(user);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
void post(Cmd cmd) {
|
|
148
|
+
{
|
|
149
|
+
std::lock_guard<std::mutex> l(q_mu);
|
|
150
|
+
if (stop) return;
|
|
151
|
+
queue.push_back(std::move(cmd));
|
|
152
|
+
}
|
|
153
|
+
cv.notify_all();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
void run(Shared &sh, const Cmd &cmd) {
|
|
158
|
+
Engine &e = *sh.engine;
|
|
159
|
+
switch (cmd.kind) {
|
|
160
|
+
case Cmd::Validate: e.validate_stored(); break;
|
|
161
|
+
case Cmd::ValidateBehind: e.validate_behind(); break;
|
|
162
|
+
case Cmd::SignIn: e.sign_in(); break;
|
|
163
|
+
case Cmd::CancelSignIn: e.cancel_sign_in(); break;
|
|
164
|
+
case Cmd::OpenPage: e.open_sign_in_page(); break;
|
|
165
|
+
case Cmd::Retry: e.validate_stored(); break;
|
|
166
|
+
case Cmd::SignOut: e.sign_out(); break;
|
|
167
|
+
case Cmd::CheckUpdate: e.check_updates(false); break;
|
|
168
|
+
case Cmd::Download: e.download_update(); break;
|
|
169
|
+
case Cmd::MarkApplied: e.mark_applied(cmd.arg); break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
void worker_main(std::shared_ptr<Shared> sh) {
|
|
174
|
+
#ifdef __APPLE__
|
|
175
|
+
pthread_setname_np("AfterPresets runtime");
|
|
176
|
+
#endif
|
|
177
|
+
cleanup_install_leftovers(sh->settings);
|
|
178
|
+
for (;;) {
|
|
179
|
+
std::optional<Cmd> cmd;
|
|
180
|
+
{
|
|
181
|
+
std::unique_lock<std::mutex> l(sh->q_mu);
|
|
182
|
+
if (sh->queue.empty() && !sh->stop) {
|
|
183
|
+
int64_t next = sh->engine->next_wake();
|
|
184
|
+
int64_t delay = next == INT64_MAX ? 60000 : next - sh->env.now_ms();
|
|
185
|
+
delay = std::clamp<int64_t>(delay, 0, 60000);
|
|
186
|
+
if (sh->logger.pending()) delay = std::min<int64_t>(delay, 250);
|
|
187
|
+
if (delay > 0) {
|
|
188
|
+
sh->cv.wait_for(l, std::chrono::milliseconds(delay), [&] { return sh->stop || !sh->queue.empty(); });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (sh->stop) break;
|
|
192
|
+
if (!sh->queue.empty()) {
|
|
193
|
+
cmd = std::move(sh->queue.front());
|
|
194
|
+
sh->queue.pop_front();
|
|
195
|
+
}
|
|
196
|
+
sh->busy = true;
|
|
197
|
+
}
|
|
198
|
+
if (cmd) run(*sh, *cmd);
|
|
199
|
+
sh->engine->tick();
|
|
200
|
+
sh->logger.flush();
|
|
201
|
+
{
|
|
202
|
+
std::lock_guard<std::mutex> l(sh->q_mu);
|
|
203
|
+
sh->busy = false;
|
|
204
|
+
}
|
|
205
|
+
sh->idle_cv.notify_all();
|
|
206
|
+
}
|
|
207
|
+
sh->logger.flush();
|
|
208
|
+
{
|
|
209
|
+
std::lock_guard<std::mutex> l(sh->q_mu);
|
|
210
|
+
sh->finished = true;
|
|
211
|
+
sh->busy = false;
|
|
212
|
+
}
|
|
213
|
+
sh->idle_cv.notify_all();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
void copy_text(char *dst, size_t cap, const std::string &src) {
|
|
217
|
+
if (!cap) return;
|
|
218
|
+
size_t n = std::min(src.size(), cap - 1);
|
|
219
|
+
// Do not cut a UTF-8 sequence in half.
|
|
220
|
+
while (n > 0 && n < src.size() && ((unsigned char)src[n] & 0xC0) == 0x80) n--;
|
|
221
|
+
std::memcpy(dst, src.data(), n);
|
|
222
|
+
dst[n] = '\0';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
} // namespace apn
|
|
226
|
+
|
|
227
|
+
using namespace apn;
|
|
228
|
+
|
|
229
|
+
struct ap_runtime {
|
|
230
|
+
std::shared_ptr<Shared> sh;
|
|
231
|
+
std::thread worker;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
extern "C" {
|
|
235
|
+
|
|
236
|
+
void ap_config_defaults(ap_config *c) {
|
|
237
|
+
if (!c) return;
|
|
238
|
+
std::memset(c, 0, sizeof *c);
|
|
239
|
+
c->license_enabled = 1;
|
|
240
|
+
c->offline_grace_days = 0;
|
|
241
|
+
c->check_in_background = 1;
|
|
242
|
+
c->update_mode = AP_UPDATES_BACKGROUND;
|
|
243
|
+
c->apply_when_staged = 1;
|
|
244
|
+
c->channel = "production";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
ap_runtime *ap_init(const ap_config *c) {
|
|
248
|
+
if (!c || !c->bundle_id || !c->version || !c->api_base) return nullptr;
|
|
249
|
+
Settings s;
|
|
250
|
+
s.bundle_id = c->bundle_id;
|
|
251
|
+
s.version = c->version;
|
|
252
|
+
s.api_base = c->api_base;
|
|
253
|
+
if (!is_bundle_id(s.bundle_id) || !is_semver(s.version) || !is_allowed_api_base(s.api_base)) return nullptr;
|
|
254
|
+
if (c->public_key && *c->public_key) {
|
|
255
|
+
s.public_key = parse_public_key(c->public_key);
|
|
256
|
+
if (!s.public_key) return nullptr; // a malformed key is a build mistake: fail closed
|
|
257
|
+
}
|
|
258
|
+
s.product_url = c->product_url ? c->product_url : "";
|
|
259
|
+
s.manage_devices_url = c->manage_devices_url ? c->manage_devices_url : "";
|
|
260
|
+
s.channel = c->channel && std::strcmp(c->channel, "test") == 0 ? "test" : "production";
|
|
261
|
+
s.license_enabled = c->license_enabled != 0;
|
|
262
|
+
s.grace_days = std::clamp(c->offline_grace_days, 0, 30);
|
|
263
|
+
s.background = c->check_in_background != 0;
|
|
264
|
+
switch (c->update_mode) {
|
|
265
|
+
case AP_UPDATES_OFF: s.update_mode = UpdateMode::Off; break;
|
|
266
|
+
case AP_UPDATES_MANUAL: s.update_mode = UpdateMode::Manual; break;
|
|
267
|
+
case AP_UPDATES_NOTIFY: s.update_mode = UpdateMode::Notify; break;
|
|
268
|
+
default: s.update_mode = UpdateMode::Background; break;
|
|
269
|
+
}
|
|
270
|
+
s.dev = c->dev != 0;
|
|
271
|
+
s.apply_when_staged = c->apply_when_staged != 0;
|
|
272
|
+
s.os = platform_os();
|
|
273
|
+
s.arch = platform_arch();
|
|
274
|
+
s.runtime_version = AP_NATIVE_VERSION;
|
|
275
|
+
std::string binary = platform_self_binary_path();
|
|
276
|
+
s.install_dir = c->install_dir ? c->install_dir : find_install_dir(binary, s.bundle_id);
|
|
277
|
+
|
|
278
|
+
auto sh = std::make_shared<Shared>();
|
|
279
|
+
sh->on_change = c->on_change;
|
|
280
|
+
sh->user = c->user;
|
|
281
|
+
Env env = platform_env(std::string("afterpresets-native/") + AP_NATIVE_VERSION + " (" + s.os + "; " + s.arch + ")");
|
|
282
|
+
if (c->data_root && *c->data_root) env.data_root = c->data_root;
|
|
283
|
+
if (env.data_root.empty()) return nullptr;
|
|
284
|
+
std::string log_dir = default_log_dir(c->data_root ? c->data_root : "");
|
|
285
|
+
if (!log_dir.empty()) sh->logger.open(fsu::join(log_dir, s.bundle_id + ".log"));
|
|
286
|
+
Shared *raw = sh.get();
|
|
287
|
+
auto user_log = c->log;
|
|
288
|
+
void *user = c->user;
|
|
289
|
+
env.log = [raw, user_log, user](int level, const std::string &m) {
|
|
290
|
+
raw->logger.push(level, m, raw->env.now_ms ? raw->env.now_ms() : 0, true);
|
|
291
|
+
if (user_log) user_log(user, level, Logger::scrub(m).c_str());
|
|
292
|
+
};
|
|
293
|
+
sh->settings = s;
|
|
294
|
+
sh->env = env;
|
|
295
|
+
sh->engine = std::make_unique<Engine>(s, env, [raw] { raw->publish(); });
|
|
296
|
+
sh->engine->set_cancel(&sh->cancel);
|
|
297
|
+
|
|
298
|
+
// The line an agent greps for to prove which build After Effects loaded.
|
|
299
|
+
env.log(1, "start " + s.bundle_id + " " + s.version + " build=" + (c->build_id ? c->build_id : "unknown") +
|
|
300
|
+
" runtime=" AP_NATIVE_VERSION " os=" + s.os + " arch=" + s.arch +
|
|
301
|
+
" license=" + (s.license_enabled ? "on" : "off") + " updates=" + std::to_string((int)c->update_mode) +
|
|
302
|
+
" installed=" + (s.install_dir.empty() ? "no" : "yes") + " binary=" + binary);
|
|
303
|
+
|
|
304
|
+
// Decide from disk before the first render (no network).
|
|
305
|
+
Engine::Launch launch = sh->engine->launch_local();
|
|
306
|
+
sh->publish();
|
|
307
|
+
if (launch == Engine::Launch::Validate) sh->queue.push_back({Cmd::Validate, ""});
|
|
308
|
+
if (launch == Engine::Launch::ValidateBehind) sh->queue.push_back({Cmd::ValidateBehind, ""});
|
|
309
|
+
|
|
310
|
+
auto *rt = new ap_runtime();
|
|
311
|
+
rt->sh = sh;
|
|
312
|
+
rt->worker = std::thread(worker_main, sh);
|
|
313
|
+
return rt;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
void ap_shutdown(ap_runtime *rt) {
|
|
317
|
+
if (!rt) return;
|
|
318
|
+
auto sh = rt->sh;
|
|
319
|
+
{
|
|
320
|
+
std::lock_guard<std::mutex> l(sh->q_mu);
|
|
321
|
+
sh->stop = true;
|
|
322
|
+
sh->cancel.store(true);
|
|
323
|
+
}
|
|
324
|
+
sh->cv.notify_all();
|
|
325
|
+
bool done;
|
|
326
|
+
{
|
|
327
|
+
std::unique_lock<std::mutex> l(sh->q_mu);
|
|
328
|
+
done = sh->idle_cv.wait_for(l, std::chrono::seconds(3), [&] { return sh->finished; });
|
|
329
|
+
}
|
|
330
|
+
if (done) rt->worker.join();
|
|
331
|
+
else rt->worker.detach(); // the thread holds its own reference to the shared state
|
|
332
|
+
delete rt;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
int ap_is_unlocked(const ap_runtime *rt) { return rt ? rt->sh->unlocked.load(std::memory_order_acquire) : 0; }
|
|
336
|
+
|
|
337
|
+
ap_status ap_status_get(const ap_runtime *rt) {
|
|
338
|
+
return rt ? (ap_status)rt->sh->status.load(std::memory_order_acquire) : AP_STATUS_ERROR;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
uint64_t ap_generation(const ap_runtime *rt) { return rt ? rt->sh->generation.load(std::memory_order_acquire) : 0; }
|
|
342
|
+
|
|
343
|
+
void ap_get_state(ap_runtime *rt, ap_state *out) {
|
|
344
|
+
if (!out) return;
|
|
345
|
+
std::memset(out, 0, sizeof *out);
|
|
346
|
+
if (!rt) { out->status = AP_STATUS_ERROR; return; }
|
|
347
|
+
std::lock_guard<std::mutex> l(rt->sh->view_mu);
|
|
348
|
+
const LicenseView &v = rt->sh->license;
|
|
349
|
+
out->status = (ap_status)v.status;
|
|
350
|
+
out->unlocked = is_runnable(v.status) ? 1 : 0;
|
|
351
|
+
out->grace_days_left = v.grace_days_left;
|
|
352
|
+
out->generation = rt->sh->generation.load();
|
|
353
|
+
copy_text(out->message, sizeof out->message, v.message);
|
|
354
|
+
copy_text(out->sign_in_url, sizeof out->sign_in_url, v.sign_in_url);
|
|
355
|
+
copy_text(out->username, sizeof out->username, v.username);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
void ap_get_update_state(ap_runtime *rt, ap_update_state *out) {
|
|
359
|
+
if (!out) return;
|
|
360
|
+
std::memset(out, 0, sizeof *out);
|
|
361
|
+
if (!rt) return;
|
|
362
|
+
std::lock_guard<std::mutex> l(rt->sh->view_mu);
|
|
363
|
+
const UpdateView &v = rt->sh->update;
|
|
364
|
+
out->status = (ap_update_status)v.status;
|
|
365
|
+
out->restart_required = v.restart_required;
|
|
366
|
+
out->needs_manual_install = v.needs_manual_install;
|
|
367
|
+
copy_text(out->version, sizeof out->version, v.version);
|
|
368
|
+
copy_text(out->direction, sizeof out->direction, v.direction);
|
|
369
|
+
copy_text(out->change_log, sizeof out->change_log, v.change_log);
|
|
370
|
+
copy_text(out->error_code, sizeof out->error_code, v.error_code);
|
|
371
|
+
copy_text(out->error_message, sizeof out->error_message, v.error_message);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
int ap_begin_sign_in(ap_runtime *rt) {
|
|
375
|
+
if (!rt || !rt->sh->settings.license_enabled) return -1;
|
|
376
|
+
rt->sh->post({Cmd::SignIn, ""});
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
void ap_cancel_sign_in(ap_runtime *rt) { if (rt) rt->sh->post({Cmd::CancelSignIn, ""}); }
|
|
381
|
+
void ap_open_sign_in_page(ap_runtime *rt) { if (rt) rt->sh->post({Cmd::OpenPage, ""}); }
|
|
382
|
+
void ap_retry(ap_runtime *rt) { if (rt) rt->sh->post({Cmd::Retry, ""}); }
|
|
383
|
+
void ap_sign_out(ap_runtime *rt) { if (rt) rt->sh->post({Cmd::SignOut, ""}); }
|
|
384
|
+
void ap_check_for_update(ap_runtime *rt) { if (rt) rt->sh->post({Cmd::CheckUpdate, ""}); }
|
|
385
|
+
void ap_download_update(ap_runtime *rt) { if (rt) rt->sh->post({Cmd::Download, ""}); }
|
|
386
|
+
|
|
387
|
+
ap_apply_result ap_apply_staged_update(ap_runtime *rt) {
|
|
388
|
+
if (!rt) return AP_APPLY_NOTHING;
|
|
389
|
+
auto &sh = *rt->sh;
|
|
390
|
+
std::string version, error;
|
|
391
|
+
ApplyResult r = apply_staged_update(sh.settings, sh.env, &version, &error);
|
|
392
|
+
if (r == ApplyResult::Ok) {
|
|
393
|
+
sh.post({Cmd::MarkApplied, version});
|
|
394
|
+
std::lock_guard<std::mutex> l(sh.view_mu);
|
|
395
|
+
sh.update.status = UpdStatus::Applied;
|
|
396
|
+
sh.update.restart_required = true;
|
|
397
|
+
sh.update.version = version;
|
|
398
|
+
sh.notice = "Update installed: restart After Effects to use v" + version;
|
|
399
|
+
} else if (r == ApplyResult::NotWritable || r == ApplyResult::NotInstalled) {
|
|
400
|
+
std::lock_guard<std::mutex> l(sh.view_mu);
|
|
401
|
+
sh.update.needs_manual_install = true;
|
|
402
|
+
sh.update.error_code = r == ApplyResult::NotWritable ? "not_writable" : "not_installed";
|
|
403
|
+
sh.update.error_message = error;
|
|
404
|
+
}
|
|
405
|
+
if (r != ApplyResult::Nothing) sh.generation.fetch_add(1);
|
|
406
|
+
sh.logger.flush();
|
|
407
|
+
return (ap_apply_result)r;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
size_t ap_update_notice(ap_runtime *rt, char *buf, size_t size) {
|
|
411
|
+
if (!rt) { if (buf && size) buf[0] = '\0'; return 0; }
|
|
412
|
+
std::lock_guard<std::mutex> l(rt->sh->view_mu);
|
|
413
|
+
if (buf && size) copy_text(buf, size, rt->sh->notice);
|
|
414
|
+
return rt->sh->notice.size();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
size_t ap_update_changelog(ap_runtime *rt, char *buf, size_t size) {
|
|
418
|
+
if (!rt) { if (buf && size) buf[0] = '\0'; return 0; }
|
|
419
|
+
std::lock_guard<std::mutex> l(rt->sh->view_mu);
|
|
420
|
+
if (buf && size) copy_text(buf, size, rt->sh->update.change_log);
|
|
421
|
+
return rt->sh->update.change_log.size();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
ap_dialog_action ap_options_dialog(ap_runtime *rt, const char *title) {
|
|
425
|
+
if (!rt) return AP_DIALOG_NONE;
|
|
426
|
+
auto &sh = *rt->sh;
|
|
427
|
+
DialogModel model;
|
|
428
|
+
{
|
|
429
|
+
std::lock_guard<std::mutex> l(sh.view_mu);
|
|
430
|
+
model = options_dialog_model(title ? title : "", sh.license, sh.update, sh.notice, sh.settings.product_url,
|
|
431
|
+
sh.settings.manage_devices_url);
|
|
432
|
+
}
|
|
433
|
+
int index = platform_show_dialog(model);
|
|
434
|
+
if (index < 0 || index >= (int)model.buttons.size()) return index < 0 ? AP_DIALOG_NONE : AP_DIALOG_CLOSE;
|
|
435
|
+
switch (model.buttons[(size_t)index].action) {
|
|
436
|
+
case DialogAction::Close: return AP_DIALOG_CLOSE;
|
|
437
|
+
case DialogAction::SignIn: sh.post({Cmd::SignIn, ""}); return AP_DIALOG_SIGN_IN;
|
|
438
|
+
case DialogAction::OpenSignInPage: sh.post({Cmd::OpenPage, ""}); return AP_DIALOG_OPEN_SIGN_IN_PAGE;
|
|
439
|
+
case DialogAction::CancelSignIn: sh.post({Cmd::CancelSignIn, ""}); return AP_DIALOG_CANCEL_SIGN_IN;
|
|
440
|
+
case DialogAction::SignOut: sh.post({Cmd::SignOut, ""}); return AP_DIALOG_SIGN_OUT;
|
|
441
|
+
case DialogAction::Retry: sh.post({Cmd::Retry, ""}); return AP_DIALOG_RETRY;
|
|
442
|
+
case DialogAction::OpenProduct: sh.env.open_url(sh.settings.product_url); return AP_DIALOG_OPEN_PRODUCT;
|
|
443
|
+
case DialogAction::ManageDevices: sh.env.open_url(sh.settings.manage_devices_url); return AP_DIALOG_MANAGE_DEVICES;
|
|
444
|
+
}
|
|
445
|
+
return AP_DIALOG_CLOSE;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
int ap_wait_idle(ap_runtime *rt, int timeout_ms) {
|
|
449
|
+
if (!rt) return -1;
|
|
450
|
+
auto &sh = *rt->sh;
|
|
451
|
+
std::unique_lock<std::mutex> l(sh.q_mu);
|
|
452
|
+
bool ok = sh.idle_cv.wait_for(l, std::chrono::milliseconds(timeout_ms),
|
|
453
|
+
[&] { return sh.finished || (sh.queue.empty() && !sh.busy); });
|
|
454
|
+
return ok ? 0 : -1;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
void ap_log(ap_runtime *rt, int level, const char *message) {
|
|
458
|
+
if (!rt || !message) return;
|
|
459
|
+
auto &sh = *rt->sh;
|
|
460
|
+
sh.logger.push(level, message, sh.env.now_ms(), false); // never blocks
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
size_t ap_log_path(ap_runtime *rt, char *buf, size_t size) {
|
|
464
|
+
if (!rt) return 0;
|
|
465
|
+
const std::string &p = rt->sh->logger.path();
|
|
466
|
+
if (buf && size) copy_text(buf, size, p);
|
|
467
|
+
return p.size();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const char *ap_status_name(ap_status s) { return status_name((Status)s); }
|
|
471
|
+
const char *ap_update_status_name(ap_update_status s) { return upd_status_name((UpdStatus)s); }
|
|
472
|
+
|
|
473
|
+
} // extern "C"
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#include "util.hpp"
|
|
2
|
+
|
|
3
|
+
#include <cctype>
|
|
4
|
+
#include <cstdio>
|
|
5
|
+
#include <regex>
|
|
6
|
+
|
|
7
|
+
#include "crypto.hpp"
|
|
8
|
+
#include "fsutil.hpp"
|
|
9
|
+
|
|
10
|
+
namespace apn {
|
|
11
|
+
|
|
12
|
+
namespace {
|
|
13
|
+
|
|
14
|
+
// Days since 1970-01-01 for a proleptic Gregorian date (Howard Hinnant's algorithm).
|
|
15
|
+
int64_t days_from_civil(int64_t y, unsigned m, unsigned d) {
|
|
16
|
+
y -= m <= 2;
|
|
17
|
+
const int64_t era = (y >= 0 ? y : y - 399) / 400;
|
|
18
|
+
const unsigned yoe = (unsigned)(y - era * 400);
|
|
19
|
+
const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
|
|
20
|
+
const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
|
21
|
+
return era * 146097 + (int64_t)doe - 719468;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
void civil_from_days(int64_t z, int64_t &y, unsigned &m, unsigned &d) {
|
|
25
|
+
z += 719468;
|
|
26
|
+
const int64_t era = (z >= 0 ? z : z - 146096) / 146097;
|
|
27
|
+
const unsigned doe = (unsigned)(z - era * 146097);
|
|
28
|
+
const unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
29
|
+
y = (int64_t)yoe + era * 400;
|
|
30
|
+
const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
31
|
+
const unsigned mp = (5 * doy + 2) / 153;
|
|
32
|
+
d = doy - (153 * mp + 2) / 5 + 1;
|
|
33
|
+
m = mp < 10 ? mp + 3 : mp - 9;
|
|
34
|
+
y += m <= 2;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
bool digits(const std::string &s, size_t pos, size_t n, int &out) {
|
|
38
|
+
if (pos + n > s.size()) return false;
|
|
39
|
+
out = 0;
|
|
40
|
+
for (size_t i = 0; i < n; i++) {
|
|
41
|
+
char c = s[pos + i];
|
|
42
|
+
if (c < '0' || c > '9') return false;
|
|
43
|
+
out = out * 10 + (c - '0');
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
} // namespace
|
|
49
|
+
|
|
50
|
+
std::optional<int64_t> parse_iso8601(const std::string &s) {
|
|
51
|
+
int Y, M, D, h, m, sec;
|
|
52
|
+
if (!digits(s, 0, 4, Y) || s.size() < 19 || s[4] != '-' || !digits(s, 5, 2, M) || s[7] != '-' ||
|
|
53
|
+
!digits(s, 8, 2, D) || (s[10] != 'T' && s[10] != 't' && s[10] != ' ') || !digits(s, 11, 2, h) ||
|
|
54
|
+
s[13] != ':' || !digits(s, 14, 2, m) || s[16] != ':' || !digits(s, 17, 2, sec)) {
|
|
55
|
+
return std::nullopt;
|
|
56
|
+
}
|
|
57
|
+
if (M < 1 || M > 12 || D < 1 || D > 31 || h > 24 || m > 59 || sec > 59) return std::nullopt;
|
|
58
|
+
size_t p = 19;
|
|
59
|
+
int64_t ms = 0;
|
|
60
|
+
if (p < s.size() && s[p] == '.') {
|
|
61
|
+
p++;
|
|
62
|
+
size_t start = p;
|
|
63
|
+
int scale = 100;
|
|
64
|
+
while (p < s.size() && std::isdigit((unsigned char)s[p])) {
|
|
65
|
+
if (scale > 0) { ms += (s[p] - '0') * scale; scale /= 10; }
|
|
66
|
+
p++;
|
|
67
|
+
}
|
|
68
|
+
if (p == start) return std::nullopt;
|
|
69
|
+
}
|
|
70
|
+
int64_t offset_min = 0;
|
|
71
|
+
if (p < s.size() && (s[p] == 'Z' || s[p] == 'z')) {
|
|
72
|
+
p++;
|
|
73
|
+
} else if (p < s.size() && (s[p] == '+' || s[p] == '-')) {
|
|
74
|
+
int oh, om;
|
|
75
|
+
int sign = s[p] == '-' ? -1 : 1;
|
|
76
|
+
if (!digits(s, p + 1, 2, oh) || p + 3 >= s.size() || s[p + 3] != ':' || !digits(s, p + 4, 2, om)) return std::nullopt;
|
|
77
|
+
offset_min = sign * (oh * 60 + om);
|
|
78
|
+
p += 6;
|
|
79
|
+
} else {
|
|
80
|
+
return std::nullopt; // no zone: refuse rather than guess local time
|
|
81
|
+
}
|
|
82
|
+
if (p != s.size()) return std::nullopt;
|
|
83
|
+
int64_t days = days_from_civil(Y, (unsigned)M, (unsigned)D);
|
|
84
|
+
int64_t total = ((days * 24 + h) * 60 + m) * 60 + sec;
|
|
85
|
+
return total * 1000 + ms - offset_min * 60 * 1000;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
std::string format_iso8601(int64_t ms) {
|
|
89
|
+
int64_t secs = ms >= 0 ? ms / 1000 : -((-ms + 999) / 1000);
|
|
90
|
+
int64_t rem_ms = ms - secs * 1000;
|
|
91
|
+
int64_t days = secs >= 0 ? secs / 86400 : -((-secs + 86399) / 86400);
|
|
92
|
+
int64_t sod = secs - days * 86400;
|
|
93
|
+
int64_t y;
|
|
94
|
+
unsigned mo, d;
|
|
95
|
+
civil_from_days(days, y, mo, d);
|
|
96
|
+
char buf[40];
|
|
97
|
+
std::snprintf(buf, sizeof buf, "%04lld-%02u-%02uT%02d:%02d:%02d.%03dZ", (long long)y, mo, d, (int)(sod / 3600),
|
|
98
|
+
(int)(sod / 60 % 60), (int)(sod % 60), (int)rem_ms);
|
|
99
|
+
return buf;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
bool is_semver(const std::string &v) {
|
|
103
|
+
static const std::regex re(
|
|
104
|
+
R"(^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)");
|
|
105
|
+
return v.size() <= 64 && std::regex_match(v, re);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
bool is_bundle_id(const std::string &id) {
|
|
109
|
+
static const std::regex re(R"(^com\.afterpresets\.[a-z0-9]{1,64}\.[a-z0-9-]{2,40}$)");
|
|
110
|
+
return id.size() <= 255 && std::regex_match(id, re);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
bool is_http_url(const std::string &url) {
|
|
114
|
+
auto lower = url.substr(0, 8);
|
|
115
|
+
for (auto &c : lower) c = (char)std::tolower((unsigned char)c);
|
|
116
|
+
return lower.rfind("https://", 0) == 0 || lower.rfind("http://", 0) == 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
bool is_allowed_api_base(const std::string &url) {
|
|
120
|
+
static const std::regex https(R"(^https://[A-Za-z0-9.-]+(:\d{1,5})?(/[^\s?#]*)?$)");
|
|
121
|
+
static const std::regex local(R"(^http://(127\.0\.0\.1|localhost|\[::1\])(:\d{1,5})?(/[^\s?#]*)?$)");
|
|
122
|
+
return std::regex_match(url, https) || std::regex_match(url, local);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
std::string computer_id_path(const std::string &data_root) {
|
|
126
|
+
return fsu::join(fsu::join(data_root, "AfterPresets"), "computer-id");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
namespace {
|
|
130
|
+
|
|
131
|
+
std::optional<std::string> read_saved_id(const std::string &file, const Env &env) {
|
|
132
|
+
auto text = fsu::read(file, 4096);
|
|
133
|
+
if (!text) return std::nullopt;
|
|
134
|
+
std::string id;
|
|
135
|
+
for (char c : *text) {
|
|
136
|
+
if (c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '\f' || c == '\v') continue;
|
|
137
|
+
id += (char)std::tolower((unsigned char)c);
|
|
138
|
+
}
|
|
139
|
+
static const std::regex re("^[a-f0-9]{64}$");
|
|
140
|
+
if (std::regex_match(id, re)) return id;
|
|
141
|
+
if (env.log) env.log(2, "Saved computer ID is malformed; creating a new one.");
|
|
142
|
+
return std::nullopt;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
} // namespace
|
|
146
|
+
|
|
147
|
+
std::string computer_id(const Env &env) {
|
|
148
|
+
std::string file = computer_id_path(env.data_root);
|
|
149
|
+
if (auto saved = read_saved_id(file, env)) return *saved;
|
|
150
|
+
std::optional<std::string> fingerprint;
|
|
151
|
+
if (env.hardware_fingerprint) fingerprint = env.hardware_fingerprint();
|
|
152
|
+
if (!fingerprint && env.mac_fingerprint) fingerprint = env.mac_fingerprint();
|
|
153
|
+
if (!fingerprint) fingerprint = "random:" + env.random_hex(32);
|
|
154
|
+
std::string id = sha256_hex(*fingerprint);
|
|
155
|
+
// Save unless another process saved one first (then use theirs), like the JS runtime.
|
|
156
|
+
if (fsu::exists(file)) fsu::remove_file(file); // only reached when the saved file is malformed
|
|
157
|
+
switch (fsu::write_exclusive(file, id)) {
|
|
158
|
+
case fsu::Excl::Ok: return id;
|
|
159
|
+
case fsu::Excl::Exists: {
|
|
160
|
+
auto other = read_saved_id(file, env);
|
|
161
|
+
return other ? *other : id;
|
|
162
|
+
}
|
|
163
|
+
default:
|
|
164
|
+
if (env.log) env.log(2, "Could not save the computer ID");
|
|
165
|
+
return id;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
} // namespace apn
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Small shared helpers: ISO-8601 time, semver, bundle id and URL checks, computer id.
|
|
2
|
+
#pragma once
|
|
3
|
+
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
#include <optional>
|
|
6
|
+
#include <string>
|
|
7
|
+
|
|
8
|
+
#include "platform.hpp"
|
|
9
|
+
|
|
10
|
+
namespace apn {
|
|
11
|
+
|
|
12
|
+
constexpr int64_t DAY_MS = 24LL * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
// Date.parse for the forms the API writes: YYYY-MM-DDTHH:MM:SS[.fff]Z or ±HH:MM offset.
|
|
15
|
+
std::optional<int64_t> parse_iso8601(const std::string &text);
|
|
16
|
+
// new Date(ms).toISOString(): 2026-11-06T11:10:38.969Z
|
|
17
|
+
std::string format_iso8601(int64_t ms);
|
|
18
|
+
|
|
19
|
+
bool is_semver(const std::string &v);
|
|
20
|
+
// The API's BUNDLE_ID_PATTERN: com.afterpresets.<seller>.<slug>.
|
|
21
|
+
bool is_bundle_id(const std::string &id);
|
|
22
|
+
// https://..., or http:// only for 127.0.0.1 / localhost / [::1] (local development).
|
|
23
|
+
bool is_allowed_api_base(const std::string &url);
|
|
24
|
+
bool is_http_url(const std::string &url);
|
|
25
|
+
|
|
26
|
+
// <data_root>/AfterPresets/computer-id, shared with every AfterPresets runtime on the machine.
|
|
27
|
+
std::string computer_id_path(const std::string &data_root);
|
|
28
|
+
// Same precedence and value as packages/runtime/src/computer-id.js: the saved
|
|
29
|
+
// file, else sha256(hardware fingerprint | MAC list | random), saved once with "wx".
|
|
30
|
+
std::string computer_id(const Env &env);
|
|
31
|
+
|
|
32
|
+
} // namespace apn
|