Skip to content

Commit

Permalink
Merge branch 'feature-issue114' #114
Browse files Browse the repository at this point in the history
* feature-issue114:
  Updated doxygen installation script to download from github instead of official homepage.
  Updated CHANGES.
  Set base directory icon as ShellAnything's main icon.
  Updated code to use doxygen 1.9.6 which fixes compilation.
  Updated UserManual.md about new sa_plugin_terminate() API call.
  Updated splashscreen. Added plugins support
  Implement sa_plugin_terminate() in the C API #114
  Enabled building on branch `feature-issue114`.
  • Loading branch information
end2endzone committed Feb 3, 2023
2 parents a9ef810 + c461a8f commit 6103716
Show file tree
Hide file tree
Showing 18 changed files with 207 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Changes for 0.8.0
* Fixed issue #105: Support for .editorconfig as a coding standard.
* Fixed issue #101: Deprecate setup.exe installer in favor of the Windows Installer (*.msi).
* Fixed issue #113: Upgrading from v0.7.0 to 0.8.0 installs at the wrong location.
* Fixed issue #114: Implement sa_plugin_terminate() in the C API.


Changes for 0.7.0
Expand Down
27 changes: 21 additions & 6 deletions UserManual.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ This manual includes a description of the system functionalities and capabilitie
* [Plugin overview](#plugin-overview)
* [C API](#c-api)
* [Creating a new plugin](#creating-a-new-plugin)
* [Register services and features to the system](#register-services-and-features-to-the-system)
* [Register a function callback when the selection changes](#register-a-function-callback-when-the-selection-changes)
* [Register additional validation attributes](#register-additional-validation-attributes)
* [Register new custom actions](#register-new-custom-actions)
* [Plugin example](#plugin_example])
* [Plugin example](#plugin-example)
* [Plugin declaration](#plugin-declaration)
* [<plugins> element](#plugins-element)
* [<plugin> element](#plugin-element)
Expand Down Expand Up @@ -1360,7 +1361,9 @@ For example, the following would define `services.wce.command.start` and `servic

## Plugin overview ##

ShellAnything supports plugins which can be used to extend ShellAnything with more functionality. **The support for plugins is in beta and features are limitted**.
ShellAnything supports plugins which can be used to extend ShellAnything with more functionality.

**The support for plugins is in beta and features are limitted**.

ShellAnything does not implement a plugin detection system. It cannot automatically detect and load plugin files. The reason is explained below.

Expand All @@ -1384,9 +1387,21 @@ The API is not designed for programming your own Windows Shell extensions. An ex

## Creating a new plugin ##

All plugins are designed to have a two generic entry points. The name of these entry points are `sa_plugin_initialize()` and `sa_plugin_register()`. When a plugin is declared in a _Configuration File_, the system calls `sa_plugin_initialize()` to let the plugin initialize. If the initialization is successful, the system calls `sa_plugin_register()` which allow the plugin to register all its features to the system through the [C API](#c-api).
All plugins are designed to have a three generic entry points. The name of these entry points are `sa_plugin_initialize()`, `sa_plugin_terminate()` and `sa_plugin_register()`.

When a plugin is declared in a _Configuration File_, the system calls `sa_plugin_initialize()` to let the plugin initialize. If the initialization succeeds, the system calls `sa_plugin_register()` which allows the plugin to register all its functionality to the system through the [C API](#c-api). When a configuration is unloaded, the system calls `sa_plugin_terminate()` to let the plugin cleanup its memory.

Note that all function entry points can be called multiple times in the same session. This is because the scope of each plugin is its _Configuration File_. For example, if the same plugin is declared in 3 configurations, then `sa_plugin_initialize()`, `sa_plugin_register()` and `sa_plugin_terminate()` will be called 3 times. If a plugin implements custom conditions, it must register the attributes in all Configurations where the plugin is declared.

Plugins must be careful not to initialize a variable that has already been initialized. The same goes for terminate. If a plugin is initialized 5 times in the same session, then only the 5th _sa_plugin_terminate()_ call should be considered the last terminate before the dll is unloaded.



## Register services and features to the system ##

The implementation of `sa_plugin_register()` allow a plugin to register one or mutiple services using the API.

The implementation of `sa_plugin_register()` must register one or mutiple services using the API:
This section explains how to implement typical plugin use cases.



Expand Down Expand Up @@ -1478,7 +1493,7 @@ The plugin must allocate an instance of this structure and fill it with the pars



### Plugin example ###
## Plugin example ##

The documentation contains a sample plugin example called `sa_plugin_demo` to help with the developpement of a new plugin. The sample files are located in `[installation directory]\bin\docs\sa_plugin_demo.zip`.

Expand Down Expand Up @@ -2141,4 +2156,4 @@ A quick way to do this is to temporary change the *Configuration Files* to anoth

If you find any issues while using ShellAnything, please report your findings using the [project issue page on GitHub](https://github.com/end2endzone/ShellAnything/issues).

Improvements to the software are also welcome and can be proposed using the same method.
Improvements to the software are also welcome and can be proposed using the same method.
1 change: 1 addition & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ version: "{branch} (#{build})"
branches:
only:
- master
- feature-issue114

# Do not build on tags (GitHub and BitBucket)
skip_tags: true
Expand Down
2 changes: 1 addition & 1 deletion ci/windows/install_doxygen.bat
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
:: https://www.doxygen.nl/download.html

echo Installing Doxygen...
set DOXYGEN_URL=https://www.doxygen.nl/files/doxygen-1.9.4.windows.x64.bin.zip
set DOXYGEN_URL=https://github.com/doxygen/doxygen/releases/download/Release_1_9_6/doxygen-1.9.6.windows.x64.bin.zip
set DOXYGEN_FILE=%TEMP%\doxygen.zip
set DOXYGEN_INSTALL_DIR=%TEMP%\doxygen

Expand Down
6 changes: 6 additions & 0 deletions desktop.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[.ShellClassInfo]
IconResource=resources\icons\0.2.0.ico,0
[ViewState]
Mode=
Vid=
FolderType=Generic
Binary file modified docs/ShellAnything-splashscreen.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/ShellAnything.xcf
Binary file not shown.
21 changes: 19 additions & 2 deletions include/shellanything/sa_plugin_definitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ typedef struct
/// <summary>
/// Defines the name of a plugin entry point for initialization.
/// </summary>
#define SA_PLUGIN_INITIALIZATION_FUNCTION_NAME sa_plugin_initialize
#define SA_PLUGIN_INITIALIZE_FUNCTION_NAME sa_plugin_initialize

/// <summary>
/// Defines the name of a plugin entry point for terminate.
/// </summary>
#define SA_PLUGIN_TERMINATE_FUNCTION_NAME sa_plugin_terminate

/// <summary>
/// Defines the name of a plugin entry point for initialization.
Expand All @@ -62,13 +67,25 @@ typedef struct
/// </summary>
/// <remarks>
/// ShellAnything is expecting a function whose name matches macro
/// SA_PLUGIN_INITIALIZATION_FUNCTION_NAME and function signature
/// SA_PLUGIN_INITIALIZE_FUNCTION_NAME and function signature
/// matches this type definition.
/// </remarks>
/// <param name="version">The version of the application.</param>
/// <returns>Returns 0 on success. Returns non-zero otherwise.</returns>
typedef sa_error_t(*sa_plugin_initialize_func)(sa_version_info_t* version);

/// <summary>
/// Type definition for a plugin's terminate entry point.
/// </summary>
/// <remarks>
/// ShellAnything is expecting a function whose name matches macro
/// SA_PLUGIN_TERMINATE_FUNCTION_NAME and function signature
/// matches this type definition.
/// </remarks>
/// <param name="version">The version of the application.</param>
/// <returns>Returns 0 on success. Returns non-zero otherwise.</returns>
typedef sa_error_t(*sa_plugin_terminate_func)();

/// <summary>
/// Function pointer definition for plugins registration function.
/// </summary>
Expand Down
44 changes: 34 additions & 10 deletions src/core/Plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ namespace shellanything
{
HMODULE hModule;
sa_plugin_initialize_func initialize_func;
sa_plugin_terminate_func terminate_func;
sa_plugin_register_func register_func;
};

Expand Down Expand Up @@ -208,11 +209,19 @@ namespace shellanything
}

//search for entry points
sa_plugin_initialize_func initialize_func = (sa_plugin_initialize_func)GetProcAddress(hModule, xstr(SA_PLUGIN_INITIALIZATION_FUNCTION_NAME));
sa_plugin_initialize_func initialize_func = (sa_plugin_initialize_func)GetProcAddress(hModule, xstr(SA_PLUGIN_INITIALIZE_FUNCTION_NAME));
if (initialize_func == NULL)
{
FreeLibrary(hModule);
LOG(ERROR) << "Missing entry point '" << xstr(SA_PLUGIN_INITIALIZATION_FUNCTION_NAME) << "' in plugin '" << path << "'.";
LOG(ERROR) << "Missing entry point '" << xstr(SA_PLUGIN_INITIALIZE_FUNCTION_NAME) << "' in plugin '" << path << "'.";
return false;
}

sa_plugin_terminate_func terminate_func = (sa_plugin_terminate_func)GetProcAddress(hModule, xstr(SA_PLUGIN_TERMINATE_FUNCTION_NAME));
if (terminate_func == NULL)
{
FreeLibrary(hModule);
LOG(ERROR) << "Missing entry point '" << xstr(SA_PLUGIN_TERMINATE_FUNCTION_NAME) << "' in plugin '" << path << "'.";
return false;
}

Expand Down Expand Up @@ -253,6 +262,7 @@ namespace shellanything
// this plugin is valid.
this->mEntryPoints->hModule = hModule;
this->mEntryPoints->initialize_func = initialize_func;
this->mEntryPoints->terminate_func = terminate_func;
this->mEntryPoints->register_func = register_func;
mLoaded = true;

Expand All @@ -274,18 +284,32 @@ namespace shellanything

bool Plugin::Unload()
{
if (mEntryPoints->hModule != 0)
{
mRegistry.Clear();
if (mEntryPoints->hModule == 0)
return false; // something is wrong, nothing to unload or already unload

FreeLibrary(mEntryPoints->hModule);
memset(mEntryPoints, 0, sizeof(Plugin::ENTRY_POINTS));
mLoaded = false;
bool success = true;

// remember this plugin while unloading. This is required for plugins that calls the API.
gLoadingPlugin = this;

sa_error_t terminate_error = this->mEntryPoints->terminate_func();
if (terminate_error != SA_ERROR_SUCCESS)
{
const char* terminate_error_str = sa_error_get_error_description(terminate_error);
LOG(ERROR) << "The plugin '" << mPath << "' has failed to terminate. Error code: " << ra::strings::Format("0x%x", terminate_error) << ", " << terminate_error_str;

return true;
success = false;
}

return false;
gLoadingPlugin = NULL;

mRegistry.Clear();

FreeLibrary(mEntryPoints->hModule);
memset(mEntryPoints, 0, sizeof(Plugin::ENTRY_POINTS));
mLoaded = false;

return success;
}

Registry& Plugin::GetRegistry()
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/sa_plugin_demo/sa_plugin_demo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ EXPORT_API sa_error_t sa_plugin_initialize(sa_version_info_t* version)
return SA_ERROR_SUCCESS;
}

EXPORT_API sa_error_t sa_plugin_terminate()
{
return SA_ERROR_SUCCESS;
}

EXPORT_API sa_error_t sa_plugin_register()
{
// register validation function for 'demo1' and 'demo2' custom attributes
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/sa_plugin_process/sa_plugin_process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,11 @@ EXPORT_API sa_error_t sa_plugin_initialize(sa_version_info_t* version)
return SA_ERROR_SUCCESS;
}

EXPORT_API sa_error_t sa_plugin_terminate()
{
return SA_ERROR_SUCCESS;
}

EXPORT_API sa_error_t sa_plugin_register()
{
sa_error_t result;
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/sa_plugin_services/sa_plugin_services.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ EXPORT_API sa_error_t sa_plugin_initialize(sa_version_info_t* version)
return SA_ERROR_SUCCESS;
}

EXPORT_API sa_error_t sa_plugin_terminate()
{
return SA_ERROR_SUCCESS;
}

EXPORT_API sa_error_t sa_plugin_register()
{
// register update callback function.
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/sa_plugin_strings/sa_plugin_strings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,11 @@ EXPORT_API sa_error_t sa_plugin_initialize(sa_version_info_t* version)
return SA_ERROR_SUCCESS;
}

EXPORT_API sa_error_t sa_plugin_terminate()
{
return SA_ERROR_SUCCESS;
}

#define PLUGIN_REGISTER_EVENT(name, event_func) \
result = sa_plugin_register_action_event(name, event_func); \
if (result != SA_ERROR_SUCCESS) \
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/sa_plugin_time/sa_plugin_time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ EXPORT_API sa_error_t sa_plugin_initialize(sa_version_info_t* version)
return SA_ERROR_SUCCESS;
}

EXPORT_API sa_error_t sa_plugin_terminate()
{
return SA_ERROR_SUCCESS;
}

EXPORT_API sa_error_t sa_plugin_register()
{
// register validation function for 'start_time' and 'end_time' custom attributes
Expand Down
1 change: 1 addition & 0 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ set(CONFIGURATION_TEST_FILES ""
${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestObjectFactory.testParseMenuMaxLength.xml
${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestObjectFactory.testParsePlugins.xml
${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestPlugins.testPluginActionGetData.xml
${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestPlugins.testPluginInitializeAndTerminate.xml
${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestPlugins.testProcess.xml
${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestPlugins.testServices.xml
${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestPlugins.testStrings.xml
Expand Down
66 changes: 66 additions & 0 deletions src/tests/TestPlugins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,72 @@ namespace shellanything
ASSERT_TRUE(workspace.Cleanup()) << "Failed deleting workspace directory '" << workspace.GetBaseDirectory() << "'.";
}
//--------------------------------------------------------------------------------------------------
TEST_F(TestPlugins, testPluginInitializeAndTerminate)
{
ConfigManager& cmgr = ConfigManager::GetInstance();
PropertyManager& pmgr = PropertyManager::GetInstance();

//Creating a temporary workspace for the test execution.
Workspace workspace;
ASSERT_FALSE(workspace.GetBaseDirectory().empty());

//Import the required files into the workspace
static const std::string path_separator = ra::filesystem::GetPathSeparatorStr();
std::string test_name = ra::testing::GetTestQualifiedName();
std::string template_source_path = std::string("test_files") + path_separator + test_name + ".xml";
ASSERT_TRUE(workspace.ImportFileUtf8(template_source_path.c_str()));

//Wait to make sure that the next file copy/modification will not have the same timestamp
ra::timing::Millisleep(1500);

//Make sure that plugin's status property is not set
static const std::string PLUGIN_STATUS_PROPERTY_NAME = "sa_plugin_test_data.status";
pmgr.ClearProperty(PLUGIN_STATUS_PROPERTY_NAME);

//Setup ConfigManager to read files from workspace
cmgr.ClearSearchPath();
cmgr.AddSearchPath(workspace.GetBaseDirectory());
cmgr.Refresh();

//ASSERT the file is loaded
Configuration::ConfigurationPtrList configs = cmgr.GetConfigurations();
ASSERT_EQ(1, configs.size());

Configuration* config0 = cmgr.GetConfigurations()[0];

//ASSERT all plugins were loaded
for (size_t i = 0; i < config0->GetPlugins().size(); i++)
{
const Plugin* plugin = config0->GetPlugins()[i];
ASSERT_TRUE(plugin->IsLoaded()) << "The plugin '" << plugin->GetPath() << "' is not loaded.";
}

//Get menus
Menu::MenuPtrList menus = cmgr.GetConfigurations()[0]->GetMenus();
ASSERT_EQ(1, menus.size());
Menu* menu0 = menus[0];
ASSERT_TRUE(menu0 != NULL);

//ASSERT that plugin's property status is now set (during initialization)
ASSERT_TRUE(pmgr.HasProperty(PLUGIN_STATUS_PROPERTY_NAME)) << "The property '" << PLUGIN_STATUS_PROPERTY_NAME << "' is not set. The plugin may not have set its status property during initialization.";
ASSERT_EQ(pmgr.GetProperty(PLUGIN_STATUS_PROPERTY_NAME), "initilized");

//force the configuration (and the plugin) to be unloaded.
ASSERT_TRUE(ra::filesystem::DeleteFile(config0->GetFilePath().c_str())) << "Failed to delete configuration file '" << config0->GetFilePath() << "'.";
cmgr.ClearSearchPath();
cmgr.Refresh();

//ASSERT the configuration is unloaded
configs = cmgr.GetConfigurations();
ASSERT_EQ(0, configs.size());

//ASSERT that plugin's property status was removed (during termination)
ASSERT_FALSE(pmgr.HasProperty(PLUGIN_STATUS_PROPERTY_NAME)) << "The property '" << PLUGIN_STATUS_PROPERTY_NAME << "' exists. The plugin may not have delete its status property during termination.";

//Cleanup
ASSERT_TRUE(workspace.Cleanup()) << "Failed deleting workspace directory '" << workspace.GetBaseDirectory() << "'.";
}
//--------------------------------------------------------------------------------------------------
TEST_F(TestPlugins, testPluginActionGetData)
{
ConfigManager& cmgr = ConfigManager::GetInstance();
Expand Down
13 changes: 13 additions & 0 deletions src/tests/sa_plugin_test_data/sa_plugin_test_data.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ extern "C" {
#endif

static const char* PLUGIN_NAME_IDENTIFIER = "sa_plugin_test_data";
static const char* PLUGIN_STATUS_PROPERTY_NAME = "sa_plugin_test_data.status";
static const char* ATTR_ID = "id";

const char* to_hex_string(void* address, char* buffer, size_t size)
Expand Down Expand Up @@ -300,6 +301,18 @@ EXPORT_API sa_error_t sa_plugin_initialize(sa_version_info_t* version)
{
if (version->major == 0 && version->minor < 8) // this plugin is designed for version 0.8.0 and over
return SA_ERROR_NOT_SUPPORTED;

// Set a property to notify the plugin is initialized
sa_properties_set(PLUGIN_STATUS_PROPERTY_NAME, "initilized");

return SA_ERROR_SUCCESS;
}

EXPORT_API sa_error_t sa_plugin_terminate()
{
// Set a property to notify the plugin has terminated
sa_properties_delete(PLUGIN_STATUS_PROPERTY_NAME);

return SA_ERROR_SUCCESS;
}

Expand Down
Loading

0 comments on commit 6103716

Please sign in to comment.