-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathworkspace_registry.cpp
More file actions
342 lines (288 loc) · 11.7 KB
/
Copy pathworkspace_registry.cpp
File metadata and controls
342 lines (288 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
#include "workspace_registry.hpp"
#include "../utils/atomic_file.hpp"
#include "../utils/cwd_hash.hpp"
#include "../utils/logger.hpp"
#include "../utils/utf8_path.hpp"
#include <nlohmann/json.hpp>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <sstream>
namespace fs = std::filesystem;
namespace acecode::desktop {
namespace {
constexpr const char* kWorkspaceJson = "workspace.json";
// 给 hash 目录 + 根 projects_dir 拼出 workspace.json 完整路径。
std::string workspace_json_path(const std::string& projects_dir, const std::string& hash) {
return path_to_utf8(path_from_utf8(projects_dir) / hash / kWorkspaceJson);
}
// 尝试读取 workspace.json。返回 nullopt 表示文件不存在 / 损坏 / 缺关键字段。
std::optional<WorkspaceMeta> read_workspace_json(const std::string& projects_dir,
const std::string& hash) {
std::string p = workspace_json_path(projects_dir, hash);
std::error_code ec;
if (!fs::exists(path_from_utf8(p), ec) || ec) return std::nullopt;
std::ifstream ifs(path_from_utf8(p));
if (!ifs.is_open()) return std::nullopt;
std::stringstream buf;
buf << ifs.rdbuf();
std::string contents = buf.str();
if (contents.empty()) return std::nullopt;
try {
auto j = nlohmann::json::parse(contents);
if (!j.is_object()) {
LOG_WARN("[workspace_registry] workspace.json is not an object at " + p);
return std::nullopt;
}
if (!j.contains("cwd") || !j["cwd"].is_string()) {
LOG_WARN("[workspace_registry] workspace.json missing cwd at " + p);
return std::nullopt;
}
WorkspaceMeta m;
m.hash = hash;
m.cwd = j["cwd"].get<std::string>();
if (j.contains("name") && j["name"].is_string()) {
m.name = j["name"].get<std::string>();
}
if (m.name.empty()) m.name = default_workspace_name(m.cwd);
if (j.contains("desktop_visible") && j["desktop_visible"].is_boolean()) {
m.desktop_visible = j["desktop_visible"].get<bool>();
}
return m;
} catch (const std::exception& e) {
LOG_WARN(std::string("[workspace_registry] workspace.json parse failed at ")
+ p + ": " + e.what());
return std::nullopt;
}
}
bool write_workspace_json(const std::string& projects_dir, const WorkspaceMeta& m) {
fs::path dir = path_from_utf8(projects_dir) / m.hash;
std::error_code ec;
fs::create_directories(dir, ec);
if (ec) {
LOG_WARN("[workspace_registry] create_directories failed: " + ec.message());
return false;
}
std::string p = path_to_utf8(dir / kWorkspaceJson);
nlohmann::json j;
j["cwd"] = m.cwd;
j["name"] = m.name;
j["desktop_visible"] = m.desktop_visible;
return atomic_write_file(p, j.dump(2));
}
} // namespace
std::string default_workspace_name(const std::string& cwd) {
// path("foo/").filename() 在 C++17 是空;Windows 上
// fs::path(std::string) 还会按本地窄编码解释 UTF-8。这里用纯字符串切
// basename,既避开尾斜杠,也保留中文等 UTF-8 名称不被转码。
std::string clean = cwd;
while (clean.size() > 1 && (clean.back() == '/' || clean.back() == '\\')) {
clean.pop_back();
}
size_t slash = std::string::npos;
for (size_t i = clean.size(); i > 0; --i) {
if (clean[i - 1] == '/' || clean[i - 1] == '\\') {
slash = i - 1;
break;
}
}
std::string base = slash == std::string::npos ? clean : clean.substr(slash + 1);
if (!base.empty()) return base;
if (clean.size() >= 2 && clean[1] == ':') {
return clean.substr(0, 2);
}
return "workspace";
}
std::optional<WorkspaceMeta> load_workspace_metadata(
const std::string& projects_dir,
const std::string& hash) {
if (projects_dir.empty() || hash.empty()) return std::nullopt;
return read_workspace_json(projects_dir, hash);
}
namespace {
// 文件系统时间戳有粒度(Windows 上实测约 15ms)。若某个目录在上次扫描之后的
// 同一个 tick 内被写入,它的 mtime 不会变 —— 只比 mtime 就会**永久**漏掉那次
// 写入,因为 mtime 此后再也不动了。所以刚变动过的目录一律不信任缓存,重读几
// 轮直到它的时间戳落到窗口之外。窗口取得比时钟粒度宽得多,代价只是对少数
// 刚动过的目录多读几次文件。
constexpr auto kFreshDirectoryWindow = std::chrono::seconds(2);
struct DirStamp {
std::int64_t tick = 0; // 0 = 读不到,调用方退回「每次都重读」
bool recent = false; // 落在 kFreshDirectoryWindow 内,缓存不可信
};
// 目录项自身的时间戳。属性由目录枚举顺带带出(Windows 上是 FindNextFile 的
// WIN32_FIND_DATA),不像路径版 fs::last_write_time 那样再 stat 一次。
DirStamp dir_entry_stamp(const fs::directory_entry& entry) {
std::error_code ec;
auto t = entry.last_write_time(ec);
if (ec) return DirStamp{};
DirStamp stamp;
stamp.tick = static_cast<std::int64_t>(t.time_since_epoch().count());
const auto now = fs::file_time_type::clock::now();
stamp.recent = now >= t && (now - t) < kFreshDirectoryWindow;
return stamp;
}
} // namespace
void WorkspaceRegistry::scan_locked(const std::string& projects_dir) {
entries_.clear();
std::error_code ec;
fs::path native_projects_dir = path_from_utf8(projects_dir);
if (!fs::exists(native_projects_dir, ec) || !fs::is_directory(native_projects_dir, ec)) {
dir_probes_.clear();
return; // 空目录或不存在 — 注册表保持空
}
// next_probes 只保留这一轮真实存在的目录,消失的目录自然从缓存里掉出去。
std::unordered_map<std::string, DirProbe> next_probes;
for (auto it = fs::directory_iterator(native_projects_dir, ec);
!ec && it != fs::directory_iterator();
it.increment(ec)) {
if (ec) break;
std::error_code entry_ec;
if (!it->is_directory(entry_ec) || entry_ec) continue;
std::string hash = path_to_utf8(it->path().filename());
// hash 目录名约定: 16 位 hex。不强制(防止过滤掉合法案例),只防过短目录。
if (hash.size() < 4) continue;
const DirStamp stamp = dir_entry_stamp(*it);
auto cached = dir_probes_.find(hash);
if (stamp.tick != 0 && !stamp.recent &&
cached != dir_probes_.end() && cached->second.mtime == stamp.tick) {
// 这个目录自上次以来没动过,沿用上次的结论(包括「没有 marker」)。
if (cached->second.has_marker) entries_[hash] = cached->second.meta;
next_probes.emplace(hash, cached->second);
continue;
}
DirProbe probe;
// 刚动过的目录仍然记下时间戳,等它落到新鲜窗口之外就能进入缓存。
probe.mtime = stamp.tick;
auto m = read_workspace_json(projects_dir, hash);
if (m && m->desktop_visible) {
probe.has_marker = true;
probe.meta = *m;
entries_[hash] = std::move(*m);
}
next_probes.emplace(std::move(hash), std::move(probe));
}
dir_probes_ = std::move(next_probes);
}
void WorkspaceRegistry::scan(const std::string& projects_dir) {
std::lock_guard<std::mutex> lk(mu_);
scan_locked(projects_dir);
}
void WorkspaceRegistry::force_full_scan(const std::string& projects_dir) {
std::lock_guard<std::mutex> lk(mu_);
dir_probes_.clear();
scan_locked(projects_dir);
}
std::vector<WorkspaceMeta> WorkspaceRegistry::list() const {
std::lock_guard<std::mutex> lk(mu_);
std::vector<WorkspaceMeta> out;
out.reserve(entries_.size());
for (const auto& [_, m] : entries_) out.push_back(m);
return out;
}
std::optional<WorkspaceMeta> WorkspaceRegistry::get(const std::string& hash) const {
std::lock_guard<std::mutex> lk(mu_);
auto it = entries_.find(hash);
if (it == entries_.end()) return std::nullopt;
return it->second;
}
WorkspaceMeta WorkspaceRegistry::register_new(const std::string& projects_dir,
const std::string& cwd) {
std::string hash = compute_cwd_hash(cwd);
{
std::lock_guard<std::mutex> lk(mu_);
auto it = entries_.find(hash);
if (it != entries_.end()) return it->second; // 已注册:返回已有 meta
}
if (auto existing = read_workspace_json(projects_dir, hash)) {
existing->cwd = existing->cwd.empty() ? cwd : existing->cwd;
if (existing->name.empty()) existing->name = default_workspace_name(existing->cwd);
existing->desktop_visible = true;
if (!write_workspace_json(projects_dir, *existing)) {
LOG_WARN("[workspace_registry] register_new restore write failed for cwd=" + cwd);
}
std::lock_guard<std::mutex> lk(mu_);
dir_probes_.erase(hash);
entries_[hash] = *existing;
return *existing;
}
WorkspaceMeta m;
m.hash = hash;
m.cwd = cwd;
m.name = default_workspace_name(cwd);
m.desktop_visible = true;
if (!write_workspace_json(projects_dir, m)) {
// 写盘失败 — 仍把 meta 入册以便用户当前会话能用,但下次启动 scan 会缺失。
// 不抛异常,让 UI 端用 daemon_state=failed 之类显示。
LOG_WARN("[workspace_registry] register_new write failed for cwd=" + cwd);
}
{
std::lock_guard<std::mutex> lk(mu_);
dir_probes_.erase(hash);
entries_[hash] = m;
}
return m;
}
bool ensure_workspace_metadata(const std::string& projects_dir, const std::string& cwd) {
if (cwd.empty()) return false;
WorkspaceMeta m;
m.hash = compute_cwd_hash(cwd);
m.cwd = cwd;
m.name = default_workspace_name(cwd);
m.desktop_visible = false;
std::string p = workspace_json_path(projects_dir, m.hash);
std::error_code ec;
if (fs::exists(path_from_utf8(p), ec) && !ec) {
return true;
}
return write_workspace_json(projects_dir, m);
}
bool WorkspaceRegistry::set_name(const std::string& projects_dir,
const std::string& hash,
const std::string& name) {
if (name.empty()) return false;
WorkspaceMeta updated;
{
std::lock_guard<std::mutex> lk(mu_);
auto it = entries_.find(hash);
if (it == entries_.end()) return false;
updated = it->second;
}
updated.name = name;
if (!write_workspace_json(projects_dir, updated)) return false;
std::lock_guard<std::mutex> lk(mu_);
dir_probes_.erase(hash);
auto it = entries_.find(hash);
if (it == entries_.end()) return false; // 极端竞态:写盘期间被 scan 清空
it->second = updated;
return true;
}
bool WorkspaceRegistry::hide(const std::string& projects_dir, const std::string& hash) {
if (hash.empty()) return false;
WorkspaceMeta updated;
bool found = false;
{
std::lock_guard<std::mutex> lk(mu_);
auto it = entries_.find(hash);
if (it != entries_.end()) {
updated = it->second;
found = true;
}
}
if (!found) {
auto existing = read_workspace_json(projects_dir, hash);
if (!existing) return false;
updated = *existing;
}
updated.desktop_visible = false;
if (!write_workspace_json(projects_dir, updated)) return false;
std::lock_guard<std::mutex> lk(mu_);
dir_probes_.erase(hash);
entries_.erase(hash);
return true;
}
bool workspace_hash_matches_cwd(const std::string& hash, const std::string& cwd) {
if (hash.empty() || cwd.empty()) return false;
return hash == compute_cwd_hash(cwd);
}
} // namespace acecode::desktop