diff --git a/CLAUDE.md b/CLAUDE.md index 82f1f9299..924976566 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,7 +99,7 @@ samples/ # Example apps (C#, C++, Node.js, Python, Rust) ### Update Flow -1. **`VelopackApp.Run()`** executes at app startup. It handles fast-exit lifecycle hooks invoked by the update binary (`--veloapp-install`, `--veloapp-updated`, `--veloapp-obsolete`, `--veloapp-uninstall` — each with a version arg). These hooks have strict time limits (15-30s) and the process is killed if exceeded. It also auto-applies pending updates if a newer local package exists, fires `OnFirstRun`/`OnRestarted` callbacks based on environment variables (`VELOPACK_FIRSTRUN`, `VELOPACK_RESTART`), and cleans up old packages. +1. **`VelopackApp.Run()`** executes at app startup. It handles fast-exit lifecycle hooks invoked by the update binary (`--veloapp-install`, `--veloapp-updated`, `--veloapp-obsolete`, `--veloapp-uninstall` — each with a version arg). The install hook has a 30-second limit and the uninstall hook has a 60-second limit. On Windows, the before-update and after-update hooks default to 60 seconds; set `VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS` to an integer from 1 to 300 to override them. The process is killed when a hook exceeds its limit. `VelopackApp.Run()` also auto-applies pending updates if a newer local package exists, fires `OnFirstRun`/`OnRestarted` callbacks based on environment variables (`VELOPACK_FIRSTRUN`, `VELOPACK_RESTART`), and cleans up old packages. 2. **`UpdateManager.CheckForUpdatesAsync()`** queries an `IUpdateSource` for the remote release feed, compares against the installed version, and builds a delta strategy. It selects deltas if: they exist, there are ≤10 in the chain, and their total size < the full package size. Returns `UpdateInfo` with `TargetFullRelease` and `DeltasToTarget[]`. diff --git a/src/bins/src/commands/apply.rs b/src/bins/src/commands/apply.rs index cf259743c..c63c7444b 100644 --- a/src/bins/src/commands/apply.rs +++ b/src/bins/src/commands/apply.rs @@ -1,8 +1,16 @@ use crate::shared::{self, OperationWait}; use anyhow::{bail, Result}; use std::{ffi::OsString, path::PathBuf}; +#[cfg(any(target_os = "windows", test))] +use std::time::Duration; use velopack::{constants, locator, locator::VelopackLocator}; +pub const DEFAULT_UPDATE_HOOK_TIMEOUT_SECS: u64 = 60; +pub const MAX_UPDATE_HOOK_TIMEOUT_SECS: u64 = 5 * 60; +pub const UPDATE_HOOK_TIMEOUT_ENV: &str = "VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS"; +#[cfg(any(target_os = "windows", test))] +const ELEVATED_APPLY_NON_HOOK_BUDGET_SECS: u64 = 10 * 60; + #[cfg(target_os = "linux")] use super::apply_linux_impl::apply_package_impl; #[cfg(target_os = "macos")] @@ -15,10 +23,17 @@ pub enum HookRunMode { /// Do not run any hooks. None, /// Run all hooks (pre and post). - All, + All { timeout_secs: u64 }, /// Only run post-apply hooks (e.g. --veloapp-updated). Used during legacy migration /// where the obsolete hook is irrelevant but the app needs to know it was updated. - PostOnly, + PostOnly { timeout_secs: u64 }, +} + +#[cfg(any(target_os = "windows", test))] +pub(crate) fn elevated_apply_wait_timeout(child_hook_timeout_secs: u64) -> Duration { + // Preserve the previous ten-minute wait as the budget for extraction and apply work, + // then account for both hooks because the elevated child always enters apply as All. + Duration::from_secs(ELEVATED_APPLY_NON_HOOK_BUDGET_SECS.saturating_add(child_hook_timeout_secs.saturating_mul(2))) } pub fn apply( @@ -70,3 +85,81 @@ pub fn apply( } } } + +pub fn configured_update_hook_timeout_secs(cli_value: Option) -> u64 { + if let Some(value) = cli_value.filter(|value| is_valid_update_hook_timeout(*value)) { + return value; + } + + let env_value = std::env::var(UPDATE_HOOK_TIMEOUT_ENV).ok(); + resolve_update_hook_timeout_secs(None, env_value.as_deref()) +} + +fn resolve_update_hook_timeout_secs(cli_value: Option, env_value: Option<&str>) -> u64 { + if let Some(value) = cli_value.filter(|value| is_valid_update_hook_timeout(*value)) { + return value; + } + + if let Some(value) = env_value { + match value.parse::() { + Ok(value) if is_valid_update_hook_timeout(value) => return value, + _ => warn!( + "Ignoring invalid {} value {:?}; expected an integer from 1 to {} seconds.", + UPDATE_HOOK_TIMEOUT_ENV, value, MAX_UPDATE_HOOK_TIMEOUT_SECS + ), + } + } + + DEFAULT_UPDATE_HOOK_TIMEOUT_SECS +} + +fn is_valid_update_hook_timeout(value: u64) -> bool { + (1..=MAX_UPDATE_HOOK_TIMEOUT_SECS).contains(&value) +} + +#[cfg(test)] +mod tests { + use super::{ + elevated_apply_wait_timeout, resolve_update_hook_timeout_secs, DEFAULT_UPDATE_HOOK_TIMEOUT_SECS, MAX_UPDATE_HOOK_TIMEOUT_SECS, + }; + use std::time::Duration; + + #[test] + fn elevated_apply_wait_timeout_uses_default_child_hook_budget() { + let child_hook_timeout_secs = resolve_update_hook_timeout_secs(None, None); + + assert_eq!(child_hook_timeout_secs, DEFAULT_UPDATE_HOOK_TIMEOUT_SECS); + assert_eq!(elevated_apply_wait_timeout(child_hook_timeout_secs), Duration::from_secs(720)); + } + + #[test] + fn elevated_apply_wait_timeout_uses_configured_child_hook_budget() { + let child_hook_timeout_secs = resolve_update_hook_timeout_secs(None, Some("120")); + + assert_eq!(elevated_apply_wait_timeout(child_hook_timeout_secs), Duration::from_secs(840)); + assert_eq!(elevated_apply_wait_timeout(MAX_UPDATE_HOOK_TIMEOUT_SECS), Duration::from_secs(1200)); + } + + #[test] + fn update_hook_timeout_defaults_to_one_minute() { + assert_eq!(resolve_update_hook_timeout_secs(None, None), DEFAULT_UPDATE_HOOK_TIMEOUT_SECS); + assert_eq!(DEFAULT_UPDATE_HOOK_TIMEOUT_SECS, 60); + } + + #[test] + fn update_hook_timeout_accepts_environment_override() { + assert_eq!(resolve_update_hook_timeout_secs(None, Some("120")), 120); + } + + #[test] + fn update_hook_timeout_cli_override_takes_precedence() { + assert_eq!(resolve_update_hook_timeout_secs(Some(90), Some("120")), 90); + } + + #[test] + fn update_hook_timeout_rejects_zero_and_invalid_environment_values() { + assert_eq!(resolve_update_hook_timeout_secs(None, Some("0")), DEFAULT_UPDATE_HOOK_TIMEOUT_SECS); + assert_eq!(resolve_update_hook_timeout_secs(None, Some("301")), DEFAULT_UPDATE_HOOK_TIMEOUT_SECS); + assert_eq!(resolve_update_hook_timeout_secs(None, Some("not-a-number")), DEFAULT_UPDATE_HOOK_TIMEOUT_SECS); + } +} diff --git a/src/bins/src/commands/apply_windows_impl.rs b/src/bins/src/commands/apply_windows_impl.rs index 2effdf5ce..829107d7b 100644 --- a/src/bins/src/commands/apply_windows_impl.rs +++ b/src/bins/src/commands/apply_windows_impl.rs @@ -1,6 +1,6 @@ use crate::{dialogs, shared, windows}; use anyhow::{bail, Context, Result}; -use std::{ffi::OsString, fs, path::PathBuf, time::Duration}; +use std::{ffi::OsString, fs, path::PathBuf}; use velopack::{bundle::load_bundle_from_file, constants, locator::VelopackLocator, process}; // fn ropycopy, P2: AsRef>(source: &P1, dest: &P2) -> Result<()> { @@ -46,6 +46,10 @@ fn remove_temp_dir_timed(path: &PathBuf) { pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, hook_mode: super::HookRunMode) -> Result { let root_path = old_locator.get_root_dir(); + let update_hook_timeout_secs = match hook_mode { + super::HookRunMode::None => None, + super::HookRunMode::All { timeout_secs } | super::HookRunMode::PostOnly { timeout_secs } => Some(timeout_secs), + }; let mut bundle = load_bundle_from_file(package).map_err(|e| { warn!("Deleting package {:?} to prevent update loop: {}", package, e); @@ -66,7 +70,7 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, hook info!("Re-launching as administrator to update in {:?}", root_path); let packages_dir = old_locator.get_packages_dir(); - let args: Vec = vec![ + let mut args: Vec = vec![ "apply".into(), "--norestart".into(), "--package".into(), @@ -76,21 +80,30 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, hook "--packageDir".into(), packages_dir.into(), ]; + let elevated_child_hook_timeout_secs = + update_hook_timeout_secs.unwrap_or_else(|| super::configured_update_hook_timeout_secs(None)); + args.push("--hookTimeoutSeconds".into()); + args.push(elevated_child_hook_timeout_secs.to_string().into()); let exe_path = std::env::current_exe()?; let work_dir: Option = None; // same as this process // NB: show_window must be true for dialogs to be shown // https://learn.microsoft.com/en-us/windows/win32/api/commctrl/nf-commctrl-taskdialogindirect#remarks let process_handle = process::run_process_as_admin(&exe_path, args, work_dir, true)?; + let elevated_wait_timeout = super::elevated_apply_wait_timeout(elevated_child_hook_timeout_secs); info!( - "Waiting (up to 10 minutes) for elevated process (pid: {}) to exit...", + "Waiting (up to {} seconds) for elevated process (pid: {}) to exit...", + elevated_wait_timeout.as_secs(), process_handle.pid() ); - let result = process::wait_for_process_to_exit(process_handle, Some(Duration::from_secs(10 * 60)))?; + let result = process::wait_for_process_to_exit(process_handle, Some(elevated_wait_timeout))?; match result { process::WaitResult::WaitTimeout => { - bail!("Elevated process has not exited within 10 minutes. (TIMEOUT)"); + bail!( + "Elevated process has not exited within {} seconds. (TIMEOUT)", + elevated_wait_timeout.as_secs() + ); } process::WaitResult::ExitCode(code) => { if code != 0 { @@ -149,8 +162,8 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, hook reporter.set_indeterminate(); // second, run application hooks (but don't care if it fails) - if hook_mode == super::HookRunMode::All { - crate::windows::run_hook(old_locator, constants::HOOK_CLI_OBSOLETE, 15); + if let super::HookRunMode::All { timeout_secs } = hook_mode { + crate::windows::run_hook(old_locator, constants::HOOK_CLI_OBSOLETE, timeout_secs); } else { info!("Skipping --veloapp-obsolete hook."); } @@ -215,10 +228,11 @@ pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, hook } // seventh, we run the post-install hooks - if hook_mode == super::HookRunMode::All || hook_mode == super::HookRunMode::PostOnly { - crate::windows::run_hook(&new_locator, constants::HOOK_CLI_UPDATED, 15); - } else { - info!("Skipping --veloapp-updated hook."); + match hook_mode { + super::HookRunMode::All { timeout_secs } | super::HookRunMode::PostOnly { timeout_secs } => { + crate::windows::run_hook(&new_locator, constants::HOOK_CLI_UPDATED, timeout_secs); + } + super::HookRunMode::None => info!("Skipping --veloapp-updated hook."), } // update application shortcuts diff --git a/src/bins/src/commands/start_windows_impl.rs b/src/bins/src/commands/start_windows_impl.rs index ffb21eca6..168472d42 100644 --- a/src/bins/src/commands/start_windows_impl.rs +++ b/src/bins/src/commands/start_windows_impl.rs @@ -169,7 +169,17 @@ fn try_legacy_migration(root_dir: &PathBuf, manifest: &Manifest) -> Result Command { .arg(arg!(-w --wait "Wait for the parent process to terminate before applying the update").hide(true)) .arg(arg!(--waitPid "Wait for the specified process to terminate before applying the update").value_parser(value_parser!(u32))) .arg(arg!(-p --package "Update package to apply").value_parser(value_parser!(PathBuf))) + .arg(arg!(--hookTimeoutSeconds "Maximum time allowed for before-update and after-update hooks").value_parser(value_parser!(u64).range(1..=commands::MAX_UPDATE_HOOK_TIMEOUT_SECS))) .arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..).value_parser(value_parser!(OsString))) ) .subcommand(Command::new("start") @@ -258,27 +259,38 @@ fn get_exe_args(matches: &ArgMatches) -> Option> { matches.get_many::("EXE_ARGS").map(|v| v.map(|f| f.to_os_string()).collect()) } -fn get_apply_args(matches: &ArgMatches) -> (OperationWait, bool, Option<&PathBuf>, Option>) { +fn get_apply_args(matches: &ArgMatches) -> (OperationWait, bool, Option<&PathBuf>, Option>, u64) { let restart = !get_flag_or_false(matches, "norestart"); let package = matches.get_one::("package"); let exe_args = get_exe_args(matches); let wait = get_op_wait(matches); - (wait, restart, package, exe_args) + let hook_timeout_secs = commands::configured_update_hook_timeout_secs(matches.get_one::("hookTimeoutSeconds").copied()); + (wait, restart, package, exe_args, hook_timeout_secs) } fn apply(context: LocationContext, matches: &ArgMatches) -> Result<()> { - let (wait, restart, package, exe_args) = get_apply_args(matches); + let (wait, restart, package, exe_args, hook_timeout_secs) = get_apply_args(matches); info!("Command: Apply"); info!(" Restart: {:?}", restart); info!(" Wait: {:?}", wait); info!(" Package: {:?}", package); info!(" Exe Args: {:?}", exe_args); + info!(" Update Hook Timeout: {}s", hook_timeout_secs); let locator = auto_locate_app_manifest(context)?; // Note: lock is NOT acquired here. It's acquired inside apply_package_impl // AFTER the self-elevation check, to avoid deadlock when the non-elevated // parent spawns an elevated child (both would try to lock the same file). - let _ = commands::apply(&locator, restart, wait, package, exe_args, commands::HookRunMode::All)?; + let _ = commands::apply( + &locator, + restart, + wait, + package, + exe_args, + commands::HookRunMode::All { + timeout_secs: hook_timeout_secs, + }, + )?; Ok(()) } @@ -363,7 +375,7 @@ fn test_cli_parse_handles_equals_spaces() { "C:\\Some Path\\With = Spaces\\Package.zip", ]; let matches = try_parse_command_line_matches(command.iter().map(|s| s.to_string()).collect()).unwrap(); - let (wait, restart, package, exe_args) = get_apply_args(matches.subcommand_matches("apply").unwrap()); + let (wait, restart, package, exe_args, _) = get_apply_args(matches.subcommand_matches("apply").unwrap()); assert_eq!(wait, OperationWait::NoWait); assert_eq!(restart, true); @@ -371,6 +383,16 @@ fn test_cli_parse_handles_equals_spaces() { assert_eq!(exe_args, None); } +#[test] +fn test_apply_cli_accepts_update_hook_timeout_override() { + let matches = root_command() + .try_get_matches_from(["Update", "apply", "--hookTimeoutSeconds", "90"]) + .unwrap(); + let (_, _, _, _, hook_timeout_secs) = get_apply_args(matches.subcommand_matches("apply").unwrap()); + + assert_eq!(hook_timeout_secs, 90); +} + #[cfg(target_os = "windows")] #[test] fn test_cli_handles_root_dir_at_end() { diff --git a/src/lib-cpp/include/Velopack.h b/src/lib-cpp/include/Velopack.h index 00b561532..b6c65de1d 100644 --- a/src/lib-cpp/include/Velopack.h +++ b/src/lib-cpp/include/Velopack.h @@ -554,7 +554,8 @@ void vpkc_app_set_hook_before_uninstall(vpkc_hook_callback_t cb_before_uninstall * Sets a callback to be run before the app is updated. * WARNING: FastCallback hooks are run during critical stages of Velopack operations. * Your code will be run and then the process will exit. - * If your code has not completed within 30 seconds, it will be terminated. + * If your code has not completed within 60 seconds by default, it will be terminated. + * Set `VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS` to an integer from 1 through 300, inclusive, to override this timeout. * Only supported on windows; On other operating systems, this will never be called. * @param cb_before_update The callback to run before the app is updated. The callback takes a user data pointer and the version of the app as a string. */ @@ -564,7 +565,8 @@ void vpkc_app_set_hook_before_update(vpkc_hook_callback_t cb_before_update); * Sets a callback to be run after the app is updated. * WARNING: FastCallback hooks are run during critical stages of Velopack operations. * Your code will be run and then the process will exit. - * If your code has not completed within 30 seconds, it will be terminated. + * If your code has not completed within 60 seconds by default, it will be terminated. + * Set `VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS` to an integer from 1 through 300, inclusive, to override this timeout. * Only supported on windows; On other operating systems, this will never be called. * @param cb_after_update The callback to run after the app is updated. The callback takes a user data pointer and the version of the app as a string. */ diff --git a/src/lib-cpp/include/Velopack.hpp b/src/lib-cpp/include/Velopack.hpp index f0c0a0d27..148df1728 100644 --- a/src/lib-cpp/include/Velopack.hpp +++ b/src/lib-cpp/include/Velopack.hpp @@ -694,7 +694,8 @@ class VelopackApp { * This hook is triggered before the app is updated. * WARNING: This hook is run during critical stages of Velopack operations. * Your code will be run and then the process will exit. - * If your code has not completed within 30 seconds, it will be terminated. + * If your code has not completed within 60 seconds by default, it will be terminated. + * Set `VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS` to an integer from 1 through 300, inclusive, to override this timeout. * Only supported on windows; On other operating systems, this will never be called. * @param cbBeforeUpdate The callback to run before the app is updated. * @returns A reference to the builder. @@ -708,7 +709,8 @@ class VelopackApp { * This hook is triggered after the app is updated. * WARNING: This hook is run during critical stages of Velopack operations. * Your code will be run and then the process will exit. - * If your code has not completed within 30 seconds, it will be terminated. + * If your code has not completed within 60 seconds by default, it will be terminated. + * Set `VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS` to an integer from 1 through 300, inclusive, to override this timeout. * Only supported on windows; On other operating systems, this will never be called. * @param cbAfterUpdate The callback to run after the app is updated. * @returns A reference to the builder. diff --git a/src/lib-cpp/src/lib.rs b/src/lib-cpp/src/lib.rs index e0eeb5cce..0c93bd9c0 100644 --- a/src/lib-cpp/src/lib.rs +++ b/src/lib-cpp/src/lib.rs @@ -709,7 +709,8 @@ pub extern "C" fn vpkc_app_set_hook_before_uninstall(cb_before_uninstall: vpkc_h /// Sets a callback to be run before the app is updated. /// WARNING: FastCallback hooks are run during critical stages of Velopack operations. /// Your code will be run and then the process will exit. -/// If your code has not completed within 30 seconds, it will be terminated. +/// If your code has not completed within 60 seconds by default, it will be terminated. +/// Set `VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS` to an integer from 1 through 300, inclusive, to override this timeout. /// Only supported on windows; On other operating systems, this will never be called. /// @param cb_before_update The callback to run before the app is updated. The callback takes a user data pointer and the version of the app as a string. #[no_mangle] @@ -722,7 +723,8 @@ pub extern "C" fn vpkc_app_set_hook_before_update(cb_before_update: vpkc_hook_ca /// Sets a callback to be run after the app is updated. /// WARNING: FastCallback hooks are run during critical stages of Velopack operations. /// Your code will be run and then the process will exit. -/// If your code has not completed within 30 seconds, it will be terminated. +/// If your code has not completed within 60 seconds by default, it will be terminated. +/// Set `VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS` to an integer from 1 through 300, inclusive, to override this timeout. /// Only supported on windows; On other operating systems, this will never be called. /// @param cb_after_update The callback to run after the app is updated. The callback takes a user data pointer and the version of the app as a string. #[no_mangle] diff --git a/src/lib-csharp/VelopackApp.cs b/src/lib-csharp/VelopackApp.cs index 790aa172d..9056e0d1d 100644 --- a/src/lib-csharp/VelopackApp.cs +++ b/src/lib-csharp/VelopackApp.cs @@ -130,7 +130,8 @@ public VelopackApp OnAfterInstallFastCallback(VelopackHook hook) /// /// WARNING: FastCallback hooks are run during critical stages of Velopack operations. /// Your code will be run and then will be called. - /// If your code has not completed within 15 seconds, it will be terminated. + /// If your code has not completed within 60 seconds, it will be terminated. + /// Set VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS to an integer from 1 to 300 to override this timeout. /// Only supported on windows; On other operating systems, this will never be called. /// [SupportedOSPlatform("windows")] @@ -143,7 +144,8 @@ public VelopackApp OnAfterUpdateFastCallback(VelopackHook hook) /// /// WARNING: FastCallback hooks are run during critical stages of Velopack operations. /// Your code will be run and then will be called. - /// If your code has not completed within 15 seconds, it will be terminated. + /// If your code has not completed within 60 seconds, it will be terminated. + /// Set VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS to an integer from 1 to 300 to override this timeout. /// Only supported on windows; On other operating systems, this will never be called. /// [SupportedOSPlatform("windows")] @@ -286,4 +288,4 @@ public void Run() } } } -} \ No newline at end of file +} diff --git a/src/lib-nodejs/src/index.ts b/src/lib-nodejs/src/index.ts index cdbdb16ba..513d011ef 100644 --- a/src/lib-nodejs/src/index.ts +++ b/src/lib-nodejs/src/index.ts @@ -80,7 +80,8 @@ export class VelopackApp { /** * WARNING: FastCallback hooks are run during critical stages of Velopack operations. * Your code will be run and then the process will exit. - * If your code has not completed within 15 seconds, it will be terminated. + * If your code has not completed within 60 seconds, it will be terminated. + * Set VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS to an integer from 1 to 300 to override this timeout. * Only supported on windows; On other operating systems, this will never be called. */ onBeforeUpdateFastCallback(callback: VelopackHook): VelopackApp { @@ -91,7 +92,8 @@ export class VelopackApp { /** * WARNING: FastCallback hooks are run during critical stages of Velopack operations. * Your code will be run and then the process will exit. - * If your code has not completed within 15 seconds, it will be terminated. + * If your code has not completed within 60 seconds, it will be terminated. + * Set VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS to an integer from 1 to 300 to override this timeout. * Only supported on windows; On other operating systems, this will never be called. */ onAfterUpdateFastCallback(callback: VelopackHook): VelopackApp { diff --git a/src/lib-rust/src/app.rs b/src/lib-rust/src/app.rs index 427994d94..93d972cd1 100644 --- a/src/lib-rust/src/app.rs +++ b/src/lib-rust/src/app.rs @@ -92,7 +92,8 @@ impl<'a> VelopackApp<'a> { /// WARNING: FastCallback hooks are run during critical stages of Velopack operations. /// Your code will be run and then the process will exit. - /// If your code has not completed within 15 seconds, it will be terminated. + /// If your code has not completed within 60 seconds, it will be terminated. + /// Set VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS to an integer from 1 to 300 to override this timeout. /// Only supported on windows; On other operating systems, this will never be called. #[cfg(target_os = "windows")] pub fn on_after_update_fast_callback(mut self, hook: F) -> Self { @@ -102,7 +103,8 @@ impl<'a> VelopackApp<'a> { /// WARNING: FastCallback hooks are run during critical stages of Velopack operations. /// Your code will be run and then the process will exit. - /// If your code has not completed within 15 seconds, it will be terminated. + /// If your code has not completed within 60 seconds, it will be terminated. + /// Set VELOPACK_UPDATE_HOOK_TIMEOUT_SECONDS to an integer from 1 to 300 to override this timeout. /// Only supported on windows; On other operating systems, this will never be called. #[cfg(target_os = "windows")] pub fn on_before_update_fast_callback(mut self, hook: F) -> Self {