Flow-Launcher / Flow.Launcher

:mag: Quick file search & app launcher for Windows with community-made plugins
https://flowlauncher.com
MIT License
7.82k stars 307 forks source link

Hot Reload Plugins #2791

Open HorridModz opened 3 months ago

HorridModz commented 3 months ago

I'm developing a plugin for FlowLauncher, and of course, the process involves frequently building and reloading the plugin. This is pretty annoying, as I can't just build the plugin. Instead, I have to exit FlowLauncher, build the plugin, open the plugin's directory in FlowLauncher's UserData folder and the folder which my plugin has been built to, copy the files over, and restart FlowLauncher.

Describe the solution you'd like

It would be great if there was a way to hot reload plugins from a specified folder, specifically for development purposes. I would love if FlowLauncher let me run a command like plugin reload <plugin> <plugin_folder>, and it would reload the plugin using the specified folder instead of the plugin's UserData folder. It couldn't directly reload from the UserData folder, though, because that would require first killing FlowLauncher so the folder could be edited - thus killing the point.

Describe alternatives you've considered

FlowLauncher has a command Reload Plugin Data, which reinitializes all plugins. However, editing the plugins in their UserData folder requires first killing FlowLauncher so the folder could be edited, so for development it's useless. And anyway, it's still a pain to have to copy the files into the UserData folder.

I could make a script to automate the process - I'm on the verge of doing that right now. But I think that a feature like this should inherently be included, as I'm sure I'm not the only plugin developer who finds this an inconvenience.

Yusyuriv commented 3 months ago

