Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 154 additions & 26 deletions harmony/pushy/src/main/cpp/pushy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,47 @@ napi_value BuildCopyGroups(napi_env env, napi_callback_info info) {
return NewCopyGroupArray(env, groups);
}

// ---------------------------------------------------------------------------
// Async work plumbing for the heavy patch operations.
//
// applyPatchFromFileSource and cleanupOldEntries run hdiff / recursive file IO
// that can take hundreds of ms to seconds. The Pushy TurboModule executes on
// the UI thread, so running these synchronously froze the UI. These are now
// wrapped in napi_create_async_work: arguments are parsed on the JS thread, the
// heavy work runs on a libuv worker thread, and the returned Promise is settled
// back on the JS thread.
// ---------------------------------------------------------------------------

// Reject an already-created deferred with an Error(message). Used when async
// work fails to be created/queued, so the Promise never hangs pending.
void RejectDeferredWithMessage(
napi_env env,
napi_deferred deferred,
const char* message) {
napi_value error = nullptr;
napi_value message_value = nullptr;
napi_create_string_utf8(env, message, NAPI_AUTO_LENGTH, &message_value);
napi_create_error(env, nullptr, message_value, &error);
napi_reject_deferred(env, deferred, error);
}

struct ApplyPatchWork {
napi_async_work work = nullptr;
napi_deferred deferred = nullptr;
pushy::patch::FileSourcePatchOptions options;
pushy::patch::Status status{false, ""};
};

struct CleanupWork {
napi_async_work work = nullptr;
napi_deferred deferred = nullptr;
std::string root_dir;
std::string keep_current;
std::string keep_previous;
int32_t max_age_days = 0;
pushy::patch::Status status{false, ""};
};

napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
napi_value args[1] = {nullptr};
size_t argc = 1;
Expand Down Expand Up @@ -755,26 +796,69 @@ napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) {
return nullptr;
}

pushy::patch::FileSourcePatchOptions options;
options.manifest = BuildManifest(copy_froms, copy_tos, deletes);
options.source_root = source_root;
options.target_root = target_root;
options.origin_bundle_path = origin_bundle_path;
options.bundle_patch_path = bundle_patch_path;
options.bundle_output_path = bundle_output_path;
options.merge_source_subdir = merge_source_subdir;
options.enable_merge = enable_merge;
auto* work_data = new ApplyPatchWork();
work_data->options.manifest = BuildManifest(copy_froms, copy_tos, deletes);
work_data->options.source_root = source_root;
work_data->options.target_root = target_root;
work_data->options.origin_bundle_path = origin_bundle_path;
work_data->options.bundle_patch_path = bundle_patch_path;
work_data->options.bundle_output_path = bundle_output_path;
work_data->options.merge_source_subdir = merge_source_subdir;
work_data->options.enable_merge = enable_merge;

const pushy::patch::Status status =
pushy::patch::ApplyPatchFromFileSource(options);
if (!status.ok) {
ThrowError(env, status.message);
napi_value promise = nullptr;
if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
delete work_data;
ThrowError(env, "Unable to create promise");
return nullptr;
}

napi_value undefined_value = nullptr;
napi_get_undefined(env, &undefined_value);
return undefined_value;
napi_value resource_name = nullptr;
napi_create_string_utf8(
env, "applyPatchFromFileSource", NAPI_AUTO_LENGTH, &resource_name);
if (napi_create_async_work(
env,
nullptr,
resource_name,
[](napi_env, void* data) {
auto* w = static_cast<ApplyPatchWork*>(data);
w->status = pushy::patch::ApplyPatchFromFileSource(w->options);
},
[](napi_env cb_env, napi_status, void* data) {
auto* w = static_cast<ApplyPatchWork*>(data);
if (w->status.ok) {
napi_value undefined_value = nullptr;
napi_get_undefined(cb_env, &undefined_value);
napi_resolve_deferred(cb_env, w->deferred, undefined_value);
} else {
napi_value error = nullptr;
napi_value message = nullptr;
napi_create_string_utf8(
cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
napi_create_error(cb_env, nullptr, message, &error);
napi_reject_deferred(cb_env, w->deferred, error);
}
napi_delete_async_work(cb_env, w->work);
delete w;
},
work_data,
&work_data->work) != napi_ok) {
// Work was never created: settle the promise and free the data so it does
// not leak / hang pending forever.
RejectDeferredWithMessage(
env, work_data->deferred, "Unable to create async work");
delete work_data;
return promise;
}
if (napi_queue_async_work(env, work_data->work) != napi_ok) {
// Queued failed: the complete callback will never run, so clean up here.
napi_delete_async_work(env, work_data->work);
RejectDeferredWithMessage(
env, work_data->deferred, "Unable to queue async work");
delete work_data;
return promise;
}
return promise;
}

napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
Expand Down Expand Up @@ -803,19 +887,63 @@ napi_value CleanupOldEntries(napi_env env, napi_callback_info info) {
return nullptr;
}

