From f67d189f794bcee56826a7330d2403e3e9a3906c Mon Sep 17 00:00:00 2001 From: qianlifeng Date: Thu, 28 Dec 2023 23:20:39 +0800 Subject: [PATCH] Auto reload dev plugins when plugin src file changes --- Wox.Plugin.Host.Nodejs/src/index.ts | 1 - Wox.Plugin.Host.Nodejs/src/jsonrpc.ts | 92 +++++++---- Wox/plugin/api.go | 2 + Wox/plugin/manager.go | 28 ++++ Wox/plugin/system/app/app.go | 2 +- Wox/plugin/system/wpm.go | 226 +++++++++++++++++--------- Wox/ui/manager.go | 4 +- Wox/util/file_watcher.go | 16 +- justfile | 2 +- 9 files changed, 255 insertions(+), 118 deletions(-) diff --git a/Wox.Plugin.Host.Nodejs/src/index.ts b/Wox.Plugin.Host.Nodejs/src/index.ts index 567db6612..c13954e99 100644 --- a/Wox.Plugin.Host.Nodejs/src/index.ts +++ b/Wox.Plugin.Host.Nodejs/src/index.ts @@ -23,7 +23,6 @@ logger.info(`wox pid: ${woxPid}`) setInterval(() => { try { process.kill(Number.parseInt(woxPid), 0) - logger.error(`wox process is alive`) } catch (e) { logger.error(`wox process is not alive, exit`) process.exit(1) diff --git a/Wox.Plugin.Host.Nodejs/src/jsonrpc.ts b/Wox.Plugin.Host.Nodejs/src/jsonrpc.ts index 6dc0c4cd5..55258c7e5 100644 --- a/Wox.Plugin.Host.Nodejs/src/jsonrpc.ts +++ b/Wox.Plugin.Host.Nodejs/src/jsonrpc.ts @@ -5,14 +5,19 @@ import { Plugin, PluginInitContext, Query, RefreshableResult, Result, ResultActi import { WebSocket } from "ws" import * as crypto from "crypto" -const pluginMap = new Map() -const actionCacheByPlugin = new Map>() -const refreshCacheByPlugin = new Map>() -const pluginApiMap = new Map() +const pluginInstances = new Map() export const PluginJsonRpcTypeRequest: string = "WOX_JSONRPC_REQUEST" export const PluginJsonRpcTypeResponse: string = "WOX_JSONRPC_RESPONSE" +export interface PluginInstance { + Plugin: Plugin + API: PluginAPI + ModulePath: string + Actions: Map + Refreshes: Map +} + export interface PluginJsonRpcRequest { Id: string PluginId: string @@ -70,23 +75,36 @@ async function loadPlugin(request: PluginJsonRpcRequest) { } logger.info(`[${request.PluginName}] load plugin successfully`) - pluginMap.set(request.PluginId, module["plugin"] as Plugin) + pluginInstances.set(request.PluginId, { + Plugin: module["plugin"] as Plugin, + API: {} as PluginAPI, + ModulePath: modulePath, + Actions: new Map(), + Refreshes: new Map() + }) } function unloadPlugin(request: PluginJsonRpcRequest) { - pluginMap.delete(request.PluginId) - actionCacheByPlugin.delete(request.PluginId) + let pluginInstance = pluginInstances.get(request.PluginId) + if (pluginInstance === undefined || pluginInstance === null) { + logger.error(`[${request.PluginName}] plugin instance not found: ${request.PluginName}`) + throw new Error(`plugin instance not found: ${request.PluginName}`) + } + + delete require.cache[require.resolve(pluginInstance.ModulePath)] + pluginInstances.delete(request.PluginId) + logger.info(`[${request.PluginName}] unload plugin successfully`) } function getMethod(request: PluginJsonRpcRequest, methodName: M): Plugin[M] { - const plugin = pluginMap.get(request.PluginId) + const plugin = pluginInstances.get(request.PluginId) if (plugin === undefined || plugin === null) { logger.error(`plugin not found: ${request.PluginName}, forget to load plugin?`) throw new Error(`plugin not found: ${request.PluginName}, forget to load plugin?`) } - const method = plugin[methodName] + const method = plugin.Plugin[methodName] if (method === undefined) { logger.info(`plugin method not found: ${request.PluginName}`) throw new Error(`plugin method not found: ${request.PluginName}`) @@ -96,28 +114,43 @@ function getMethod(request: PluginJsonRpcRequest, method } async function initPlugin(request: PluginJsonRpcRequest, ws: WebSocket) { + const plugin = pluginInstances.get(request.PluginId) + if (plugin === undefined || plugin === null) { + logger.error(`plugin not found: ${request.PluginName}, forget to load plugin?`) + throw new Error(`plugin not found: ${request.PluginName}, forget to load plugin?`) + } + const init = getMethod(request, "init") const pluginApi = new PluginAPI(ws, request.PluginId, request.PluginName) - pluginApiMap.set(request.PluginId, pluginApi) + plugin.API = pluginApi return init({ API: pluginApi, PluginDirectory: request.Params.PluginDirectory } as PluginInitContext) } async function onPluginSettingChange(request: PluginJsonRpcRequest) { + const plugin = pluginInstances.get(request.PluginId) + if (plugin === undefined || plugin === null) { + logger.error(`plugin not found: ${request.PluginName}, forget to load plugin?`) + throw new Error(`plugin not found: ${request.PluginName}, forget to load plugin?`) + } + const settingKey = request.Params.Key const settingValue = request.Params.Value const callbackId = request.Params.CallbackId - pluginApiMap.get(request.PluginId)?.settingChangeCallbacks.get(callbackId)?.(settingKey, settingValue) + plugin.API.settingChangeCallbacks.get(callbackId)?.(settingKey, settingValue) } async function query(request: PluginJsonRpcRequest) { + const plugin = pluginInstances.get(request.PluginId) + if (plugin === undefined || plugin === null) { + logger.error(`plugin not found: ${request.PluginName}, forget to load plugin?`) + throw new Error(`plugin not found: ${request.PluginName}, forget to load plugin?`) + } + const query = getMethod(request, "query") //clean action cache for current plugin - actionCacheByPlugin.set(request.PluginId, new Map()) - refreshCacheByPlugin.set(request.PluginId, new Map()) - - const actionCache = actionCacheByPlugin.get(request.PluginId)! - const refreshCache = refreshCacheByPlugin.get(request.PluginId)! + plugin.Actions.clear() + plugin.Refreshes.clear() const results = await query({ Type: request.Params.Type, @@ -143,7 +176,7 @@ async function query(request: PluginJsonRpcRequest) { if (action.Id === undefined || action.Id === null) { action.Id = crypto.randomUUID() } - actionCache.set(action.Id, action.Action) + plugin.Actions.set(action.Id, action.Action) }) } if (result.RefreshInterval === undefined || result.RefreshInterval === null) { @@ -151,7 +184,7 @@ async function query(request: PluginJsonRpcRequest) { } if (result.RefreshInterval > 0) { if (result.OnRefresh !== undefined && result.OnRefresh !== null) { - refreshCache.set(result.Id, result.OnRefresh) + plugin.Refreshes.set(result.Id, result.OnRefresh) } } }) @@ -160,13 +193,13 @@ async function query(request: PluginJsonRpcRequest) { } async function action(request: PluginJsonRpcRequest) { - const pluginActionCache = actionCacheByPlugin.get(request.PluginId) - if (pluginActionCache === undefined || pluginActionCache === null) { - logger.error(`[${request.PluginName}] plugin action cache not found: ${request.PluginName}`) - return + const plugin = pluginInstances.get(request.PluginId) + if (plugin === undefined || plugin === null) { + logger.error(`plugin not found: ${request.PluginName}, forget to load plugin?`) + throw new Error(`plugin not found: ${request.PluginName}, forget to load plugin?`) } - const pluginAction = pluginActionCache.get(request.Params.ActionId) + const pluginAction = plugin.Actions.get(request.Params.ActionId) if (pluginAction === undefined || pluginAction === null) { logger.error(`[${request.PluginName}] plugin action not found: ${request.PluginName}`) return @@ -178,19 +211,18 @@ async function action(request: PluginJsonRpcRequest) { } async function refresh(request: PluginJsonRpcRequest) { - const pluginRefreshCache = refreshCacheByPlugin.get(request.PluginId) - if (pluginRefreshCache === undefined || pluginRefreshCache === null) { - logger.error(`[${request.PluginName}] plugin refresh cache not found: ${request.PluginName}`) - return + const plugin = pluginInstances.get(request.PluginId) + if (plugin === undefined || plugin === null) { + logger.error(`plugin not found: ${request.PluginName}, forget to load plugin?`) + throw new Error(`plugin not found: ${request.PluginName}, forget to load plugin?`) } - const result = JSON.parse(request.Params.RefreshableResult) as RefreshableResult - - const pluginRefresh = pluginRefreshCache.get(request.Params.ResultId) + const pluginRefresh = plugin.Refreshes.get(request.Params.ResultId) if (pluginRefresh === undefined || pluginRefresh === null) { logger.error(`[${request.PluginName}] plugin refresh not found: ${request.PluginName}`) return } + const result = JSON.parse(request.Params.RefreshableResult) as RefreshableResult return await pluginRefresh(result) } diff --git a/Wox/plugin/api.go b/Wox/plugin/api.go index e96667f18..c2293171a 100644 --- a/Wox/plugin/api.go +++ b/Wox/plugin/api.go @@ -2,6 +2,7 @@ package plugin import ( "context" + "fmt" "github.com/samber/lo" "path" "wox/i18n" @@ -49,6 +50,7 @@ func (a *APIImpl) ShowMsg(ctx context.Context, title string, description string, func (a *APIImpl) Log(ctx context.Context, msg string) { a.logger.Info(ctx, msg) + logger.Info(ctx, fmt.Sprintf("[%s] %s", a.pluginInstance.Metadata.Name, msg)) } func (a *APIImpl) GetTranslation(ctx context.Context, key string) string { diff --git a/Wox/plugin/manager.go b/Wox/plugin/manager.go index ce64e327d..c026c773a 100644 --- a/Wox/plugin/manager.go +++ b/Wox/plugin/manager.go @@ -148,6 +148,34 @@ func (m *Manager) loadPlugins(ctx context.Context) error { return nil } +func (m *Manager) ReloadPlugin(ctx context.Context, metadata MetadataWithDirectory) error { + logger.Info(ctx, fmt.Sprintf("start reloading plugin: %s", metadata.Metadata.Name)) + + pluginHost, exist := lo.Find(AllHosts, func(item Host) bool { + return strings.ToLower(string(item.GetRuntime(ctx))) == strings.ToLower(metadata.Metadata.Runtime) + }) + if !exist { + return fmt.Errorf("unsupported runtime: %s", metadata.Metadata.Runtime) + } + + pluginInstance, pluginInstanceExist := lo.Find(m.instances, func(item *Instance) bool { + return item.Metadata.Id == metadata.Metadata.Id + }) + if pluginInstanceExist { + logger.Info(ctx, fmt.Sprintf("plugin(%s) is loaded, unload first", metadata.Metadata.Name)) + m.UnloadPlugin(ctx, pluginInstance) + } else { + logger.Info(ctx, fmt.Sprintf("plugin(%s) is not loaded, skip unload", metadata.Metadata.Name)) + } + + loadErr := m.loadHostPlugin(ctx, pluginHost, metadata) + if loadErr != nil { + return loadErr + } + + return nil +} + func (m *Manager) loadHostPlugin(ctx context.Context, host Host, metadata MetadataWithDirectory) error { loadStartTimestamp := util.GetSystemTimestamp() plugin, loadErr := host.LoadPlugin(ctx, metadata.Metadata, metadata.Directory) diff --git a/Wox/plugin/system/app/app.go b/Wox/plugin/system/app/app.go index 3648f5d46..c2b2d45a5 100644 --- a/Wox/plugin/system/app/app.go +++ b/Wox/plugin/system/app/app.go @@ -165,7 +165,7 @@ func (a *ApplicationPlugin) watchAppChanges(ctx context.Context) { var appExtensions = a.retriever.GetAppExtensions(ctx) for _, d := range appDirectories { var directory = d - util.WatchDirectories(ctx, directory.Path, func(e fsnotify.Event) { + util.WatchDirectoryChanges(ctx, directory.Path, func(e fsnotify.Event) { var appPath = e.Name var isExtensionMatch = lo.ContainsBy(appExtensions, func(ext string) bool { return strings.HasSuffix(e.Name, fmt.Sprintf(".%s", ext)) diff --git a/Wox/plugin/system/wpm.go b/Wox/plugin/system/wpm.go index ca375f8c6..6b57517e0 100644 --- a/Wox/plugin/system/wpm.go +++ b/Wox/plugin/system/wpm.go @@ -4,12 +4,14 @@ import ( "context" "encoding/json" "fmt" + "github.com/fsnotify/fsnotify" "github.com/google/uuid" cp "github.com/otiai10/copy" "github.com/samber/lo" "os" "path" "strings" + "time" "wox/plugin" "wox/share" "wox/util" @@ -25,14 +27,17 @@ var pluginTemplates = []pluginTemplate{ } func init() { - plugin.AllSystemPlugin = append(plugin.AllSystemPlugin, &WPMPlugin{}) + plugin.AllSystemPlugin = append(plugin.AllSystemPlugin, &WPMPlugin{ + reloadPluginTimers: util.NewHashMap[string, *time.Timer](), + }) } type WPMPlugin struct { api plugin.API creatingProcess string localPluginDirectories []string - localPlugins []plugin.MetadataWithDirectory + localPlugins []localPlugin + reloadPluginTimers *util.HashMap[string, *time.Timer] } type pluginTemplate struct { @@ -41,7 +46,12 @@ type pluginTemplate struct { Url string } -func (i *WPMPlugin) GetMetadata() plugin.Metadata { +type localPlugin struct { + metadata plugin.MetadataWithDirectory + watcher *fsnotify.Watcher +} + +func (w *WPMPlugin) GetMetadata() plugin.Metadata { return plugin.Metadata{ Id: "e2c5f005-6c73-43c8-bc53-ab04def265b2", Name: "Wox Plugin Manager", @@ -75,8 +85,8 @@ func (i *WPMPlugin) GetMetadata() plugin.Metadata { Description: "Create Wox plugin", }, { - Command: "local", - Description: "List local Wox plugins", + Command: "dev", + Description: "Develop Wox plugins", }, }, SupportedOS: []string{ @@ -87,46 +97,73 @@ func (i *WPMPlugin) GetMetadata() plugin.Metadata { } } -func (i *WPMPlugin) Init(ctx context.Context, initParams plugin.InitParams) { - i.api = initParams.API +func (w *WPMPlugin) Init(ctx context.Context, initParams plugin.InitParams) { + w.api = initParams.API - localPluginDirs := i.api.GetSetting(ctx, "localPluginDirectories") + localPluginDirs := w.api.GetSetting(ctx, "localPluginDirectories") if localPluginDirs != "" { - unmarshalErr := json.Unmarshal([]byte(localPluginDirs), &i.localPluginDirectories) + unmarshalErr := json.Unmarshal([]byte(localPluginDirs), &w.localPluginDirectories) if unmarshalErr != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to unmarshal local plugin directories: %s", unmarshalErr.Error())) + w.api.Log(ctx, fmt.Sprintf("Failed to unmarshal local plugin directories: %s", unmarshalErr.Error())) } } // remove invalid directories - i.localPluginDirectories = lo.Filter(i.localPluginDirectories, func(directory string, _ int) bool { + w.localPluginDirectories = lo.Filter(w.localPluginDirectories, func(directory string, _ int) bool { _, statErr := os.Stat(directory) if statErr != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to stat local plugin directory, remove it: %s", statErr.Error())) + w.api.Log(ctx, fmt.Sprintf("Failed to stat local plugin directory, remove it: %s", statErr.Error())) return false } return true }) - i.saveLocalPluginDirectories(ctx) + w.saveLocalPluginDirectories(ctx) } -func (i *WPMPlugin) loadLocalPlugins(ctx context.Context) { - i.localPlugins = nil - for _, localPluginDirectory := range i.localPluginDirectories { - p, err := i.loadLocalPluginsFromDirectory(ctx, localPluginDirectory) +func (w *WPMPlugin) loadLocalPlugins(ctx context.Context) { + w.localPlugins = nil + for _, localPluginDirectory := range w.localPluginDirectories { + p, err := w.loadLocalPluginsFromDirectory(ctx, localPluginDirectory) if err != nil { - i.api.Log(ctx, err.Error()) + w.api.Log(ctx, err.Error()) continue } - i.api.Log(ctx, fmt.Sprintf("Loaded local plugin: %s", p.Metadata.Name)) - i.localPlugins = append(i.localPlugins, p) + w.api.Log(ctx, fmt.Sprintf("Loaded local plugin: %s", p.Metadata.Name)) + + lp := localPlugin{ + metadata: p, + } + + // watch dist directory changes and auto reload plugin + distDirectory := path.Join(localPluginDirectory, "dist") + if _, statErr := os.Stat(distDirectory); statErr == nil { + w.api.Log(ctx, fmt.Sprintf("Watching dist directory: %s", distDirectory)) + watcher, watchErr := util.WatchDirectoryChanges(ctx, distDirectory, func(e fsnotify.Event) { + if e.Op != fsnotify.Chmod { + // debounce reload plugin to avoid reload multiple times in a short time + if t, ok := w.reloadPluginTimers.Load(p.Metadata.Id); ok { + t.Stop() + } + w.reloadPluginTimers.Store(p.Metadata.Id, time.AfterFunc(time.Second*2, func() { + w.reloadLocalPlugins(ctx, p) + })) + } + }) + if watchErr != nil { + w.api.Log(ctx, fmt.Sprintf("Failed to watch dist directory: %s", watchErr.Error())) + } else { + lp.watcher = watcher + } + } + + w.localPlugins = append(w.localPlugins, lp) } } -func (i *WPMPlugin) loadLocalPluginsFromDirectory(ctx context.Context, directory string) (plugin.MetadataWithDirectory, error) { +func (w *WPMPlugin) loadLocalPluginsFromDirectory(ctx context.Context, directory string) (plugin.MetadataWithDirectory, error) { // parse plugin.json in directory metadata, metadataErr := plugin.GetPluginManager().ParseMetadata(ctx, directory) if metadataErr != nil { @@ -138,19 +175,19 @@ func (i *WPMPlugin) loadLocalPluginsFromDirectory(ctx context.Context, directory }, nil } -func (i *WPMPlugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult { +func (w *WPMPlugin) Query(ctx context.Context, query plugin.Query) []plugin.QueryResult { var results []plugin.QueryResult if query.Command == "create" { - if i.creatingProcess != "" { + if w.creatingProcess != "" { results = append(results, plugin.QueryResult{ Id: uuid.NewString(), - Title: i.creatingProcess, + Title: w.creatingProcess, SubTitle: "Please wait...", Icon: wpmIcon, RefreshInterval: 300, OnRefresh: func(current plugin.RefreshableResult) plugin.RefreshableResult { - current.Title = i.creatingProcess + current.Title = w.creatingProcess return current }, }) @@ -172,9 +209,9 @@ func (i *WPMPlugin) Query(ctx context.Context, query plugin.Query) []plugin.Quer Action: func(actionContext plugin.ActionContext) { pluginName := query.Search util.Go(ctx, "create plugin", func() { - i.createPlugin(ctx, pluginTemplateDummy, pluginName, query) + w.createPlugin(ctx, pluginTemplateDummy, pluginName, query) }) - i.api.ChangeQuery(ctx, share.ChangedQuery{ + w.api.ChangeQuery(ctx, share.ChangedQuery{ QueryType: plugin.QueryTypeInput, QueryText: fmt.Sprintf("%s create ", query.TriggerKeyword), }) @@ -184,16 +221,16 @@ func (i *WPMPlugin) Query(ctx context.Context, query plugin.Query) []plugin.Quer } } - if query.Command == "local" { + if query.Command == "dev" { //list all local plugins - return lo.Map(i.localPlugins, func(metadataWithDirectory plugin.MetadataWithDirectory, _ int) plugin.QueryResult { - iconImage := plugin.ParseWoxImageOrDefault(metadataWithDirectory.Metadata.Icon, wpmIcon) - iconImage = plugin.ConvertIcon(ctx, iconImage, metadataWithDirectory.Directory) + return lo.Map(w.localPlugins, func(lp localPlugin, _ int) plugin.QueryResult { + iconImage := plugin.ParseWoxImageOrDefault(lp.metadata.Metadata.Icon, wpmIcon) + iconImage = plugin.ConvertIcon(ctx, iconImage, lp.metadata.Directory) return plugin.QueryResult{ Id: uuid.NewString(), - Title: metadataWithDirectory.Metadata.Name, - SubTitle: metadataWithDirectory.Metadata.Description, + Title: lp.metadata.Metadata.Name, + SubTitle: lp.metadata.Metadata.Description, Icon: iconImage, Preview: plugin.WoxPreview{ PreviewType: plugin.WoxPreviewTypeMarkdown, @@ -210,43 +247,50 @@ func (i *WPMPlugin) Query(ctx context.Context, query plugin.Query) []plugin.Quer - **Commands**: %s - **SupportedOS**: %s - **Features**: %s -`, metadataWithDirectory.Metadata.Name, metadataWithDirectory.Metadata.Description, metadataWithDirectory.Metadata.Author, - metadataWithDirectory.Metadata.Website, metadataWithDirectory.Metadata.Version, metadataWithDirectory.Metadata.MinWoxVersion, - metadataWithDirectory.Metadata.Runtime, metadataWithDirectory.Metadata.Entry, metadataWithDirectory.Metadata.TriggerKeywords, - metadataWithDirectory.Metadata.Commands, metadataWithDirectory.Metadata.SupportedOS, metadataWithDirectory.Metadata.Features), +`, lp.metadata.Metadata.Name, lp.metadata.Metadata.Description, lp.metadata.Metadata.Author, + lp.metadata.Metadata.Website, lp.metadata.Metadata.Version, lp.metadata.Metadata.MinWoxVersion, + lp.metadata.Metadata.Runtime, lp.metadata.Metadata.Entry, lp.metadata.Metadata.TriggerKeywords, + lp.metadata.Metadata.Commands, lp.metadata.Metadata.SupportedOS, lp.metadata.Metadata.Features), }, Actions: []plugin.QueryResultAction{ { - Name: "open", + Name: "Reload", + IsDefault: true, + Action: func(actionContext plugin.ActionContext) { + w.reloadLocalPlugins(ctx, lp.metadata) + }, + }, + { + Name: "Open plugin directory", Action: func(actionContext plugin.ActionContext) { - openErr := util.ShellOpen(metadataWithDirectory.Directory) + openErr := util.ShellOpen(lp.metadata.Directory) if openErr != nil { - i.api.ShowMsg(ctx, "Failed to open plugin directory", openErr.Error(), wpmIcon.String()) + w.api.ShowMsg(ctx, "Failed to open plugin directory", openErr.Error(), wpmIcon.String()) } }, }, { Name: "Remove", Action: func(actionContext plugin.ActionContext) { - i.localPluginDirectories = lo.Filter(i.localPluginDirectories, func(directory string, _ int) bool { - return directory != metadataWithDirectory.Directory + w.localPluginDirectories = lo.Filter(w.localPluginDirectories, func(directory string, _ int) bool { + return directory != lp.metadata.Directory }) - i.saveLocalPluginDirectories(ctx) + w.saveLocalPluginDirectories(ctx) }, }, { Name: "Remove and delete plugin directory", Action: func(actionContext plugin.ActionContext) { - deleteErr := os.RemoveAll(metadataWithDirectory.Directory) + deleteErr := os.RemoveAll(lp.metadata.Directory) if deleteErr != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to delete plugin directory: %s", deleteErr.Error())) + w.api.Log(ctx, fmt.Sprintf("Failed to delete plugin directory: %s", deleteErr.Error())) return } - i.localPluginDirectories = lo.Filter(i.localPluginDirectories, func(directory string, _ int) bool { - return directory != metadataWithDirectory.Directory + w.localPluginDirectories = lo.Filter(w.localPluginDirectories, func(directory string, _ int) bool { + return directory != lp.metadata.Directory }) - i.saveLocalPluginDirectories(ctx) + w.saveLocalPluginDirectories(ctx) }, }, }, @@ -311,30 +355,30 @@ func (i *WPMPlugin) Query(ctx context.Context, query plugin.Query) []plugin.Quer return results } -func (i *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, pluginName string, query plugin.Query) { - i.creatingProcess = "Downloading template..." +func (w *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, pluginName string, query plugin.Query) { + w.creatingProcess = "Downloading template..." tempPluginDirectory := path.Join(os.TempDir(), uuid.NewString()) if err := util.GetLocation().EnsureDirectoryExist(tempPluginDirectory); err != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to create temp plugin directory: %s", err.Error())) - i.creatingProcess = fmt.Sprintf("Failed to create temp plugin directory: %s", err.Error()) + w.api.Log(ctx, fmt.Sprintf("Failed to create temp plugin directory: %s", err.Error())) + w.creatingProcess = fmt.Sprintf("Failed to create temp plugin directory: %s", err.Error()) return } - i.creatingProcess = fmt.Sprintf("Downloading %s template to %s", template.Runtime, tempPluginDirectory) + w.creatingProcess = fmt.Sprintf("Downloading %s template to %s", template.Runtime, tempPluginDirectory) tempZipPath := path.Join(tempPluginDirectory, "template.zip") err := util.HttpDownload(ctx, template.Url, tempZipPath) if err != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to download template: %s", err.Error())) - i.creatingProcess = fmt.Sprintf("Failed to download template: %s", err.Error()) + w.api.Log(ctx, fmt.Sprintf("Failed to download template: %s", err.Error())) + w.creatingProcess = fmt.Sprintf("Failed to download template: %s", err.Error()) return } - i.creatingProcess = "Extracting template..." + w.creatingProcess = "Extracting template..." err = util.Unzip(tempZipPath, tempPluginDirectory) if err != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to extract template: %s", err.Error())) - i.creatingProcess = fmt.Sprintf("Failed to extract template: %s", err.Error()) + w.api.Log(ctx, fmt.Sprintf("Failed to extract template: %s", err.Error())) + w.creatingProcess = fmt.Sprintf("Failed to extract template: %s", err.Error()) return } @@ -342,8 +386,8 @@ func (i *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, p pluginDirectory := path.Join(util.GetLocation().GetPluginDirectory(), pluginName) cpErr := cp.Copy(path.Join(tempPluginDirectory, template.Name+"-main"), pluginDirectory) if cpErr != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to copy template: %s", cpErr.Error())) - i.creatingProcess = fmt.Sprintf("Failed to copy template: %s", cpErr.Error()) + w.api.Log(ctx, fmt.Sprintf("Failed to copy template: %s", cpErr.Error())) + w.creatingProcess = fmt.Sprintf("Failed to copy template: %s", cpErr.Error()) return } @@ -351,8 +395,8 @@ func (i *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, p pluginJsonPath := path.Join(pluginDirectory, "plugin.json") pluginJson, readErr := os.ReadFile(pluginJsonPath) if readErr != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to read plugin.json: %s", readErr.Error())) - i.creatingProcess = fmt.Sprintf("Failed to read plugin.json: %s", readErr.Error()) + w.api.Log(ctx, fmt.Sprintf("Failed to read plugin.json: %s", readErr.Error())) + w.creatingProcess = fmt.Sprintf("Failed to read plugin.json: %s", readErr.Error()) return } @@ -364,8 +408,8 @@ func (i *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, p writeErr := os.WriteFile(pluginJsonPath, []byte(pluginJsonString), 0644) if writeErr != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to write plugin.json: %s", writeErr.Error())) - i.creatingProcess = fmt.Sprintf("Failed to write plugin.json: %s", writeErr.Error()) + w.api.Log(ctx, fmt.Sprintf("Failed to write plugin.json: %s", writeErr.Error())) + w.creatingProcess = fmt.Sprintf("Failed to write plugin.json: %s", writeErr.Error()) return } @@ -374,8 +418,8 @@ func (i *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, p packageJsonPath := path.Join(pluginDirectory, "package.json") packageJson, readPackageErr := os.ReadFile(packageJsonPath) if readPackageErr != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to read package.json: %s", readPackageErr.Error())) - i.creatingProcess = fmt.Sprintf("Failed to read package.json: %s", readPackageErr.Error()) + w.api.Log(ctx, fmt.Sprintf("Failed to read package.json: %s", readPackageErr.Error())) + w.creatingProcess = fmt.Sprintf("Failed to read package.json: %s", readPackageErr.Error()) return } @@ -384,27 +428,53 @@ func (i *WPMPlugin) createPlugin(ctx context.Context, template pluginTemplate, p writePackageErr := os.WriteFile(packageJsonPath, []byte(packageJsonString), 0644) if writePackageErr != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to write package.json: %s", writePackageErr.Error())) - i.creatingProcess = fmt.Sprintf("Failed to write package.json: %s", writePackageErr.Error()) + w.api.Log(ctx, fmt.Sprintf("Failed to write package.json: %s", writePackageErr.Error())) + w.creatingProcess = fmt.Sprintf("Failed to write package.json: %s", writePackageErr.Error()) return } } - i.creatingProcess = "" - i.localPluginDirectories = append(i.localPluginDirectories, pluginDirectory) - i.saveLocalPluginDirectories(ctx) - i.api.ChangeQuery(ctx, share.ChangedQuery{ + w.creatingProcess = "" + w.localPluginDirectories = append(w.localPluginDirectories, pluginDirectory) + w.saveLocalPluginDirectories(ctx) + w.api.ChangeQuery(ctx, share.ChangedQuery{ QueryType: plugin.QueryTypeInput, - QueryText: fmt.Sprintf("%s local ", query.TriggerKeyword), + QueryText: fmt.Sprintf("%s dev ", query.TriggerKeyword), }) } -func (i *WPMPlugin) saveLocalPluginDirectories(ctx context.Context) { - data, marshalErr := json.Marshal(i.localPluginDirectories) +func (w *WPMPlugin) saveLocalPluginDirectories(ctx context.Context) { + data, marshalErr := json.Marshal(w.localPluginDirectories) if marshalErr != nil { - i.api.Log(ctx, fmt.Sprintf("Failed to marshal local plugin directories: %s", marshalErr.Error())) + w.api.Log(ctx, fmt.Sprintf("Failed to marshal local plugin directories: %s", marshalErr.Error())) + return + } + w.api.SaveSetting(ctx, "localPluginDirectories", string(data), false) + w.loadLocalPlugins(ctx) +} + +func (w *WPMPlugin) reloadLocalPlugins(ctx context.Context, localPlugin plugin.MetadataWithDirectory) { + w.api.Log(ctx, fmt.Sprintf("Reloading plugin: %s", localPlugin.Metadata.Name)) + + // find dist directory, if not exist, prompt user to build it + distDirectory := path.Join(localPlugin.Directory, "dist") + _, statErr := os.Stat(distDirectory) + if statErr != nil { + w.api.Log(ctx, fmt.Sprintf("Failed to stat dist directory: %s", statErr.Error())) + return + } + + distPluginMetadata, err := w.loadLocalPluginsFromDirectory(ctx, distDirectory) + if err != nil { + w.api.Log(ctx, fmt.Sprintf("Failed to load local plugin: %s", err.Error())) + return + } + + reloadErr := plugin.GetPluginManager().ReloadPlugin(ctx, distPluginMetadata) + if reloadErr != nil { + w.api.Log(ctx, fmt.Sprintf("Failed to reload plugin: %s", reloadErr.Error())) return + } else { + w.api.Log(ctx, fmt.Sprintf("Reloaded plugin: %s", localPlugin.Metadata.Name)) } - i.api.SaveSetting(ctx, "localPluginDirectories", string(data), false) - i.loadLocalPlugins(ctx) } diff --git a/Wox/ui/manager.go b/Wox/ui/manager.go index 4cb8e8d43..e26dfd65c 100644 --- a/Wox/ui/manager.go +++ b/Wox/ui/manager.go @@ -124,13 +124,13 @@ func (m *Manager) Start(ctx context.Context) error { util.Go(ctx, "watch embed themes", func() { workingDirectory, wdErr := os.Getwd() if wdErr == nil { - util.WatchDirectories(ctx, path.Join(workingDirectory, "resource", "ui", "themes"), onThemeChange) + util.WatchDirectoryChanges(ctx, path.Join(workingDirectory, "resource", "ui", "themes"), onThemeChange) } }) //watch user themes folder and reload themes util.Go(ctx, "watch user themes", func() { - util.WatchDirectories(ctx, userThemesDirectory, onThemeChange) + util.WatchDirectoryChanges(ctx, userThemesDirectory, onThemeChange) }) } diff --git a/Wox/util/file_watcher.go b/Wox/util/file_watcher.go index d73479be4..482baa4ba 100644 --- a/Wox/util/file_watcher.go +++ b/Wox/util/file_watcher.go @@ -6,17 +6,18 @@ import ( "github.com/fsnotify/fsnotify" ) -func WatchDirectories(ctx context.Context, filePath string, callback func(event fsnotify.Event)) error { +func WatchDirectoryChanges(ctx context.Context, directory string, callback func(event fsnotify.Event)) (*fsnotify.Watcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { GetLogger().Error(ctx, fmt.Sprintf("failed to create directory watcher: %s", err.Error())) - return err + return nil, err } - err = watcher.Add(filePath) + err = watcher.Add(directory) if err != nil { GetLogger().Error(ctx, fmt.Sprintf("failed to add directory to watcher: %s", err.Error())) - return err + watcher.Close() + return nil, err } Go(ctx, "watch directory change", func() { @@ -24,12 +25,17 @@ func WatchDirectories(ctx context.Context, filePath string, callback func(event select { case event, ok := <-watcher.Events: if !ok { + GetLogger().Error(ctx, "watch directory closed: not receive event") watcher.Close() return } callback(event) + case <-ctx.Done(): + watcher.Close() + return case watchErr, ok := <-watcher.Errors: if !ok { + GetLogger().Error(ctx, "watch directory closed: not receive error") watcher.Close() return } @@ -41,5 +47,5 @@ func WatchDirectories(ctx context.Context, filePath string, callback func(event watcher.Close() }) - return nil + return watcher, nil } diff --git a/justfile b/justfile index e6c4fa021..b3d3ab8e1 100644 --- a/justfile +++ b/justfile @@ -9,7 +9,7 @@ default: lefthook install -f just _build_hosts - just _build_flutter + # just _build_flutter @precommit: cd Wox.UI.React && pnpm build && cd ..