プロジェクト

全般

プロフィール

Vote #81718

未完了

ActiveSupport::Reloader.to_prepare not working in trunk 21287

Admin Redmine さんが約2年前に追加. 約2年前に更新.

ステータス:
Resolved
優先度:
通常
担当者:
-
カテゴリ:
Plugin API_20
対象バージョン:
-
開始日:
2022/05/09
期日:
進捗率:

0%

予定工数:
category_id:
20
version_id:
0
issue_org_id:
36245
author_id:
29628
assigned_to_id:
0
comments:
14
status_id:
3
tracker_id:
1
plus1:
0
affected_version:
closed_on:
affected_version_id:
ステータス-->[Resolved]

説明

With r21283 in trunk ActiveSupport::Reloader.to_prepare and Rails.configuration.to_prepare is not fired anymore.

ActiveSupport::Reloader.to_prepare is required for plugins (as an example), if some code should loaded after all plugins (other plugins) are loaded.

Maybe there is another possibility with Rails 6 or Zeitwerk for doing this, but imho ActiveSupport::Reloader.to_prepare should work within plugins, too. See https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoloading-when-the-application-boots


journals

The defect is for the usage of ActiveSupport::Reloader.to_prepare in a plugin - not in Redmine itself (just to make it clear).

I did not find a solution to fix this behavior until now.

Here is an example for a plugin init.rb:

<pre><code class="ruby">
Redmine::Plugin.register :my_plugin do
name 'My plugin'
version '0.01'
end

ActiveSupport::Reloader.to_prepare do
raise 'this is never called'
end

Rails.configuration.to_prepare do
raise 'this is never called, too'
end

</pre>
--------------------------------------------------------------------------------
I apologize for the delay in responding.
Before the introduction of zeitwerk, the autoloader loads Redmine plugins on initializing.

After the introduction of zeitwerk, to make the Redmine::Plugin class manageable for zeitwerk, Redmine::PluginLoader runs the "init.rb" for all plugins inside the "Rails.configuration.to_prepare" block (and run on every reload).

https://www.redmine.org/projects/redmine/repository/entry/trunk/lib/redmine/plugin_loader.rb#L108

Now processing written inside the "Rails.configuration.to_prepare" block in "init.rb" can be written directly in "init.rb".
Let me know if there are any problems in creating plugins.
--------------------------------------------------------------------------------
Alexander
It may not be a plugin you want to fix, but I found your plugin on GitHub and made it compatible with zeitwerk.

https://github.com/tohosaku/redmine_emojibutton/commits/zeitwerk
--------------------------------------------------------------------------------
Hi Takashi,

thanks for your answer. The problem is, you cannot use Classes from Plugin B with Plugin A - because Plugin B is not initialized at this moment. Because of this, till now without zeitwerk, the solution was to use this Classes after all plugins are initialized (with Rails.configuration.to_prepare).

If Redmine::PluginLoader loads all plugins in "Rails.configuration.to_prepare" block, it is not possible to call an "Rails.configuration.to_prepare" block in a plugin again. This would be a "Rails.configuration.to_prepare" block in a "Rails.configuration.to_prepare" block - and this does not work - as it looks at the moment.

Here are some examples: https://github.com/AlphaNodes/additional_tags/blob/master/init.rb or https://github.com/AlphaNodes/redmine_saml/blob/master/init.rb or https://github.com/AlphaNodes/redmine_sudo/blob/master/init.rb (we build an "plugin loader":https://github.com/AlphaNodes/additionals/blob/master/app/models/additionals_loader.rb#L37 for that)

I am not sure, if you get me right. I try to explain, that if you have dependencies between plugins (which we have a lot), there is no way (or I do not know it), how we can run code from a plugin, after all plugins are initialized.

An example: _Plugin B_ requires _Plugin C_. You cannot use _Plugin C_ code in _Plugin B_, till it is initialized - and this worked perfectly with Rails.configuration.to_prepare before zeitwerk. Maybe to provide a hook after initializing all plugins could be a solution.
--------------------------------------------------------------------------------
Hi Alexander,

I still haven't tried Redmine trunk yet, but I encountered the similar situation which needs to control plugins load orders in Redmine @4.2-stable@ branch.
https://github.com/gtt-project/redmine_gtt/pull/130

From glance of Takashi's comment,
> https://www.redmine.org/projects/redmine/repository/entry/trunk/lib/redmine/plugin_loader.rb#L108

I noticed that there seems to be @after_plugins_loaded@ hook which seems to be called when all plugins are loaded, so I guess that we can try to use this way as a workaround.
https://www.redmine.org/issues/20263
--------------------------------------------------------------------------------
Hi Ko,

indeed I found _after_plugins_loaded_ hook some hours ago. But the problem with that is, you cannot use a patched method in Redmine::Plugin.register block (e.g. to add a link to menu for special conditions, which requires a patch, which is applied later with _after_plugins_loaded_ hook).