const pushy::patch::Status status = pushy::patch::CleanupOldEntries(
root_dir,
keep_current,
keep_previous,
max_age_days);
if (!status.ok) {
ThrowError(env, status.message);
auto* work_data = new CleanupWork();
work_data->root_dir = root_dir;
work_data->keep_current = keep_current;
work_data->keep_previous = keep_previous;
work_data->max_age_days = max_age_days;

napi_value promise = nullptr;
if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) {
delete work_data;
ThrowError(env, "Unable to create promise");
return nullptr;
}

napi_value undefined_value = nullptr;
napi_get_undefined(env, &undefined_value);
return undefined_value;
napi_value resource_name = nullptr;
napi_create_string_utf8(
env, "cleanupOldEntries", NAPI_AUTO_LENGTH, &resource_name);
if (napi_create_async_work(
env,
nullptr,
resource_name,
[](napi_env, void* data) {
auto* w = static_cast<CleanupWork*>(data);
w->status = pushy::patch::CleanupOldEntries(
w->root_dir, w->keep_current, w->keep_previous, w->max_age_days);
},
[](napi_env cb_env, napi_status, void* data) {
auto* w = static_cast<CleanupWork*>(data);
if (w->status.ok) {
napi_value undefined_value = nullptr;
napi_get_undefined(cb_env, &undefined_value);
napi_resolve_deferred(cb_env, w->deferred, undefined_value);
} else {
napi_value error = nullptr;
napi_value message = nullptr;
napi_create_string_utf8(
cb_env, w->status.message.c_str(), NAPI_AUTO_LENGTH, &message);
napi_create_error(cb_env, nullptr, message, &error);
napi_reject_deferred(cb_env, w->deferred, error);
}
napi_delete_async_work(cb_env, w->work);
delete w;
},
work_data,
&work_data->work) != napi_ok) {
RejectDeferredWithMessage(
env, work_data->deferred, "Unable to create async work");
delete work_data;
return promise;
}
if (napi_queue_async_work(env, work_data->work) != napi_ok) {
napi_delete_async_work(env, work_data->work);
RejectDeferredWithMessage(
env, work_data->deferred, "Unable to queue async work");
delete work_data;
return promise;
}
return promise;
}

bool ExportFunction(
Expand Down
6 changes: 3 additions & 3 deletions harmony/pushy/src/main/ets/DownloadTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export class DownloadTask {
const originBundlePath = `${workingDirectory}/${TEMP_ORIGIN_BUNDLE_ENTRY}`;
try {
await this.writeFileContent(originBundlePath, originContent);
NativePatchCore.applyPatchFromFileSource({
await NativePatchCore.applyPatchFromFileSource({
copyFroms: [],
copyTos: [],
deletes: [],
Expand Down Expand Up @@ -568,7 +568,7 @@ export class DownloadTask {
manifestArrays.deletes,
HARMONY_BUNDLE_PATCH_ENTRY,
);
NativePatchCore.applyPatchFromFileSource({
await NativePatchCore.applyPatchFromFileSource({
copyFroms: manifestArrays.copyFroms,
copyTos: manifestArrays.copyTos,
deletes: manifestArrays.deletes,
Expand Down Expand Up @@ -664,7 +664,7 @@ export class DownloadTask {

private async doCleanUp(params: DownloadTaskParams): Promise<void> {
try {
NativePatchCore.cleanupOldEntries(
await NativePatchCore.cleanupOldEntries(
params.unzipDirectory,
params.hash || '',
params.originHash || '',
Expand Down
4 changes: 2 additions & 2 deletions harmony/pushy/src/main/ets/NativePatchCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ interface NativePatchCoreBindings {
bundlePatchEntryName?: string,
): ArchivePatchPlanResult;
buildCopyGroups(copyFroms: string[], copyTos: string[]): CopyGroupResult[];
applyPatchFromFileSource(options: FileSourcePatchRequest): void;
applyPatchFromFileSource(options: FileSourcePatchRequest): Promise<void>;
cleanupOldEntries(
rootDir: string,
keepCurrent: string,
keepPrevious: string,
maxAgeDays: number,
): void;
): Promise<void>;
}

export default NativeUpdateCore as unknown as NativePatchCoreBindings;
8 changes: 7 additions & 1 deletion harmony/pushy/src/main/ets/UpdateContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,12 +520,18 @@ export class UpdateContext {

public cleanUp(): void {
const state = this.getStateSnapshot();
// cleanupOldEntries now runs on a native worker thread (returns a Promise).
// Cleanup is best-effort background maintenance and no caller depends on its
// completion, so fire-and-forget it off the UI thread and just log failures
// instead of blocking the state operation (or cold start) on disk I/O.
NativePatchCore.cleanupOldEntries(
this.rootDir,
state.currentVersion || '',
state.lastVersion || '',
3,
);
).catch((error: Object) => {
console.error('cleanupOldEntries failed:', error);
});
}

public getIsUsingBundleUrl(): boolean {
Expand Down