Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,6 @@ jobs:
${{ steps.info.outputs.CHANGELOG }}

### 🔧 Assets
- **Installer:** [GenHub-win-Setup.exe](https://github.com/${{ github.repository }}/releases/download/v${{ steps.info.outputs.VERSION }}/GenHub-win-Setup.exe)
- **Installer:** [GenHub-win-Setup.exe](https://github.com/${{ github.repository }}/releases/download/v${{ steps.info.outputs.VERSION }}/GenHub-win-Setup.exe) *(supports custom install directory via `--installto <path>` / `-t <path>`)*
- **Portable:** [GenHub-${{ steps.info.outputs.VERSION }}-win-portable.zip](https://github.com/${{ github.repository }}/releases/download/v${{ steps.info.outputs.VERSION }}/GenHub-${{ steps.info.outputs.VERSION }}-win-portable.zip)
files: final-assets/*
5 changes: 5 additions & 0 deletions GenHub/GenHub.Core/Constants/SettingsConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public static class SettingsConstants
/// </summary>
public const string SectionDataDirectories = "data-directories";

/// <summary>
/// Section ID for Migrate Installation.
/// </summary>
public const string SectionMigrateInstallation = "migrate-installation";

/// <summary>
/// Section ID for Logs.
/// </summary>
Expand Down
52 changes: 52 additions & 0 deletions GenHub/GenHub.Core/Constants/StorageMigrationConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace GenHub.Core.Constants;

/// <summary>
/// Constants for the storage and installation migration feature.
/// </summary>
public static class StorageMigrationConstants
{
/// <summary>
/// Update script resource name for Windows.
/// </summary>
public const string WindowsUpdateScriptName = "update_genhub.ps1";

/// <summary>
/// Update script resource name for Linux.
/// </summary>
public const string LinuxUpdateScriptName = "update_genhub.sh";

/// <summary>
/// Safety margin in bytes added to disk space calculations during migration preflight (50 MB).
/// </summary>
public const long DiskSpaceSafetyMarginBytes = 50 * 1024 * 1024;

/// <summary>
/// Preflight stage name.
/// </summary>
public const string StagePreflight = "Preflight Validation";

/// <summary>
/// Staging data stage name.
/// </summary>
public const string StageStagingData = "Relocating Application Data";

/// <summary>
/// Relocating CAS storage and workspace stage name.
/// </summary>
public const string StageRelocatingStorage = "Relocating CAS and Workspaces";

/// <summary>
/// Preparing binary migration stage name.
/// </summary>
public const string StagePreparingBinaries = "Preparing Binary Migration";

/// <summary>
/// Launching migration assistant stage name.
/// </summary>
public const string StageLaunchingAssistant = "Launching Migration Assistant";

/// <summary>
/// Finalizing stage name.
/// </summary>
public const string StageFinalizing = "Finalizing Migration";
}
37 changes: 37 additions & 0 deletions GenHub/GenHub.Core/Interfaces/Storage/IStorageMigrationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using GenHub.Core.Models.Results;
using GenHub.Core.Models.Storage;

namespace GenHub.Core.Interfaces.Storage;

/// <summary>
/// Provides backend operations for validating and migrating the GenHub installation and its storage locations.
/// </summary>
public interface IStorageMigrationService
{
/// <summary>
/// Performs pre-flight checks before an installation migration is executed.
/// </summary>
/// <param name="targetPath">The destination directory where GenHub should be relocated.</param>
/// <param name="relocateCasAndWorkspace">Whether the user also intends to relocate CAS storage and workspaces.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>An operation result containing the pre-flight check outcomes.</returns>
Task<OperationResult<StorageMigrationPreflightResult>> ValidatePreflightAsync(
string targetPath,
bool relocateCasAndWorkspace,
CancellationToken cancellationToken = default);

/// <summary>
/// Executes the installation migration, moving data, staging updates, and relaunching the application from the new path.
/// </summary>
/// <param name="request">The migration configuration request.</param>
/// <param name="progress">An optional progress reporter for tracking migration stages.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>An operation result indicating whether the migration was initiated successfully.</returns>
Task<OperationResult<bool>> MigrateAsync(
StorageMigrationRequest request,
IProgress<StorageMigrationProgress>? progress = null,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Collections.Generic;

namespace GenHub.Core.Models.Storage;

/// <summary>
/// Represents the outcome of pre-flight validation checks before an installation migration is executed.
/// </summary>
public class StorageMigrationPreflightResult
{
/// <summary>
/// Gets or sets a value indicating whether all pre-flight checks passed and migration is safe to proceed.
/// </summary>
public bool IsValid { get; set; }

/// <summary>
/// Gets or sets the estimated disk space required for the migration in bytes.
/// </summary>
public long RequiredBytes { get; set; }

/// <summary>
/// Gets or sets the available free disk space on the target volume in bytes.
/// </summary>
public long AvailableBytes { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the target drive has sufficient free space for the migration.
/// </summary>
public bool HasSufficientSpace { get; set; }

/// <summary>
/// Gets or sets a value indicating whether GenHub has write permissions in the target location.
/// </summary>
public bool HasWritePermission { get; set; }

/// <summary>
/// Gets or sets a value indicating whether active game instances or locking processes are currently running.
/// </summary>
public bool HasActiveProcesses { get; set; }

/// <summary>
/// Gets or sets the list of active process names or launch descriptions detected during pre-flight.
/// </summary>
public IReadOnlyList<string> ActiveProcessNames { get; set; } = [];

/// <summary>
/// Gets or sets a value indicating whether the selected target path is inside the current application directory.
/// </summary>
public bool IsTargetInsideApplicationDirectory { get; set; }

/// <summary>
/// Gets or sets the detailed error or warning message if pre-flight checks failed.
/// </summary>
public string? ErrorMessage { get; set; }
}
22 changes: 22 additions & 0 deletions GenHub/GenHub.Core/Models/Storage/StorageMigrationProgress.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace GenHub.Core.Models.Storage;

/// <summary>
/// Represents progress updates during an installation and storage migration operation.
/// </summary>
public class StorageMigrationProgress
{
/// <summary>
/// Gets or sets the name of the current migration stage.
/// </summary>
public string Stage { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the progress completion percentage (0 - 100).
/// </summary>
public double Percentage { get; set; }

/// <summary>
/// Gets or sets a descriptive status message for the current operation.
/// </summary>
public string Message { get; set; } = string.Empty;
}
29 changes: 29 additions & 0 deletions GenHub/GenHub.Core/Models/Storage/StorageMigrationRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace GenHub.Core.Models.Storage;

/// <summary>
/// Represents a request to migrate the GenHub installation and its storage components to a new target directory.
/// </summary>
public class StorageMigrationRequest
{
/// <summary>
/// Gets or sets the target installation directory path.
/// </summary>
public string TargetPath { get; set; } = string.Empty;

/// <summary>
/// Gets or sets a value indicating whether to also relocate the CAS storage pool and workspaces to the new target directory.
/// </summary>
public bool RelocateCasAndWorkspace { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to exit the application upon successfully launching the migration assistant.
/// Default is <see langword="true"/> in production; can be set to <see langword="false"/> for unit testing.
/// </summary>
public bool ExitApplicationOnSuccess { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether to start the detached helper updater process.
/// Default is <see langword="true"/> in production; can be set to <see langword="false"/> for unit testing.
/// </summary>
public bool LaunchHelperProcess { get; set; } = true;
}
45 changes: 23 additions & 22 deletions GenHub/GenHub.Linux/Resources/update_genhub.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/bin/bash

# Parameters are placeholders to be replaced by the application
LOG_FILE="{{LOG_FILE}}"
PROCESS_ID={{PROCESS_ID}}
SOURCE_DIR="{{SOURCE_DIR}}"
TARGET_DIR="{{TARGET_DIR}}"
CURRENT_EXE="{{CURRENT_EXE}}"
BACKUP_DIR="{{BACKUP_DIR}}"
# Arguments passed from application
PROCESS_ID="${1:-{{PROCESS_ID}}}"
SOURCE_DIR="${2:-{{SOURCE_DIR}}}"
TARGET_DIR="${3:-{{TARGET_DIR}}}"
CURRENT_EXE="${4:-{{CURRENT_EXE}}}"
LOG_FILE="${5:-{{LOG_FILE}}}"
BACKUP_DIR="${6:-{{BACKUP_DIR}}}"

write_log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
Expand All @@ -17,23 +17,23 @@ write_log "Waiting for main application (PID: $PROCESS_ID) to close..."

# Wait for the main process to exit
for i in {1..60}; do
if ! kill -0 $PROCESS_ID 2>/dev/null; then
if ! kill -0 "$PROCESS_ID" 2>/dev/null; then
write_log "Main process has exited"
break
fi
sleep 1
done

# Force terminate if still running
if kill -0 $PROCESS_ID 2>/dev/null; then
if kill -0 "$PROCESS_ID" 2>/dev/null; then
write_log "Timeout waiting for main process. Attempting to terminate..."
kill -TERM $PROCESS_ID 2>/dev/null
kill -TERM "$PROCESS_ID" 2>/dev/null
sleep 2
kill -KILL $PROCESS_ID 2>/dev/null
kill -KILL "$PROCESS_ID" 2>/dev/null
fi

write_log "Ensuring all GenHub processes are closed..."
+pkill -f "^$CURRENT_EXE\$" || true
pkill -f "^$CURRENT_EXE\$" || true
sleep 2

write_log "Starting file replacement..."
Expand All @@ -45,28 +45,29 @@ mkdir -p "$BACKUP_DIR"
# Backup existing files
write_log "Backing up existing files..."
if [ -d "$TARGET_DIR" ]; then
cp -r "$TARGET_DIR"/* "$BACKUP_DIR" 2>/dev/null || true
cp -a "$TARGET_DIR/." "$BACKUP_DIR/" 2>/dev/null || true
fi

# Copy new files
# Copy new files including hidden files
write_log "Copying new files from $SOURCE_DIR to $TARGET_DIR"
if ! cp -r "$SOURCE_DIR"/* "$TARGET_DIR" 2>&1; then
mkdir -p "$TARGET_DIR"
if ! cp -a "$SOURCE_DIR/." "$TARGET_DIR/" 2>&1; then
write_log "Error: Failed to copy update files"
# Attempt to restore backup
if [ -d "$BACKUP_DIR" ]; then
write_log "Attempting to restore backup..."
cp -r "$BACKUP_DIR"/* "$TARGET_DIR" 2>/dev/null || true
rm -rf "${TARGET_DIR:?}"/* "${TARGET_DIR:?}"/.[!.]* 2>/dev/null || true
cp -a "${BACKUP_DIR:?}/." "$TARGET_DIR/" 2>/dev/null || true
fi
exit 1
fi

# Start the updated application
write_log "Starting updated application: $CURRENT_EXE"
if [ -f "$CURRENT_EXE" ]; then
# Change to the executable's directory before running
EXE_DIR=$(dirname "$CURRENT_EXE")
EXE_NAME=$(basename "$CURRENT_EXE")
cd "$EXE_DIR"
cd "$EXE_DIR" || exit 1

if [ ! -x "$EXE_NAME" ]; then
chmod +x "$EXE_NAME"
Expand All @@ -78,13 +79,13 @@ else
write_log "Warning: Updated executable not found: $CURRENT_EXE"
fi

# Cleanup
write_log "Cleaning up..."
rm -rf "$SOURCE_DIR" 2>/dev/null || true
# Cleanup source directory only after verified successful copy
write_log "Cleaning up source directory..."
rm -rf "${SOURCE_DIR:?}" 2>/dev/null || true

# Self-destruct the updater script's parent directory
UPDATER_DIR=$(dirname "$0")
sleep 2
rm -rf "$UPDATER_DIR" 2>/dev/null || true
rm -rf "${UPDATER_DIR:?}" 2>/dev/null || true

write_log "Linux update script completed"
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using GenHub.Core.Constants;
using Xunit;

namespace GenHub.Tests.Core.Constants;

/// <summary>
/// Tests for <see cref="StorageMigrationConstants"/>.
/// </summary>
public class StorageMigrationConstantsTests
{
/// <summary>
/// Verifies that all storage migration constants have valid and expected values.
/// </summary>
[Fact]
public void StorageMigrationConstants_HaveExpectedValues()
{
Assert.Multiple(() =>
{
Assert.Equal("update_genhub.ps1", StorageMigrationConstants.WindowsUpdateScriptName);
Assert.Equal("update_genhub.sh", StorageMigrationConstants.LinuxUpdateScriptName);
Assert.True(StorageMigrationConstants.DiskSpaceSafetyMarginBytes > 0);
Assert.Equal("Preflight Validation", StorageMigrationConstants.StagePreflight);
Assert.Equal("Relocating Application Data", StorageMigrationConstants.StageStagingData);
Assert.Equal("Relocating CAS and Workspaces", StorageMigrationConstants.StageRelocatingStorage);
Assert.Equal("Preparing Binary Migration", StorageMigrationConstants.StagePreparingBinaries);
Assert.Equal("Launching Migration Assistant", StorageMigrationConstants.StageLaunchingAssistant);
Assert.Equal("Finalizing Migration", StorageMigrationConstants.StageFinalizing);
});
}

/// <summary>
/// Verifies that the safety margin is reasonable (at least 50MB).
/// </summary>
[Fact]
public void StorageMigrationConstants_SafetyMargin_IsReasonable()
{
Assert.True(StorageMigrationConstants.DiskSpaceSafetyMarginBytes >= 50 * 1024 * 1024L);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ private static (SettingsViewModel SettingsVm, Mock<IUserSettingsService> UserSet
var mockStorageLocationService = new Mock<IStorageLocationService>();
var mockUserDataTracker = new Mock<IUserDataTracker>();
var mockDialogService = new Mock<IDialogService>();
var mockStorageMigrationService = new Mock<IStorageMigrationService>();
var mockGitHubTokenStorage = new Mock<IGitHubTokenStorage>();

var settingsVm = new SettingsViewModel(
Expand All @@ -239,6 +240,7 @@ private static (SettingsViewModel SettingsVm, Mock<IUserSettingsService> UserSet
mockStorageLocationService.Object,
mockUserDataTracker.Object,
mockDialogService.Object,
mockStorageMigrationService.Object,
themeService: null,
gitHubTokenStorage: mockGitHubTokenStorage.Object);
return (settingsVm, mockUserSettings);
Expand Down
Loading
Loading