afterpresets 0.2.0 → 0.4.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 +9 -6
- 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 +7 -4
- package/runtime/docs/cep/host-after-effects.md +3 -3
- package/runtime/docs/cep/host-premiere.md +4 -4
- package/runtime/docs/cep/host.md +10 -2
- package/runtime/docs/common/README.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/src/agent/cli-group.js +9 -0
- package/src/agent/extendscript.js +10 -0
- package/src/agent/host-target.js +1 -0
- package/src/agent/panels.js +1 -0
- package/src/agent/png.js +1 -0
- package/src/agent/run-tool.js +3 -0
- package/src/{mcp → agent}/snapshot.js +2 -2
- package/src/agent/tool-error.js +1 -0
- package/src/agent/tools/capture.js +3 -0
- package/src/agent/tools/host.js +1 -0
- package/src/agent/tools/native.js +1 -0
- package/src/agent/tools/panel.js +7 -0
- package/src/agent/tools.js +2 -0
- package/src/api/error-messages.js +1 -1
- package/src/build/build.js +9 -8
- package/src/check/check.js +3 -3
- package/src/check/native.js +14 -0
- package/src/check/pricing.js +1 -1
- package/src/cli.js +3 -3
- package/src/commands/build.js +8 -2
- package/src/commands/check.js +10 -2
- package/src/commands/comp.js +6 -0
- package/src/commands/dev.js +29 -2
- package/src/commands/docs.js +5 -1
- package/src/commands/host.js +1 -0
- package/src/commands/init.js +12 -7
- package/src/commands/panel.js +1 -0
- package/src/commands/push.js +9 -2
- package/src/commands/releases.js +8 -3
- 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/dev/bridge.js +8 -0
- package/src/dev/native.js +3 -0
- package/src/dev/session.js +5 -4
- 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/docs.js +3 -3
- package/src/project/project.js +2 -2
- package/src/project/template.js +5 -4
- package/src/prompt.js +1 -1
- package/src/release/confirm.js +5 -0
- package/src/release/native.js +3 -0
- package/src/release/publish.js +1 -1
- package/src/system/run-command.js +1 -1
- package/src/targets.js +1 -1
- package/template/cep/AGENTS.md +102 -164
- package/template/cep/README.md +7 -3
- package/template/cep/docs/code_guide.md +67 -0
- package/template/cep/docs/initial_setup.md +63 -42
- package/template/cep/docs/release_checklist.md +25 -0
- package/template/cep/gitignore +2 -1
- package/template/cep/src/features/update-screen/update-screen.js +1 -1
- package/template/hosts/AGENTS.two-apps.md +12 -0
- package/template/native/AGENTS.md +106 -0
- package/template/native/README.md +71 -0
- package/template/native/docs/code_guide.md +72 -0
- package/template/native/docs/initial_setup.md +154 -0
- package/template/native/docs/release_checklist.md +25 -0
- package/template/native/effect.json +16 -0
- package/template/native/gitignore +9 -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/src/commands/mcp.js +0 -7
- package/src/mcp/extendscript.js +0 -9
- package/src/mcp/panels.js +0 -1
- package/src/mcp/protocol.js +0 -4
- package/src/mcp/server.js +0 -2
- package/src/mcp/tools/account.js +0 -3
- package/src/mcp/tools/host.js +0 -1
- package/src/mcp/tools/panel.js +0 -7
- package/src/mcp/tools/project.js +0 -40
- package/src/mcp/tools/releases.js +0 -29
- package/template/cep/.mcp.json +0 -8
- /package/src/{mcp → agent}/capture.js +0 -0
- /package/src/{mcp → agent}/redact.js +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// License state machine and updater for ap_native. A port of
|
|
2
|
+
// packages/runtime/src/license.js and updates.js with the same API calls,
|
|
3
|
+
// statuses, cache file and proof rules. Single-threaded: only the worker
|
|
4
|
+
// thread (or a test) calls into an Engine.
|
|
5
|
+
#pragma once
|
|
6
|
+
|
|
7
|
+
#include <atomic>
|
|
8
|
+
#include <cstdint>
|
|
9
|
+
#include <functional>
|
|
10
|
+
#include <optional>
|
|
11
|
+
#include <string>
|
|
12
|
+
|
|
13
|
+
#include "crypto.hpp"
|
|
14
|
+
#include "fsutil.hpp"
|
|
15
|
+
#include "json.hpp"
|
|
16
|
+
#include "platform.hpp"
|
|
17
|
+
|
|
18
|
+
namespace apn {
|
|
19
|
+
|
|
20
|
+
enum class Status { Disabled, SignedOut, SigningIn, Checking, Active, NotOwned, Refunded, OfflineGrace, OfflineExpired, Error };
|
|
21
|
+
enum class UpdateMode { Off, Manual, Notify, Background };
|
|
22
|
+
enum class UpdStatus { Idle, Checking, UpToDate, Available, Downloading, Staged, Applied, Error };
|
|
23
|
+
enum class ApplyResult { Nothing = 0, Ok = 1, NotInstalled = -1, NotWritable = -2, Failed = -3, Dev = -4 };
|
|
24
|
+
|
|
25
|
+
const char *status_name(Status s);
|
|
26
|
+
const char *upd_status_name(UpdStatus s);
|
|
27
|
+
bool is_runnable(Status s);
|
|
28
|
+
|
|
29
|
+
struct Settings {
|
|
30
|
+
std::string bundle_id;
|
|
31
|
+
std::string version;
|
|
32
|
+
std::string api_base;
|
|
33
|
+
std::string product_url;
|
|
34
|
+
std::string manage_devices_url;
|
|
35
|
+
std::string channel = "production";
|
|
36
|
+
std::optional<PublicKey> public_key; // nullopt: proofs cannot be verified (always check online)
|
|
37
|
+
bool license_enabled = true;
|
|
38
|
+
int grace_days = 0;
|
|
39
|
+
bool background = true;
|
|
40
|
+
UpdateMode update_mode = UpdateMode::Background;
|
|
41
|
+
bool dev = false;
|
|
42
|
+
bool apply_when_staged = true; // replace the installed plugin right after staging (in-session notice)
|
|
43
|
+
std::string install_dir; // installed <bundleId> folder, "" when not installed
|
|
44
|
+
std::string os = "macos";
|
|
45
|
+
std::string arch = "arm64";
|
|
46
|
+
std::string runtime_version = "0.1.0";
|
|
47
|
+
int64_t first_update_check_delay_ms = 15 * 1000;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
struct LicenseView {
|
|
51
|
+
Status status = Status::Checking;
|
|
52
|
+
std::string message;
|
|
53
|
+
int grace_days_left = 0;
|
|
54
|
+
std::string sign_in_url;
|
|
55
|
+
std::string username;
|
|
56
|
+
bool operator==(const LicenseView &o) const {
|
|
57
|
+
return status == o.status && message == o.message && grace_days_left == o.grace_days_left &&
|
|
58
|
+
sign_in_url == o.sign_in_url && username == o.username;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
struct UpdateView {
|
|
63
|
+
UpdStatus status = UpdStatus::Idle;
|
|
64
|
+
bool restart_required = false;
|
|
65
|
+
bool needs_manual_install = false;
|
|
66
|
+
std::string version;
|
|
67
|
+
std::string direction;
|
|
68
|
+
std::string change_log;
|
|
69
|
+
std::string error_code;
|
|
70
|
+
std::string error_message;
|
|
71
|
+
bool operator==(const UpdateView &o) const {
|
|
72
|
+
return status == o.status && restart_required == o.restart_required &&
|
|
73
|
+
needs_manual_install == o.needs_manual_install && version == o.version && direction == o.direction &&
|
|
74
|
+
change_log == o.change_log && error_code == o.error_code && error_message == o.error_message;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Outcome of one API call, classified like the JS runtime's _.api().
|
|
79
|
+
struct ApiResult {
|
|
80
|
+
enum Kind { Ok, Network, Rejected } kind = Network;
|
|
81
|
+
Json json;
|
|
82
|
+
std::string code;
|
|
83
|
+
std::string message;
|
|
84
|
+
int status = 0;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Paths under <data_root>/AfterPresets/extensions/<bundleId>/ (the JS runtime's layout).
|
|
88
|
+
struct Paths {
|
|
89
|
+
std::string data_dir;
|
|
90
|
+
std::string license; // license.json
|
|
91
|
+
std::string updates; // updates.json
|
|
92
|
+
std::string lock; // update.lock
|
|
93
|
+
std::string updates_dir; // updates/
|
|
94
|
+
std::string staged; // updates/staged/ (holds <Name>.plugin or <Name>.aex)
|
|
95
|
+
std::string staged_meta; // updates/staged.json
|
|
96
|
+
static Paths make(const std::string &data_root, const std::string &bundle_id);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
class Engine {
|
|
100
|
+
public:
|
|
101
|
+
static constexpr int64_t CLOCK_SKEW_MS = 24LL * 60 * 60 * 1000;
|
|
102
|
+
static constexpr int64_t MIN_POLL_MS = 250;
|
|
103
|
+
static constexpr int64_t QUIET_RETRY_MS = 30LL * 60 * 1000;
|
|
104
|
+
static constexpr int64_t CACHE_WATCH_MS = 30LL * 1000;
|
|
105
|
+
static constexpr int64_t UPDATE_INTERVAL_MS = 6LL * 60 * 60 * 1000;
|
|
106
|
+
static constexpr int64_t LOCK_STALE_MS = 10LL * 60 * 1000;
|
|
107
|
+
|
|
108
|
+
Engine(Settings settings, Env env, std::function<void()> on_change);
|
|
109
|
+
|
|
110
|
+
// ---- license ----
|
|
111
|
+
enum class Launch { None, Validate, ValidateBehind };
|
|
112
|
+
// Launch decision from disk only (no network). Validate: status CHECKING,
|
|
113
|
+
// call validate_stored(); ValidateBehind: unlocked from a signed stale
|
|
114
|
+
// check, call validate_behind().
|
|
115
|
+
Launch launch_local();
|
|
116
|
+
void validate_stored(); // JS validateStored (retry, another process signed in/out)
|
|
117
|
+
void validate_behind(); // JS launchCheck's background request
|
|
118
|
+
void sign_in();
|
|
119
|
+
void poll_sign_in();
|
|
120
|
+
void cancel_sign_in();
|
|
121
|
+
void open_sign_in_page();
|
|
122
|
+
void sign_out();
|
|
123
|
+
void recheck_quietly();
|
|
124
|
+
void check_cache_file(); // notices another process writing license.json
|
|
125
|
+
|
|
126
|
+
// ---- updates ----
|
|
127
|
+
void check_updates(bool automatic);
|
|
128
|
+
void download_update();
|
|
129
|
+
// Called by the runtime after ap_apply_staged_update replaced the files.
|
|
130
|
+
void mark_applied(const std::string &version);
|
|
131
|
+
// Applies a staged update now (filesystem only) and updates the view.
|
|
132
|
+
ApplyResult apply_now();
|
|
133
|
+
// "Update installed: restart After Effects to use v1.2.0", or "" (see ap_update_notice).
|
|
134
|
+
std::string notice() const;
|
|
135
|
+
|
|
136
|
+
// ---- timers ----
|
|
137
|
+
void tick(); // runs every due timer
|
|
138
|
+
int64_t next_wake() const; // absolute ms; INT64_MAX when nothing is scheduled
|
|
139
|
+
|
|
140
|
+
const LicenseView &license() const { return license_; }
|
|
141
|
+
const UpdateView &update() const { return update_; }
|
|
142
|
+
bool unlocked() const { return is_runnable(license_.status); }
|
|
143
|
+
const Settings &settings() const { return s_; }
|
|
144
|
+
const Paths &paths() const { return paths_; }
|
|
145
|
+
std::string computer(); // cached computer id
|
|
146
|
+
void set_cancel(const std::atomic<bool> *cancel) { cancel_ = cancel; }
|
|
147
|
+
bool sign_in_pending() const { return pending_.active; }
|
|
148
|
+
|
|
149
|
+
// Exposed for tests.
|
|
150
|
+
struct Check {
|
|
151
|
+
int64_t validated_at;
|
|
152
|
+
std::optional<int64_t> fresh_until;
|
|
153
|
+
};
|
|
154
|
+
std::optional<Check> trusted_check(const Json &cache);
|
|
155
|
+
std::optional<Json> read_cache();
|
|
156
|
+
|
|
157
|
+
private:
|
|
158
|
+
struct Pending {
|
|
159
|
+
bool active = false;
|
|
160
|
+
std::string device_code;
|
|
161
|
+
std::string url;
|
|
162
|
+
int64_t expires_at = 0;
|
|
163
|
+
int64_t interval_ms = 3000;
|
|
164
|
+
int64_t poll_at = 0;
|
|
165
|
+
};
|
|
166
|
+
struct UpdateInfo {
|
|
167
|
+
std::string version, direction, change_log, download_url, sha256, signature;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
ApiResult api(const std::string &suffix, const Json &body);
|
|
171
|
+
void set_state(Status status, const std::string &message = "", int grace_days = 0, const std::string &url = "");
|
|
172
|
+
void set_update(const UpdateView &view);
|
|
173
|
+
void log(int level, const std::string &message);
|
|
174
|
+
int64_t now() const { return env_.now_ms(); }
|
|
175
|
+
|
|
176
|
+
bool write_cache(const Json &cache);
|
|
177
|
+
void delete_cache();
|
|
178
|
+
void remember_cache_stamp();
|
|
179
|
+
bool is_fresh(const std::optional<Check> &check);
|
|
180
|
+
std::optional<int> grace_for(const Json &cache);
|
|
181
|
+
void apply_network_failure(const Json &cache, const std::string &error);
|
|
182
|
+
void apply_rejection(const ApiResult &err, const std::optional<Json> &cache);
|
|
183
|
+
void on_active(const std::string &token, const Json &json);
|
|
184
|
+
void unlock_from_cache(const Json &cache);
|
|
185
|
+
void schedule_recheck(const std::optional<Check> &check, int64_t delay = -2);
|
|
186
|
+
void clear_recheck() { recheck_at_ = -1; }
|
|
187
|
+
void stop_polling() { pending_ = Pending(); }
|
|
188
|
+
void sign_in_failed(const std::string &message);
|
|
189
|
+
Json token_body(const std::string &token);
|
|
190
|
+
std::optional<std::string> stored_token();
|
|
191
|
+
|
|
192
|
+
// updates
|
|
193
|
+
Json read_update_settings();
|
|
194
|
+
void write_update_settings(const Json &settings);
|
|
195
|
+
void schedule_first_update_check();
|
|
196
|
+
bool stage(const UpdateInfo &info, std::string &code, std::string &message);
|
|
197
|
+
bool acquire_lock();
|
|
198
|
+
void release_lock();
|
|
199
|
+
|
|
200
|
+
Settings s_;
|
|
201
|
+
Env env_;
|
|
202
|
+
Paths paths_;
|
|
203
|
+
std::function<void()> on_change_;
|
|
204
|
+
const std::atomic<bool> *cancel_ = nullptr;
|
|
205
|
+
|
|
206
|
+
LicenseView license_;
|
|
207
|
+
UpdateView update_;
|
|
208
|
+
Pending pending_;
|
|
209
|
+
std::string computer_id_;
|
|
210
|
+
std::string username_;
|
|
211
|
+
int64_t recheck_at_ = -1;
|
|
212
|
+
int64_t watch_at_ = -1;
|
|
213
|
+
std::optional<fsu::Stamp> cache_stamp_; // license.json as this engine last wrote or read it
|
|
214
|
+
|
|
215
|
+
int64_t update_at_ = -1;
|
|
216
|
+
std::string failed_version_; // automatic install of this version failed this session
|
|
217
|
+
std::optional<UpdateInfo> available_; // last found update (notify / manual modes)
|
|
218
|
+
bool waiting_for_sign_in_ = false; // automatic check skipped: no license token yet
|
|
219
|
+
bool lock_held_ = false;
|
|
220
|
+
std::string launch_token_; // token unlocked at launch (validate_behind checks it)
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Replaces the installed <bundleId> folder's entries with the staged update
|
|
224
|
+
// (apply.cpp). Filesystem only. version/error receive details.
|
|
225
|
+
ApplyResult apply_staged_update(const Settings &settings, const Env &env, std::string *version, std::string *error);
|
|
226
|
+
// Removes "(ap-old)" / "(ap-update)" folders left in the install folder by an
|
|
227
|
+
// earlier apply (Windows keeps renamed loaded files until the next launch).
|
|
228
|
+
void cleanup_install_leftovers(const Settings &settings);
|
|
229
|
+
// The nearest ancestor of binary_path named exactly bundle_id, or "".
|
|
230
|
+
std::string find_install_dir(const std::string &binary_path, const std::string &bundle_id);
|
|
231
|
+
|
|
232
|
+
} // namespace apn
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#include "fsutil.hpp"
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <atomic>
|
|
5
|
+
#include <cerrno>
|
|
6
|
+
#include <chrono>
|
|
7
|
+
#include <cstdio>
|
|
8
|
+
#include <filesystem>
|
|
9
|
+
#include <fstream>
|
|
10
|
+
#include <random>
|
|
11
|
+
#include <system_error>
|
|
12
|
+
|
|
13
|
+
#ifdef _WIN32
|
|
14
|
+
#include <windows.h>
|
|
15
|
+
#include <io.h>
|
|
16
|
+
#else
|
|
17
|
+
#include <fcntl.h>
|
|
18
|
+
#include <sys/stat.h>
|
|
19
|
+
#include <unistd.h>
|
|
20
|
+
#endif
|
|
21
|
+
#ifdef __APPLE__
|
|
22
|
+
#include <stdio.h> // renamex_np
|
|
23
|
+
#endif
|
|
24
|
+
|
|
25
|
+
namespace fs = std::filesystem;
|
|
26
|
+
|
|
27
|
+
namespace apn {
|
|
28
|
+
namespace fsu {
|
|
29
|
+
|
|
30
|
+
namespace {
|
|
31
|
+
|
|
32
|
+
fs::path P(const std::string &s) {
|
|
33
|
+
#if defined(_WIN32)
|
|
34
|
+
return fs::u8path(s);
|
|
35
|
+
#else
|
|
36
|
+
return fs::path(s);
|
|
37
|
+
#endif
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
std::string S(const fs::path &p) {
|
|
41
|
+
#if defined(_WIN32)
|
|
42
|
+
auto u = p.u8string();
|
|
43
|
+
return std::string(u.begin(), u.end());
|
|
44
|
+
#else
|
|
45
|
+
return p.string();
|
|
46
|
+
#endif
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
std::string unique_suffix() {
|
|
50
|
+
static std::atomic<unsigned> counter{0};
|
|
51
|
+
std::random_device rd;
|
|
52
|
+
char buf[48];
|
|
53
|
+
std::snprintf(buf, sizeof buf, ".tmp-%08x%04x", (unsigned)rd(), (unsigned)(counter++ & 0xffff));
|
|
54
|
+
return buf;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
bool write_plain(const std::string &path, const std::string &data, int mode) {
|
|
58
|
+
#ifdef _WIN32
|
|
59
|
+
(void)mode;
|
|
60
|
+
std::ofstream f(P(path), std::ios::binary | std::ios::trunc);
|
|
61
|
+
if (!f) return false;
|
|
62
|
+
f.write(data.data(), (std::streamsize)data.size());
|
|
63
|
+
f.close();
|
|
64
|
+
return !f.fail();
|
|
65
|
+
#else
|
|
66
|
+
int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, mode);
|
|
67
|
+
if (fd < 0) return false;
|
|
68
|
+
::fchmod(fd, (mode_t)mode);
|
|
69
|
+
const char *p = data.data();
|
|
70
|
+
size_t left = data.size();
|
|
71
|
+
while (left) {
|
|
72
|
+
ssize_t n = ::write(fd, p, left);
|
|
73
|
+
if (n < 0) {
|
|
74
|
+
if (errno == EINTR) continue;
|
|
75
|
+
::close(fd);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
p += n;
|
|
79
|
+
left -= (size_t)n;
|
|
80
|
+
}
|
|
81
|
+
bool ok = ::fsync(fd) == 0 || errno == EINVAL;
|
|
82
|
+
return ::close(fd) == 0 && ok;
|
|
83
|
+
#endif
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
} // namespace
|
|
87
|
+
|
|
88
|
+
std::string join(const std::string &a, const std::string &b) { return S(P(a) / P(b)); }
|
|
89
|
+
std::string parent(const std::string &path) { return S(P(path).parent_path()); }
|
|
90
|
+
std::string basename(const std::string &path) {
|
|
91
|
+
fs::path p = P(path);
|
|
92
|
+
if (!p.has_filename()) p = p.parent_path();
|
|
93
|
+
return S(p.filename());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
bool exists(const std::string &path) {
|
|
97
|
+
std::error_code ec;
|
|
98
|
+
return fs::exists(fs::symlink_status(P(path), ec)) && !ec;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
bool is_dir(const std::string &path) {
|
|
102
|
+
std::error_code ec;
|
|
103
|
+
return fs::is_directory(P(path), ec);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
bool mkdirs(const std::string &path) {
|
|
107
|
+
std::error_code ec;
|
|
108
|
+
fs::create_directories(P(path), ec);
|
|
109
|
+
return is_dir(path);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
bool remove_all(const std::string &path) {
|
|
113
|
+
std::error_code ec;
|
|
114
|
+
fs::remove_all(P(path), ec);
|
|
115
|
+
return !ec;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
bool remove_file(const std::string &path) {
|
|
119
|
+
std::error_code ec;
|
|
120
|
+
fs::remove(P(path), ec);
|
|
121
|
+
return !ec;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
bool rename(const std::string &from, const std::string &to) {
|
|
125
|
+
std::error_code ec;
|
|
126
|
+
fs::rename(P(from), P(to), ec);
|
|
127
|
+
return !ec;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
std::vector<std::string> list(const std::string &dir) {
|
|
131
|
+
std::vector<std::string> out;
|
|
132
|
+
std::error_code ec;
|
|
133
|
+
for (fs::directory_iterator it(P(dir), ec), end; !ec && it != end; it.increment(ec)) {
|
|
134
|
+
out.push_back(S(it->path().filename()));
|
|
135
|
+
}
|
|
136
|
+
std::sort(out.begin(), out.end());
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
std::optional<std::string> read(const std::string &path, uint64_t max_bytes) {
|
|
141
|
+
std::ifstream f(P(path), std::ios::binary);
|
|
142
|
+
if (!f) return std::nullopt;
|
|
143
|
+
std::string data;
|
|
144
|
+
char buf[65536];
|
|
145
|
+
while (f) {
|
|
146
|
+
f.read(buf, sizeof buf);
|
|
147
|
+
data.append(buf, (size_t)f.gcount());
|
|
148
|
+
if (data.size() > max_bytes) return std::nullopt;
|
|
149
|
+
}
|
|
150
|
+
if (f.bad()) return std::nullopt;
|
|
151
|
+
return data;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
bool write_atomic(const std::string &path, const std::string &data, bool private_file) {
|
|
155
|
+
if (!mkdirs(parent(path))) return false;
|
|
156
|
+
std::string tmp = path + unique_suffix();
|
|
157
|
+
if (!write_plain(tmp, data, private_file ? 0600 : 0644)) {
|
|
158
|
+
remove_file(tmp);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (!rename(tmp, path)) {
|
|
162
|
+
// Windows can refuse to replace a file another process has open; retry once after deleting.
|
|
163
|
+
remove_file(path);
|
|
164
|
+
if (!rename(tmp, path)) {
|
|
165
|
+
remove_file(tmp);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Excl write_exclusive(const std::string &path, const std::string &data) {
|
|
173
|
+
if (!mkdirs(parent(path))) return Excl::Error;
|
|
174
|
+
#ifdef _WIN32
|
|
175
|
+
HANDLE h = CreateFileW(P(path).c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
176
|
+
if (h == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_FILE_EXISTS ? Excl::Exists : Excl::Error;
|
|
177
|
+
DWORD written = 0;
|
|
178
|
+
BOOL ok = WriteFile(h, data.data(), (DWORD)data.size(), &written, nullptr);
|
|
179
|
+
CloseHandle(h);
|
|
180
|
+
return ok && written == data.size() ? Excl::Ok : Excl::Error;
|
|
181
|
+
#else
|
|
182
|
+
int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
|
183
|
+
if (fd < 0) return errno == EEXIST ? Excl::Exists : Excl::Error;
|
|
184
|
+
ssize_t n = ::write(fd, data.data(), data.size());
|
|
185
|
+
::close(fd);
|
|
186
|
+
return n == (ssize_t)data.size() ? Excl::Ok : Excl::Error;
|
|
187
|
+
#endif
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
bool write_file(const std::string &path, const std::string &data, bool executable) {
|
|
191
|
+
if (!mkdirs(parent(path))) return false;
|
|
192
|
+
return write_plain(path, data, executable ? 0755 : 0644);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
std::optional<Stamp> stamp(const std::string &path) {
|
|
196
|
+
#ifdef _WIN32
|
|
197
|
+
std::error_code ec;
|
|
198
|
+
auto size = fs::file_size(P(path), ec);
|
|
199
|
+
if (ec) return std::nullopt;
|
|
200
|
+
auto t = fs::last_write_time(P(path), ec);
|
|
201
|
+
if (ec) return std::nullopt;
|
|
202
|
+
Stamp s;
|
|
203
|
+
s.size = size;
|
|
204
|
+
s.mtime_ns = (int64_t)std::chrono::duration_cast<std::chrono::nanoseconds>(t.time_since_epoch()).count();
|
|
205
|
+
return s;
|
|
206
|
+
#else
|
|
207
|
+
struct stat st;
|
|
208
|
+
if (::stat(path.c_str(), &st) != 0) return std::nullopt;
|
|
209
|
+
Stamp s;
|
|
210
|
+
s.size = (uint64_t)st.st_size;
|
|
211
|
+
#ifdef __APPLE__
|
|
212
|
+
s.mtime_ns = (int64_t)st.st_mtimespec.tv_sec * 1000000000LL + st.st_mtimespec.tv_nsec;
|
|
213
|
+
#else
|
|
214
|
+
s.mtime_ns = (int64_t)st.st_mtim.tv_sec * 1000000000LL + st.st_mtim.tv_nsec;
|
|
215
|
+
#endif
|
|
216
|
+
s.inode = (uint64_t)st.st_ino;
|
|
217
|
+
return s;
|
|
218
|
+
#endif
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
int64_t age_ms(const std::string &path, int64_t now_ms) {
|
|
222
|
+
#ifdef _WIN32
|
|
223
|
+
WIN32_FILE_ATTRIBUTE_DATA d;
|
|
224
|
+
if (!GetFileAttributesExW(P(path).c_str(), GetFileExInfoStandard, &d)) return -1;
|
|
225
|
+
ULARGE_INTEGER t;
|
|
226
|
+
t.LowPart = d.ftLastWriteTime.dwLowDateTime;
|
|
227
|
+
t.HighPart = d.ftLastWriteTime.dwHighDateTime;
|
|
228
|
+
int64_t unix_ms = (int64_t)(t.QuadPart / 10000ULL) - 11644473600000LL;
|
|
229
|
+
return now_ms - unix_ms;
|
|
230
|
+
#else
|
|
231
|
+
struct stat st;
|
|
232
|
+
if (::stat(path.c_str(), &st) != 0) return -1;
|
|
233
|
+
return now_ms - (int64_t)st.st_mtime * 1000;
|
|
234
|
+
#endif
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
bool copy_tree(const std::string &from, const std::string &to, std::string *error) {
|
|
238
|
+
std::error_code ec;
|
|
239
|
+
fs::path src = P(from), dst = P(to);
|
|
240
|
+
fs::create_directories(dst, ec);
|
|
241
|
+
if (ec) { if (error) *error = "cannot create " + basename(to); return false; }
|
|
242
|
+
for (fs::recursive_directory_iterator it(src, ec), end; !ec && it != end; it.increment(ec)) {
|
|
243
|
+
fs::path rel = it->path().lexically_relative(src);
|
|
244
|
+
fs::path target = dst / rel;
|
|
245
|
+
auto st = it->symlink_status(ec);
|
|
246
|
+
if (ec) break;
|
|
247
|
+
if (fs::is_symlink(st)) { if (error) *error = "symbolic link in update: " + S(rel); return false; }
|
|
248
|
+
if (fs::is_directory(st)) {
|
|
249
|
+
fs::create_directories(target, ec);
|
|
250
|
+
} else if (fs::is_regular_file(st)) {
|
|
251
|
+
fs::copy_file(it->path(), target, fs::copy_options::overwrite_existing, ec);
|
|
252
|
+
#ifndef _WIN32
|
|
253
|
+
if (!ec) {
|
|
254
|
+
bool exec = (st.permissions() & fs::perms::owner_exec) != fs::perms::none;
|
|
255
|
+
fs::permissions(target, exec ? (fs::perms)0755 : (fs::perms)0644, ec);
|
|
256
|
+
}
|
|
257
|
+
#endif
|
|
258
|
+
} else {
|
|
259
|
+
if (error) *error = "unsupported file type: " + S(rel);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (ec) break;
|
|
263
|
+
}
|
|
264
|
+
if (ec) { if (error) *error = "copy failed: " + ec.message(); return false; }
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static std::atomic<bool> swap_disabled{false};
|
|
269
|
+
void test_disable_swap(bool disabled) { swap_disabled.store(disabled); }
|
|
270
|
+
|
|
271
|
+
bool swap(const std::string &a, const std::string &b) {
|
|
272
|
+
if (swap_disabled.load()) return false;
|
|
273
|
+
#ifdef __APPLE__
|
|
274
|
+
return ::renamex_np(a.c_str(), b.c_str(), RENAME_SWAP) == 0;
|
|
275
|
+
#else
|
|
276
|
+
(void)a;
|
|
277
|
+
(void)b;
|
|
278
|
+
return false;
|
|
279
|
+
#endif
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
bool dir_writable(const std::string &dir) {
|
|
283
|
+
if (!is_dir(dir)) return false;
|
|
284
|
+
std::string probe = join(dir, "(ap-write-test)" + unique_suffix());
|
|
285
|
+
if (write_exclusive(probe, "") != Excl::Ok) return false;
|
|
286
|
+
remove_file(probe);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
} // namespace fsu
|
|
291
|
+
} // namespace apn
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// UTF-8 path helpers over std::filesystem. Never throw; report failure.
|
|
2
|
+
#pragma once
|
|
3
|
+
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
#include <optional>
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <vector>
|
|
8
|
+
|
|
9
|
+
namespace apn {
|
|
10
|
+
namespace fsu {
|
|
11
|
+
|
|
12
|
+
std::string join(const std::string &a, const std::string &b);
|
|
13
|
+
std::string parent(const std::string &path);
|
|
14
|
+
std::string basename(const std::string &path);
|
|
15
|
+
|
|
16
|
+
bool exists(const std::string &path);
|
|
17
|
+
bool is_dir(const std::string &path);
|
|
18
|
+
bool mkdirs(const std::string &path);
|
|
19
|
+
bool remove_all(const std::string &path);
|
|
20
|
+
bool remove_file(const std::string &path);
|
|
21
|
+
bool rename(const std::string &from, const std::string &to);
|
|
22
|
+
std::vector<std::string> list(const std::string &dir); // entry names, sorted
|
|
23
|
+
|
|
24
|
+
std::optional<std::string> read(const std::string &path, uint64_t max_bytes = 16 * 1024 * 1024);
|
|
25
|
+
// temp file + rename. private_file: 0600 on POSIX (files that hold a token).
|
|
26
|
+
bool write_atomic(const std::string &path, const std::string &data, bool private_file);
|
|
27
|
+
enum class Excl { Ok, Exists, Error };
|
|
28
|
+
// O_CREAT|O_EXCL (the JS runtime's flag "wx").
|
|
29
|
+
Excl write_exclusive(const std::string &path, const std::string &data);
|
|
30
|
+
// mode 0755 when executable on POSIX.
|
|
31
|
+
bool write_file(const std::string &path, const std::string &data, bool executable);
|
|
32
|
+
|
|
33
|
+
// Size and modification time; nullopt when missing. Used to notice another
|
|
34
|
+
// process writing a shared cache file.
|
|
35
|
+
struct Stamp {
|
|
36
|
+
uint64_t size = 0;
|
|
37
|
+
int64_t mtime_ns = 0;
|
|
38
|
+
uint64_t inode = 0; // an atomic replace with the same size within one mtime tick still differs
|
|
39
|
+
bool operator==(const Stamp &o) const { return size == o.size && mtime_ns == o.mtime_ns && inode == o.inode; }
|
|
40
|
+
bool operator!=(const Stamp &o) const { return !(*this == o); }
|
|
41
|
+
};
|
|
42
|
+
std::optional<Stamp> stamp(const std::string &path);
|
|
43
|
+
int64_t age_ms(const std::string &path, int64_t now_ms); // -1 when missing
|
|
44
|
+
|
|
45
|
+
// Copies a directory tree (files and directories only; refuses symlinks),
|
|
46
|
+
// keeping the POSIX executable bit.
|
|
47
|
+
bool copy_tree(const std::string &from, const std::string &to, std::string *error);
|
|
48
|
+
// Atomically exchanges two paths (macOS renamex_np RENAME_SWAP). false when
|
|
49
|
+
// unsupported or failed; the caller falls back to two renames.
|
|
50
|
+
bool swap(const std::string &a, const std::string &b);
|
|
51
|
+
// Tests: exercise the rename fallback (the Windows path) on macOS.
|
|
52
|
+
void test_disable_swap(bool disabled);
|
|
53
|
+
// True when this process may create, rename and delete entries in dir.
|
|
54
|
+
bool dir_writable(const std::string &dir);
|
|
55
|
+
|
|
56
|
+
} // namespace fsu
|
|
57
|
+
} // namespace apn
|