├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── Directory.Build.props ├── Directory.Build.targets ├── HotAvalonia.sln ├── LICENSE.md ├── README.md ├── global.json ├── media ├── examples │ ├── hot_reload_app.gif │ ├── hot_reload_resources.gif │ ├── hot_reload_styles.gif │ ├── hot_reload_user_control.gif │ ├── hot_reload_view.gif │ └── hot_reload_window.gif └── icon.png ├── samples ├── Directory.Build.targets ├── HotReloadDemo.Android │ ├── HotReloadDemo.Android.csproj │ ├── MainActivity.cs │ ├── Properties │ │ └── AndroidManifest.xml │ └── Resources │ │ ├── drawable-night-v31 │ │ └── avalonia_anim.xml │ │ ├── drawable-v31 │ │ └── avalonia_anim.xml │ │ ├── drawable │ │ └── splash_screen.xml │ │ ├── values-night │ │ └── colors.xml │ │ ├── values-v31 │ │ └── styles.xml │ │ └── values │ │ ├── colors.xml │ │ └── styles.xml ├── HotReloadDemo.Desktop │ ├── HotReloadDemo.Desktop.csproj │ ├── Program.cs │ └── app.manifest └── HotReloadDemo │ ├── App.axaml │ ├── App.axaml.cs │ ├── Assets │ └── icon.ico │ ├── Controls │ ├── ToDoItemControl.axaml │ └── ToDoItemControl.axaml.cs │ ├── Converters │ └── TitleCaseConverter.cs │ ├── HotReloadDemo.csproj │ ├── Models │ └── ToDoItem.cs │ ├── Resources │ └── AppResources.axaml │ ├── Services │ ├── FakeToDoItemProvider.cs │ └── IToDoItemProvider.cs │ ├── Styles │ └── AppStyles.axaml │ ├── ViewLocator.cs │ ├── ViewModels │ ├── AddItemViewModel.cs │ ├── MainViewModel.cs │ ├── ToDoListViewModel.cs │ └── ViewModelBase.cs │ └── Views │ ├── AddItemView.axaml │ ├── AddItemView.axaml.cs │ ├── MainView.axaml │ ├── MainView.axaml.cs │ ├── MainWindow.axaml │ ├── MainWindow.axaml.cs │ ├── ToDoListView.axaml │ └── ToDoListView.axaml.cs └── src ├── HotAvalonia.Core ├── Assets │ ├── AssetInfo.cs │ ├── DynamicAsset.cs │ ├── DynamicAssetLoader.cs │ └── DynamicAssetTypeConverter.cs ├── AvaloniaAssetManager.cs ├── AvaloniaControlManager.cs ├── AvaloniaHotReload.cs ├── AvaloniaHotReloadContext.cs ├── AvaloniaProjectLocator.cs ├── BannedSymbols.txt ├── Collections │ ├── MemoryCache.cs │ └── WeakSet.cs ├── Compat │ └── System │ │ ├── BitConverter.cs │ │ └── IO │ │ └── StreamCompatExtensions.cs ├── DependencyInjection │ └── AvaloniaServiceProvider.cs ├── Helpers │ ├── AssemblyHelper.cs │ ├── ILGeneratorHelper.cs │ ├── LoggingHelper.cs │ ├── MethodHelper.cs │ ├── OpCodeHelper.cs │ ├── StopwatchHelper.cs │ ├── TypeHelper.cs │ └── UriHelper.cs ├── HotAvalonia.Core.csproj ├── HotReloadFeatures.cs ├── IHotReloadContext.cs ├── IO │ ├── CachingFileSystem.cs │ ├── EmptyFileSystem.cs │ ├── EmptyFileSystemWatcher.cs │ ├── FileObserver.cs │ ├── FileSystem.cs │ ├── FileSystemState.cs │ ├── FileWatcher.cs │ ├── IFileSystem.cs │ ├── IFileSystemWatcher.cs │ ├── LocalFileSystem.cs │ ├── LocalFileSystemWatcher.cs │ ├── RemoteFileSystem.Shared.cs │ ├── RemoteFileSystem.cs │ ├── RemoteFileSystemException.cs │ ├── RemoteFileSystemWatcher.Shared.cs │ └── RemoteFileSystemWatcher.cs ├── Net │ └── SslTcpClient.cs ├── README.md ├── Reflection │ ├── DynamicAssembly.cs │ ├── Inject │ │ ├── CallbackInjector.cs │ │ ├── CallbackParameterType.cs │ │ ├── CallbackResultAttribute.cs │ │ ├── CallerAttribute.cs │ │ ├── CallerMemberAttribute.cs │ │ ├── IInjection.cs │ │ ├── InjectionType.cs │ │ └── MethodInjector.cs │ └── MethodBodyReader.cs └── Xaml │ ├── CompileXamlFunc.cs │ ├── CompiledXamlDocument.cs │ ├── DynamicSreAssembly.cs │ ├── NamedControlReference.cs │ ├── XamlCompiler.cs │ ├── XamlDocument.cs │ ├── XamlPatcher.cs │ └── XamlScanner.cs ├── HotAvalonia.Extensions ├── AvaloniaHotReloadExtensions.cs ├── AvaloniaHotReloadExtensions.fs ├── AvaloniaHotReloadExtensions.vb ├── HotAvalonia.Extensions.csproj ├── HotAvalonia.Extensions.props └── README.md ├── HotAvalonia.Fody ├── Cecil │ ├── CecilField.cs │ ├── CecilMethod.cs │ ├── CecilProperty.cs │ ├── CecilType.cs │ ├── ITypeResolver.cs │ ├── TypeName.cs │ └── WeakType.cs ├── FeatureWeaver.cs ├── FileSystemCredentialsWeaver.cs ├── Helpers │ ├── MethodReferenceHelper.cs │ ├── ModuleReferenceHelper.cs │ ├── StringHelper.cs │ └── TypeReferenceHelper.cs ├── HotAvalonia.Fody.csproj ├── HotAvalonia.Fody.props ├── MSBuild │ ├── MSBuildFile.cs │ ├── MSBuildProject.cs │ └── MSBuildSolution.cs ├── ModuleWeaver.cs ├── PopulateOverrideWeaver.cs ├── README.md ├── ReferencesWeaver.cs ├── Reflection │ └── BindingFlag.cs ├── UnreferencedTypes.cs └── UseHotReloadWeaver.cs ├── HotAvalonia.Remote ├── HotAvalonia.Remote.csproj ├── IO │ ├── RemoteFileSystem.cs │ ├── RemoteFileSystemClient.cs │ └── RemoteFileSystemWatcher.cs ├── Net │ └── SslTcpListener.cs ├── Program.cs └── README.md └── HotAvalonia ├── FileSystemServerConfig.cs ├── GenerateFileSystemServerConfigTask.cs ├── GetFileSystemClientConfigTask.cs ├── Helpers └── NetworkHelper.cs ├── HotAvalonia.csproj ├── HotAvalonia.props ├── HotAvalonia.targets ├── MSBuildTask.cs └── StartFileSystemServerTask.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - development 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | env: 12 | DOTNET_CLI_TELEMETRY_OPTOUT: true 13 | DOTNET_NOLOGO: true 14 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 15 | DOTNET_GENERATE_ASPNET_CERTIFICATE: false 16 | DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: true 17 | NUGET_XMLDOC_MODE: skip 18 | BUILD_OUTPUT: ${{ github.workspace }}/dist 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Setup .NET SDK 27 | uses: actions/setup-dotnet@v4 28 | 29 | - name: Build (Debug) 30 | run: dotnet build -tl -c Debug 31 | 32 | - name: Build (Release) 33 | run: dotnet build -tl -c Release 34 | 35 | - name: Run tests 36 | run: dotnet test -tl -c Release --no-build 37 | 38 | - name: Create NuGet packages 39 | run: dotnet pack -tl -c Release --no-build -o "${{ env.BUILD_OUTPUT }}" 40 | 41 | - name: Upload build artifacts 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: build 45 | if-no-files-found: error 46 | path: ${{ env.BUILD_OUTPUT }}/*.nupkg 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | env: 9 | DOTNET_CLI_TELEMETRY_OPTOUT: true 10 | DOTNET_NOLOGO: true 11 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 12 | DOTNET_GENERATE_ASPNET_CERTIFICATE: false 13 | DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: true 14 | NUGET_XMLDOC_MODE: skip 15 | BUILD_OUTPUT: ${{ github.workspace }}/dist 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: write 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Setup .NET SDK 26 | uses: actions/setup-dotnet@v4 27 | 28 | - name: Build (Debug) 29 | run: dotnet build -tl -c Debug 30 | 31 | - name: Build (Release) 32 | run: dotnet build -tl -c Release 33 | 34 | - name: Run tests 35 | run: dotnet test -tl -c Release --no-build 36 | 37 | - name: Create NuGet packages 38 | run: dotnet pack -tl -c Release --no-build -o "${{ env.BUILD_OUTPUT }}" 39 | 40 | - name: Upload NuGets to GitHub 41 | uses: Kira-NT/mc-publish@v3.3 42 | with: 43 | files: ${{ env.BUILD_OUTPUT }}/*.nupkg 44 | github-token: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Upload NuGets to NuGet.org 47 | run: dotnet nuget push "${{ env.BUILD_OUTPUT }}/*.nupkg" --api-key "${{ secrets.NUGET_TOKEN }}" --source "https://api.nuget.org/v3/index.json" --skip-duplicate 48 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Launch (Demo)", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceFolder}/samples/HotReloadDemo.Desktop/bin/Debug/net9.0/HotReloadDemo.Desktop.dll", 10 | "env": { 11 | "HOTAVALONIA_STATICRESOURCEPATCHER": "true", 12 | "HOTAVALONIA_MERGERESOURCEINCLUDEPATCHER": "true", 13 | "HOTAVALONIA_SKIP_INITIAL_PATCHING": "false", 14 | "HOTAVALONIA_DISABLE_INJECTIONS": "false", 15 | "HOTAVALONIA_LOG_LEVEL_OVERRIDE": "error", 16 | }, 17 | "args": [], 18 | "cwd": "${workspaceFolder}/samples/HotReloadDemo.Desktop", 19 | "console": "internalConsole", 20 | "stopAtEntry": false, 21 | "logging": { 22 | "moduleLoad": false, 23 | } 24 | }, 25 | { 26 | "name": ".NET Launch (Android Demo)", 27 | "type": "coreclr", 28 | "request": "launch", 29 | "preLaunchTask": "build", 30 | "program": "dotnet", 31 | "args": [ 32 | "build", 33 | "${workspaceFolder}/samples/HotReloadDemo.Android", 34 | "/property:Configuration=Debug", 35 | "/property:GenerateFullPaths=true", 36 | "/consoleloggerparameters:NoSummary", 37 | "/property:AndroidSdkDirectory=${input:androidSdkDir}", 38 | "/property:JavaSdkDirectory=${input:javaSdkDir}", 39 | "/t:Run", 40 | ], 41 | "cwd": "${workspaceFolder}", 42 | "console": "integratedTerminal", 43 | }, 44 | { 45 | "name": ".NET Attach", 46 | "type": "coreclr", 47 | "request": "attach", 48 | }, 49 | ], 50 | "inputs": [ 51 | { 52 | "id": "androidSdkDir", 53 | "type": "promptString", 54 | "description": "Enter Android SDK Directory", 55 | "default": "${env:HOME}/.android", 56 | }, 57 | { 58 | "id": "javaSdkDir", 59 | "type": "promptString", 60 | "description": "Enter Java SDK Directory", 61 | "default": "${env:HOME}/.android/java", 62 | }, 63 | ], 64 | } 65 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "HotAvalonia.sln", 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/HotAvalonia.sln", 11 | "/property:Configuration=Debug", 12 | "/property:GenerateFullPaths=true", 13 | "/consoleloggerparameters:NoSummary", 14 | "-tl", 15 | ], 16 | "problemMatcher": "$msCompile", 17 | }, 18 | { 19 | "label": "prepare-android-environment", 20 | "dependsOrder": "sequence", 21 | "dependsOn": [ 22 | "workload-install-android", 23 | "install-android-sdk", 24 | ], 25 | }, 26 | { 27 | "label": "workload-install-android", 28 | "type": "process", 29 | "command": "dotnet", 30 | "args": [ 31 | "workload", 32 | "install", 33 | "android", 34 | ], 35 | "problemMatcher": "$msCompile", 36 | }, 37 | { 38 | "label": "install-android-sdk", 39 | "type": "process", 40 | "command": "dotnet", 41 | "args": [ 42 | "build", 43 | "${workspaceFolder}/samples/HotReloadDemo.Android", 44 | "/property:AndroidSdkDirectory=${input:androidSdkDir}", 45 | "/property:JavaSdkDirectory=${input:javaSdkDir}", 46 | "/property:AcceptAndroidSdkLicenses=True", 47 | "/t:InstallAndroidDependencies", 48 | ], 49 | "problemMatcher": "$msCompile", 50 | }, 51 | { 52 | "label": "start-android-emulator", 53 | "type": "shell", 54 | "command": "${input:androidSdkDir}/emulator/emulator -avd ${input:androidEmulatorName} -partition-size 512", 55 | "windows": { 56 | "command": "${input:androidSdkDir}/emulator/emulator.exe -avd ${input:androidEmulatorName} -partition-size 512", 57 | }, 58 | "problemMatcher": [], 59 | "isBackground": true, 60 | "presentation": { 61 | "echo": false, 62 | "reveal": "never", 63 | "focus": false, 64 | "panel": "shared", 65 | "showReuseMessage": false, 66 | "clear": true, 67 | } 68 | } 69 | ], 70 | "inputs": [ 71 | { 72 | "id": "androidSdkDir", 73 | "type": "promptString", 74 | "description": "Enter Android SDK Directory", 75 | "default": "${env:HOME}/.android", 76 | }, 77 | { 78 | "id": "javaSdkDir", 79 | "type": "promptString", 80 | "description": "Enter Java SDK Directory", 81 | "default": "${env:HOME}/.android/java", 82 | }, 83 | { 84 | "id": "androidEmulatorName", 85 | "type": "promptString", 86 | "description": "Enter the name of your Android emulator", 87 | "default": "Pixel", 88 | }, 89 | ], 90 | } 91 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [kira.canary@proton.me](mailto:kira.canary@proton.me). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | enable 5 | enable 6 | true 7 | preview 8 | true 9 | false 10 | $(DefineConstants)$(FeatureFlags.Replace("#",";")) 11 | 12 | 13 | 14 | 3.0.0 15 | Kira-NT 16 | 2023 17 | true 18 | avalonia avaloniaui hot-reload dynamic hot reload xaml axaml ui development tools net netstandard 19 | 20 | 21 | 22 | 11.0.0 23 | 6.9.2 24 | 25 | 26 | 27 | false 28 | $(Author) 29 | $(Author) 30 | Copyright © $(ReleaseYear)-$([System.DateTime]::Now.Year) $(Authors) 31 | Copyright © $(ReleaseYear) $(Authors) 32 | $([System.IO.Path]::GetFileNameWithoutExtension('$([System.IO.Directory]::GetFiles(`$(MSBuildThisFileDirectory)`, `*.sln`)[0])')) 33 | https://github.com/$(Author)/$(ProjectName) 34 | git 35 | $(RepositoryUrl) 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(DefineConstants);NATIVE_AOT 5 | 6 | 7 | 8 | $(Version)-build.$(GITHUB_RUN_NUMBER) 9 | 10 | 11 | 12 | README.md 13 | LICENSE.md 14 | icon.png 15 | false 16 | true 17 | true 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 | -------------------------------------------------------------------------------- /HotAvalonia.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.0.31903.59 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotAvalonia", "src/HotAvalonia/HotAvalonia.csproj", "{8434892B-7F24-441A-9623-D1E387F0C992}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotAvalonia.Core", "src/HotAvalonia.Core/HotAvalonia.Core.csproj", "{4DC5A913-2573-47FE-8434-0F7D030B2116}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotAvalonia.Extensions", "src/HotAvalonia.Extensions/HotAvalonia.Extensions.csproj", "{B196D2B3-DCDE-48A2-B783-590E19B396C4}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotAvalonia.Fody", "src/HotAvalonia.Fody/HotAvalonia.Fody.csproj", "{4D3AC651-616F-4FCC-9285-45A824A95F94}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotAvalonia.Remote", "src/HotAvalonia.Remote/HotAvalonia.Remote.csproj", "{F5BF4495-B20F-4828-893D-464B08C3ACF0}" 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotReloadDemo", "samples/HotReloadDemo/HotReloadDemo.csproj", "{692200A4-8EDB-493C-A2D2-B10CDE68DA0B}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotReloadDemo.Desktop", "samples/HotReloadDemo.Desktop/HotReloadDemo.Desktop.csproj", "{347F6CC8-192B-4BF2-AC25-BB9A3B045F95}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {8434892B-7F24-441A-9623-D1E387F0C992}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {8434892B-7F24-441A-9623-D1E387F0C992}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {8434892B-7F24-441A-9623-D1E387F0C992}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {8434892B-7F24-441A-9623-D1E387F0C992}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {4DC5A913-2573-47FE-8434-0F7D030B2116}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {4DC5A913-2573-47FE-8434-0F7D030B2116}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {4DC5A913-2573-47FE-8434-0F7D030B2116}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {4DC5A913-2573-47FE-8434-0F7D030B2116}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {B196D2B3-DCDE-48A2-B783-590E19B396C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {B196D2B3-DCDE-48A2-B783-590E19B396C4}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {B196D2B3-DCDE-48A2-B783-590E19B396C4}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {B196D2B3-DCDE-48A2-B783-590E19B396C4}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {4D3AC651-616F-4FCC-9285-45A824A95F94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {4D3AC651-616F-4FCC-9285-45A824A95F94}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {4D3AC651-616F-4FCC-9285-45A824A95F94}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {4D3AC651-616F-4FCC-9285-45A824A95F94}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {F5BF4495-B20F-4828-893D-464B08C3ACF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {F5BF4495-B20F-4828-893D-464B08C3ACF0}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {F5BF4495-B20F-4828-893D-464B08C3ACF0}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {F5BF4495-B20F-4828-893D-464B08C3ACF0}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {692200A4-8EDB-493C-A2D2-B10CDE68DA0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {692200A4-8EDB-493C-A2D2-B10CDE68DA0B}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {692200A4-8EDB-493C-A2D2-B10CDE68DA0B}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {692200A4-8EDB-493C-A2D2-B10CDE68DA0B}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {347F6CC8-192B-4BF2-AC25-BB9A3B045F95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {347F6CC8-192B-4BF2-AC25-BB9A3B045F95}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {347F6CC8-192B-4BF2-AC25-BB9A3B045F95}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {347F6CC8-192B-4BF2-AC25-BB9A3B045F95}.Release|Any CPU.Build.0 = Release|Any CPU 56 | EndGlobalSection 57 | EndGlobal 58 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Kira NT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.100", 4 | "rollForward": "latestMinor", 5 | "allowPrerelease": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /media/examples/hot_reload_app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kira-NT/HotAvalonia/9e4983a3faa6b0e29af106522dc3bfe9e62c51bb/media/examples/hot_reload_app.gif -------------------------------------------------------------------------------- /media/examples/hot_reload_resources.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kira-NT/HotAvalonia/9e4983a3faa6b0e29af106522dc3bfe9e62c51bb/media/examples/hot_reload_resources.gif -------------------------------------------------------------------------------- /media/examples/hot_reload_styles.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kira-NT/HotAvalonia/9e4983a3faa6b0e29af106522dc3bfe9e62c51bb/media/examples/hot_reload_styles.gif -------------------------------------------------------------------------------- /media/examples/hot_reload_user_control.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kira-NT/HotAvalonia/9e4983a3faa6b0e29af106522dc3bfe9e62c51bb/media/examples/hot_reload_user_control.gif -------------------------------------------------------------------------------- /media/examples/hot_reload_view.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kira-NT/HotAvalonia/9e4983a3faa6b0e29af106522dc3bfe9e62c51bb/media/examples/hot_reload_view.gif -------------------------------------------------------------------------------- /media/examples/hot_reload_window.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kira-NT/HotAvalonia/9e4983a3faa6b0e29af106522dc3bfe9e62c51bb/media/examples/hot_reload_window.gif -------------------------------------------------------------------------------- /media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kira-NT/HotAvalonia/9e4983a3faa6b0e29af106522dc3bfe9e62c51bb/media/icon.png -------------------------------------------------------------------------------- /samples/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | <_HotAvaloniaHarfsFile>$(MSBuildThisFileDirectory)../src/HotAvalonia.Remote/bin/$(Configuration)/net7.0/HotAvalonia.Remote.dll 14 | <_HotAvaloniaAssemblyFile>$(MSBuildThisFileDirectory)../src/HotAvalonia/bin/$(Configuration)/netstandard2.0/HotAvalonia.dll 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Android/HotReloadDemo.Android.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0-android 6 | 21 7 | apk 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.Content.PM; 2 | using Avalonia; 3 | using Avalonia.Android; 4 | 5 | namespace HotReloadDemo.Android; 6 | 7 | [Activity( 8 | Label = "HotReloadDemo.Android", 9 | Theme = "@style/HotReloadDemoTheme.NoActionBar", 10 | Icon = "@drawable/icon", 11 | MainLauncher = true, 12 | ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] 13 | public class MainActivity : AvaloniaMainActivity 14 | { 15 | protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) 16 | => base.CustomizeAppBuilder(builder) 17 | .LogToTrace() 18 | .WithInterFont(); 19 | } 20 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Android/Properties/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Android/Resources/drawable-night-v31/avalonia_anim.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 15 | 16 | 20 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 42 | 43 | 44 | 45 | 46 | 53 | 54 | 55 | 56 | 57 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Android/Resources/drawable-v31/avalonia_anim.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 15 | 16 | 21 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 46 | 47 | 48 | 49 | 50 | 57 | 58 | 59 | 60 | 61 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Android/Resources/drawable/splash_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Android/Resources/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #212121 4 | 5 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Android/Resources/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | 5 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Android/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Desktop/HotReloadDemo.Desktop.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net9.0 6 | app.manifest 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Desktop/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.ReactiveUI; 3 | 4 | namespace HotReloadDemo.Desktop; 5 | 6 | static class Program 7 | { 8 | [STAThread] 9 | public static void Main(string[] args) => BuildAvaloniaApp() 10 | .StartWithClassicDesktopLifetime(args); 11 | 12 | public static AppBuilder BuildAvaloniaApp() 13 | => AppBuilder.Configure() 14 | .UsePlatformDetect() 15 | .WithInterFont() 16 | .LogToTrace() 17 | .UseReactiveUI(); 18 | } 19 | -------------------------------------------------------------------------------- /samples/HotReloadDemo.Desktop/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/App.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | using HotReloadDemo.ViewModels; 5 | using HotReloadDemo.Views; 6 | 7 | namespace HotReloadDemo; 8 | 9 | public partial class App : Application 10 | { 11 | public override void Initialize() => AvaloniaXamlLoader.Load(this); 12 | 13 | public override void OnFrameworkInitializationCompleted() 14 | { 15 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 16 | { 17 | desktop.MainWindow = new MainWindow 18 | { 19 | DataContext = new MainViewModel(), 20 | }; 21 | } 22 | else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) 23 | { 24 | singleViewPlatform.MainView = new MainView 25 | { 26 | DataContext = new MainViewModel(), 27 | }; 28 | } 29 | 30 | base.OnFrameworkInitializationCompleted(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kira-NT/HotAvalonia/9e4983a3faa6b0e29af106522dc3bfe9e62c51bb/samples/HotReloadDemo/Assets/icon.ico -------------------------------------------------------------------------------- /samples/HotReloadDemo/Controls/ToDoItemControl.axaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Controls/ToDoItemControl.axaml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Avalonia.Controls; 3 | using Avalonia.Interactivity; 4 | using HotAvalonia; 5 | 6 | namespace HotReloadDemo.Controls; 7 | 8 | internal sealed partial class ToDoItemControl : UserControl 9 | { 10 | public ToDoItemControl() 11 | { 12 | InitializeComponent(); 13 | Initialize(); 14 | } 15 | 16 | [AvaloniaHotReload] 17 | private void Initialize() 18 | { 19 | // Let's pretend that we did something very important here. 20 | int hashCode = GetHashCode(); 21 | Debug.WriteLine("Initializing {0}#{1}...", this, hashCode); 22 | } 23 | 24 | private void CheckBox_Click(object? sender, RoutedEventArgs e) 25 | { 26 | int hashCode = GetHashCode(); 27 | Debug.WriteLine("Clicked {0}#{1}", this, hashCode); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Converters/TitleCaseConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Avalonia.Data; 3 | using Avalonia.Data.Converters; 4 | 5 | namespace HotReloadDemo.Converters; 6 | 7 | /// 8 | /// Represents a converter that converts the first character of a string to its uppercase equivalent. 9 | /// 10 | public sealed class TitleCaseConverter : IValueConverter 11 | { 12 | /// 13 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 14 | { 15 | if (value is not string text || !targetType.IsAssignableFrom(typeof(string))) 16 | return new BindingNotification(new InvalidCastException(), BindingErrorType.Error); 17 | 18 | return string.IsNullOrEmpty(text) ? text : (char.ToUpper(text[0]) + text.Substring(1)); 19 | } 20 | 21 | /// 22 | object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 23 | => throw new NotSupportedException(); 24 | } 25 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/HotReloadDemo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Models/ToDoItem.cs: -------------------------------------------------------------------------------- 1 | namespace HotReloadDemo.Models; 2 | 3 | public class ToDoItem 4 | { 5 | public string Description { get; set; } = string.Empty; 6 | 7 | public bool IsChecked { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Resources/AppResources.axaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | Enter your todo item 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Services/FakeToDoItemProvider.cs: -------------------------------------------------------------------------------- 1 | using HotReloadDemo.Models; 2 | 3 | namespace HotReloadDemo.Services; 4 | 5 | public sealed class FakeToDoItemProvider : IToDoItemProvider 6 | { 7 | public IEnumerable GetItems() => new[] 8 | { 9 | new ToDoItem { Description = "walk the dog" }, 10 | new ToDoItem { Description = "buy some milk" }, 11 | new ToDoItem { Description = "learn Avalonia", IsChecked = true }, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Services/IToDoItemProvider.cs: -------------------------------------------------------------------------------- 1 | using HotReloadDemo.Models; 2 | 3 | namespace HotReloadDemo.Services; 4 | 5 | public interface IToDoItemProvider 6 | { 7 | IEnumerable GetItems(); 8 | } 9 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Styles/AppStyles.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Controls.Templates; 3 | using HotReloadDemo.ViewModels; 4 | 5 | namespace HotReloadDemo; 6 | 7 | public class ViewLocator : IDataTemplate 8 | { 9 | public Control? Build(object? data) 10 | { 11 | string? name = data?.GetType().FullName?.Replace("ViewModel", "View"); 12 | Type? type = name is null ? null : Type.GetType(name); 13 | 14 | if (type is not null) 15 | return (Control?)Activator.CreateInstance(type); 16 | 17 | return new TextBlock { Text = "Not Found: " + name }; 18 | } 19 | 20 | public bool Match(object? data) => data is ViewModelBase; 21 | } 22 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/ViewModels/AddItemViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive; 2 | using HotReloadDemo.Models; 3 | using ReactiveUI; 4 | 5 | namespace HotReloadDemo.ViewModels; 6 | 7 | public class AddItemViewModel : ViewModelBase 8 | { 9 | private string _description = string.Empty; 10 | 11 | public AddItemViewModel() 12 | { 13 | IObservable isValidObservable = this.WhenAnyValue( 14 | x => x.Description, 15 | x => !string.IsNullOrWhiteSpace(x)); 16 | 17 | OkCommand = ReactiveCommand.Create(() => new ToDoItem { Description = Description }, isValidObservable); 18 | CancelCommand = ReactiveCommand.Create(() => { }); 19 | } 20 | 21 | public string Description 22 | { 23 | get => _description; 24 | set => this.RaiseAndSetIfChanged(ref _description, value); 25 | } 26 | 27 | public ReactiveCommand OkCommand { get; } 28 | 29 | public ReactiveCommand CancelCommand { get; } 30 | } 31 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/ViewModels/MainViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive; 2 | using System.Reactive.Linq; 3 | using HotReloadDemo.Models; 4 | using HotReloadDemo.Services; 5 | using ReactiveUI; 6 | 7 | namespace HotReloadDemo.ViewModels; 8 | 9 | public class MainViewModel : ViewModelBase 10 | { 11 | private object _content; 12 | 13 | public MainViewModel() 14 | : this(new FakeToDoItemProvider()) 15 | { 16 | } 17 | 18 | public MainViewModel(IToDoItemProvider provider) 19 | { 20 | ToDoList = new(provider.GetItems()); 21 | _content = ToDoList; 22 | } 23 | 24 | public ToDoListViewModel ToDoList { get; } 25 | 26 | public object Content 27 | { 28 | get => _content; 29 | private set => this.RaiseAndSetIfChanged(ref _content, value); 30 | } 31 | 32 | public void AddItem() 33 | { 34 | AddItemViewModel addItemViewModel = new(); 35 | 36 | Observable.Merge( 37 | addItemViewModel.OkCommand, 38 | addItemViewModel.CancelCommand.Select(_ => default(ToDoItem))) 39 | .Take(1) 40 | .Subscribe(Observer.Create(newItem => 41 | { 42 | if (newItem is not null) 43 | ToDoList.ToDoItems.Add(newItem); 44 | 45 | Content = ToDoList; 46 | })); 47 | 48 | Content = addItemViewModel; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/ViewModels/ToDoListViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using HotReloadDemo.Models; 3 | 4 | namespace HotReloadDemo.ViewModels; 5 | 6 | public class ToDoListViewModel : ViewModelBase 7 | { 8 | public ObservableCollection ToDoItems { get; } 9 | 10 | public ToDoListViewModel(IEnumerable items) 11 | { 12 | ArgumentNullException.ThrowIfNull(items); 13 | 14 | ToDoItems = new(items); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace HotReloadDemo.ViewModels; 4 | 5 | public class ViewModelBase : ReactiveObject 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Views/AddItemView.axaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 20 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Views/AddItemView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace HotReloadDemo.Views; 4 | 5 | public partial class AddItemView : UserControl 6 | { 7 | public AddItemView() => InitializeComponent(); 8 | } 9 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Views/MainView.axaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Views/MainView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace HotReloadDemo.Views; 4 | 5 | public partial class MainView : UserControl 6 | { 7 | public MainView() => InitializeComponent(); 8 | } 9 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace HotReloadDemo.Views; 4 | 5 | public partial class MainWindow : Window 6 | { 7 | public MainWindow() => InitializeComponent(); 8 | } 9 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Views/ToDoListView.axaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/HotReloadDemo/Views/ToDoListView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace HotReloadDemo.Views; 4 | 5 | public partial class ToDoListView : UserControl 6 | { 7 | public ToDoListView() => InitializeComponent(); 8 | } 9 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Assets/AssetInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace HotAvalonia.Assets; 4 | 5 | /// 6 | /// Represents metadata about an Avalonia asset. 7 | /// 8 | internal class AssetInfo 9 | { 10 | /// 11 | /// Gets the URI of the asset. 12 | /// 13 | public Uri Uri { get; } 14 | 15 | /// 16 | /// Gets the assembly associated with the asset. 17 | /// 18 | public Assembly Assembly { get; } 19 | 20 | /// 21 | /// Gets the URI of the project root containing the asset. 22 | /// 23 | public Uri Project { get; } 24 | 25 | /// 26 | /// Gets the path of the asset. 27 | /// 28 | public string Path { get; } 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// The URI of the asset. 34 | /// The assembly associated with the asset. 35 | /// The URI of the project root containing the asset. 36 | /// The path of the asset. 37 | public AssetInfo(Uri uri, Assembly assembly, Uri project, string path) 38 | { 39 | Uri = uri; 40 | Assembly = assembly; 41 | Project = project; 42 | Path = path; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/AvaloniaControlManager.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Threading; 2 | using HotAvalonia.Collections; 3 | using HotAvalonia.Helpers; 4 | using HotAvalonia.Reflection.Inject; 5 | using HotAvalonia.Xaml; 6 | 7 | namespace HotAvalonia; 8 | 9 | /// 10 | /// Manages the lifecycle and state of Avalonia controls. 11 | /// 12 | internal sealed class AvaloniaControlManager : IDisposable 13 | { 14 | /// 15 | /// The document associated with controls managed by this instance. 16 | /// 17 | private readonly CompiledXamlDocument _document; 18 | 19 | /// 20 | /// The set of weak references to the controls managed by this instance. 21 | /// 22 | private readonly WeakSet _controls; 23 | 24 | /// 25 | /// The instance responsible for injecting 26 | /// a callback into the control's populate method. 27 | /// 28 | private readonly IInjection? _populateInjection; 29 | 30 | /// 31 | /// The most recent version of the document associated 32 | /// with controls managed by this instance, if any. 33 | /// 34 | private CompiledXamlDocument? _recompiledDocument; 35 | 36 | /// 37 | /// Initializes a new instance of the class. 38 | /// 39 | /// The document associated with controls managed by this instance. 40 | public AvaloniaControlManager(CompiledXamlDocument document) 41 | { 42 | _document = document ?? throw new ArgumentNullException(nameof(document)); 43 | _controls = new(); 44 | 45 | if (!TryInjectPopulateCallback(document, OnPopulate, out _populateInjection)) 46 | LoggingHelper.LogWarning("Failed to subscribe to the 'Populate' event of {ControlUri}. The control won't be reloaded upon file changes.", document.Uri); 47 | } 48 | 49 | /// 50 | /// Gets the document associated with controls managed by this instance. 51 | /// 52 | public CompiledXamlDocument Document => _recompiledDocument ?? _document; 53 | 54 | /// 55 | public void Dispose() 56 | => _populateInjection?.Dispose(); 57 | 58 | /// 59 | /// Reloads the controls associated with this manager asynchronously. 60 | /// 61 | /// The XAML markup to reload the control from. 62 | /// The token to monitor for cancellation requests. 63 | public Task ReloadAsync(string xaml, CancellationToken cancellationToken = default) 64 | => Dispatcher.UIThread.InvokeAsync(() => UnsafeReloadAsync(xaml, cancellationToken), DispatcherPriority.Render); 65 | 66 | /// 67 | private async Task UnsafeReloadAsync(string xaml, CancellationToken cancellationToken) 68 | { 69 | cancellationToken.ThrowIfCancellationRequested(); 70 | await Task.Yield(); 71 | 72 | CompiledXamlDocument compiledXaml = XamlCompiler.Compile(xaml, _document.Uri, _document.RootType.Assembly); 73 | _recompiledDocument = new(compiledXaml.Uri, compiledXaml.BuildMethod, compiledXaml.PopulateMethod, _document); 74 | 75 | foreach (object control in _controls) 76 | { 77 | cancellationToken.ThrowIfCancellationRequested(); 78 | _recompiledDocument.Populate(serviceProvider: null, control); 79 | } 80 | } 81 | 82 | /// 83 | /// Handles the population of a control. 84 | /// 85 | /// The service provider. 86 | /// The control to be populated. 87 | /// true if the control was populated successfully; otherwise, false. 88 | private bool OnPopulate(IServiceProvider? provider, object control) 89 | { 90 | _controls.Add(control); 91 | if (_recompiledDocument is null) 92 | return false; 93 | 94 | _recompiledDocument.Populate(provider, control); 95 | return true; 96 | } 97 | 98 | /// 99 | /// Attempts to inject a callback into the populate method of the given control. 100 | /// 101 | /// The document associated with controls managed by this instance. 102 | /// The callback to invoke when a control is populated. 103 | /// 104 | /// When this method returns, contains the instance if the injection was successful; 105 | /// otherwise, null. 106 | /// 107 | /// 108 | /// true if the injection was successful; 109 | /// otherwise, false. 110 | /// 111 | private static bool TryInjectPopulateCallback( 112 | CompiledXamlDocument document, 113 | Func onPopulate, 114 | out IInjection? injection) 115 | { 116 | // At this point, we have two different fallbacks at our disposal: 117 | // - First, we try to perform an injection via MonoMod. It's great 118 | // and reliable; however, it doesn't support architectures other 119 | // than AMD64 (at least at the time of writing), and it requires 120 | // explicit support for every single new .NET release. 121 | // - In case this whole endeavor is run on a non-AMD64 device, 122 | // rendering `CallbackInjector` unusable, we fall back to 123 | // undocumented `!XamlIlPopulateOverride` fields. 124 | 125 | if (CallbackInjector.IsSupported) 126 | { 127 | injection = CallbackInjector.Inject(document.PopulateMethod, onPopulate); 128 | return true; 129 | } 130 | 131 | void PopulateOverride(IServiceProvider? provider, object control) 132 | { 133 | if (!onPopulate(provider, control)) 134 | document.Populate(provider, control); 135 | } 136 | return document.TryOverridePopulate(PopulateOverride, out injection); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/AvaloniaHotReload.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Reflection; 3 | using System.Runtime.CompilerServices; 4 | using Avalonia; 5 | using Avalonia.Threading; 6 | using HotAvalonia.Helpers; 7 | 8 | namespace HotAvalonia; 9 | 10 | /// 11 | /// Provides functionality to enable or disable hot reload for Avalonia applications. 12 | /// 13 | [EditorBrowsable(EditorBrowsableState.Never)] 14 | public static class AvaloniaHotReload 15 | { 16 | /// 17 | /// The hot reload contexts mapped to their respective applications. 18 | /// 19 | private static readonly ConditionalWeakTable s_contexts = new(); 20 | 21 | /// 22 | /// Enables hot reload for the application managed by the provided . 23 | /// 24 | /// The to configure. 25 | /// 26 | /// A factory function that creates the hot reload context for 27 | /// the application managed by . 28 | /// 29 | public static void Enable(AppBuilder builder, Func contextFactory) 30 | { 31 | _ = builder ?? throw new ArgumentNullException(nameof(builder)); 32 | _ = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory)); 33 | 34 | if (builder.Instance is not null) 35 | { 36 | Enable(builder.Instance, contextFactory); 37 | return; 38 | } 39 | 40 | string appFactoryFieldName = "_appFactory"; 41 | FieldInfo appFactoryField = typeof(AppBuilder).GetInstanceField(appFactoryFieldName, typeof(Func)) 42 | ?? typeof(AppBuilder).GetInstanceFields().FirstOrDefault(x => x.FieldType == typeof(Func)) 43 | ?? throw new MissingFieldException(typeof(AppBuilder).FullName, appFactoryFieldName); 44 | 45 | if (appFactoryField.GetValue(builder) is not Func appFactory) 46 | throw new InvalidOperationException("Could not enable hot reload: The 'AppBuilder' instance has not been properly initialized."); 47 | 48 | // If the factory function has already been replaced with our own, there's nothing left to do. 49 | if (appFactory.Method.Module.Assembly == typeof(AvaloniaHotReload).Assembly) 50 | return; 51 | 52 | appFactoryField.SetValue(builder, () => 53 | { 54 | IHotReloadContext context = CreateHotReloadContext(contextFactory); 55 | Application app = appFactory(); 56 | s_contexts.Add(app, context); 57 | return app; 58 | }); 59 | } 60 | 61 | /// 62 | /// Enables hot reload for the provided application. 63 | /// 64 | /// The instance to enable hot reload for. 65 | /// 66 | /// A factory function that creates the hot reload context for the given application. 67 | /// 68 | public static void Enable(Application app, Func contextFactory) 69 | { 70 | _ = app ?? throw new ArgumentNullException(nameof(app)); 71 | _ = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory)); 72 | 73 | if (s_contexts.TryGetValue(app, out IHotReloadContext? context)) 74 | { 75 | context.EnableHotReload(); 76 | } 77 | else 78 | { 79 | context = CreateHotReloadContext(contextFactory); 80 | s_contexts.Add(app, context); 81 | } 82 | } 83 | 84 | /// 85 | /// Disables hot reload for the provided application. 86 | /// 87 | /// The instance to disable hot reload for. 88 | public static void Disable(Application app) 89 | { 90 | _ = app ?? throw new ArgumentNullException(nameof(app)); 91 | 92 | if (s_contexts.TryGetValue(app, out IHotReloadContext? context)) 93 | context.DisableHotReload(); 94 | } 95 | 96 | /// 97 | /// Creates and initializes a new hot reload context using the specified factory method. 98 | /// 99 | /// A factory function that produces an instance. 100 | /// A fully initialized instance. 101 | private static IHotReloadContext CreateHotReloadContext(Func contextFactory) 102 | { 103 | IHotReloadContext context = contextFactory(); 104 | ISupportInitialize? initContext = context as ISupportInitialize; 105 | 106 | initContext?.BeginInit(); 107 | context.EnableHotReload(); 108 | 109 | // Since Avalonia doesn't provide anything similar to an "AppStarted" event 110 | // of some sort (at least as far as I'm aware), we use this small "hack": 111 | // actions posted to the UI thread are processed only after both the framework 112 | // and the app are fully initialized, which is exactly what we need. 113 | if (initContext is not null) 114 | Dispatcher.UIThread.Post(initContext.EndInit); 115 | 116 | return context; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/BannedSymbols.txt: -------------------------------------------------------------------------------- 1 | T:System.IO.Directory;Use 'HotAvalonia.IO.IFileSystem' instead. 2 | T:System.IO.File;Use 'HotAvalonia.IO.IFileSystem' instead. 3 | T:System.IO.Path;Use 'HotAvalonia.IO.IFileSystem' instead. 4 | 5 | T:System.IO.FileSystemWatcher;Use 'HotAvalonia.IO.IFileSystemWatcher' instead. 6 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Collections/MemoryCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using HotAvalonia.Helpers; 3 | 4 | namespace HotAvalonia.Collections; 5 | 6 | /// 7 | /// Represents a memory cache that stores items of type . 8 | /// 9 | /// The type of items to be stored in the cache. 10 | internal sealed class MemoryCache : ICollection, IReadOnlyCollection 11 | { 12 | /// 13 | /// The list of entries stored in the memory cache. 14 | /// 15 | private readonly List _entries; 16 | 17 | /// 18 | /// The lifespan of items in the cache. 19 | /// 20 | private readonly double _lifespan; 21 | 22 | /// 23 | /// Initializes a new instance of the class with the specified lifespan. 24 | /// 25 | /// The lifespan of items in the cache. 26 | public MemoryCache(TimeSpan lifespan) 27 | { 28 | _entries = new(); 29 | _lifespan = lifespan.TotalMilliseconds; 30 | } 31 | 32 | /// 33 | /// Gets the lifespan of items in the cache. 34 | /// 35 | public TimeSpan Lifespan => TimeSpan.FromMilliseconds(_lifespan); 36 | 37 | /// 38 | public int Count 39 | { 40 | get 41 | { 42 | RemoveStale(); 43 | return _entries.Count; 44 | } 45 | } 46 | 47 | /// 48 | bool ICollection.IsReadOnly => false; 49 | 50 | /// 51 | public void Add(T item) => _entries.Add(new(item)); 52 | 53 | /// 54 | public bool Remove(T item) => _entries.RemoveAll(x => EqualityComparer.Default.Equals(item, x.Value)) != 0; 55 | 56 | /// 57 | /// Removes stale entries from the cache based on their timestamp. 58 | /// 59 | private void RemoveStale() 60 | { 61 | long currentTimestamp = StopwatchHelper.GetTimestamp(); 62 | _entries.RemoveAll(x => StopwatchHelper.GetElapsedTime(x.Timestamp, currentTimestamp).TotalMilliseconds > _lifespan); 63 | } 64 | 65 | /// 66 | public void Clear() => _entries.Clear(); 67 | 68 | /// 69 | public bool Contains(T item) => _entries.Any(x => EqualityComparer.Default.Equals(item, x.Value)); 70 | 71 | /// 72 | public void CopyTo(T[] array, int arrayIndex) 73 | { 74 | RemoveStale(); 75 | _entries.ConvertAll(static x => x.Value).CopyTo(array, arrayIndex); 76 | } 77 | 78 | /// 79 | public IEnumerator GetEnumerator() 80 | { 81 | RemoveStale(); 82 | return _entries.Select(static x => x.Value).GetEnumerator(); 83 | } 84 | 85 | /// 86 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 87 | 88 | /// 89 | /// Represents an entry in the cache containing the value and its timestamp. 90 | /// 91 | private sealed class Entry 92 | { 93 | /// 94 | /// Gets the value stored in the cache entry. 95 | /// 96 | public T Value { get; } 97 | 98 | /// 99 | /// Gets the timestamp when the cache entry was added. 100 | /// 101 | public long Timestamp { get; } 102 | 103 | /// 104 | /// Initializes a new instance of the class with the specified value. 105 | /// 106 | /// The value to be stored in the cache entry. 107 | public Entry(T value) 108 | : this(value, StopwatchHelper.GetTimestamp()) 109 | { 110 | } 111 | 112 | /// 113 | /// Initializes a new instance of the class with the specified value and timestamp. 114 | /// 115 | /// The value to be stored in the cache entry. 116 | /// The timestamp when the cache entry was added. 117 | public Entry(T value, long timestamp) 118 | { 119 | Value = value; 120 | Timestamp = timestamp; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Compat/System/IO/StreamCompatExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace System.IO; 2 | 3 | /// 4 | /// Provides extension methods for 5 | /// to backport functionality from newer .NET versions. 6 | /// 7 | internal static class StreamCompatExtensions 8 | { 9 | /// 10 | /// Asynchronously reads number of bytes from the given stream, 11 | /// advances the position within the stream, and monitors cancellation requests. 12 | /// 13 | /// The stream to read the bytes from. 14 | /// The buffer to write the data into. 15 | /// The byte offset in at which to begin writing data from the stream. 16 | /// The number of bytes to be read from the current stream. 17 | /// The token to monitor for cancellation requests. 18 | /// A task that represents the asynchronous read operation. 19 | /// The end of the stream is reached before reading count number of bytes. 20 | public static async Task ReadExactlyAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) 21 | { 22 | _ = stream ?? throw new ArgumentNullException(nameof(stream)); 23 | 24 | int totalRead = 0; 25 | while (totalRead < count) 26 | { 27 | int read = await stream.ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken).ConfigureAwait(false); 28 | if (read == 0) 29 | throw new EndOfStreamException(); 30 | 31 | totalRead += read; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/DependencyInjection/AvaloniaServiceProvider.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using HotAvalonia.Helpers; 3 | 4 | namespace HotAvalonia.DependencyInjection; 5 | 6 | /// 7 | /// Provides services for Avalonia applications. 8 | /// 9 | internal sealed class AvaloniaServiceProvider : IServiceProvider 10 | { 11 | /// 12 | /// A registry that maps service types to their corresponding factory functions. 13 | /// 14 | private readonly IDictionary> _registry; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// 20 | /// A registry containing service type to factory mappings. 21 | /// 22 | private AvaloniaServiceProvider(IDictionary> registry) 23 | { 24 | _registry = registry ?? throw new ArgumentNullException(nameof(registry)); 25 | } 26 | 27 | /// 28 | /// Gets the current instance of the . 29 | /// 30 | public static AvaloniaServiceProvider Current => FromAvaloniaLocator(CurrentLocator); 31 | 32 | /// 33 | /// Gets the current instance of the . 34 | /// 35 | private static AvaloniaLocator CurrentLocator => typeof(AvaloniaLocator) 36 | .GetStaticFields() 37 | .Select(static x => x.GetValue(null)) 38 | .OfType() 39 | .First(); 40 | 41 | /// 42 | /// Creates an instance 43 | /// from the specified . 44 | /// 45 | /// 46 | /// An instance used to resolve service factories. 47 | /// 48 | /// 49 | /// An initialized with services from the given locator. 50 | /// 51 | private static AvaloniaServiceProvider FromAvaloniaLocator(AvaloniaLocator locator) 52 | { 53 | _ = locator ?? throw new ArgumentNullException(nameof(locator)); 54 | 55 | IDictionary> registry = locator.GetType() 56 | .GetInstanceFields() 57 | .Select(x => x.GetValue(locator)) 58 | .OfType>>() 59 | .First()!; 60 | 61 | return new(registry); 62 | } 63 | 64 | /// 65 | public object? GetService(Type serviceType) 66 | { 67 | _ = serviceType ?? throw new ArgumentNullException(nameof(serviceType)); 68 | 69 | if (_registry.TryGetValue(serviceType, out Func? factory)) 70 | return factory(); 71 | 72 | return null; 73 | } 74 | 75 | /// 76 | /// Registers a factory for the specified service type. 77 | /// 78 | /// The of the service to register. 79 | /// A factory function that produces the service instance. 80 | public void SetService(Type serviceType, Func factory) 81 | { 82 | _ = serviceType ?? throw new ArgumentNullException(nameof(serviceType)); 83 | _ = factory ?? throw new ArgumentNullException(nameof(factory)); 84 | 85 | _registry[serviceType] = () => factory(this); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Helpers/OpCodeHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Reflection.Emit; 3 | 4 | namespace HotAvalonia.Helpers; 5 | 6 | /// 7 | /// Provides helper methods for working with CIL opcodes. 8 | /// 9 | internal static class OpCodeHelper 10 | { 11 | /// 12 | /// The flag indicating that the is represented by 2 bytes. 13 | /// 14 | private const int Int16OpCodeFlag = 0xFE00; 15 | 16 | /// 17 | /// The marker value for two-byte opcodes. 18 | /// 19 | private const int Int16OpCodeMarker = 0xFE; 20 | 21 | /// 22 | /// The instruction set containing all instances. 23 | /// 24 | private static readonly Lazy s_opCodes = new(CreateInstructionSet, isThreadSafe: true); 25 | 26 | /// 27 | /// Attempts to read an from the given IL span. 28 | /// 29 | /// The span containing the IL bytecode. 30 | /// When this method returns, contains the if the method is successful. 31 | /// true if an was successfully read; otherwise, false. 32 | public static bool TryReadOpCode(ReadOnlySpan il, out OpCode opCode) 33 | { 34 | opCode = OpCodes.Nop; 35 | 36 | if (il.IsEmpty) 37 | return false; 38 | 39 | int opCodeValue = il[0]; 40 | if (opCodeValue >= Int16OpCodeMarker) 41 | { 42 | if (il.Length < 1) 43 | return false; 44 | 45 | opCodeValue = (opCodeValue << 8) | il[1]; 46 | } 47 | 48 | return TryGetOpCode(opCodeValue, out opCode); 49 | } 50 | 51 | /// 52 | /// Tries to get the associated with the given value. 53 | /// 54 | /// The opcode value. 55 | /// When this method returns, contains the if the method is successful. 56 | /// true if the was found for the given value; otherwise, false. 57 | public static bool TryGetOpCode(int value, out OpCode opCode) 58 | { 59 | int index = GetOpCodeIndex(value); 60 | OpCode[] opCodes = s_opCodes.Value; 61 | if ((uint)index < (uint)opCodes.Length) 62 | { 63 | opCode = opCodes[index]; 64 | return true; 65 | } 66 | 67 | opCode = OpCodes.Nop; 68 | return false; 69 | } 70 | 71 | /// 72 | /// Creates an instruction set containing all instances. 73 | /// 74 | /// An array of instances. 75 | private static OpCode[] CreateInstructionSet() 76 | { 77 | List opCodes = new(256); 78 | opCodes.AddRange(ExtractAllOpCodes()); 79 | 80 | int maxIndex = opCodes.Max(static x => GetOpCodeIndex(x.Value)); 81 | int instructionSetSize = maxIndex + 1; 82 | OpCode[] instructionSet = new OpCode[instructionSetSize]; 83 | 84 | foreach (OpCode opCode in opCodes) 85 | { 86 | instructionSet[GetOpCodeIndex(opCode.Value)] = opCode; 87 | } 88 | 89 | return instructionSet; 90 | } 91 | 92 | /// 93 | /// Gets the index of the given opcode in the instruction set. 94 | /// 95 | /// The opcode value. 96 | /// The index of the opcode. 97 | private static int GetOpCodeIndex(int value) 98 | => (value & Int16OpCodeFlag) == Int16OpCodeFlag ? (256 + (value & 0xFF)) : (value & 0xFF); 99 | 100 | /// 101 | /// Extracts all opcodes from the class. 102 | /// 103 | /// 104 | /// All opcodes defined in the class. 105 | /// 106 | private static IEnumerable ExtractAllOpCodes() 107 | { 108 | FieldInfo[] fields = typeof(OpCodes).GetStaticFields(); 109 | return fields.Where(static x => x.FieldType == typeof(OpCode)).Select(static x => (OpCode)x.GetValue(null)); 110 | } 111 | 112 | /// 113 | /// Calculates the size of the operand associated with the given opcode. 114 | /// 115 | /// The operation code in question. 116 | /// 117 | /// The size of the operand in bytes; or 0 if the opcode has no operand. 118 | /// 119 | public static int GetOperandSize(this OpCode opCode) 120 | => opCode.Size is 0 ? 0 : GetOperandSize(opCode.OperandType); 121 | 122 | /// 123 | /// Determines the size of an operand based on its type. 124 | /// 125 | /// The type of the operand. 126 | /// 127 | /// The size of the operand in bytes. 128 | /// 129 | public static int GetOperandSize(this OperandType operandType) => operandType switch 130 | { 131 | OperandType.InlineBrTarget or OperandType.InlineField or OperandType.InlineI 132 | or OperandType.InlineMethod or OperandType.InlineSig or OperandType.InlineString 133 | or OperandType.InlineSwitch or OperandType.InlineTok or OperandType.InlineType 134 | or OperandType.ShortInlineR => sizeof(int), 135 | OperandType.InlineI8 or OperandType.InlineR => sizeof(long), 136 | OperandType.InlineVar => sizeof(short), 137 | OperandType.ShortInlineBrTarget or OperandType.ShortInlineI or OperandType.ShortInlineVar => sizeof(byte), 138 | _ => 0, 139 | }; 140 | } 141 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Helpers/StopwatchHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace HotAvalonia.Helpers; 4 | 5 | /// 6 | /// Provides utility methods for working with the class and measuring time intervals. 7 | /// 8 | internal static class StopwatchHelper 9 | { 10 | /// 11 | /// The number of ticks per millisecond. 12 | /// 13 | private const long TicksPerMillisecond = 10000; 14 | 15 | /// 16 | /// The number of ticks per second. 17 | /// 18 | private const long TicksPerSecond = TicksPerMillisecond * 1000; 19 | 20 | /// 21 | /// The tick frequency for the underlying timer mechanism. 22 | /// 23 | private static readonly double s_tickFrequency = (double)TicksPerSecond / Stopwatch.Frequency; 24 | 25 | /// 26 | /// Returns the current number of ticks in the timer mechanism. 27 | /// 28 | /// 29 | /// A long integer representing the tick counter value of the underlying timer mechanism. 30 | /// 31 | public static long GetTimestamp() => Stopwatch.GetTimestamp(); 32 | 33 | /// 34 | /// Returns the elapsed time since the value retrieved using . 35 | /// 36 | /// The timestamp marking the beginning of the time period. 37 | /// 38 | /// A for the elapsed time between the starting timestamp and the time of this call. 39 | /// 40 | public static TimeSpan GetElapsedTime(long startingTimestamp) 41 | => GetElapsedTime(startingTimestamp, GetTimestamp()); 42 | 43 | /// 44 | /// Returns the elapsed time between two timestamps retrieved using . 45 | /// 46 | /// The timestamp marking the beginning of the time period. 47 | /// The timestamp marking the end of the time period. 48 | /// 49 | /// A for the elapsed time between the starting and ending timestamps. 50 | /// 51 | public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) 52 | => new((long)((endingTimestamp - startingTimestamp) * s_tickFrequency)); 53 | } 54 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/HotAvalonia.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HotAvalonia 5 | netstandard2.0;netstandard2.1 6 | 7 | 8 | 9 | true 10 | A library that provides the core components necessary to enable XAML hot reload for Avalonia apps. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/HotReloadFeatures.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Avalonia.Logging; 3 | 4 | namespace HotAvalonia; 5 | 6 | /// 7 | /// Provides functionality to retrieve configuration values for 8 | /// various hot reload features from environment variables. 9 | /// 10 | internal static class HotReloadFeatures 11 | { 12 | /// 13 | /// Gets a value that indicates whether injections should be disabled. 14 | /// 15 | public static bool DisableInjections => GetBoolean("DISABLE_INJECTIONS"); 16 | 17 | /// 18 | /// Gets a value that indicates whether the initial patching should be skipped. 19 | /// 20 | public static bool SkipInitialPatching => GetBoolean("SKIP_INITIAL_PATCHING"); 21 | 22 | /// 23 | /// Gets the log level override for hot reload-related events. 24 | /// 25 | public static LogEventLevel LogLevelOverride => GetEnum("LOG_LEVEL_OVERRIDE"); 26 | 27 | /// 28 | /// Retrieves the environment variable value associated with the specified feature name. 29 | /// 30 | /// The feature name. 31 | /// The default value to return if the environment variable is not set. 32 | /// 33 | /// The string value of the environment variable, or 34 | /// if the variable is not set. 35 | /// 36 | [return: NotNullIfNotNull(nameof(defaultValue))] 37 | internal static string? GetString(string featureName, string? defaultValue = null) 38 | { 39 | string variableName = $"{nameof(HotAvalonia)}_{featureName}".ToUpperInvariant(); 40 | string? variableValue = Environment.GetEnvironmentVariable(variableName); 41 | return string.IsNullOrEmpty(variableValue) ? defaultValue : variableValue; 42 | } 43 | 44 | /// 45 | /// Retrieves the environment variable value associated with the specified feature name 46 | /// and converts it to a . 47 | /// 48 | /// The feature name. 49 | /// The default value to return if the conversion fails. 50 | /// 51 | /// The boolean representation of the environment variable, or 52 | /// if the conversion fails. 53 | /// 54 | internal static bool GetBoolean(string featureName, bool defaultValue = false) 55 | { 56 | string? stringValue = GetString(featureName); 57 | if (bool.TryParse(stringValue, out bool boolValue)) 58 | return boolValue; 59 | 60 | if (int.TryParse(stringValue, out int intValue)) 61 | return intValue != 0; 62 | 63 | return defaultValue; 64 | } 65 | 66 | /// 67 | /// Retrieves the environment variable value associated with the specified feature name 68 | /// and converts it to a 32-bit integer. 69 | /// 70 | /// The feature name. 71 | /// The default integer value to return if the conversion fails. 72 | /// 73 | /// The 32-bit integer representation of the environment variable, or 74 | /// if the conversion fails. 75 | /// 76 | internal static int GetInt32(string featureName, int defaultValue = 0) 77 | => int.TryParse(GetString(featureName), out int value) ? value : defaultValue; 78 | 79 | /// 80 | /// Retrieves the environment variable value associated with the specified feature name 81 | /// and converts it to the specified enumeration type. 82 | /// 83 | /// The enumeration type to which the value should be converted. 84 | /// The feature name. 85 | /// The default enumeration value to return if the conversion fails. 86 | /// 87 | /// The enumeration value corresponding to the environment variable, or 88 | /// if the conversion fails. 89 | /// 90 | internal static TEnum GetEnum(string featureName, TEnum defaultValue = default) where TEnum : struct, Enum 91 | => Enum.TryParse(GetString(featureName), ignoreCase: true, out TEnum value) ? value : defaultValue; 92 | } 93 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/IO/EmptyFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace HotAvalonia.IO; 4 | 5 | #pragma warning disable RS0030 // Do not use banned APIs 6 | 7 | /// 8 | /// Represents an empty, read-only file system. 9 | /// 10 | internal sealed class EmptyFileSystem : IFileSystem 11 | { 12 | /// 13 | public StringComparer PathComparer => StringComparer.CurrentCulture; 14 | 15 | /// 16 | public StringComparison PathComparison => StringComparison.CurrentCulture; 17 | 18 | /// 19 | public char DirectorySeparatorChar => Path.DirectorySeparatorChar; 20 | 21 | /// 22 | public char AltDirectorySeparatorChar => Path.AltDirectorySeparatorChar; 23 | 24 | /// 25 | public char VolumeSeparatorChar => Path.VolumeSeparatorChar; 26 | 27 | /// 28 | public IFileSystemWatcher CreateFileSystemWatcher() => new EmptyFileSystemWatcher(this); 29 | 30 | /// 31 | ValueTask IFileSystem.CreateFileSystemWatcherAsync(CancellationToken cancellationToken) => new(CreateFileSystemWatcher()); 32 | 33 | /// 34 | public DateTime GetLastWriteTimeUtc(string path) => DateTime.MinValue; 35 | 36 | /// 37 | ValueTask IFileSystem.GetLastWriteTimeUtcAsync(string path, CancellationToken cancellationToken) => new(GetLastWriteTimeUtc(path)); 38 | 39 | /// 40 | public string GetFullPath(string path) => Path.GetFullPath(path); 41 | 42 | /// 43 | public string? GetDirectoryName(string? path) => Path.GetDirectoryName(path); 44 | 45 | /// 46 | [return: NotNullIfNotNull(nameof(path))] 47 | public string? GetFileName(string? path) => Path.GetFileName(path); 48 | 49 | /// 50 | public string Combine(string path1, string path2) => Path.Combine(path1, path2); 51 | 52 | /// 53 | public string ChangeExtension(string path, string? extension) => Path.ChangeExtension(path, extension); 54 | 55 | /// 56 | public bool FileExists(string? path) => false; 57 | 58 | /// 59 | ValueTask IFileSystem.FileExistsAsync(string? path, CancellationToken cancellationToken) => new(FileExists(path)); 60 | 61 | /// 62 | public bool DirectoryExists(string? path) => false; 63 | 64 | /// 65 | ValueTask IFileSystem.DirectoryExistsAsync(string? path, CancellationToken cancellationToken) => new(DirectoryExists(path)); 66 | 67 | /// 68 | public IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) => throw new DirectoryNotFoundException($"Could not find a part of the path '{path}'."); 69 | 70 | /// 71 | IAsyncEnumerable IFileSystem.EnumerateFilesAsync(string path, string searchPattern, SearchOption searchOption, CancellationToken cancellationToken) => throw new DirectoryNotFoundException($"Could not find a part of the path '{path}'."); 72 | 73 | /// 74 | public Stream OpenRead(string path) => throw new FileNotFoundException($"Could not find a part of the path '{path}'.", path); 75 | 76 | /// 77 | Task IFileSystem.OpenReadAsync(string path, CancellationToken cancellationToken) => Task.FromException(new FileNotFoundException($"Could not find a part of the path '{path}'.", path)); 78 | 79 | /// 80 | void IDisposable.Dispose() { } 81 | 82 | /// 83 | ValueTask IAsyncDisposable.DisposeAsync() => default; 84 | } 85 | 86 | #pragma warning restore RS0030 // Do not use banned APIs 87 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/IO/EmptyFileSystemWatcher.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia.IO; 2 | 3 | /// 4 | /// Represents a file system watcher that does nothing and never raises events. 5 | /// 6 | internal sealed class EmptyFileSystemWatcher : IFileSystemWatcher 7 | { 8 | /// 9 | public IFileSystem FileSystem { get; } 10 | 11 | /// 12 | public string Path { get; set; } 13 | 14 | /// 15 | public bool EnableRaisingEvents { get; set; } 16 | 17 | /// 18 | public bool IncludeSubdirectories { get; set; } 19 | 20 | /// 21 | public string Filter { get; set; } 22 | 23 | /// 24 | public NotifyFilters NotifyFilter { get; set; } 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// 29 | /// The file system associated with this watcher. 30 | public EmptyFileSystemWatcher(EmptyFileSystem fileSystem) 31 | { 32 | FileSystem = fileSystem; 33 | Path = string.Empty; 34 | EnableRaisingEvents = false; 35 | IncludeSubdirectories = false; 36 | Filter = "*.*"; 37 | NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; 38 | } 39 | 40 | /// 41 | public event FileSystemEventHandler Created { add { } remove { } } 42 | 43 | /// 44 | public event FileSystemEventHandler Deleted { add { } remove { } } 45 | 46 | /// 47 | public event FileSystemEventHandler Changed { add { } remove { } } 48 | 49 | /// 50 | public event RenamedEventHandler Renamed { add { } remove { } } 51 | 52 | /// 53 | public event ErrorEventHandler Error { add { } remove { } } 54 | 55 | /// 56 | public void Dispose() { } 57 | } 58 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/IO/IFileSystemWatcher.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia.IO; 2 | 3 | /// 4 | /// Listens to the file system change notifications and raises 5 | /// events when a directory, or file in a directory, changes. 6 | /// 7 | public interface IFileSystemWatcher : IDisposable 8 | { 9 | /// 10 | /// Gets the file system associated with this instance. 11 | /// 12 | IFileSystem FileSystem { get; } 13 | 14 | /// 15 | /// Gets or sets the path of the directory to watch. 16 | /// 17 | string Path { get; set; } 18 | 19 | /// 20 | /// Gets or sets a value indicating whether the component is enabled. 21 | /// 22 | bool EnableRaisingEvents { get; set; } 23 | 24 | /// 25 | /// Gets or sets a value indicating whether subdirectories 26 | /// within the specified path should be monitored. 27 | /// 28 | bool IncludeSubdirectories { get; set; } 29 | 30 | /// 31 | /// Gets or sets the filter string used to determine 32 | /// what files are monitored in a directory. 33 | /// 34 | string Filter { get; set; } 35 | 36 | /// 37 | /// Gets or sets the type of changes to watch for. 38 | /// 39 | NotifyFilters NotifyFilter { get; set; } 40 | 41 | /// 42 | /// Occurs when a file or directory in the specified is created. 43 | /// 44 | event FileSystemEventHandler Created; 45 | 46 | /// 47 | /// Occurs when a file or directory in the specified is deleted. 48 | /// 49 | event FileSystemEventHandler Deleted; 50 | 51 | /// 52 | /// Occurs when a file or directory in the specified is changed. 53 | /// 54 | event FileSystemEventHandler Changed; 55 | 56 | /// 57 | /// Occurs when a file or directory in the specified is renamed. 58 | /// 59 | event RenamedEventHandler Renamed; 60 | 61 | /// 62 | /// Occurs when this instance is unable to continue monitoring changes. 63 | /// 64 | event ErrorEventHandler Error; 65 | } 66 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/IO/LocalFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace HotAvalonia.IO; 6 | 7 | #pragma warning disable RS0030 // Do not use banned APIs 8 | 9 | /// 10 | /// Provides functionality for interacting with the local file system. 11 | /// 12 | internal sealed class LocalFileSystem : IFileSystem 13 | { 14 | /// 15 | public StringComparer PathComparer { get; } = 16 | RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 17 | ? StringComparer.CurrentCultureIgnoreCase 18 | : StringComparer.CurrentCulture; 19 | 20 | /// 21 | public StringComparison PathComparison { get; } = 22 | RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 23 | ? StringComparison.CurrentCultureIgnoreCase 24 | : StringComparison.CurrentCulture; 25 | 26 | /// 27 | public char DirectorySeparatorChar => Path.DirectorySeparatorChar; 28 | 29 | /// 30 | public char AltDirectorySeparatorChar => Path.AltDirectorySeparatorChar; 31 | 32 | /// 33 | public char VolumeSeparatorChar => Path.VolumeSeparatorChar; 34 | 35 | /// 36 | public IFileSystemWatcher CreateFileSystemWatcher() => new LocalFileSystemWatcher(this); 37 | 38 | /// 39 | ValueTask IFileSystem.CreateFileSystemWatcherAsync(CancellationToken cancellationToken) => new(CreateFileSystemWatcher()); 40 | 41 | /// 42 | public DateTime GetLastWriteTimeUtc(string path) => File.GetLastWriteTimeUtc(path); 43 | 44 | /// 45 | ValueTask IFileSystem.GetLastWriteTimeUtcAsync(string path, CancellationToken cancellationToken) => new(GetLastWriteTimeUtc(path)); 46 | 47 | /// 48 | public string GetFullPath(string path) => Path.GetFullPath(path); 49 | 50 | /// 51 | public string? GetDirectoryName(string? path) => Path.GetDirectoryName(path); 52 | 53 | /// 54 | [return: NotNullIfNotNull(nameof(path))] 55 | public string? GetFileName(string? path) => Path.GetFileName(path); 56 | 57 | /// 58 | public string Combine(string path1, string path2) => Path.Combine(path1, path2); 59 | 60 | /// 61 | public string ChangeExtension(string path, string? extension) => Path.ChangeExtension(path, extension); 62 | 63 | /// 64 | public bool FileExists(string? path) => File.Exists(path); 65 | 66 | /// 67 | ValueTask IFileSystem.FileExistsAsync(string? path, CancellationToken cancellationToken) => new(FileExists(path)); 68 | 69 | /// 70 | public bool DirectoryExists(string? path) => Directory.Exists(path); 71 | 72 | /// 73 | ValueTask IFileSystem.DirectoryExistsAsync(string? path, CancellationToken cancellationToken) => new(DirectoryExists(path)); 74 | 75 | /// 76 | public IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) => Directory.EnumerateFiles(path, searchPattern, searchOption); 77 | 78 | /// 79 | async IAsyncEnumerable IFileSystem.EnumerateFilesAsync(string path, string searchPattern, SearchOption searchOption, [EnumeratorCancellation] CancellationToken cancellationToken) 80 | { 81 | await default(ValueTask); 82 | foreach (string filePath in EnumerateFiles(path, searchPattern, searchOption)) 83 | yield return filePath; 84 | } 85 | 86 | /// 87 | public Stream OpenRead(string path) => File.OpenRead(path); 88 | 89 | /// 90 | Task IFileSystem.OpenReadAsync(string path, CancellationToken cancellationToken) => Task.FromResult(OpenRead(path)); 91 | 92 | /// 93 | void IDisposable.Dispose() { } 94 | 95 | /// 96 | ValueTask IAsyncDisposable.DisposeAsync() => default; 97 | } 98 | 99 | #pragma warning restore RS0030 // Do not use banned APIs 100 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/IO/LocalFileSystemWatcher.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia.IO; 2 | 3 | /// 4 | /// Listens to the local file system change notifications and raises 5 | /// events when a directory, or file in a directory, changes. 6 | /// 7 | internal sealed class LocalFileSystemWatcher : FileSystemWatcher, IFileSystemWatcher 8 | { 9 | /// 10 | public IFileSystem FileSystem { get; } 11 | 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | /// The file system associated with this system. 16 | public LocalFileSystemWatcher(LocalFileSystem fileSystem) => FileSystem = fileSystem; 17 | } 18 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/IO/RemoteFileSystemException.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace HotAvalonia.IO; 4 | 5 | /// 6 | /// Represents an exception that occurs during remote file system operations. 7 | /// 8 | internal sealed class RemoteFileSystemException : Exception 9 | { 10 | /// 11 | /// Gets the unique identifier associated with the exception. 12 | /// 13 | public ushort Id { get; } 14 | 15 | /// 16 | /// Initializes a new instance of the class 17 | /// with the specified identifier and exception data in byte array format. 18 | /// 19 | /// The unique identifier for the exception. 20 | /// A byte array representing the serialized exception details. 21 | public RemoteFileSystemException(ushort id, byte[] value) 22 | : this(id, ToException(value)) 23 | { 24 | } 25 | 26 | /// 27 | /// Initializes a new instance of the class 28 | /// with the specified identifier and an underlying exception. 29 | /// 30 | /// The unique identifier for the exception. 31 | /// The underlying exception instance. 32 | public RemoteFileSystemException(ushort id, Exception exception) 33 | : base(exception.Message, exception) 34 | { 35 | Id = id; 36 | } 37 | 38 | /// 39 | /// Serializes the provided instance into a byte array. 40 | /// 41 | /// The exception to serialize. 42 | /// A byte array representing the serialized exception. 43 | internal static byte[] GetBytes(Exception exception) 44 | { 45 | _ = exception ?? throw new ArgumentNullException(nameof(exception)); 46 | 47 | exception = (exception as RemoteFileSystemException)?.InnerException ?? exception; 48 | Type exceptionType = string.IsNullOrEmpty(exception.GetType().FullName) ? typeof(Exception) : exception.GetType(); 49 | string typeName = $"{exceptionType.FullName}, {exceptionType.Assembly.GetName()?.Name}"; 50 | string message = exception.Message ?? string.Empty; 51 | 52 | int typeNameByteCount = Encoding.UTF8.GetByteCount(typeName); 53 | int messageByteCount = Encoding.UTF8.GetByteCount(message); 54 | int byteCount = sizeof(int) + typeNameByteCount + messageByteCount; 55 | byte[] buffer = new byte[byteCount]; 56 | 57 | BitConverter.TryWriteBytes(buffer.AsSpan(0, sizeof(int)), typeNameByteCount); 58 | Encoding.UTF8.GetBytes(typeName, 0, typeName.Length, buffer, sizeof(int)); 59 | Encoding.UTF8.GetBytes(message, 0, message.Length, buffer, sizeof(int) + typeNameByteCount); 60 | return buffer; 61 | } 62 | 63 | /// 64 | /// Converts a serialized exception represented as a byte array back to an instance. 65 | /// 66 | /// The byte array representing the serialized exception. 67 | /// An instance based on the byte array content. 68 | internal static Exception ToException(byte[] value) 69 | { 70 | _ = value ?? throw new ArgumentNullException(nameof(value)); 71 | 72 | int typeNameByteCount = BitConverter.ToInt32(value.AsSpan(0, sizeof(int))); 73 | int messageByteCount = value.Length - sizeof(int) - typeNameByteCount; 74 | string typeName = Encoding.UTF8.GetString(value, sizeof(int), typeNameByteCount); 75 | string message = Encoding.UTF8.GetString(value, sizeof(int) + typeNameByteCount, messageByteCount); 76 | 77 | Exception? exception = null; 78 | #if !NATIVE_AOT 79 | try 80 | { 81 | Type? exceptionType = Type.GetType(typeName); 82 | if (!typeof(Exception).IsAssignableFrom(exceptionType)) 83 | exceptionType = typeof(Exception); 84 | 85 | exception = (Exception?)Activator.CreateInstance(exceptionType, message); 86 | } 87 | catch { } 88 | #endif 89 | 90 | exception ??= new(message); 91 | return exception; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/IO/RemoteFileSystemWatcher.Shared.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia.IO; 2 | 3 | /// 4 | /// Provides functionality for monitoring file system changes remotely. 5 | /// 6 | partial class RemoteFileSystemWatcher 7 | { 8 | /// 9 | /// Asynchronously reads a packet from the provided stream. 10 | /// 11 | /// The stream to read the packet from. 12 | /// The token to monitor for cancellation requests. 13 | /// A freshly received packet. 14 | private static async Task<(ActionType Action, byte[] Data)> ReadPacketAsync(Stream stream, CancellationToken cancellationToken = default) 15 | { 16 | const int bufferLength = sizeof(byte) + sizeof(int); 17 | byte[] buffer = new byte[bufferLength]; 18 | await stream.ReadExactlyAsync(buffer, 0, bufferLength, cancellationToken).ConfigureAwait(false); 19 | 20 | ActionType action = (ActionType)buffer[0]; 21 | int length = BitConverter.ToInt32(buffer.AsSpan(sizeof(byte), sizeof(int))); 22 | 23 | byte[] data = new byte[length]; 24 | await stream.ReadExactlyAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false); 25 | 26 | return (action, data); 27 | } 28 | 29 | /// 30 | /// Asynchronously writes a packet to the specified stream. 31 | /// 32 | /// The stream to write the packet to. 33 | /// The action type. 34 | /// The data to send. 35 | /// The token to monitor for cancellation requests. 36 | /// A task that represents the asynchronous write operation. 37 | private static async Task WritePacketAsync(Stream stream, ActionType action, byte[] data, CancellationToken cancellationToken = default) 38 | { 39 | const int bufferLength = sizeof(byte) + sizeof(int); 40 | byte[] buffer = new byte[bufferLength]; 41 | 42 | buffer[0] = (byte)action; 43 | BitConverter.TryWriteBytes(buffer.AsSpan(sizeof(byte), sizeof(int)), data.Length); 44 | 45 | await stream.WriteAsync(buffer, 0, bufferLength, cancellationToken).ConfigureAwait(false); 46 | await stream.WriteAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false); 47 | } 48 | 49 | /// 50 | /// Represents the type of actions that can be sent or received in file system watcher operations. 51 | /// 52 | private enum ActionType : byte 53 | { 54 | /// 55 | /// Indicates that the connection should be kept alive. 56 | /// 57 | KeepAlive = 0, 58 | 59 | /// 60 | /// Sets the path of the directory to watch. 61 | /// 62 | SetPath, 63 | 64 | /// 65 | /// Sets a value indicating whether the component is enabled. 66 | /// 67 | SetEnableRaisingEvents, 68 | 69 | /// 70 | /// Sets a value indicating whether subdirectories within the specified path should be monitored. 71 | /// 72 | SetIncludeSubdirectories, 73 | 74 | /// 75 | /// Sets the filter string used to determine what files are monitored in a directory. 76 | /// 77 | SetFilter, 78 | 79 | /// 80 | /// Sets the type of changes to watch for. 81 | /// 82 | SetNotifyFilter, 83 | 84 | /// 85 | /// Occurs when a file or directory is created. 86 | /// 87 | RaiseCreated, 88 | 89 | /// 90 | /// Occurs when a file or directory is deleted. 91 | /// 92 | RaiseDeleted, 93 | 94 | /// 95 | /// Occurs when a file or directory is changed. 96 | /// 97 | RaiseChanged, 98 | 99 | /// 100 | /// Occurs when a file or directory is renamed. 101 | /// 102 | RaiseRenamed, 103 | 104 | /// 105 | /// Occurs when the file system watcher is unable to continue monitoring changes. 106 | /// 107 | RaiseError, 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Reflection/DynamicAssembly.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Reflection.Emit; 3 | using HotAvalonia.Helpers; 4 | 5 | namespace HotAvalonia.Reflection; 6 | 7 | /// 8 | /// Encapsulates a dynamic assembly, providing mechanisms to manage 9 | /// runtime access to types and metadata within the assembly. 10 | /// 11 | public class DynamicAssembly 12 | { 13 | /// 14 | /// The underlying assembly. 15 | /// 16 | private readonly Assembly _assembly; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The assembly to wrap. 22 | public DynamicAssembly(Assembly assembly) 23 | { 24 | _assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); 25 | } 26 | 27 | /// 28 | /// Gets the name of the assembly. 29 | /// 30 | public string Name => _assembly.GetName()?.Name ?? string.Empty; 31 | 32 | /// 33 | /// Gets the underlying assembly. 34 | /// 35 | public Assembly Assembly => _assembly; 36 | 37 | /// 38 | /// Grants the dynamic assembly access to another assembly. 39 | /// 40 | /// The assembly to allow access to. 41 | public virtual void AllowAccessTo(Assembly assembly) 42 | { 43 | _ = assembly ?? throw new ArgumentNullException(nameof(assembly)); 44 | 45 | if (_assembly is AssemblyBuilder assemblyBuilder) 46 | assemblyBuilder.AllowAccessTo(assembly); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Reflection/Inject/CallbackParameterType.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace HotAvalonia.Reflection.Inject; 5 | 6 | /// 7 | /// Enumerates the types of special callback parameters identified by their attributes. 8 | /// 9 | internal enum CallbackParameterType 10 | { 11 | /// 12 | /// Indicates that the parameter is not specialized. 13 | /// 14 | None, 15 | 16 | /// 17 | /// Indicates that the parameter serves as the return value if the callback overrides the original method. 18 | /// 19 | CallbackResult, 20 | 21 | /// 22 | /// Indicates that the parameter receives the instance of the object that is the caller of the method. 23 | /// 24 | Caller, 25 | 26 | /// 27 | /// Indicates that the parameter receives the metadata of the member that is the caller of the method. 28 | /// 29 | CallerMember, 30 | 31 | /// 32 | /// Indicates that the parameter receives the name of the member that is the caller of the method. 33 | /// 34 | CallerMemberName, 35 | } 36 | 37 | /// 38 | /// Provides extension methods for the enumeration. 39 | /// 40 | internal static class CallbackParameterTypeExtensions 41 | { 42 | /// 43 | /// Determines the for a given parameter based on its attributes. 44 | /// 45 | /// The parameter to inspect. 46 | /// The identified for the parameter. 47 | public static CallbackParameterType GetCallbackParameterType(this ParameterInfo parameter) 48 | { 49 | foreach (CustomAttributeData customAttribute in parameter.CustomAttributes) 50 | { 51 | switch (customAttribute.AttributeType.Name) 52 | { 53 | case nameof(CallbackResultAttribute) when parameter.IsOut: 54 | return CallbackParameterType.CallbackResult; 55 | 56 | case nameof(CallerAttribute): 57 | return CallbackParameterType.Caller; 58 | 59 | case nameof(CallerMemberAttribute) when typeof(MemberInfo).IsAssignableFrom(parameter.ParameterType): 60 | return CallbackParameterType.CallerMember; 61 | 62 | case nameof(CallerMemberNameAttribute) when parameter.ParameterType == typeof(string): 63 | return CallbackParameterType.CallerMemberName; 64 | } 65 | } 66 | 67 | return CallbackParameterType.None; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Reflection/Inject/CallbackResultAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia.Reflection.Inject; 2 | 3 | /// 4 | /// Allows you to specify a parameter that will serve as the return value if the callback overrides the original method. 5 | /// 6 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] 7 | internal sealed class CallbackResultAttribute : Attribute 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Reflection/Inject/CallerAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia.Reflection.Inject; 2 | 3 | /// 4 | /// Allows you to obtain the instance of the object that is the caller of the method. 5 | /// 6 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] 7 | internal sealed class CallerAttribute : Attribute 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Reflection/Inject/CallerMemberAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia.Reflection.Inject; 2 | 3 | /// 4 | /// Allows you to obtain the metadata of the member that is the caller of the method. 5 | /// 6 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] 7 | internal sealed class CallerMemberAttribute : Attribute 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Reflection/Inject/IInjection.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia.Reflection.Inject; 2 | 3 | /// 4 | /// Represents a method injection technique. 5 | /// 6 | /// 7 | /// Implementations of this interface should handle the injection process and 8 | /// properly manage all the associated resources. 9 | /// 10 | /// When the injection is no longer needed, the instance should be disposed 11 | /// in order to release any allocated resources and revert all the effects 12 | /// caused by the injection. 13 | /// 14 | internal interface IInjection : IDisposable; 15 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Reflection/Inject/InjectionType.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia.Reflection.Inject; 2 | 3 | /// 4 | /// Represents the different types of injection techniques that can be performed. 5 | /// 6 | internal enum InjectionType 7 | { 8 | /// 9 | /// Indicates that no injection technique is available in the current environment. 10 | /// 11 | None, 12 | 13 | /// 14 | /// Represents a native-level injection. 15 | /// 16 | /// 17 | /// This technique hijacks the natively compiled (by JIT) methods and 18 | /// replaces them with stubs that lead to injected methods. 19 | /// 20 | /// It is the most reliable technique, however it's highly sensitive to such things as 21 | /// the runtime version, system architecture (e.g., x86, x86_64), etc., 22 | /// making it much less portable. 23 | /// 24 | Native, 25 | } 26 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Xaml/CompileXamlFunc.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Markup.Xaml; 2 | 3 | namespace HotAvalonia.Xaml; 4 | 5 | /// 6 | /// Compiles XAML documents into a collection of types. 7 | /// 8 | /// A read-only collection of XAML documents to compile. 9 | /// The configuration settings for the XAML compiler. 10 | /// An enumerable collection of compiled types. 11 | internal delegate IEnumerable CompileXamlFunc(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration config); 12 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Xaml/NamedControlReference.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Avalonia.Controls; 3 | using Avalonia.LogicalTree; 4 | 5 | namespace HotAvalonia.Xaml; 6 | 7 | /// 8 | /// Represents a reference to a named control. 9 | /// 10 | internal sealed class NamedControlReference 11 | { 12 | /// 13 | /// The name of the control. 14 | /// 15 | private readonly string _name; 16 | 17 | /// 18 | /// The type of the control. 19 | /// 20 | private readonly Type _controlType; 21 | 22 | /// 23 | /// A cache field associated with this control reference. 24 | /// 25 | private readonly FieldInfo? _cache; 26 | 27 | /// 28 | public NamedControlReference(string name, Type type) 29 | { 30 | _name = name ?? throw new ArgumentNullException(nameof(name)); 31 | _controlType = type ?? throw new ArgumentNullException(nameof(type)); 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | /// The name of the control. 38 | /// The type of the control. 39 | /// A cache field associated with this control reference. 40 | internal NamedControlReference(string name, Type type, FieldInfo? cache) 41 | : this(name, type) 42 | { 43 | _cache = cache; 44 | } 45 | 46 | /// 47 | /// The name of the control. 48 | /// 49 | public string Name => _name; 50 | 51 | /// 52 | /// The type of the control. 53 | /// 54 | public Type ControlType => _controlType; 55 | 56 | /// 57 | /// Resolves the named control within the specified scope. 58 | /// 59 | /// The scope within which to resolve the control. 60 | /// The resolved control, if found; otherwise, null. 61 | public object? Resolve(object scope) 62 | { 63 | _ = scope ?? throw new ArgumentNullException(nameof(scope)); 64 | 65 | object? control = (scope as ILogical)?.FindNameScope()?.Find(_name); 66 | return _controlType.IsAssignableFrom(control?.GetType()) ? control : null; 67 | } 68 | 69 | /// 70 | /// Invalidates the internal cache associated with this reference 71 | /// within the specified scope. 72 | /// 73 | /// The scope within which to refresh the cache. 74 | internal void Refresh(object scope) 75 | { 76 | _ = scope ?? throw new ArgumentNullException(nameof(scope)); 77 | if (_cache is { IsStatic: false } && !_cache.DeclaringType.IsAssignableFrom(scope.GetType())) 78 | return; 79 | 80 | _cache?.SetValue(scope, Resolve(scope)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/HotAvalonia.Core/Xaml/XamlDocument.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace HotAvalonia.Xaml; 4 | 5 | /// 6 | /// Represents a XAML document and its contents. 7 | /// 8 | public readonly struct XamlDocument 9 | { 10 | /// 11 | /// Gets the URI associated with the XAML document. 12 | /// 13 | public Uri Uri { get; } 14 | 15 | /// 16 | /// Gets the stream containing the XAML content. 17 | /// 18 | public Stream Stream { get; } 19 | 20 | /// 21 | public XamlDocument(Uri uri, Stream stream) 22 | { 23 | Uri = uri; 24 | Stream = stream; 25 | } 26 | 27 | /// 28 | public XamlDocument(Uri uri, string xaml) 29 | { 30 | Uri = uri; 31 | Stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml)); 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of the struct. 36 | /// 37 | /// The URI associated with the document. 38 | /// The stream containing the XAML content. 39 | public XamlDocument(string uri, Stream stream) 40 | { 41 | Uri = new(uri); 42 | Stream = stream; 43 | } 44 | 45 | /// 46 | /// Initializes a new instance of the struct. 47 | /// 48 | /// The URI associated with the document. 49 | /// The string containing the XAML content. 50 | public XamlDocument(string uri, string xaml) 51 | { 52 | Uri = new(uri); 53 | Stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/HotAvalonia.Extensions/HotAvalonia.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 8.0 6 | disable 7 | false 8 | $(NoWarn);NU5128 9 | 10 | 11 | 12 | true 13 | A companion library for HotAvalonia that provides extension methods for Avalonia.AppBuilder and Avalonia.Application, designed to make it easy to enable or disable hot reload for your application. 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/HotAvalonia.Extensions/HotAvalonia.Extensions.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/Cecil/CecilField.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Mono.Cecil; 3 | 4 | namespace HotAvalonia.Fody.Cecil; 5 | 6 | /// 7 | /// Encapsulates a field within a type definition. 8 | /// 9 | internal sealed class CecilField 10 | { 11 | /// 12 | /// The type that declares this field. 13 | /// 14 | private readonly CecilType _declaringType; 15 | 16 | /// 17 | /// A function that selects the field definition from a . 18 | /// 19 | private readonly Func _selector; 20 | 21 | /// 22 | /// The cached field definition, if any. 23 | /// 24 | private FieldDefinition? _definition; 25 | 26 | /// 27 | /// The cached field reference, if any. 28 | /// 29 | private FieldReference? _reference; 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The type that declares this field. 35 | /// A function that selects a field definition from a . 36 | public CecilField(CecilType declaringType, Func selector) 37 | { 38 | _declaringType = declaringType; 39 | _selector = selector; 40 | } 41 | 42 | /// 43 | /// Implicitly converts a to a . 44 | /// 45 | /// The to convert. 46 | /// A instance representing the specified field. 47 | [return: NotNullIfNotNull(nameof(field))] 48 | public static implicit operator FieldDefinition?(CecilField? field) => field?.Definition; 49 | 50 | /// 51 | /// Implicitly converts a to a . 52 | /// 53 | /// The to convert. 54 | /// A instance representing the specified field. 55 | [return: NotNullIfNotNull(nameof(field))] 56 | public static implicit operator FieldReference?(CecilField? field) => field?.Reference; 57 | 58 | /// 59 | /// Gets the name of the field. 60 | /// 61 | public string Name => Definition.Name; 62 | 63 | /// 64 | /// Gets the type that declares this field. 65 | /// 66 | public CecilType DeclaringType => _declaringType; 67 | 68 | /// 69 | /// Gets the field definition. 70 | /// 71 | public FieldDefinition Definition => _definition ??= _selector(_declaringType.Definition) ?? throw new MissingFieldException(); 72 | 73 | /// 74 | /// Gets the imported field reference. 75 | /// 76 | public FieldReference Reference => _reference ??= ImportFieldReference(_declaringType, Definition); 77 | 78 | /// 79 | /// Imports a field reference using the declaring type's resolver. 80 | /// 81 | /// The declaring type of the field. 82 | /// The field definition to import. 83 | /// The imported . 84 | private static FieldReference ImportFieldReference(CecilType type, FieldDefinition definition) 85 | { 86 | FieldReference baseReference = type.TypeResolver.ModuleDefinition.ImportReference(definition); 87 | if (!type.WeakType.IsGenericType || type.WeakType.IsGenericTypeDefinition) 88 | return baseReference; 89 | 90 | FieldReference genericReference = new(baseReference.Name, baseReference.FieldType, type.Reference); 91 | return type.TypeResolver.ModuleDefinition.ImportReference(genericReference, type.Reference); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/Cecil/CecilMethod.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Mono.Cecil; 3 | 4 | namespace HotAvalonia.Fody.Cecil; 5 | 6 | /// 7 | /// Encapsulates a method within a type definition. 8 | /// 9 | internal sealed class CecilMethod 10 | { 11 | /// 12 | /// The type that declares this method. 13 | /// 14 | private readonly CecilType _declaringType; 15 | 16 | /// 17 | /// A function that selects the method definition from a . 18 | /// 19 | private readonly Func _selector; 20 | 21 | /// 22 | /// The cached method definition, if any. 23 | /// 24 | private MethodDefinition? _definition; 25 | 26 | /// 27 | /// The cached method reference, if any. 28 | /// 29 | private MethodReference? _reference; 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The type that declares this method. 35 | /// A function that selects a method definition from a . 36 | public CecilMethod(CecilType declaringType, Func selector) 37 | { 38 | _declaringType = declaringType; 39 | _selector = selector; 40 | } 41 | 42 | /// 43 | /// Implicitly converts a to a . 44 | /// 45 | /// The to convert. 46 | /// A instance representing the specified method. 47 | [return: NotNullIfNotNull(nameof(method))] 48 | public static implicit operator MethodDefinition?(CecilMethod? method) => method?.Definition; 49 | 50 | /// 51 | /// Implicitly converts a to a . 52 | /// 53 | /// The to convert. 54 | /// A instance representing the specified method. 55 | [return: NotNullIfNotNull(nameof(method))] 56 | public static implicit operator MethodReference?(CecilMethod? method) => method?.Reference; 57 | 58 | /// 59 | /// Gets the name of the method. 60 | /// 61 | public string Name => Definition.Name; 62 | 63 | /// 64 | /// Gets the type that declares this method. 65 | /// 66 | public CecilType DeclaringType => _declaringType; 67 | 68 | /// 69 | /// Gets the method definition. 70 | /// 71 | public MethodDefinition Definition => _definition ??= _selector(_declaringType.Definition) ?? throw new MissingMethodException(); 72 | 73 | /// 74 | /// Gets the imported method reference. 75 | /// 76 | public MethodReference Reference => _reference ??= ImportMethodReference(_declaringType, Definition); 77 | 78 | /// 79 | /// Imports a method reference from using the declaring type's resolver. 80 | /// 81 | /// The declaring type of the method. 82 | /// The method definition to import. 83 | /// The imported . 84 | private static MethodReference ImportMethodReference(CecilType type, MethodDefinition definition) 85 | { 86 | MethodReference baseReference = type.TypeResolver.ModuleDefinition.ImportReference(definition); 87 | if (!type.WeakType.IsGenericType || type.WeakType.IsGenericTypeDefinition) 88 | return baseReference; 89 | 90 | MethodReference genericReference = new(baseReference.Name, baseReference.ReturnType) 91 | { 92 | DeclaringType = type.Reference, 93 | HasThis = baseReference.HasThis, 94 | ExplicitThis = baseReference.ExplicitThis, 95 | CallingConvention = baseReference.CallingConvention, 96 | }; 97 | foreach (ParameterDefinition parameter in baseReference.Parameters) 98 | genericReference.Parameters.Add(parameter); 99 | 100 | return type.TypeResolver.ModuleDefinition.ImportReference(genericReference, type.Reference); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/Cecil/CecilProperty.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Mono.Cecil; 3 | 4 | namespace HotAvalonia.Fody.Cecil; 5 | 6 | /// 7 | /// Encapsulates a property within a type definition. 8 | /// 9 | internal sealed class CecilProperty 10 | { 11 | /// 12 | /// The type that declares this property. 13 | /// 14 | private readonly CecilType _declaringType; 15 | 16 | /// 17 | /// A function that selects the property definition from a . 18 | /// 19 | private readonly Func _selector; 20 | 21 | /// 22 | /// The cached property definition, if any. 23 | /// 24 | private PropertyDefinition? _definition; 25 | 26 | /// 27 | /// The cached method representing the getter of the property, if any. 28 | /// 29 | private CecilMethod? _getMethod; 30 | 31 | /// 32 | /// The cached method representing the setter of the property, if any. 33 | /// 34 | private CecilMethod? _setMethod; 35 | 36 | /// 37 | /// Initializes a new instance of the class. 38 | /// 39 | /// The type that declares this property. 40 | /// A function that selects a property definition from a . 41 | public CecilProperty(CecilType declaringType, Func selector) 42 | { 43 | _declaringType = declaringType; 44 | _selector = selector; 45 | } 46 | 47 | /// 48 | /// Implicitly converts a to a . 49 | /// 50 | /// The to convert. 51 | /// A instance representing the specified property. 52 | [return: NotNullIfNotNull(nameof(property))] 53 | public static implicit operator PropertyDefinition?(CecilProperty? property) => property?.Definition; 54 | 55 | /// 56 | /// Implicitly converts a to a . 57 | /// 58 | /// The to convert. 59 | /// A instance representing the specified property. 60 | [return: NotNullIfNotNull(nameof(property))] 61 | public static implicit operator PropertyReference?(CecilProperty? property) => property?.Reference; 62 | 63 | /// 64 | /// Gets the name of the property. 65 | /// 66 | public string Name => Definition.Name; 67 | 68 | /// 69 | /// Gets the type that declares this property. 70 | /// 71 | public CecilType DeclaringType => _declaringType; 72 | 73 | /// 74 | /// Gets the method used to retrieve the property value, if available. 75 | /// 76 | public CecilMethod? GetMethod => _getMethod ??= Definition.GetMethod is { } m ? new(_declaringType, _ => m) : null; 77 | 78 | /// 79 | /// Gets the method used to set the property value, if available. 80 | /// 81 | public CecilMethod? SetMethod => _setMethod ??= Definition.SetMethod is { } m ? new(_declaringType, _ => m) : null; 82 | 83 | /// 84 | /// Gets the property definition. 85 | /// 86 | public PropertyDefinition Definition => _definition ??= _selector(_declaringType.Definition) ?? throw new MissingMemberException(); 87 | 88 | /// 89 | /// Gets the imported property reference. 90 | /// 91 | public PropertyReference Reference => Definition; 92 | } 93 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/Cecil/ITypeResolver.cs: -------------------------------------------------------------------------------- 1 | using Mono.Cecil; 2 | 3 | namespace HotAvalonia.Fody.Cecil; 4 | 5 | /// 6 | /// Provides methods to resolve type definitions and module definitions. 7 | /// 8 | internal interface ITypeResolver 9 | { 10 | /// 11 | /// Gets the module definition associated with the resolver. 12 | /// 13 | ModuleDefinition ModuleDefinition { get; } 14 | 15 | /// 16 | /// Finds the type definition for the specified type name. 17 | /// 18 | /// The full name of the type to locate. 19 | /// The corresponding to the specified name. 20 | TypeDefinition FindTypeDefinition(string name); 21 | } 22 | 23 | /// 24 | /// Provides extension methods for . 25 | /// 26 | internal static class TypeResolverExtensions 27 | { 28 | /// 29 | /// Gets the corresponding to the specified . 30 | /// 31 | /// The type resolver used to resolve the type. 32 | /// The weak type to resolve. 33 | /// A representing the resolved type. 34 | public static CecilType GetType(this ITypeResolver resolver, WeakType type) => new(type, resolver); 35 | } 36 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/Cecil/TypeName.cs: -------------------------------------------------------------------------------- 1 | using Mono.Cecil; 2 | 3 | namespace HotAvalonia.Fody.Cecil; 4 | 5 | /// 6 | /// Represents a type name. 7 | /// 8 | internal readonly struct TypeName : IEquatable, IEquatable, IEquatable, IEquatable, IEquatable 9 | { 10 | /// 11 | /// Gets the full name of the type. 12 | /// 13 | public string FullName { get; } 14 | 15 | /// 16 | /// Initializes a new instance of the struct. 17 | /// 18 | /// The full name of the type. 19 | private TypeName(string fullName) => FullName = fullName; 20 | 21 | /// 22 | /// Implicitly converts a to a . 23 | /// 24 | /// The instance to convert. 25 | /// The full name of the type as a string. 26 | public static implicit operator string(TypeName typeName) => typeName.FullName; 27 | 28 | /// 29 | /// Implicitly converts a to a . 30 | /// 31 | /// The full name of the type. 32 | /// A new instance. 33 | public static implicit operator TypeName(string fullName) => new(fullName.Contains('[') ? FixGenericTypeName(fullName) : fullName); 34 | 35 | /// 36 | /// Implicitly converts a to a . 37 | /// 38 | /// The to convert. 39 | /// A new instance representing the specified type. 40 | public static implicit operator TypeName(Type type) => new(type.IsGenericType && !type.IsGenericTypeDefinition ? FixGenericTypeName(type.ToString()) : type.FullName); 41 | 42 | /// 43 | /// Implicitly converts a to a . 44 | /// 45 | /// The to convert. 46 | /// A new instance representing the specified type. 47 | public static implicit operator TypeName(WeakType type) => new(type.IsGenericType && !type.IsGenericTypeDefinition ? FixGenericTypeName(type.ToString()) : type.FullName); 48 | 49 | /// 50 | /// Implicitly converts a to a . 51 | /// 52 | /// The to convert. 53 | /// A new instance representing the specified type. 54 | public static implicit operator TypeName(TypeReference type) => new(type.IsGenericInstance ? type.ToString() : type.FullName); 55 | 56 | /// 57 | /// Determines whether two instances have the same full name. 58 | /// 59 | /// The left instance. 60 | /// The right instance. 61 | /// true if the full names are equal; otherwise, false. 62 | public static bool operator ==(TypeName left, TypeName right) => left.FullName == right.FullName; 63 | 64 | /// 65 | /// Determines whether two instances do not have the same full name. 66 | /// 67 | /// The left instance. 68 | /// The right instance. 69 | /// true if the full names are not equal; otherwise, false. 70 | public static bool operator !=(TypeName left, TypeName right) => left.FullName != right.FullName; 71 | 72 | /// 73 | public bool Equals(TypeName other) => this == other; 74 | 75 | /// 76 | public bool Equals(string other) => this == other; 77 | 78 | /// 79 | public bool Equals(Type other) => this == other; 80 | 81 | /// 82 | public bool Equals(WeakType other) => this == other; 83 | 84 | /// 85 | public bool Equals(TypeReference other) => this == other; 86 | 87 | /// 88 | public override bool Equals(object? obj) => obj switch 89 | { 90 | TypeName cecilType => cecilType == this, 91 | Type type => type == this, 92 | WeakType type => type == this, 93 | string fullName => fullName == this, 94 | TypeReference typeRef => typeRef == this, 95 | _ => false, 96 | }; 97 | 98 | /// 99 | public override int GetHashCode() => FullName.GetHashCode(); 100 | 101 | /// 102 | public override string ToString() => FullName; 103 | 104 | /// 105 | /// Replaces square brackets with angle brackets in generic type names. 106 | /// 107 | /// 108 | /// Cecil is weird sometimes and uses '<>' instead of '[]' for some reason. 109 | /// 110 | /// The type name potentially containing square brackets. 111 | /// The fixed type name with angle brackets instead of square brackets. 112 | private static string FixGenericTypeName(string name) => name.Replace('[', '<').Replace(']', '>'); 113 | } 114 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/Helpers/MethodReferenceHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using HotAvalonia.Fody.Cecil; 3 | using Mono.Cecil; 4 | using Mono.Cecil.Cil; 5 | 6 | namespace HotAvalonia.Fody.Helpers; 7 | 8 | /// 9 | /// Provides extension methods and related functionality for working with method references. 10 | /// 11 | internal static class MethodReferenceHelper 12 | { 13 | /// 14 | /// Replaces references to the target method with references 15 | /// to the replacement method in the specified method body. 16 | /// 17 | /// The method in which to replace references. 18 | /// The original target method reference. 19 | /// The replacement method reference. 20 | public static void ReplaceMethodReferences(this MethodDefinition? method, MethodReference target, MethodReference replacement) 21 | { 22 | if (method is not { HasBody: true }) 23 | return; 24 | 25 | foreach (Instruction instruction in method.Body.Instructions) 26 | { 27 | // Obviously, determining method equality solely by name is not correct. However, it suits our 28 | // needs in this simple case, and I couldn't be bothered to implement it properly right now. 29 | // So what? Bite me. 30 | if (instruction.Operand is MethodReference callee && callee.Name == target.Name) 31 | instruction.Operand = replacement; 32 | } 33 | } 34 | 35 | /// 36 | /// Attempts to locate the first call instruction within the method that calls a method with the specified return type. 37 | /// 38 | /// The method definition in which to search for the call instruction. 39 | /// The expected return type of the called method. 40 | /// 41 | /// When this method returns, contains the first matching the criteria, 42 | /// or null if no matching instruction was found. 43 | /// 44 | /// 45 | /// true if a matching call instruction was found; otherwise, false. 46 | /// 47 | public static bool TryGetCallInstruction(this MethodDefinition? method, TypeName returnType, [NotNullWhen(true)] out Instruction? call) 48 | { 49 | call = method is not { HasBody: true } ? null : method.Body.Instructions.FirstOrDefault(x => x is { OpCode.Code: Code.Call or Code.Callvirt, Operand: MethodReference callee } && callee.ReturnType == returnType); 50 | return call is not null; 51 | } 52 | 53 | /// 54 | /// Attempts to locate the last call instruction within the method that calls a method with the specified return type. 55 | /// 56 | /// The method definition in which to search for the call instruction. 57 | /// The expected return type of the called method. 58 | /// 59 | /// When this method returns, contains the last matching the criteria, 60 | /// or null if no matching instruction was found. 61 | /// 62 | /// 63 | /// true if a matching call instruction was found; otherwise, false. 64 | /// 65 | public static bool TryGetLastCallInstruction(this MethodDefinition? method, TypeName returnType, [NotNullWhen(true)] out Instruction? call) 66 | { 67 | call = method is not { HasBody: true } ? null : method.Body.Instructions.LastOrDefault(x => x is { OpCode.Code: Code.Call or Code.Callvirt, Operand: MethodReference callee } && callee.ReturnType == returnType); 68 | return call is not null; 69 | } 70 | 71 | /// 72 | /// Attempts to retrieve a single return instruction from the method's body. 73 | /// 74 | /// The method definition in which to search for the return instruction. 75 | /// 76 | /// When this method returns, contains the single representing a return operation, 77 | /// or null if there is not exactly one return instruction. 78 | /// 79 | /// 80 | /// true if exactly one return instruction was found; otherwise, false. 81 | /// 82 | public static bool TryGetSingleRetInstruction(this MethodDefinition? method, [NotNullWhen(true)] out Instruction? ret) 83 | { 84 | ret = method is not { HasBody: true } ? null : method.Body.Instructions.Where(x => x.OpCode.Code is Code.Ret).Select((x, i) => i == 0 ? x : null).Take(2).LastOrDefault(); 85 | return ret is not null; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/Helpers/ModuleReferenceHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using HotAvalonia.Fody.Cecil; 3 | using Mono.Cecil; 4 | 5 | namespace HotAvalonia.Fody.Helpers; 6 | 7 | /// 8 | /// Provides extension methods and related functionality for working with module references. 9 | /// 10 | internal static class ModuleReferenceHelper 11 | { 12 | /// 13 | public static IEnumerable GetMethods(this ModuleDefinition? module, BindingFlags bindingFlags) 14 | => module.GetMethodsCore(null, bindingFlags, null); 15 | 16 | /// 17 | public static IEnumerable GetMethods(this ModuleDefinition? module, string name) 18 | => module.GetMethodsCore(name, BindingFlags.Default, null); 19 | 20 | /// 21 | public static IEnumerable GetMethods(this ModuleDefinition? module, string name, BindingFlags bindingFlags) 22 | => module.GetMethodsCore(name, bindingFlags, null); 23 | 24 | /// 25 | public static IEnumerable GetMethods(this ModuleDefinition? module, string name, TypeName[] parameterTypes) 26 | => module.GetMethodsCore(name, BindingFlags.Default, parameterTypes); 27 | 28 | /// 29 | public static IEnumerable GetMethods(this ModuleDefinition? module, string name, TypeName[] parameterTypes, TypeName returnType) 30 | => module.GetMethods(name, BindingFlags.Default, parameterTypes, returnType); 31 | 32 | /// 33 | public static IEnumerable GetMethods(this ModuleDefinition? module, string name, BindingFlags bindingFlags, TypeName[] parameterTypes) 34 | => module.GetMethodsCore(name, bindingFlags, parameterTypes); 35 | 36 | /// 37 | /// The return type of the methods. 38 | public static IEnumerable GetMethods(this ModuleDefinition? module, string name, BindingFlags bindingFlags, TypeName[] parameterTypes, TypeName returnType) 39 | => module.GetMethodsCore(name, bindingFlags, parameterTypes).Where(x => x.ReturnType == returnType); 40 | 41 | /// 42 | /// Searches for the methods defined in the given that match the specified constraints. 43 | /// 44 | /// The module to search for methods. 45 | /// The name of the methods to find. 46 | /// A bitwise combination of the enumeration values that specify how the search is conducted. 47 | /// An array of objects representing the expected parameter types. 48 | /// An enumerable containing methods defined in the given module that match the specified constraints. 49 | internal static IEnumerable GetMethodsCore(this ModuleDefinition? module, string? name, BindingFlags bindingFlags, TypeName[]? parameterTypes) 50 | { 51 | if (module is not { HasTypes: true }) 52 | return []; 53 | 54 | return module.Types.SelectMany(x => x.GetMethodsCore(name, bindingFlags, parameterTypes)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/Helpers/StringHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace HotAvalonia.Fody.Helpers; 4 | 5 | /// 6 | /// Provides helper methods for working with strings. 7 | /// 8 | internal static class StringHelper 9 | { 10 | /// 11 | /// Attempts to decode the provided Base64-encoded string into a byte array. 12 | /// 13 | /// The Base64-encoded string to decode. 14 | /// 15 | /// When this method returns, contains the decoded byte array 16 | /// if the conversion succeeded; otherwise, null. 17 | /// 18 | /// 19 | /// true if the string was successfully decoded; 20 | /// otherwise, false. 21 | /// 22 | public static bool TryGetBase64Bytes(string value, [NotNullWhen(true)] out byte[]? bytes) 23 | { 24 | try 25 | { 26 | bytes = Convert.FromBase64String(value.Trim()); 27 | return true; 28 | } 29 | catch 30 | { 31 | bytes = null; 32 | return false; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/HotAvalonia.Fody.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | weavers 6 | $(NoWarn);NU5100;NU5128 7 | true 8 | 9 | 10 | 11 | true 12 | A Fody weaver created to overcome certain limitations that would otherwise render hot reload infeasible on non-x64 devices, while also enhancing the overall experience for HotAvalonia users. 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/HotAvalonia.Fody.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/MSBuild/MSBuildFile.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia.Fody.MSBuild; 2 | 3 | /// 4 | /// Represents an MSBuild file and provides basic file-related properties. 5 | /// 6 | public class MSBuildFile 7 | { 8 | /// 9 | /// The full path of the file. 10 | /// 11 | private readonly string _fullPath; 12 | 13 | /// 14 | /// The cached content of the file, if any. 15 | /// 16 | private string? _content; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The path to the file. 22 | public MSBuildFile(string path) 23 | { 24 | _fullPath = string.IsNullOrEmpty(path) ? string.Empty : Path.GetFullPath(path); 25 | _content = null; 26 | } 27 | 28 | /// 29 | /// Gets a value indicating whether the file exists at the specified path. 30 | /// 31 | public bool Exists => !string.IsNullOrEmpty(_fullPath) && File.Exists(_fullPath); 32 | 33 | /// 34 | /// Gets the full path of the file. 35 | /// 36 | public string FullPath => _fullPath; 37 | 38 | /// 39 | /// Gets the file name extracted from the full path. 40 | /// 41 | public string FileName => string.IsNullOrEmpty(_fullPath) ? string.Empty : Path.GetFileName(_fullPath); 42 | 43 | /// 44 | /// Gets the directory name where the file is located. 45 | /// 46 | public string DirectoryName => string.IsNullOrEmpty(_fullPath) ? string.Empty : Path.GetDirectoryName(_fullPath); 47 | 48 | /// 49 | /// Gets the content of the file. 50 | /// 51 | /// 52 | /// If the file does not exist, an empty string is returned. 53 | /// 54 | public string Content => _content ??= Exists ? File.ReadAllText(_fullPath) : string.Empty; 55 | 56 | /// 57 | public override bool Equals(object? obj) => obj is MSBuildFile file && file._fullPath == _fullPath; 58 | 59 | /// 60 | public override int GetHashCode() => _fullPath.GetHashCode(); 61 | 62 | /// 63 | public override string ToString() => _fullPath; 64 | } 65 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/MSBuild/MSBuildProject.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia.Fody.MSBuild; 2 | 3 | /// 4 | /// Represents an MSBuild project file. 5 | /// 6 | public sealed class MSBuildProject : MSBuildFile 7 | { 8 | /// 9 | /// The cached assembly name of the project, if any. 10 | /// 11 | private string? _assemblyName; 12 | 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// The path to the project file. 17 | public MSBuildProject(string path) : base(path) 18 | { 19 | _assemblyName = null; 20 | } 21 | 22 | /// 23 | /// Gets the assembly name associated with the project. 24 | /// 25 | public string AssemblyName => _assemblyName ??= ReadAssemblyName(); 26 | 27 | /// 28 | /// Reads and parses the assembly name from the project file content. 29 | /// 30 | /// 31 | /// The assembly name extracted from the project file. 32 | /// 33 | private string ReadAssemblyName() 34 | { 35 | // 99% of projects out there default to using 36 | // the project file name as the assembly name. 37 | // 38 | // Also, there are a few strange folks who like 39 | // to specify the assembly name explicitly, as in: 40 | // ProjectFileNameWithoutExtension 41 | // 42 | // However, if neither of the above applies, we can't determine 43 | // what the assembly name is supposed to be without actually 44 | // running MSBuild, which, obviously, isn't an option. 45 | // 46 | // Honestly, there's also an edge case where 47 | // might be specified outside of the project file 48 | // (e.g., in a .props or .targets file), 49 | // but if your setup is THAT weird - my man, that's on you! 50 | string assemblyName = Path.GetFileNameWithoutExtension(FullPath); 51 | if (!Content.Contains("")) 52 | return assemblyName; 53 | 54 | if (Content.Contains($"{assemblyName}")) 55 | return assemblyName; 56 | 57 | return string.Empty; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/MSBuild/MSBuildSolution.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace HotAvalonia.Fody.MSBuild; 5 | 6 | /// 7 | /// Represents an MSBuild solution file and provides access to the projects it contains. 8 | /// 9 | public sealed class MSBuildSolution : MSBuildFile 10 | { 11 | /// 12 | /// The cached array of projects contained in the solution, if any. 13 | /// 14 | private MSBuildProject[]? _projects; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The path to the solution file. 20 | public MSBuildSolution(string path) : base(path) 21 | { 22 | _projects = null; 23 | } 24 | 25 | /// 26 | /// Gets the projects defined in the solution. 27 | /// 28 | public IEnumerable Projects => _projects ??= ReadProjects().ToArray(); 29 | 30 | /// 31 | /// Reads and parses the solution file content to retrieve the projects it contains. 32 | /// 33 | /// 34 | /// A collection of instances representing the projects in the solution. 35 | /// 36 | private IEnumerable ReadProjects() 37 | { 38 | // If you're thinking, "Oh, parsing files with regex is baaaaaad", 39 | // please first take a look at the monstrosity that is the .sln spec. 40 | // Then, try saying that to my face while staring deep into my sleep-deprived eyes. 41 | string directoryName = DirectoryName; 42 | return Regex.Matches(Content, "\\\"([^\"]+\\.[^\"]*proj)\\\"") 43 | .Cast() 44 | .Select(x => x.Groups[1].Value) 45 | .Where(x => !string.IsNullOrEmpty(x)) 46 | .Select(x => Uri.UnescapeDataString(x.Replace('\\', Path.DirectorySeparatorChar))) 47 | .Select(x => new MSBuildProject(Path.Combine(directoryName, x))); 48 | } 49 | 50 | /// 51 | /// Attempts to locate a solution file in the specified directory. 52 | /// 53 | /// The directory path to search for a solution file. 54 | /// 55 | /// When this method returns, contains the first 56 | /// found in the directory, if one exists; otherwise, null. 57 | /// 58 | /// 59 | /// true if a solution file was found; otherwise, false. 60 | /// 61 | public static bool TryGetFromDirectory(string? path, [NotNullWhen(true)] out MSBuildSolution? solution) 62 | { 63 | solution = null; 64 | try 65 | { 66 | if (path is { Length: > 0 } && Directory.Exists(path)) 67 | solution = Directory.EnumerateFiles(path, "*.sln", SearchOption.TopDirectoryOnly).Select(x => new MSBuildSolution(x)).FirstOrDefault(); 68 | } 69 | catch { } 70 | 71 | return solution is not null; 72 | } 73 | 74 | /// 75 | /// Attempts to locate a top-level solution file by searching upward 76 | /// in the directory hierarchy starting from the specified path. 77 | /// 78 | /// The starting directory path for the search. 79 | /// 80 | /// When this method returns, contains the top-level if found; 81 | /// otherwise, null. 82 | /// 83 | /// 84 | /// true if a top-level solution file was found; otherwise, false. 85 | /// 86 | public static bool TryGetTopLevel(string? path, [NotNullWhen(true)] out MSBuildSolution? solution) 87 | { 88 | while (path is { Length: > 0 } && Directory.Exists(path)) 89 | { 90 | if (TryGetFromDirectory(path, out solution)) 91 | return true; 92 | 93 | path = Path.GetDirectoryName(path); 94 | } 95 | 96 | solution = null; 97 | return false; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/ModuleWeaver.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | using Fody; 3 | using HotAvalonia.Fody.Cecil; 4 | using HotAvalonia.Fody.MSBuild; 5 | 6 | namespace HotAvalonia.Fody; 7 | 8 | /// 9 | /// Represents the main module weaver that orchestrates feature-specific weaving tasks. 10 | /// 11 | public sealed class ModuleWeaver : BaseModuleWeaver, ITypeResolver 12 | { 13 | /// 14 | /// The collection of feature weavers used to perform specific weaving tasks. 15 | /// 16 | private readonly FeatureWeaver[] _features; 17 | 18 | /// 19 | /// The target solution, if any. 20 | /// 21 | private MSBuildSolution? _solution; 22 | 23 | /// 24 | /// The target project, if any. 25 | /// 26 | private MSBuildProject? _project; 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | public ModuleWeaver() => _features = 32 | [ 33 | new FileSystemCredentialsWeaver(this), 34 | new PopulateOverrideWeaver(this), 35 | new ReferencesWeaver(this), 36 | new UseHotReloadWeaver(this), 37 | ]; 38 | 39 | /// 40 | /// Gets the target solution. 41 | /// 42 | public MSBuildSolution Solution => _solution ??= GetSolution(Config, SolutionDirectoryPath, ProjectDirectoryPath); 43 | 44 | /// 45 | /// Gets the full file path of the target solution. 46 | /// 47 | public string SolutionFilePath => Solution.FullPath; 48 | 49 | /// 50 | /// Gets the target project. 51 | /// 52 | public MSBuildProject Project => _project ??= new(ProjectFilePath); 53 | 54 | /// 55 | public override IEnumerable GetAssembliesForScanning() 56 | => _features 57 | .SelectMany(x => x.GetAssembliesForScanning()) 58 | .Concat(["mscorlib", "netstandard", "System.Runtime"]); 59 | 60 | /// 61 | public override void Execute() 62 | { 63 | WriteInfo($"Starting weaving '{AssemblyFilePath}'..."); 64 | 65 | foreach (FeatureWeaver feature in _features) 66 | { 67 | if (!feature.Enabled) 68 | continue; 69 | 70 | WriteInfo($"Running '{feature.GetType().Name}' against '{AssemblyFilePath}'..."); 71 | feature.Execute(); 72 | } 73 | } 74 | 75 | /// 76 | public override void AfterWeaving() 77 | { 78 | foreach (FeatureWeaver feature in _features) 79 | { 80 | if (!feature.Enabled) 81 | continue; 82 | 83 | feature.AfterWeaving(); 84 | } 85 | 86 | WriteInfo($"Finished weaving '{AssemblyFilePath}'!"); 87 | } 88 | 89 | /// 90 | public override void Cancel() 91 | { 92 | foreach (FeatureWeaver feature in _features) 93 | { 94 | if (!feature.Enabled) 95 | continue; 96 | 97 | feature.Cancel(); 98 | } 99 | } 100 | 101 | /// 102 | /// Resolves an instance from the provided context. 103 | /// 104 | /// The configuration containing the solution path. 105 | /// The directory in which to search for an existing solution file. 106 | /// The project directory used as a fallback to locate the solution. 107 | /// A new instance of resolved from the provided context. 108 | private static MSBuildSolution GetSolution(XElement config, string? solutionDirectory, string? projectDirectory) 109 | { 110 | string? solutionFilePath = config?.Attribute("SolutionPath")?.Value; 111 | if (solutionFilePath is { Length: > 0 } && File.Exists(solutionFilePath)) 112 | return new(solutionFilePath); 113 | 114 | if (MSBuildSolution.TryGetFromDirectory(solutionDirectory, out MSBuildSolution? solution)) 115 | return solution; 116 | 117 | if (MSBuildSolution.TryGetTopLevel(projectDirectory, out solution)) 118 | return solution; 119 | 120 | return new(string.Empty); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > 3 | > You probably don't want to install this package manually. 4 | > Please, check out the main [`HotAvalonia`](https://nuget.org/packages/HotAvalonia) package, which will set everything up for you automatically. 5 | 6 | # HotAvalonia.Fody 7 | 8 | [![GitHub Build Status](https://img.shields.io/github/actions/workflow/status/Kira-NT/HotAvalonia/build.yml?logo=github)](https://github.com/Kira-NT/HotAvalonia/actions/workflows/build.yml) 9 | [![Version](https://img.shields.io/github/v/release/Kira-NT/HotAvalonia?sort=date&label=version)](https://github.com/Kira-NT/HotAvalonia/releases/latest) 10 | [![License](https://img.shields.io/github/license/Kira-NT/HotAvalonia?cacheSeconds=36000)](https://github.com/Kira-NT/HotAvalonia/blob/HEAD/LICENSE.md) 11 | 12 | `HotAvalonia.Fody` is a [Fody](https://github.com/Fody/Fody) weaver created to overcome certain limitations that would otherwise render hot reload infeasible on non-x64 devices, while also enhancing the overall experience for HotAvalonia users. 13 | 14 | ---- 15 | 16 | ## Usage 17 | 18 | Like any other Fody weaver, `HotAvalonia.Fody` can be configured either via `FodyWeavers.xml`, or directly in your project file using the `WeaverConfiguration` property, as can be seen below: 19 | 20 | ```xml 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ``` 34 | 35 | Here's a quick overview of the weaver-specific properties: 36 | 37 | ### HotAvalonia 38 | 39 | The root configuration element represents the entry-point weaver that orchestrates and manages feature-specific weavers, while its child nodes represent the individual feature-specific weavers themselves. 40 | 41 | | Name | Description | Default | Examples | 42 | | :--- | :---------- | :------ | :------- | 43 | | `SolutionPath` | Sets the path to a solution file *(`.sln`)* related to this project. | If not set, the weaver will attempt to search for the first `*.sln` file from the current project directory upwards. | `$(SolutionPath)` | 44 | 45 | ### PopulateOverride 46 | 47 | This feature weaver is responsible for recompiling Avalonia resources *(such as styles and resource dictionaries)* to make them hot-reloadable even on non‑x64 devices, where injection‑based hot reload is unavailable. 48 | 49 | | Name | Description | Default | Examples | 50 | | :--- | :---------- | :------ | :------- | 51 | | `Enable` | Sets a value indicating whether this feature weaver is enabled. | `false` | `true`
`false` | 52 | 53 | Note that for this weaver to work properly, Fody needs to run **after** Avalonia has finished compiling the XAML files; otherwise, there will be nothing to recompile. This can be achieved by: 54 | 55 | ```xml 56 | $(FodyDependsOnTargets);CompileAvaloniaXaml 57 | ``` 58 | 59 | ### UseHotReload 60 | 61 | This feature weaver is responsible for automatically invoking `HotAvalonia.AvaloniaHotReloadExtensions.UseHotReload(AppBuilder)` on the `AppBuilder` instance created during app initialization, thereby kicking HotAvalonia into action. Thanks to this, users do not need to modify their source code to enable hot reload for their application. 62 | 63 | | Name | Description | Default | Examples | 64 | | :--- | :---------- | :------ | :------- | 65 | | `Enable` | Sets a value indicating whether this feature weaver is enabled. | `false` | `true`
`false` | 66 | | `GeneratePathResolver` | Sets a value indicating whether this feature weaver should automatically generate `HotAvalonia.AvaloniaHotReloadExtensions.ResolveProjectPath(Assembly)` from the solution file if this method does not already exist. | `false` | `true`
`false` | 67 | 68 | ### FileSystemCredentials 69 | 70 | This feature weaver is responsible for injecting remote file system credentials into the app, enabling it to connect to the appropriate host to retrieve all the information necessary for hot reload to function. 71 | 72 | | Name | Description | Default | Examples | 73 | | :--- | :---------- | :------ | :------- | 74 | | `Enable` | Sets a value indicating whether this feature weaver is enabled. | `false` | `true`
`false` | 75 | | `Address` | Provides the address of the remote file system to the app. | `127.0.0.1` | `192.168.0.2` | 76 | | `Port` | Provides the port of the remote file system to the app. | `20158` | `8080` | 77 | | `Secret` | Provides the secret required by the remote file system to the app. | N/A | `TXkgU3VwZXIgU2VjcmV0IFZhbHVl` | 78 | 79 | ### References 80 | 81 | This feature weaver is responsible for removing specified assembly references from the main module definition of your project and cleaning up related copy-local paths. 82 | 83 | | Name | Description | Default | Examples | 84 | | :--- | :---------- | :------ | :------- | 85 | | `Enable` | Sets a value indicating whether this feature weaver is enabled. | `false` | `true`
`false` | 86 | | `Exclude` | Provides a semicolon-separated list of assembly names, all mentions of which should be erased from the target project. | N/A | `HotAvalonia.Core;Avalonia.Markup.Xaml.Loader` | 87 | 88 | ---- 89 | 90 | ## License 91 | 92 | Licensed under the terms of the [MIT License](https://github.com/Kira-NT/HotAvalonia/blob/HEAD/LICENSE.md). 93 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/ReferencesWeaver.cs: -------------------------------------------------------------------------------- 1 | using Mono.Cecil; 2 | 3 | namespace HotAvalonia.Fody; 4 | 5 | /// 6 | /// Excludes specified assembly references from module definitions and cleans up related copy-local paths. 7 | /// 8 | internal sealed class ReferencesWeaver : FeatureWeaver 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// The root module weaver providing context and shared functionality. 14 | public ReferencesWeaver(ModuleWeaver root) : base(root) 15 | { 16 | } 17 | 18 | /// 19 | /// Gets the collection of assembly reference names to exclude. 20 | /// 21 | private IEnumerable Exclude => this[nameof(Exclude)].Split([';'], StringSplitOptions.RemoveEmptyEntries); 22 | 23 | /// 24 | public override void Execute() 25 | { 26 | foreach (string referenceName in Exclude) 27 | ExcludeReference(referenceName, ModuleDefinition, _root.ReferenceCopyLocalPaths, _root.RuntimeCopyLocalPaths); 28 | } 29 | 30 | /// 31 | /// Removes the specified assembly reference from the module and cleans up related copy-local paths. 32 | /// 33 | /// The name of the reference to exclude. 34 | /// The module definition from which to remove the reference. 35 | /// The collections of paths to remove the reference from. 36 | private static void ExcludeReference(string referenceName, ModuleDefinition module, params IEnumerable> copyLocalPathsCollections) 37 | { 38 | referenceName = referenceName.Trim(); 39 | if (string.IsNullOrWhiteSpace(referenceName)) 40 | return; 41 | 42 | AssemblyNameReference assemblyReference = module.AssemblyReferences.FirstOrDefault(x => x.Name == referenceName); 43 | if (assemblyReference is not null) 44 | module.AssemblyReferences.Remove(assemblyReference); 45 | 46 | foreach (List copyLocalPaths in copyLocalPathsCollections) 47 | copyLocalPaths.RemoveAll(x => referenceName.Equals(Path.GetFileNameWithoutExtension(x), StringComparison.OrdinalIgnoreCase)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/Reflection/BindingFlag.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace HotAvalonia.Fody.Reflection; 4 | 5 | /// 6 | /// Provides pre-combined values for common reflection scenarios. 7 | /// 8 | internal static class BindingFlag 9 | { 10 | /// 11 | /// Gets the binding flags for public instance members. 12 | /// 13 | public const BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance; 14 | 15 | /// 16 | /// Gets the binding flags for non-public instance members. 17 | /// 18 | public const BindingFlags NonPublicInstance = BindingFlags.NonPublic | BindingFlags.Instance; 19 | 20 | /// 21 | /// Gets the binding flags for any instance members, both public and non-public. 22 | /// 23 | public const BindingFlags AnyInstance = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; 24 | 25 | /// 26 | /// Gets the binding flags for public static members. 27 | /// 28 | public const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; 29 | 30 | /// 31 | /// Gets the binding flags for non-public static members. 32 | /// 33 | public const BindingFlags NonPublicStatic = BindingFlags.NonPublic | BindingFlags.Static; 34 | 35 | /// 36 | /// Gets the binding flags for any static members, both public and non-public. 37 | /// 38 | public const BindingFlags AnyStatic = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; 39 | 40 | /// 41 | /// Gets the binding flags for any members, including both instance and static as well as public and non-public. 42 | /// 43 | public const BindingFlags Any = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; 44 | } 45 | -------------------------------------------------------------------------------- /src/HotAvalonia.Fody/UnreferencedTypes.cs: -------------------------------------------------------------------------------- 1 | using HotAvalonia.Fody.Cecil; 2 | 3 | namespace HotAvalonia.Fody; 4 | 5 | /// 6 | /// Contains type names for types that are referenced by name only and may be used for reflection or other late-binding purposes. 7 | /// 8 | internal static class UnreferencedTypes 9 | { 10 | /// 11 | /// Represents the fully-qualified type name for Avalonia.AppBuilder. 12 | /// 13 | public static readonly TypeName Avalonia_AppBuilder = "Avalonia.AppBuilder"; 14 | 15 | /// 16 | /// Represents the fully-qualified type name for the compiled Avalonia XAML resources. 17 | /// 18 | public static readonly TypeName CompiledAvaloniaXaml_AvaloniaResources = "CompiledAvaloniaXaml.!AvaloniaResources"; 19 | 20 | /// 21 | /// Represents the fully-qualified type name for HotAvalonia.AvaloniaHotReloadExtensions. 22 | /// 23 | public static readonly TypeName HotAvalonia_AvaloniaHotReloadExtensions = "HotAvalonia.AvaloniaHotReloadExtensions"; 24 | 25 | /// 26 | /// Represents the fully-qualified type name for HotAvalonia.IO.IFileSystem. 27 | /// 28 | public static readonly TypeName HotAvalonia_IO_IFileSystem = "HotAvalonia.IO.IFileSystem"; 29 | } 30 | -------------------------------------------------------------------------------- /src/HotAvalonia.Remote/HotAvalonia.Remote.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HotAvalonia 5 | Exe 6 | net7.0;net9.0 7 | LatestMajor 8 | $(NoWarn);NETSDK1138;IL3000 9 | 10 | 11 | 12 | true 13 | true 14 | true 15 | full 16 | size 17 | false 18 | false 19 | false 20 | false 21 | false 22 | true 23 | false 24 | false 25 | false 26 | true 27 | false 28 | 29 | 30 | false 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/HotAvalonia.Remote/IO/RemoteFileSystemClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Security; 2 | using HotAvalonia.Net; 3 | 4 | namespace HotAvalonia.IO; 5 | 6 | /// 7 | /// Provides a client to interact with a remote file system consumer 8 | /// over an SSL/TLS-secured connection. 9 | /// 10 | internal sealed class RemoteFileSystemClient : IDisposable, IAsyncDisposable 11 | { 12 | /// 13 | /// The used to send and receive data. 14 | /// 15 | private readonly SslTcpClient _client; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The used to send and receive data. 21 | public RemoteFileSystemClient(SslTcpClient client) 22 | { 23 | _client = client; 24 | } 25 | 26 | /// 27 | /// Gets or sets a value indicating whether the client has requested 28 | /// the server to shut down when the end of the stream is reached. 29 | /// 30 | public bool ShouldShutdownOnEndOfStream { get; set; } 31 | 32 | /// 33 | /// Gets the underlying used to send and receive data. 34 | /// 35 | public SslTcpClient Client => _client; 36 | 37 | /// 38 | /// Returns the used to send and receive data. 39 | /// 40 | /// The underlying . 41 | public SslStream GetStream() => _client.GetStream(); 42 | 43 | /// 44 | public void Dispose() => _client.Dispose(); 45 | 46 | /// 47 | public ValueTask DisposeAsync() => _client.DisposeAsync(); 48 | } 49 | -------------------------------------------------------------------------------- /src/HotAvalonia.Remote/README.md: -------------------------------------------------------------------------------- 1 | # HotAvalonia.Remote 2 | 3 | [![GitHub Build Status](https://img.shields.io/github/actions/workflow/status/Kira-NT/HotAvalonia/build.yml?logo=github)](https://github.com/Kira-NT/HotAvalonia/actions/workflows/build.yml) 4 | [![Version](https://img.shields.io/github/v/release/Kira-NT/HotAvalonia?sort=date&label=version)](https://github.com/Kira-NT/HotAvalonia/releases/latest) 5 | [![License](https://img.shields.io/github/license/Kira-NT/HotAvalonia?cacheSeconds=36000)](https://github.com/Kira-NT/HotAvalonia/blob/HEAD/LICENSE.md) 6 | 7 | `HotAvalonia.Remote` *(also known as `HotAvalonia Remote File System`, or just `HARFS` for short)* is a minimalistic, secure *(hopefully)*, read-only file system server compatible with HotAvalonia. It was designed with the sole purpose of enabling hot reload on remote devices. 8 | 9 | ---- 10 | 11 | ## Build 12 | 13 | If you want to compile the project as a standalone native executable, run: 14 | 15 | ```sh 16 | dotnet publish --ucr -f net9.0 -p:AssemblyName=harfs -o ./dist 17 | ``` 18 | 19 | Alternatively, if you really care about the size of the resulting binary, you can also add an option that enables UPX compression: 20 | 21 | ```sh 22 | dotnet publish --ucr -f net9.0 -p:AssemblyName=harfs -p:PublishLzmaCompressed=true -o ./dist 23 | ``` 24 | 25 | After that, retrieve your very own `harfs` *(or `harfs.exe`)* executable from the `./dist` directory. 26 | 27 | ---- 28 | 29 | ## Usage 30 | 31 | ``` 32 | Usage: harfs [options] 33 | 34 | Run a secure remote file system server compatible with HotAvalonia. 35 | 36 | Examples: 37 | harfs --root "C:/Files" --secret text:MySecret --address 192.168.1.100 --port 8080 38 | harfs -r "/home/user/files" -s env:MY_SECRET -e 0.0.0.0:20158 --allow-shutdown-requests 39 | 40 | Options: 41 | -h, --help 42 | Displays this help page. 43 | 44 | -v, --version 45 | Displays the application version. 46 | 47 | -r, --root 48 | Specifies the root directory for the remote file system. 49 | The value must be a valid URI. 50 | Default: The current working directory. 51 | 52 | -s, --secret 53 | Specifies the secret used for authenticating connections. 54 | The secret can be provided in several formats: 55 | • text: 56 | Provide the secret as plain text (UTF-8 encoded). 57 | • text:utf8: 58 | Provide the secret as plain text (UTF-8 encoded). 59 | • text:base64: 60 | Provide the secret as a Base64-encoded string. 61 | • env: 62 | Read the secret from the specified environment variable as plain text. 63 | • env:utf8: 64 | Read the secret from the specified environment variable as plain text. 65 | • env:base64: 66 | Read the secret from the specified environment variable as a Base64-encoded string. 67 | • file: 68 | Read the secret from the file at the specified path. 69 | • stdin 70 | Read the secret from standard input as plain text. 71 | • stdin:utf8 72 | Read the secret from standard input as plain text. 73 | • stdin:base64 74 | Read the secret from standard input as a Base64-encoded string. 75 | 76 | -a, --address
77 | Specifies the IP address on which the server listens. 78 | Default: All available network interfaces. 79 | 80 | -p, --port 81 | Specifies the port number on which the server listens. 82 | The port must be a positive integer between 1 and 65535. 83 | Default: 20158. 84 | 85 | -e, --endpoint 86 | Specifies the complete endpoint (IP address and port) for the server in the format "IP:port". 87 | This option overrides the individual --address and --port settings. 88 | 89 | -c, --certificate 90 | Specifies the path to the X.509 certificate file used for securing connections. 91 | If provided, the server will use the certificate to establish SSL/TLS communication. 92 | 93 | -d, --max-search-depth 94 | Specifies the maximum search depth for file searches. 95 | A positive value limits the number of file paths returned. 96 | A value of 0 or less indicates no limit. 97 | Default: 0. 98 | 99 | -t, --timeout 100 | Specifies the timeout duration in milliseconds before the server shuts down 101 | if no clients have connected during the provided time frame. 102 | A positive value sets the timeout period. 103 | A value of 0 or less indicates no timeout. 104 | Default: 0. 105 | 106 | --allow-shutdown-requests 107 | When specified, allows the server to accept shutdown requests from clients. 108 | ``` 109 | 110 | ---- 111 | 112 | ## License 113 | 114 | Licensed under the terms of the [MIT License](https://github.com/Kira-NT/HotAvalonia/blob/HEAD/LICENSE.md). 115 | -------------------------------------------------------------------------------- /src/HotAvalonia/FileSystemServerConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Xml.Serialization; 3 | 4 | namespace HotAvalonia; 5 | 6 | /// 7 | /// Represents the configuration settings for a file system server. 8 | /// 9 | public sealed class FileSystemServerConfig 10 | { 11 | /// 12 | /// Gets or sets the root directory for the file system server. 13 | /// 14 | public string? Root { get; set; } 15 | 16 | /// 17 | /// Gets or sets the secret used for authentication. 18 | /// 19 | public string? Secret { get; set; } 20 | 21 | /// 22 | /// Gets or sets the network address on which the file system server listens. 23 | /// 24 | public string? Address { get; set; } 25 | 26 | /// 27 | /// Gets or sets the port number on which the file system server listens. 28 | /// 29 | public int Port { get; set; } 30 | 31 | /// 32 | /// Gets or sets the path to the certificate used for secure communications. 33 | /// 34 | public string? Certificate { get; set; } 35 | 36 | /// 37 | /// Gets or sets the maximum search depth for directory file searches. 38 | /// 39 | public int MaxSearchDepth { get; set; } 40 | 41 | /// 42 | /// Gets or sets the timeout duration in milliseconds before the server shuts down 43 | /// if no clients have connected during the provided time frame. 44 | /// 45 | public int Timeout { get; set; } 46 | 47 | /// 48 | /// Gets or sets a value indicating whether the server should accept shutdown requests from clients. 49 | /// 50 | public bool AllowShutDownRequests { get; set; } 51 | 52 | /// 53 | /// Converts the configuration settings into a command-line arguments string. 54 | /// 55 | /// 56 | /// An optional secret name used to reference the secret via an environment variable. 57 | /// If not provided, the secret is included inline using the prefix text:; 58 | /// otherwise, it is referenced using the given name and prefix env:. 59 | /// 60 | /// A string containing the command-line arguments representing the current configuration. 61 | public string ToArguments(string? secretName = null) 62 | { 63 | StringBuilder arguments = new(); 64 | 65 | if (!string.IsNullOrEmpty(Root)) 66 | arguments.Append("--root \"").Append(Path.GetFullPath(Root)).Append("\" "); 67 | 68 | if (!string.IsNullOrEmpty(Secret)) 69 | { 70 | arguments.Append("--secret \""); 71 | if (string.IsNullOrEmpty(secretName)) 72 | { 73 | arguments.Append("text:base64:").Append(Secret); 74 | } 75 | else 76 | { 77 | arguments.Append("env:base64:").Append(secretName); 78 | } 79 | arguments.Append("\" "); 80 | } 81 | 82 | if (!string.IsNullOrEmpty(Address)) 83 | arguments.Append("--address \"").Append(Address).Append("\" "); 84 | 85 | if (Port > 0) 86 | arguments.Append("--port ").Append(Port).Append(' '); 87 | 88 | if (!string.IsNullOrEmpty(Certificate)) 89 | arguments.Append("--certificate \"").Append(Path.GetFullPath(Certificate)).Append("\" "); 90 | 91 | if (MaxSearchDepth > 0) 92 | arguments.Append("--max-search-depth ").Append(MaxSearchDepth).Append(' '); 93 | 94 | if (Timeout > 0) 95 | arguments.Append("--timeout ").Append(Timeout).Append(' '); 96 | 97 | if (AllowShutDownRequests) 98 | arguments.Append("--allow-shutdown-requests"); 99 | 100 | return arguments.ToString(); 101 | } 102 | 103 | /// 104 | /// Saves the current configuration settings to the specified file. 105 | /// 106 | /// The file path where the configuration will be saved. 107 | public void Save(string path) 108 | { 109 | string fullPath = Path.GetFullPath(path); 110 | string? directoryName = Path.GetDirectoryName(path); 111 | if (!string.IsNullOrEmpty(directoryName)) 112 | Directory.CreateDirectory(directoryName); 113 | 114 | using FileStream file = File.Open(fullPath, FileMode.Create); 115 | XmlSerializer serializer = new(typeof(FileSystemServerConfig)); 116 | serializer.Serialize(file, this); 117 | } 118 | 119 | /// 120 | /// Loads configuration settings from the specified file. 121 | /// 122 | /// The file path from which to load the configuration. 123 | /// A instance populated with the settings from the file. 124 | public static FileSystemServerConfig Load(string path) 125 | { 126 | using FileStream file = File.OpenRead(path); 127 | XmlSerializer serializer = new(typeof(FileSystemServerConfig)); 128 | return (FileSystemServerConfig)serializer.Deserialize(file); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/HotAvalonia/GenerateFileSystemServerConfigTask.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Sockets; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | using HotAvalonia.Helpers; 5 | using Microsoft.Build.Framework; 6 | 7 | namespace HotAvalonia; 8 | 9 | /// 10 | /// Generates a file system server configuration file. 11 | /// 12 | public sealed class GenerateFileSystemServerConfigTask : MSBuildTask 13 | { 14 | /// 15 | /// Gets or sets the root directory for the file system server. 16 | /// 17 | public string? Root { get; set; } 18 | 19 | /// 20 | /// Gets or sets the fallback root directory to use if does not refer to an existing directory. 21 | /// 22 | public string? FallbackRoot { get; set; } 23 | 24 | /// 25 | /// Gets or sets the secret used for authentication. 26 | /// If not provided, a random secret may be generated or is used. 27 | /// 28 | public string? Secret { get; set; } 29 | 30 | /// 31 | /// Gets or sets the secret used for authentication in UTF-8 format. 32 | /// If provided, it is converted to a Base64 string when is not specified. 33 | /// 34 | public string? SecretUtf8 { get; set; } 35 | 36 | /// 37 | /// Gets or sets the network address on which the file system server listens. 38 | /// 39 | public string? Address { get; set; } 40 | 41 | /// 42 | /// Gets or sets the port number on which the file system server listens. 43 | /// If not specified, a new port is allocated automatically. 44 | /// 45 | public string? Port { get; set; } 46 | 47 | /// 48 | /// Gets or sets the path to the certificate used for secure communications. 49 | /// 50 | public string? Certificate { get; set; } 51 | 52 | /// 53 | /// Gets or sets the maximum search depth for directory file searches. 54 | /// 55 | public string? MaxSearchDepth { get; set; } 56 | 57 | /// 58 | /// Gets or sets the timeout duration in milliseconds before the server shuts down 59 | /// if no clients have connected during the provided time frame. 60 | /// 61 | public string? Timeout { get; set; } 62 | 63 | /// 64 | /// Gets or sets a value indicating whether the server should accept shutdown requests from clients. 65 | /// 66 | public string? AllowShutDownRequests { get; set; } 67 | 68 | /// 69 | /// Gets or sets the file path where the generated configuration file will be saved. 70 | /// 71 | [Required] 72 | public string OutputPath { get; set; } = null!; 73 | 74 | /// 75 | protected override void ExecuteCore() 76 | { 77 | FileSystemServerConfig config = new() 78 | { 79 | Root = !string.IsNullOrEmpty(Root) && Directory.Exists(Root) ? Path.GetFullPath(Root) : FindRoot(FallbackRoot), 80 | Secret = string.IsNullOrEmpty(Secret) ? Convert.ToBase64String(string.IsNullOrEmpty(SecretUtf8) ? GenerateSecret() : Encoding.UTF8.GetBytes(SecretUtf8)) : Secret, 81 | Address = Address, 82 | Port = int.TryParse(Port, out int port) && port is > 0 and <= ushort.MaxValue ? port : NetworkHelper.GetAvailablePort(ProtocolType.Tcp), 83 | Certificate = !string.IsNullOrEmpty(Certificate) && File.Exists(Certificate) ? Path.GetFullPath(Certificate) : null, 84 | MaxSearchDepth = int.TryParse(MaxSearchDepth, out int maxSearchDepth) ? maxSearchDepth : 0, 85 | Timeout = int.TryParse(Timeout, out int timeout) ? timeout : 0, 86 | AllowShutDownRequests = bool.TryParse(AllowShutDownRequests, out bool allowShutDownRequests) && allowShutDownRequests, 87 | }; 88 | 89 | config.Save(OutputPath); 90 | } 91 | 92 | /// 93 | /// Generates a random secret as a byte array. 94 | /// 95 | /// A byte array containing the generated secret. 96 | private static byte[] GenerateSecret() 97 | { 98 | const int MinByteCount = 32; 99 | const int MaxByteCount = 64; 100 | 101 | int byteCount = new Random().Next(MinByteCount, MaxByteCount); 102 | byte[] bytes = new byte[byteCount]; 103 | using RNGCryptoServiceProvider rng = new(); 104 | rng.GetBytes(bytes); 105 | return bytes; 106 | } 107 | 108 | /// 109 | /// Finds the server root directory by searching upward from the specified candidate directory 110 | /// until a directory containing a solution file (*.sln) is found. 111 | /// 112 | /// The candidate directory to start the search. 113 | /// 114 | /// The root directory if found; otherwise, if exists, 115 | /// it is returned; or null if no suitable root is found. 116 | /// 117 | private static string? FindRoot(string? rootDirectoryCandidate) 118 | { 119 | if (rootDirectoryCandidate is not null) 120 | rootDirectoryCandidate = Path.GetFullPath(rootDirectoryCandidate); 121 | 122 | string? currentDirectory = rootDirectoryCandidate; 123 | while (currentDirectory is { Length: > 0 }) 124 | { 125 | try 126 | { 127 | bool hasSolution = Directory.EnumerateFiles(currentDirectory, "*.sln", SearchOption.TopDirectoryOnly).Any(); 128 | if (hasSolution) 129 | return currentDirectory; 130 | } 131 | catch 132 | { 133 | // Ignore directories we don't have access to. 134 | } 135 | currentDirectory = Path.GetDirectoryName(currentDirectory); 136 | } 137 | 138 | return !string.IsNullOrEmpty(rootDirectoryCandidate) && Directory.Exists(rootDirectoryCandidate) ? rootDirectoryCandidate : null; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/HotAvalonia/GetFileSystemClientConfigTask.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using HotAvalonia.Helpers; 3 | using Microsoft.Build.Framework; 4 | 5 | namespace HotAvalonia; 6 | 7 | /// 8 | /// Retrieves file system client configuration settings from an existing server configuration file. 9 | /// 10 | public sealed class GetFileSystemClientConfigTask : MSBuildTask 11 | { 12 | /// 13 | /// Gets or sets the file path to the file system server configuration. 14 | /// 15 | [Required] 16 | public string FileSystemServerConfigPath { get; set; } = null!; 17 | 18 | /// 19 | /// Gets or sets the network address the client should connect to. 20 | /// 21 | [Output] 22 | public string? Address { get; set; } 23 | 24 | /// 25 | /// Gets or sets the fallback network address used if 26 | /// is not set and cannot be determined automatically. 27 | /// 28 | public string? FallbackAddress { get; set; } 29 | 30 | /// 31 | /// Gets the port number the client should connect to. 32 | /// 33 | [Output] 34 | public string? Port { get; private set; } 35 | 36 | /// 37 | /// Gets the secret used for authentication. 38 | /// 39 | [Output] 40 | public string? Secret { get; private set; } 41 | 42 | /// 43 | protected override void ExecuteCore() 44 | { 45 | FileSystemServerConfig config = FileSystemServerConfig.Load(FileSystemServerConfigPath); 46 | if (!IPAddress.TryParse(Address, out IPAddress? ip)) 47 | { 48 | ip = NetworkHelper.GetLocalAddress(); 49 | if (ip is null && !IPAddress.TryParse(FallbackAddress, out ip)) 50 | ip = IPAddress.Loopback; 51 | } 52 | 53 | Address = ip.ToString(); 54 | Port = config.Port.ToString(); 55 | Secret = config.Secret ?? string.Empty; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/HotAvalonia/Helpers/NetworkHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.NetworkInformation; 3 | using System.Net.Sockets; 4 | 5 | namespace HotAvalonia.Helpers; 6 | 7 | /// 8 | /// Provides helper methods for network-related operations. 9 | /// 10 | internal static class NetworkHelper 11 | { 12 | /// 13 | /// Attempts to retrieve the local IP address of the current machine. 14 | /// 15 | /// The representing the local address if one is found; otherwise, null. 16 | public static IPAddress? GetLocalAddress() 17 | { 18 | try 19 | { 20 | // Since no actual connection is established, the address may (and in this case, probably will) 21 | // be completely unreachable. However, it should at least be valid, and it's also a good idea 22 | // to choose one from our preferred subnet (i.e., 192.168.0.0/16). 23 | using Socket socket = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Unspecified); 24 | socket.Connect("192.168.255.255", ushort.MaxValue); 25 | IPAddress? address = (socket.LocalEndPoint as IPEndPoint)?.Address; 26 | if (address is not null) 27 | return address; 28 | } 29 | catch { } 30 | 31 | return NetworkInterface.GetAllNetworkInterfaces() 32 | .Where(x => x is { NetworkInterfaceType: not NetworkInterfaceType.Loopback, OperationalStatus: OperationalStatus.Up }) 33 | .SelectMany(x => x.GetIPProperties().UnicastAddresses) 34 | .Where(x => x.Address.AddressFamily is AddressFamily.InterNetwork) 35 | .OrderByDescending(x => x.Address.GetAddressBytes()[0]) 36 | .FirstOrDefault()?.Address; 37 | } 38 | 39 | /// 40 | /// Gets an available network port for the specified protocol. 41 | /// 42 | /// The protocol for which to obtain an available port. 43 | /// An available port number. 44 | public static int GetAvailablePort(ProtocolType protocol) 45 | { 46 | SocketType socketType = protocol switch 47 | { 48 | ProtocolType.Tcp => SocketType.Stream, 49 | ProtocolType.Udp => SocketType.Dgram, 50 | _ => SocketType.Unknown, 51 | }; 52 | 53 | try 54 | { 55 | // The TCP/UDP stack will allocate a new port for us if we set it to 0. 56 | using Socket? socket = socketType is SocketType.Unknown ? null : new(AddressFamily.InterNetwork, socketType, protocol); 57 | socket?.Bind(new IPEndPoint(IPAddress.Loopback, 0)); 58 | if (socket?.LocalEndPoint is IPEndPoint endpoint) 59 | return endpoint.Port; 60 | } 61 | catch { } 62 | 63 | // If something went wrong, just choose a random port from the range 49152-65535, 64 | // which represents private (or ephemeral) ports that cannot be registered with IANA, 65 | // and then pray that we get lucky. 66 | // Note, '65536' is not a typo, because the upper bound of `.Next()` is exclusive. 67 | return new Random().Next(49152, 65536); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/HotAvalonia/HotAvalonia.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | net7.0 6 | tasks 7 | $(NoWarn);NU5100;NU5128 8 | true 9 | $(TargetsForTfmSpecificContentInPackage);ExpandProps 10 | 11 | 12 | 13 | true 14 | A hot reload plugin for Avalonia that enables you to see UI changes in real time as you edit XAML files, drastically accelerating your design and development workflow. 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 | -------------------------------------------------------------------------------- /src/HotAvalonia/HotAvalonia.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $version$ 5 | $avalonia_version$ 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/HotAvalonia/MSBuildTask.cs: -------------------------------------------------------------------------------- 1 | namespace HotAvalonia; 2 | 3 | /// 4 | /// Represents an MSBuild task. 5 | /// 6 | public abstract class MSBuildTask : Microsoft.Build.Utilities.Task 7 | { 8 | /// 9 | /// Executes the task. 10 | /// 11 | /// true if the task executed successfully; otherwise, false. 12 | public sealed override bool Execute() 13 | { 14 | try 15 | { 16 | ExecuteCore(); 17 | } 18 | catch (Exception e) 19 | { 20 | Log.LogErrorFromException(e); 21 | } 22 | return !Log.HasLoggedErrors; 23 | } 24 | 25 | /// 26 | /// When overridden in a derived class, executes the core logic of the task. 27 | /// 28 | protected abstract void ExecuteCore(); 29 | } 30 | -------------------------------------------------------------------------------- /src/HotAvalonia/StartFileSystemServerTask.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | using Microsoft.Build.Framework; 5 | 6 | namespace HotAvalonia; 7 | 8 | /// 9 | /// Starts the file system server process using the specified configuration. 10 | /// 11 | public sealed class StartFileSystemServerTask : MSBuildTask 12 | { 13 | /// 14 | /// Gets or sets the file path to the file system server executable. 15 | /// 16 | [Required] 17 | public string FileSystemServerPath { get; set; } = null!; 18 | 19 | /// 20 | /// Gets or sets the file path to the file system server configuration. 21 | /// 22 | [Required] 23 | public string FileSystemServerConfigPath { get; set; } = null!; 24 | 25 | /// 26 | /// Gets or sets the path to the dotnet executable. 27 | /// If not provided, the task attempts to locate it automatically. 28 | /// 29 | public string? DotnetPath { get; set; } 30 | 31 | /// 32 | protected override void ExecuteCore() 33 | { 34 | const string SecretEnvironmentVariableName = "HARFS_SECRET"; 35 | 36 | string serverFullPath = Path.GetFullPath(FileSystemServerPath); 37 | FileSystemServerConfig config = FileSystemServerConfig.Load(FileSystemServerConfigPath); 38 | 39 | string runnerPath; 40 | StringBuilder arguments = new(); 41 | if (serverFullPath.EndsWith(".dll")) 42 | { 43 | runnerPath = DotnetPath ?? GetDotnetFileName(); 44 | arguments.Append("exec \"").Append(serverFullPath).Append("\" "); 45 | } 46 | else 47 | { 48 | runnerPath = serverFullPath; 49 | } 50 | arguments.Append(config.ToArguments(SecretEnvironmentVariableName)); 51 | 52 | ProcessStartInfo processInfo = new(runnerPath) 53 | { 54 | Arguments = arguments.ToString(), 55 | Environment = { { SecretEnvironmentVariableName, config.Secret } }, 56 | UseShellExecute = false, 57 | CreateNoWindow = true, 58 | }; 59 | Process.Start(processInfo); 60 | } 61 | 62 | /// 63 | /// Gets the file name of the dotnet executable. 64 | /// 65 | /// The file name of the dotnet executable. 66 | private static string GetDotnetFileName() 67 | { 68 | string fileName = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); 69 | if (!string.IsNullOrEmpty(fileName)) 70 | return fileName; 71 | 72 | fileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; 73 | string directoryName = Environment.GetEnvironmentVariable("DOTNET_ROOT"); 74 | if (string.IsNullOrEmpty(directoryName)) 75 | directoryName = Environment.GetEnvironmentVariable("DOTNET_ROOT(x86)"); 76 | 77 | return string.IsNullOrEmpty(directoryName) ? fileName : Path.Combine(directoryName, fileName); 78 | } 79 | } 80 | --------------------------------------------------------------------------------