Maybe the way with _after_plugins_loaded_ hook is the right direction. Not sure, if there are more problems with that.
But dispense with Rails.configuration.to_prepare means a lot of rework/adjustments in plugins.

--------------------------------------------------------------------------------
_after_plugins_loaded_ hook works for me as a replacement for Rails.configuration.to_prepare
--------------------------------------------------------------------------------

--------------------------------------------------------------------------------

--------------------------------------------------------------------------------
Hi Alexander,

Glad that this is solved!
Thank you for your very meaningful report as it was a case that I hadn't really anticipated.
--------------------------------------------------------------------------------
Sorry for the very late reply.

I tried to check Zeitwerk plugin load sequence by "puts" debug on the latest @4.2-stable@ and @master (trunk)@ branches with using ruby 2.7.4, and the difference was as follows:
|_. |_. 4.2-stable |_. master (trunk) |
| code | <pre><code class="ruby">
puts 'MyPlugin - init.rb'

Redmine::Plugin.register :my_plugin do
puts 'MyPlugin - Redmine::Plugin.register'
name 'My Plugin plugin'
author 'Author name'
description 'This is a plugin for Redmine'
version '0.0.1'
url 'http://example.com/path/to/plugin'
author_url 'http://example.com/about'
end

ActiveSupport::Reloader.to_prepare do
puts 'MyPlugin - ActiveSupport::Reloader.to_prepare'
end

Rails.configuration.to_prepare do
puts 'MyPlugin - Rails.configuration.to_prepare'
end

Rails.application.config.to_prepare do
puts 'MyPlugin - Rails.application.config.to_prepare'
end

Rails.application.reloader.to_prepare do
puts 'MyPlugin - Rails.application.reloader.to_prepare'
end

Rails.application.config.after_initialize do
puts 'MyPlugin - Rails.application.config.after_initialize'
end

class AfterPluginsLoadedHook < Redmine::Hook::Listener
def after_plugins_loaded(context = {})
puts 'MyPlugin - after_plugins_loaded hook'
end
end
</code></pre> | <pre><code class="ruby">
puts 'MyPlugin - init.rb'

Redmine::Plugin.register :my_plugin do
puts 'MyPlugin - Redmine::Plugin.register'
name 'My Plugin plugin'
author 'Author name'
description 'This is a plugin for Redmine'
version '0.0.1'
url 'http://example.com/path/to/plugin'
author_url 'http://example.com/about'
end

ActiveSupport::Reloader.to_prepare do
puts 'MyPlugin - ActiveSupport::Reloader.to_prepare'
end

Rails.configuration.to_prepare do
puts 'MyPlugin - Rails.configuration.to_prepare'
end

Rails.application.config.to_prepare do
puts 'MyPlugin - Rails.application.config.to_prepare'
end

Rails.application.reloader.to_prepare do
puts 'MyPlugin - Rails.application.reloader.to_prepare'
end

Rails.application.config.after_initialize do
puts 'MyPlugin - Rails.application.config.after_initialize'
end

#class AfterPluginsLoadedHook < Redmine::Hook::Listener
Class.new(Redmine::Hook::ViewListener) do |c|
def after_plugins_loaded(context = {})
puts 'MyPlugin - after_plugins_loaded hook'
end
end
</code></pre> |
| result | <pre>
# Init server
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - after_plugins_loaded hook
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
MyPlugin - Rails.configuration.to_prepare
MyPlugin - Rails.application.config.to_prepare
MyPlugin - Rails.application.config.after_initialize

# Reload 1st after editing "plugins/my_plugin/config/locales/en.yml"
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
MyPlugin - Rails.configuration.to_prepare
MyPlugin - Rails.application.config.to_prepare

# Reload 2nd after editing "plugins/my_plugin/config/locales/en.yml", again
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
MyPlugin - Rails.configuration.to_prepare
MyPlugin - Rails.application.config.to_prepare
</pre> | <pre>
# Init server
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - after_plugins_loaded hook
MyPlugin - Rails.application.config.after_initialize

# Reload 1st after editing "plugins/my_plugin/config/locales/en.yml"
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - Rails.application.config.after_initialize
MyPlugin - after_plugins_loaded hook
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare

# Reload 2nd after editing "plugins/my_plugin/config/locales/en.yml", again
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - Rails.application.config.after_initialize
MyPlugin - after_plugins_loaded hook
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
</pre> |