I also faced this issue when I started developing plugins for myself. I found two possible solutions:

  1. I made it so the build configuration in my IDE builds into FlowLauncher\Plugins\PluginName directly. That way I don't have to copy anything manually, I just need to stop Flow Launcher, build my plugin, run Flow Launcher again. This is the solution I use because if anyone else were to build my plugins, they wouldn't need to edit anything.
  2. You could theoretically add something like this to your .csproj file:

      <Target Name="PreBuild" Condition="'$(Configuration)' == 'Debug'" BeforeTargets="PreBuildEvent">
        <Exec Command="taskkill /f /fi &quot;IMAGENAME eq Flow.Launcher.exe&quot;" />
      </Target>
    
      <Target Name="PostBuild" Condition="'$(Configuration)' == 'Debug'" AfterTargets="PostBuildEvent">
        <Exec Command="start &quot;&quot; /d C:\Users\YOUR_USERNAME\AppData\Local\FlowLauncher\ Flow.Launcher.exe" />
      </Target>

    This will stop Flow Launcher process before build (if you're building with debug configuration), build the plugin, start Flow Launcher again. I don't know if it's a good idea or not. Anyone trying to build your plugin themselves would have to adjust the path before they can build with debug configuration.

Both of these solutions are not great, but it's the best I've found without Flow Launcher natively supporting hot reload.

HorridModz commented 3 months ago

I also faced this issue when I started developing plugins for myself. I found two possible solutions:

  1. I made it so the build configuration in my IDE builds into FlowLauncher\Plugins\PluginName directly. That way I don't have to copy anything manually, I just need to stop Flow Launcher, build my plugin, run Flow Launcher again. This is the solution I use because if anyone else were to build my plugins, they wouldn't need to edit anything.
  2. You could theoretically add something like this to your .csproj file:

     <Target Name="PreBuild" Condition="'$(Configuration)' == 'Debug'" BeforeTargets="PreBuildEvent">
       <Exec Command="taskkill /f /fi &quot;IMAGENAME eq Flow.Launcher.exe&quot;" />
     </Target>
    
     <Target Name="PostBuild" Condition="'$(Configuration)' == 'Debug'" AfterTargets="PostBuildEvent">
       <Exec Command="start &quot;&quot; /d C:\Users\YOUR_USERNAME\AppData\Local\FlowLauncher\ Flow.Launcher.exe" />
     </Target>

    This will stop Flow Launcher process before build (if you're building with debug configuration), build the plugin, start Flow Launcher again. I don't know if it's a good idea or not. Anyone trying to build your plugin themselves would have to adjust the path before they can build with debug configuration.

Both of these solutions are not great, but it's the best I've found without Flow Launcher natively supporting hot reload.

Gee, thanks! I said I was on the verge of making a script to automate the process, but I guess you already have! Obviously not a solution, like you said, but at least now I have something to help.

taooceros commented 3 months ago

Hotreload is hard. We have so many interdependencies of the current plugin system. I could share some resource if anyone would like to tackle the potential of reloadable plugins. Probably they will need to be loaded differently and with different features set.

HorridModz commented 3 months ago

Hotreload is hard. We have so many interdependencies of the current plugin system. I could share some resource if anyone would like to tackle the potential of reloadable plugins. Probably they will need to be loaded differently and with different features set.

Interesting; can you elaborate on what exactly is meant by "interdependencies"? Because, with my limited understanding as a plugin author, I can't see why replacing the functions in my plugin's code would affect anything else. Anyway, if it did, it would be possible to reload all of the affected plugins from their normal directories (like the existing Reload Plugin Data command does) after hot reloading the designated plugin. Just some thoughts, but correct me as I'm probably missing something.

taooceros commented 3 months ago

Basically the current method I know about hot reloading plugin requires flow to cleanup all references to the plugin assembly. However that seems very hard when I tried it years ago. https://learn.microsoft.com/en-us/dotnet/standard/assembly/unloadability

HorridModz commented 3 months ago

Basically the current method I know about hot reloading plugin requires flow to cleanup all references to the plugin assembly. However that seems very hard when I tried it years ago. learn.microsoft.com/en-us/dotnet/standard/assembly/unloadability

Interesting. So you're making it sound like with all the effort / required reloading, you might as well just reload FlowLauncher and hot reloading is really not worth it.

taooceros commented 3 months ago

Yeah that's what I am thinking. However it is certainly possible that I am not knowledgeable enough to make this feature.

On the other hand, hotreload doesn't gain us too much. Restarting flow with some script copying the plugin around does not consume too much time and resource. It is also possible to attach the ide automatically.

.net does have a hotreload feature via dotnet watch. However I don't know whether it is possible to adapt it to flow...

HorridModz commented 3 months ago

By the way, I've been trying to make my silly post-build script work for my FlowLauncher, which is tough because it runs as admin. I ended up with this, which works in terminal but for some reason exits with error code 1 in visual studio, so I'll dump it here. Not that it matters or anything.

<!--
    Post-build script to make sure FlowLauncher is closed, copy build output to plugin path, and restart FlowLauncher when building as Debug
    Make sure to close FlowLauncher before building
    -->
    <Target Name="PostBuild" Condition="'$(Configuration)' == 'Debug'" AfterTargets="PostBuildEvent">
        <Exec Command="@tasklist /FI &quot;IMAGENAME eq Flow.Launcher.exe&quot; 2&gt;NUL | find /I &quot;Flow.Launcher.exe&quot; &gt;NUL &amp;&amp; (echo Cannot copy files to plugin directory - Flow.Launcher.exe is running. Close FlowLauncher and rebuild. &amp; exit /b 1) &amp;&amp; copy &quot;C:\Users\zachy\VSCodeProjects\Flow.Launcher.Plugin.Add2Path\bin\Debug&quot; &quot;C:\Users\zachy\AppData\Roaming\FlowLauncher\Plugins\Add2Path-1.0.0&quot; /Y &amp;&amp; &quot;C:\Users\zachy\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Flow Launcher\Flow Launcher.lnk&quot;" />
    </Target>
taooceros commented 3 months ago

I think you can task kill flow?

taooceros commented 3 months ago

Maybe when you test plugin you don't have to run flow as admin. I am not sure when would you need to though.

HorridModz commented 3 months ago

Maybe when you test plugin you don't have to run flow as admin. I am not sure when would you need to though.

Yes, but unfortunately my plugin needs it because it is working with system environment variables. It's a special case.

taooceros commented 3 months ago

What about run visual studio as admin?

HorridModz commented 3 months ago

What about run visual studio as admin?

Doesn't carry over to build commands. Of course, it's pretty easy to just make powershell scripts for pre and post build and call then from the csproj pre / post build commands. Not as elegant, but it's a workaround and seems to be the best you can do.

HorridModz commented 3 months ago

@taooceros Would you like me to close the issue or keep it open? Or maybe assign a label. This is obviously just a pipe dream, so it might be better to just close it.

taooceros commented 3 months ago

I add a lebel for help wanted. We can leave it open I think.

github-actions[bot] commented 1 month ago

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 60 days.\n\nAlternatively this issue can be kept open by adding one of the following labels:\nkeep-fresh

aquafir commented 1 month ago

I've used McMaster's DotNetCorePlugins for easy hotreloading. Only a 33kb dll, if that's a concern.

If the plugins have their own dependencies adding a <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> to the plugin csproj has been enough.

For conflicting/redundant dependencies I've had some luck with controlling how they assemblies are resolved, something like:

    AssemblyLoadContext pluginLoaderContext = new AssemblyLoadContext("PluginLoaderContext", true);
    pluginLoaderContext.Resolving += (context, assemblyName) =>
    {
        var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name == assemblyName.Name
            && a.GetName().Version.CompareTo(assemblyName.Version) >= 0);

        return assemblies.LastOrDefault();
    };