@halcyonlabs/app 0.47.0-halcyon.327 → 0.47.0-halcyon.328
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/dist/index.d.ts +13 -13
- package/package.json +1 -1
- package/server/dist/builtin-plugins/orbit/dist/assets/orbit/bundle/scripts/orbit-job-supervisor +225 -11
- package/server/dist/builtin-plugins/orbit/dist/assets/orbit/revision +1 -1
- package/server/dist/builtin-plugins/orbit/dist/server.js +188 -188
package/dist/index.d.ts
CHANGED
|
@@ -83,8 +83,8 @@ declare const appSettingsSchema: z$1.ZodObject<{
|
|
|
83
83
|
providerOrder: z$1.ZodArray<z$1.ZodString>;
|
|
84
84
|
defaultProviderId: z$1.ZodNullable<z$1.ZodString>;
|
|
85
85
|
providerCompletedTurnDisplay: z$1.ZodRecord<z$1.ZodString, z$1.ZodEnum<{
|
|
86
|
-
collapse: "collapse";
|
|
87
86
|
flat: "flat";
|
|
87
|
+
collapse: "collapse";
|
|
88
88
|
}>>;
|
|
89
89
|
streamerMode: z$1.ZodBoolean;
|
|
90
90
|
allowFastServiceTier: z$1.ZodBoolean;
|
|
@@ -105,8 +105,8 @@ declare const appSettingsUpdateSchema: z$1.ZodUnion<readonly [z$1.ZodObject<{
|
|
|
105
105
|
providerOrder: z$1.ZodArray<z$1.ZodString>;
|
|
106
106
|
defaultProviderId: z$1.ZodNullable<z$1.ZodString>;
|
|
107
107
|
providerCompletedTurnDisplay: z$1.ZodRecord<z$1.ZodString, z$1.ZodEnum<{
|
|
108
|
-
collapse: "collapse";
|
|
109
108
|
flat: "flat";
|
|
109
|
+
collapse: "collapse";
|
|
110
110
|
}>>;
|
|
111
111
|
streamerMode: z$1.ZodBoolean;
|
|
112
112
|
managedBranchPrefix: z$1.ZodString;
|
|
@@ -143,8 +143,8 @@ declare const appSettingsUpdateSchema: z$1.ZodUnion<readonly [z$1.ZodObject<{
|
|
|
143
143
|
providerOrder: z$1.ZodArray<z$1.ZodString>;
|
|
144
144
|
defaultProviderId: z$1.ZodNullable<z$1.ZodString>;
|
|
145
145
|
providerCompletedTurnDisplay: z$1.ZodRecord<z$1.ZodString, z$1.ZodEnum<{
|
|
146
|
-
collapse: "collapse";
|
|
147
146
|
flat: "flat";
|
|
147
|
+
collapse: "collapse";
|
|
148
148
|
}>>;
|
|
149
149
|
streamerMode: z$1.ZodBoolean;
|
|
150
150
|
machineServerUrl: z$1.ZodNullable<z$1.ZodString>;
|
|
@@ -316,8 +316,8 @@ declare const appKeybindingOverridesSchema: z$1.ZodArray<z$1.ZodObject<{
|
|
|
316
316
|
shift: z$1.ZodBoolean;
|
|
317
317
|
}, z$1.core.$strict>>;
|
|
318
318
|
platform: z$1.ZodOptional<z$1.ZodEnum<{
|
|
319
|
-
windows: "windows";
|
|
320
319
|
mac: "mac";
|
|
320
|
+
windows: "windows";
|
|
321
321
|
linux: "linux";
|
|
322
322
|
}>>;
|
|
323
323
|
}, z$1.core.$strict>>;
|
|
@@ -3008,8 +3008,8 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
|
|
|
3008
3008
|
}>;
|
|
3009
3009
|
name: z$1.ZodString;
|
|
3010
3010
|
source: z$1.ZodEnum<{
|
|
3011
|
-
skill: "skill";
|
|
3012
3011
|
command: "command";
|
|
3012
|
+
skill: "skill";
|
|
3013
3013
|
}>;
|
|
3014
3014
|
origin: z$1.ZodEnum<{
|
|
3015
3015
|
user: "user";
|
|
@@ -3098,8 +3098,8 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
|
|
|
3098
3098
|
}>;
|
|
3099
3099
|
name: z$1.ZodString;
|
|
3100
3100
|
source: z$1.ZodEnum<{
|
|
3101
|
-
skill: "skill";
|
|
3102
3101
|
command: "command";
|
|
3102
|
+
skill: "skill";
|
|
3103
3103
|
}>;
|
|
3104
3104
|
origin: z$1.ZodEnum<{
|
|
3105
3105
|
user: "user";
|
|
@@ -4016,8 +4016,8 @@ declare const promptTextMentionSchema: z$1.ZodObject<{
|
|
|
4016
4016
|
}>;
|
|
4017
4017
|
name: z$1.ZodString;
|
|
4018
4018
|
source: z$1.ZodEnum<{
|
|
4019
|
-
skill: "skill";
|
|
4020
4019
|
command: "command";
|
|
4020
|
+
skill: "skill";
|
|
4021
4021
|
}>;
|
|
4022
4022
|
origin: z$1.ZodEnum<{
|
|
4023
4023
|
user: "user";
|
|
@@ -4081,8 +4081,8 @@ declare const promptInputSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{
|
|
|
4081
4081
|
}>;
|
|
4082
4082
|
name: z$1.ZodString;
|
|
4083
4083
|
source: z$1.ZodEnum<{
|
|
4084
|
-
skill: "skill";
|
|
4085
4084
|
command: "command";
|
|
4085
|
+
skill: "skill";
|
|
4086
4086
|
}>;
|
|
4087
4087
|
origin: z$1.ZodEnum<{
|
|
4088
4088
|
user: "user";
|
|
@@ -4240,8 +4240,8 @@ declare const providerInfoSchema: z$1.ZodObject<{
|
|
|
4240
4240
|
}, z$1.core.$strip>;
|
|
4241
4241
|
}, z$1.core.$strip>], "kind">>;
|
|
4242
4242
|
completedTurnDisplay: z$1.ZodEnum<{
|
|
4243
|
-
collapse: "collapse";
|
|
4244
4243
|
flat: "flat";
|
|
4244
|
+
collapse: "collapse";
|
|
4245
4245
|
}>;
|
|
4246
4246
|
available: z$1.ZodBoolean;
|
|
4247
4247
|
strings: z$1.ZodOptional<z$1.ZodObject<{
|
|
@@ -4329,8 +4329,8 @@ declare const threadTimelinePendingTodosSchema: z$1.ZodObject<{
|
|
|
4329
4329
|
type ThreadTimelinePendingTodos = z$1.infer<typeof threadTimelinePendingTodosSchema>;
|
|
4330
4330
|
|
|
4331
4331
|
declare const threadParentNotificationsSchema: z$1.ZodEnum<{
|
|
4332
|
-
off: "off";
|
|
4333
4332
|
auto: "auto";
|
|
4333
|
+
off: "off";
|
|
4334
4334
|
stuck: "stuck";
|
|
4335
4335
|
}>;
|
|
4336
4336
|
type ThreadParentNotifications = z$1.infer<typeof threadParentNotificationsSchema>;
|
|
@@ -4393,8 +4393,8 @@ declare const threadQueuedMessageSchema: z$1.ZodObject<{
|
|
|
4393
4393
|
}>;
|
|
4394
4394
|
name: z$1.ZodString;
|
|
4395
4395
|
source: z$1.ZodEnum<{
|
|
4396
|
-
skill: "skill";
|
|
4397
4396
|
command: "command";
|
|
4397
|
+
skill: "skill";
|
|
4398
4398
|
}>;
|
|
4399
4399
|
origin: z$1.ZodEnum<{
|
|
4400
4400
|
user: "user";
|
|
@@ -7860,13 +7860,13 @@ declare const hostDaemonCommandRegistry: {
|
|
|
7860
7860
|
modifiedAtMs: z$1.ZodOptional<z$1.ZodNumber>;
|
|
7861
7861
|
sha256: z$1.ZodString;
|
|
7862
7862
|
}, z$1.core.$strip>, z$1.ZodObject<{
|
|
7863
|
-
path: z$1.ZodString;
|
|
7864
7863
|
contentEncoding: z$1.ZodEnum<{
|
|
7865
7864
|
base64: "base64";
|
|
7866
7865
|
utf8: "utf8";
|
|
7867
7866
|
}>;
|
|
7868
7867
|
sha256: z$1.ZodString;
|
|
7869
7868
|
sizeBytes: z$1.ZodNumber;
|
|
7869
|
+
path: z$1.ZodString;
|
|
7870
7870
|
mimeType: z$1.ZodOptional<z$1.ZodString>;
|
|
7871
7871
|
modifiedAtMs: z$1.ZodOptional<z$1.ZodNumber>;
|
|
7872
7872
|
notModified: z$1.ZodLiteral<true>;
|
|
@@ -8131,8 +8131,8 @@ declare const hostDaemonCommandRegistry: {
|
|
|
8131
8131
|
supported: z$1.ZodLiteral<true>;
|
|
8132
8132
|
health: z$1.ZodObject<{
|
|
8133
8133
|
status: z$1.ZodEnum<{
|
|
8134
|
-
unknown: "unknown";
|
|
8135
8134
|
ready: "ready";
|
|
8135
|
+
unknown: "unknown";
|
|
8136
8136
|
not_installed: "not_installed";
|
|
8137
8137
|
unauthenticated: "unauthenticated";
|
|
8138
8138
|
expired: "expired";
|
package/package.json
CHANGED
package/server/dist/builtin-plugins/orbit/dist/assets/orbit/bundle/scripts/orbit-job-supervisor
CHANGED
|
@@ -564,6 +564,49 @@ class Workspaces:
|
|
|
564
564
|
os.close(self.open_workspace(job_id))
|
|
565
565
|
return unpushed_work(self.workspace_root / (self.prefix + job_id), account)
|
|
566
566
|
|
|
567
|
+
def save_and_clean(self, job_id, account):
|
|
568
|
+
with self.locked():
|
|
569
|
+
manifest = self.read(job_id)
|
|
570
|
+
if not has_workspace(manifest) or manifest['state'] not in ('reserved', 'running', 'parked'):
|
|
571
|
+
raise ValueError('Only an open workspace can save and clean.')
|
|
572
|
+
path = self.workspace_root / (self.prefix + job_id)
|
|
573
|
+
users, unknown = workspace_users(path)
|
|
574
|
+
if users or unknown:
|
|
575
|
+
raise ValueError('Workspace writers must be confirmed stopped before saving and cleaning.')
|
|
576
|
+
manifest = self.declared_outputs(manifest)
|
|
577
|
+
root = self.open_workspace(job_id)
|
|
578
|
+
try:
|
|
579
|
+
checkouts = as_account(account, lambda: dict(paths=[str(p.relative_to(path)) for p in git_checkouts(path, time.monotonic() + UNPUSHED_SECONDS, include_build=True)]))['paths']
|
|
580
|
+
work = dict(checkouts=[dict(path=name, preserved=True) for name in checkouts])
|
|
581
|
+
caches = [tuple([name]) for name in JOB_CACHES] + declared_rebuildable(root, manifest['outputs'])
|
|
582
|
+
caches += [tuple(parts) for parts in as_account(account, lambda: dict(paths=recovery_caches(path, checkouts, account, manifest['outputs'])))['paths']]
|
|
583
|
+
safe_caches = []
|
|
584
|
+
for parts in caches:
|
|
585
|
+
cache = '/'.join(parts)
|
|
586
|
+
if any(checkout == cache or checkout.startswith(cache + '/') for checkout in checkouts):
|
|
587
|
+
if parts == ('tmp',) or parts[-2:] == ('.orbit-build', 'tmp'):
|
|
588
|
+
continue
|
|
589
|
+
raise ValueError('A rebuildable folder contains a checkout; unpublished work is kept.')
|
|
590
|
+
safe_caches.append(parts)
|
|
591
|
+
caches = safe_caches
|
|
592
|
+
name = 'results/storage-recovery-' + secrets.token_hex(16) + '.tar.gz'
|
|
593
|
+
outputs, _ = declared_output_names(manifest['outputs'] + [name], strict=True)
|
|
594
|
+
as_account(account, lambda: save_recovery(root, caches, account, name, outputs))
|
|
595
|
+
write_record(root, '.orbit-outputs.json', outputs)
|
|
596
|
+
declaration = os.open('.orbit-outputs.json', os.O_RDONLY | os.O_NOFOLLOW, dir_fd=root)
|
|
597
|
+
try:
|
|
598
|
+
os.fchown(declaration, account.pw_uid, account.pw_gid)
|
|
599
|
+
finally:
|
|
600
|
+
os.close(declaration)
|
|
601
|
+
manifest['outputs'] = outputs
|
|
602
|
+
write_record(self.state, manifest['id'] + '.json', manifest)
|
|
603
|
+
reclaim_workspace(root, outputs, caches)
|
|
604
|
+
if manifest['state'] != 'parked':
|
|
605
|
+
self.save_transition(manifest, 'parked')
|
|
606
|
+
return dict(output=name, unpublished=work['checkouts'])
|
|
607
|
+
finally:
|
|
608
|
+
os.close(root)
|
|
609
|
+
|
|
567
610
|
def measure(self, job_id, sized=True):
|
|
568
611
|
"""Free space and file entries for the stop rule; the folder's size only when low.
|
|
569
612
|
|
|
@@ -1064,13 +1107,50 @@ def declared_rebuildable(root, outputs):
|
|
|
1064
1107
|
return paths[:REBUILDABLE_LIMIT]
|
|
1065
1108
|
|
|
1066
1109
|
|
|
1067
|
-
def
|
|
1110
|
+
def cache_has_checkout(parent, name, device):
|
|
1111
|
+
stack = []
|
|
1112
|
+
deadline = time.monotonic() + 5
|
|
1113
|
+
try:
|
|
1114
|
+
stack.append((os.open(name, DIR_FLAGS, dir_fd=parent), None))
|
|
1115
|
+
while stack:
|
|
1116
|
+
current, entries = stack[-1]
|
|
1117
|
+
if entries is None:
|
|
1118
|
+
names = os.listdir(current)
|
|
1119
|
+
if '.git' in names or {'HEAD', 'objects', 'config'}.issubset(names):
|
|
1120
|
+
return True
|
|
1121
|
+
entries = iter(names)
|
|
1122
|
+
stack[-1] = (current, entries)
|
|
1123
|
+
entry = next(entries, None)
|
|
1124
|
+
if entry is None:
|
|
1125
|
+
os.close(current)
|
|
1126
|
+
stack.pop()
|
|
1127
|
+
continue
|
|
1128
|
+
if time.monotonic() > deadline:
|
|
1129
|
+
return True
|
|
1130
|
+
info = os.stat(entry, dir_fd=current, follow_symlinks=False)
|
|
1131
|
+
if info.st_dev != device:
|
|
1132
|
+
return True
|
|
1133
|
+
if stat.S_ISDIR(info.st_mode):
|
|
1134
|
+
stack.append((os.open(entry, DIR_FLAGS, dir_fd=current), None))
|
|
1135
|
+
return False
|
|
1136
|
+
except (OSError, ValueError):
|
|
1137
|
+
return True
|
|
1138
|
+
finally:
|
|
1139
|
+
for fd, _ in reversed(stack):
|
|
1140
|
+
os.close(fd)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def reclaim_workspace(root, outputs=(), paths=None):
|
|
1068
1144
|
"""Empty the caches the supervisor gives every job, then the folders the job declared rebuildable."""
|
|
1069
1145
|
device = os.fstat(root).st_dev
|
|
1070
1146
|
for name in JOB_CACHES:
|
|
1071
|
-
if
|
|
1147
|
+
if paths is not None and (name,) not in paths:
|
|
1148
|
+
continue
|
|
1149
|
+
if directory_at(root, name, device) and not cache_has_checkout(root, name, device):
|
|
1072
1150
|
clear_contents(root, name, device)
|
|
1073
|
-
for parts in declared_rebuildable(root, outputs):
|
|
1151
|
+
for parts in declared_rebuildable(root, outputs) if paths is None else paths:
|
|
1152
|
+
if len(parts) == 1 and parts[0] in JOB_CACHES:
|
|
1153
|
+
continue
|
|
1074
1154
|
holder = os.dup(root)
|
|
1075
1155
|
try:
|
|
1076
1156
|
for part in parts[:-1]:
|
|
@@ -1080,12 +1160,140 @@ def reclaim_workspace(root, outputs=()):
|
|
|
1080
1160
|
os.close(holder)
|
|
1081
1161
|
holder = nested
|
|
1082
1162
|
else:
|
|
1083
|
-
if directory_at(holder, parts[-1], device):
|
|
1163
|
+
if directory_at(holder, parts[-1], device) and not cache_has_checkout(holder, parts[-1], device):
|
|
1084
1164
|
remove_tree(holder, parts[-1], device)
|
|
1085
1165
|
finally:
|
|
1086
1166
|
os.close(holder)
|
|
1087
1167
|
|
|
1088
1168
|
|
|
1169
|
+
def recovery_caches(root, checkouts, account, outputs):
|
|
1170
|
+
caches = []
|
|
1171
|
+
for name in checkouts:
|
|
1172
|
+
checkout = root / name
|
|
1173
|
+
if not (checkout / '.git').exists():
|
|
1174
|
+
continue
|
|
1175
|
+
tracked = git_output(account, checkout, name, 'ls-files', '-z', offline=True).stdout.split('\0')
|
|
1176
|
+
directories = {Path(file).parent for file in tracked if Path(file).name in ('package.json', 'Cargo.toml')}
|
|
1177
|
+
candidates = [checkout / '.orbit-build' / folder for folder in (*JOB_CACHES, 'rustup', 'rustup-home', 'home/.rustup')]
|
|
1178
|
+
candidates += [checkout / '.orbit-build/cargo-home' / folder for folder in ('registry', 'git')]
|
|
1179
|
+
candidates += [checkout / directory / folder for directory in directories for folder in ('node_modules', 'target')]
|
|
1180
|
+
for candidate in candidates:
|
|
1181
|
+
relative = str(candidate.relative_to(root))
|
|
1182
|
+
local = str(candidate.relative_to(checkout))
|
|
1183
|
+
if (any(file == local or file.startswith(local + '/') for file in tracked if file)
|
|
1184
|
+
or any(output == relative or output.startswith(relative + '/') or relative.startswith(output + '/') for output in outputs)):
|
|
1185
|
+
continue
|
|
1186
|
+
caches.append(tuple(candidate.relative_to(root).parts))
|
|
1187
|
+
return caches
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def workspace_metadata(root, excluded=()):
|
|
1191
|
+
device = os.fstat(root).st_dev
|
|
1192
|
+
def visit(directory, prefix):
|
|
1193
|
+
for entry in os.listdir(directory):
|
|
1194
|
+
parts = (*prefix, entry)
|
|
1195
|
+
if any(parts[:len(path)] == path for path in excluded):
|
|
1196
|
+
continue
|
|
1197
|
+
info = os.stat(entry, dir_fd=directory, follow_symlinks=False)
|
|
1198
|
+
if info.st_dev != device:
|
|
1199
|
+
raise ValueError('Recovery source crosses a filesystem.')
|
|
1200
|
+
yield parts, info
|
|
1201
|
+
if stat.S_ISDIR(info.st_mode):
|
|
1202
|
+
child = os.open(entry, DIR_FLAGS, dir_fd=directory)
|
|
1203
|
+
try:
|
|
1204
|
+
yield from visit(child, parts)
|
|
1205
|
+
finally:
|
|
1206
|
+
os.close(child)
|
|
1207
|
+
yield from visit(root, ())
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def source_links(root, excluded):
|
|
1211
|
+
shared = {(info.st_dev, info.st_ino): [file_identity(info), 0]
|
|
1212
|
+
for _, info in workspace_metadata(root, excluded)
|
|
1213
|
+
if stat.S_ISREG(info.st_mode) and info.st_nlink > 1}
|
|
1214
|
+
if shared:
|
|
1215
|
+
for _, info in workspace_metadata(root):
|
|
1216
|
+
entry = shared.get((info.st_dev, info.st_ino))
|
|
1217
|
+
if entry is not None:
|
|
1218
|
+
entry[1] += 1
|
|
1219
|
+
if any(identity[3] != count for identity, count in shared.values()):
|
|
1220
|
+
raise ValueError('Source has a file name outside this workspace; originals are kept.')
|
|
1221
|
+
return {key: identity for key, (identity, _) in shared.items()}
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def save_recovery(root, caches, account, name, outputs):
|
|
1225
|
+
import tarfile
|
|
1226
|
+
try:
|
|
1227
|
+
os.mkdir('results', 0o700, dir_fd=root)
|
|
1228
|
+
except FileExistsError:
|
|
1229
|
+
pass
|
|
1230
|
+
results = os.open('results', DIR_FLAGS, dir_fd=root)
|
|
1231
|
+
os.fchown(results, account.pw_uid, account.pw_gid)
|
|
1232
|
+
device = os.fstat(root).st_dev
|
|
1233
|
+
declared = {tuple(output.split('/')) for output in outputs}
|
|
1234
|
+
pending = '.storage-recovery-' + secrets.token_hex(16)
|
|
1235
|
+
shared = source_links(root, [*caches, *declared])
|
|
1236
|
+
descriptor = None
|
|
1237
|
+
try:
|
|
1238
|
+
if os.fstat(results).st_dev != device:
|
|
1239
|
+
raise ValueError('Recovery output crosses a filesystem.')
|
|
1240
|
+
descriptor = os.open(pending, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=results)
|
|
1241
|
+
os.fchown(descriptor, account.pw_uid, account.pw_gid)
|
|
1242
|
+
with os.fdopen(os.dup(descriptor), 'wb') as stream:
|
|
1243
|
+
with tarfile.open(fileobj=stream, mode='w|gz') as archive:
|
|
1244
|
+
def visit(directory, prefix):
|
|
1245
|
+
for entry in sorted(os.listdir(directory)):
|
|
1246
|
+
parts = (*prefix, entry)
|
|
1247
|
+
if (any(parts[:len(cache)] == cache for cache in caches)
|
|
1248
|
+
or parts == ('results', pending) or parts in declared):
|
|
1249
|
+
continue
|
|
1250
|
+
info = os.stat(entry, dir_fd=directory, follow_symlinks=False)
|
|
1251
|
+
if info.st_dev != device:
|
|
1252
|
+
raise ValueError('Recovery source crosses a filesystem.')
|
|
1253
|
+
member = tarfile.TarInfo('/'.join(parts))
|
|
1254
|
+
member.mode = info.st_mode & 0o777
|
|
1255
|
+
member.mtime = info.st_mtime
|
|
1256
|
+
if stat.S_ISDIR(info.st_mode):
|
|
1257
|
+
member.type = tarfile.DIRTYPE
|
|
1258
|
+
archive.addfile(member)
|
|
1259
|
+
child = os.open(entry, DIR_FLAGS, dir_fd=directory)
|
|
1260
|
+
try:
|
|
1261
|
+
visit(child, parts)
|
|
1262
|
+
finally:
|
|
1263
|
+
os.close(child)
|
|
1264
|
+
elif stat.S_ISLNK(info.st_mode):
|
|
1265
|
+
member.type = tarfile.SYMTYPE
|
|
1266
|
+
member.linkname = os.readlink(entry, dir_fd=directory)
|
|
1267
|
+
archive.addfile(member)
|
|
1268
|
+
elif stat.S_ISREG(info.st_mode):
|
|
1269
|
+
if info.st_nlink > 1 and shared.get((info.st_dev, info.st_ino)) != file_identity(info):
|
|
1270
|
+
raise ValueError('Source links changed; originals are kept.')
|
|
1271
|
+
fd = os.open(entry, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory)
|
|
1272
|
+
with os.fdopen(fd, 'rb') as source:
|
|
1273
|
+
if file_identity(os.fstat(source.fileno())) != file_identity(info):
|
|
1274
|
+
raise ValueError('Recovery source changed; originals are kept.')
|
|
1275
|
+
member.size = info.st_size
|
|
1276
|
+
archive.addfile(member, source)
|
|
1277
|
+
if file_identity(os.fstat(source.fileno())) != file_identity(info):
|
|
1278
|
+
raise ValueError('Recovery source changed; originals are kept.')
|
|
1279
|
+
else:
|
|
1280
|
+
raise ValueError('Recovery source is not a private file; originals are kept.')
|
|
1281
|
+
visit(root, ())
|
|
1282
|
+
stream.flush()
|
|
1283
|
+
os.fsync(stream.fileno())
|
|
1284
|
+
os.link(pending, Path(name).name, src_dir_fd=results, dst_dir_fd=results, follow_symlinks=False)
|
|
1285
|
+
os.unlink(pending, dir_fd=results)
|
|
1286
|
+
os.fsync(results)
|
|
1287
|
+
finally:
|
|
1288
|
+
if descriptor is not None:
|
|
1289
|
+
os.close(descriptor)
|
|
1290
|
+
try:
|
|
1291
|
+
os.unlink(pending, dir_fd=results)
|
|
1292
|
+
except FileNotFoundError:
|
|
1293
|
+
pass
|
|
1294
|
+
os.close(results)
|
|
1295
|
+
|
|
1296
|
+
|
|
1089
1297
|
def available(fd):
|
|
1090
1298
|
disk = os.fstatvfs(fd)
|
|
1091
1299
|
return disk.f_bavail * disk.f_frsize
|
|
@@ -1377,7 +1585,7 @@ GIT_SAFE = ('-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', '-c'
|
|
|
1377
1585
|
'-c', 'diff.external=', '-c', 'submodule.recurse=false')
|
|
1378
1586
|
|
|
1379
1587
|
|
|
1380
|
-
def git_checkouts(root, deadline):
|
|
1588
|
+
def git_checkouts(root, deadline, include_build=False):
|
|
1381
1589
|
def refuse(error):
|
|
1382
1590
|
raise error
|
|
1383
1591
|
|
|
@@ -1386,20 +1594,20 @@ def git_checkouts(root, deadline):
|
|
|
1386
1594
|
if time.monotonic() > deadline:
|
|
1387
1595
|
raise ValueError('Searching the work folder for Git checkouts took too long.')
|
|
1388
1596
|
here = Path(directory)
|
|
1389
|
-
checkout = '.git' in folders or '.git' in files
|
|
1597
|
+
checkout = '.git' in folders or '.git' in files or (include_build and {'HEAD', 'objects', 'config'}.issubset(set(folders) | set(files)))
|
|
1390
1598
|
if '.git' in folders and not (here / '.git').is_symlink():
|
|
1391
1599
|
with os.scandir(here / '.git') as entries:
|
|
1392
1600
|
checkout = next(entries, None) is not None
|
|
1393
1601
|
if checkout:
|
|
1394
1602
|
found.append(here)
|
|
1395
|
-
if len(here.relative_to(root).parts) >= UNPUSHED_DEPTH:
|
|
1603
|
+
if not include_build and len(here.relative_to(root).parts) >= UNPUSHED_DEPTH:
|
|
1396
1604
|
folders[:] = []
|
|
1397
1605
|
else:
|
|
1398
|
-
folders[:] = sorted(name for name in folders if name not in UNPUSHED_SKIPPED)
|
|
1606
|
+
folders[:] = sorted(name for name in folders if name not in UNPUSHED_SKIPPED or (include_build and name == '.orbit-build'))
|
|
1399
1607
|
return found
|
|
1400
1608
|
|
|
1401
1609
|
|
|
1402
|
-
def git_output(account, checkout, name, *args, allowed=(0,), stdin=''):
|
|
1610
|
+
def git_output(account, checkout, name, *args, allowed=(0,), stdin='', offline=False):
|
|
1403
1611
|
import shutil
|
|
1404
1612
|
import subprocess
|
|
1405
1613
|
program = shutil.which('git', path=GIT_PATH)
|
|
@@ -1407,6 +1615,8 @@ def git_output(account, checkout, name, *args, allowed=(0,), stdin=''):
|
|
|
1407
1615
|
raise ValueError('Git is not installed, so unpushed work cannot be checked.')
|
|
1408
1616
|
environment = dict(PATH=GIT_PATH, HOME=account.pw_dir, LC_ALL='C', GIT_CONFIG_NOSYSTEM='1',
|
|
1409
1617
|
GIT_CONFIG_GLOBAL='/dev/null', GIT_OPTIONAL_LOCKS='0', GIT_TERMINAL_PROMPT='0')
|
|
1618
|
+
if offline:
|
|
1619
|
+
environment.update(GIT_NO_LAZY_FETCH='1', GIT_ALLOW_PROTOCOL='', GIT_PAGER='cat')
|
|
1410
1620
|
try:
|
|
1411
1621
|
result = subprocess.run([program, '-C', str(checkout), *GIT_SAFE, *args], cwd='/', env=environment, input=stdin,
|
|
1412
1622
|
capture_output=True, text=True, errors='replace', timeout=GIT_SECONDS)
|
|
@@ -1449,10 +1659,10 @@ def unpushed_checkout(account, checkout, name):
|
|
|
1449
1659
|
return dict(branches=branches, detached=detached, uncommitted=len(changed), untracked=len(untracked), stashes=stashes)
|
|
1450
1660
|
|
|
1451
1661
|
|
|
1452
|
-
def search_unpushed(root, account, seconds):
|
|
1662
|
+
def search_unpushed(root, account, seconds, include_build=False):
|
|
1453
1663
|
deadline = time.monotonic() + seconds
|
|
1454
1664
|
found = []
|
|
1455
|
-
for checkout in git_checkouts(root, deadline):
|
|
1665
|
+
for checkout in git_checkouts(root, deadline, include_build=include_build):
|
|
1456
1666
|
if time.monotonic() > deadline:
|
|
1457
1667
|
raise ValueError('Checking the work folder for unpushed commits took too long.')
|
|
1458
1668
|
name = str(checkout.relative_to(root))
|
|
@@ -1641,6 +1851,10 @@ def managed_operation(store, account, request):
|
|
|
1641
1851
|
os.close(fd)
|
|
1642
1852
|
elif request['operation'] == 'unpushed':
|
|
1643
1853
|
print(json.dumps(store.unpushed(request['id'], account)))
|
|
1854
|
+
elif request['operation'] == 'save-clean':
|
|
1855
|
+
if set(request) - {'operation', 'id', 'user', 'data'}:
|
|
1856
|
+
raise ValueError('Saving and cleanup accept no command or build arguments.')
|
|
1857
|
+
print(json.dumps(store.save_and_clean(request['id'], account)))
|
|
1644
1858
|
elif request['operation'] == 'retire':
|
|
1645
1859
|
store.retire(request['id'])
|
|
1646
1860
|
elif request['operation'] == 'park':
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
9b5d54c9c031f64c8ca64213bd2c5907735b0da9
|