In @master (trunk)@ branch, I had to change the Hook class definition, because of the following error when reloading.
(Thanks @tohosaku and @matobaa for the @redmine_ld_rize@ plugin's commit!)
<pre>
TypeError (superclass mismatch for class AfterPluginsLoadedHook):

plugins/my_plugin/init.rb:29:in `<top (required)>'
lib/redmine/plugin_loader.rb:31:in `load'
lib/redmine/plugin_loader.rb:31:in `run_initializer'
lib/redmine/plugin_loader.rb:108:in `each'
lib/redmine/plugin_loader.rb:108:in `block in load'
</pre>

In @master (trunk)@ branch,
* "ActiveSupport::Reloader.to_prepare" and "Rails.application.reloader.to_prepare" are actually called when reloading, but not called at initialization.
* "ActiveSupport::Reloader.to_prepare" and "Rails.application.reloader.to_prepare" seem to be called multiple times after 2nd reloading, and I think that this behavior needs to be fixed.
* "after_plugins_loaded" hook called timing is different between @4.2-stable@ branch (only once) and @master (trunk)@ (every load/reload), and I think that same called timing is preferable (especially when supporting both Redmine 4.2 and 5.0 in the plugin).

*2022-04-04: Added "Rails.application.config.after_initialize" in above table*
--------------------------------------------------------------------------------
> "after_plugins_loaded" hook called timing is different between 4.2-stable branch (only once) and master (trunk) (every load/reload), and I think that same called timing is preferable (especially when supporting both Redmine 4.2 and 5.0 in the plugin).

Well, about this, just separating the event handler to "Rails.application.config.after_initialize" seems to be enough.
{{collapse
<pre><code class="diff">
--- a/lib/redmine/plugin_loader.rb
+++ b/lib/redmine/plugin_loader.rb
@@ -106,7 +106,9 @@ module Redmine

Rails.application.config.to_prepare do
PluginLoader.directories.each(&:run_initializer)
+ end

+ Rails.application.config.after_initialize do
Redmine::Hook.call_hook :after_plugins_loaded
end
end
</code></pre>
}}

Also, in current @master (trunk)@, "Rails.application.config.after_initialize" event handler (in the plugin's "init.rb") seems to be called at every load/reload timing without duplicate call, so this can be also used instead of other ".to_prepare" functions of Redmine <= 4.2. (But I am not sure whether this is expected result...)
{{collapse
<pre><code class="diff">
+ Rails.application.config.after_initialize do
+ puts 'MyPlugin - Rails.application.config.after_initialize'
+ end
</code></pre>
}}

Here is the combination result from above diffs.
{{collapse
<pre><code>
# Init server
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - after_plugins_loaded hook
MyPlugin - Rails.application.config.after_initialize

# Reload 1st after editing "plugins/my_plugin/config/locales/en.yml"
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - Rails.application.config.after_initialize
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare

# Reload 2nd after editing "plugins/my_plugin/config/locales/en.yml", again
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - Rails.application.config.after_initialize
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
</code></pre>
}}
FYI
--------------------------------------------------------------------------------
> "ActiveSupport::Reloader.to_prepare" and "Rails.application.reloader.to_prepare" seem to be called multiple times after 2nd reloading, and I think that this behavior needs to be fixed.

About this, just adding once execution guard by class variable may be enough.
{{collapse
<pre><code class="diff">
--- a/lib/redmine/plugin_loader.rb
+++ b/lib/redmine/plugin_loader.rb
@@ -86,6 +86,8 @@ module Redmine
cattr_accessor :directory
self.directory = Rails.root.join('plugins')

+ @@initialized = false
+
# Absolute path to the plublic directory where plugins assets are copied
cattr_accessor :public_directory
self.public_directory = Rails.root.join('public/plugin_assets')
@@ -105,9 +107,12 @@ module Redmine
add_autoload_paths

Rails.application.config.to_prepare do
- PluginLoader.directories.each(&:run_initializer)
+ if !@@initialized
+ PluginLoader.directories.each(&:run_initializer)

- Redmine::Hook.call_hook :after_plugins_loaded
+ Redmine::Hook.call_hook :after_plugins_loaded
+ @@initialized = true
+ end
end
end
</code></pre>
}}

With this change, the result becomes as follows, and I think that this is the most similar with past (Redmine <= 4.2) sequence.
{{collapse
<pre><code>
# Init server
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - after_plugins_loaded hook
MyPlugin - Rails.application.config.after_initialize

# Reload 1st after editing "plugins/my_plugin/config/locales/en.yml"
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare

# Reload 2nd after editing "plugins/my_plugin/config/locales/en.yml", again
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
</code></pre>
}}
--------------------------------------------------------------------------------
Sorry, above note-13 comment seemed to be completely wrong...

Now, I am using "Rails.application.config.after_initialize" with current @master (trunk)@ branch, and it seems to be no problem.
https://github.com/gtt-project/redmine_custom_fields_groups/pull/14
--------------------------------------------------------------------------------


related_issues

relates,Closed,34072,Hook after plugins were loaded
relates,Closed,32938,Rails 6: Zeitwerk support

Admin Redmine さんが約2年前に更新

  • カテゴリPlugin API_20 にセット

他の形式にエクスポート: Atom PDF

いいね!0
いいね!0