{"record":{"id":"3b4d5f9fe1282f40","repo":"router-for-me/CLIProxyAPI","slug":"install-plugin-file-w","errorCode":null,"errorMessage":"install plugin file: %w","messagePattern":"install plugin file: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/pluginstore/install.go","lineNumber":567,"sourceCode":"\t\treturn fmt.Errorf(\"write temp plugin file: %w\", errWrite)\n\t}\n\tif errSync := temp.Sync(); errSync != nil {\n\t\treturn fmt.Errorf(\"sync temp plugin file: %w\", errSync)\n\t}\n\tif errClose := temp.Close(); errClose != nil {\n\t\treturn fmt.Errorf(\"close temp plugin file: %w\", errClose)\n\t}\n\tclosed = true\n\tif errRename := os.Rename(tempPath, targetPath); errRename != nil {\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tif errRemove := os.Remove(targetPath); errRemove != nil && !errors.Is(errRemove, os.ErrNotExist) {\n\t\t\t\treturn fmt.Errorf(\"remove old plugin file: %w\", errRemove)\n\t\t\t}\n\t\t\tif errRenameRetry := os.Rename(tempPath, targetPath); errRenameRetry == nil {\n\t\t\t\tremoveTemp = false\n\t\t\t\treturn nil\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"install plugin file: %w\", errRenameRetry)\n\t\t\t}\n\t\t}\n\t\treturn fmt.Errorf(\"install plugin file: %w\", errRename)\n\t}\n\tremoveTemp = false\n\treturn nil\n}\n\nfunc loadedPluginInstallBlocked(options InstallOptions) bool {\n\treturn options.PluginLoaded != nil && strings.EqualFold(options.GOOS, \"windows\") && options.PluginLoaded()\n}\n\nfunc normalizeInstallOptions(options InstallOptions) InstallOptions {\n\toptions.PluginsDir = strings.TrimSpace(options.PluginsDir)\n\tif options.PluginsDir == \"\" {\n\t\toptions.PluginsDir = \"plugins\"\n\t}\n\toptions.GOOS = strings.TrimSpace(options.GOOS)","sourceCodeStart":549,"sourceCodeEnd":585,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/pluginstore/install.go#L549-L585","documentation":"Windows retry path in writeFileAtomic (install.go:564-566). After the first rename failed and the old target was successfully removed, the second os.Rename(tempPath, targetPath) still failed. The original lock may have been transient (file recreated/relocked between remove and rename) or the directory itself denies renames.","triggerScenarios":"InstallArchive on windows under contention: an antivirus re-locks the freshly removed path, another install thread recreates the target, or directory ACLs forbid the rename. The deferred cleanup removes the orphaned temp file.","commonSituations":"Concurrent installs of the same plugin; aggressive real-time AV scanning of the plugins directory; DFS/SMB-hosted PluginsDir with rename consistency issues.","solutions":["Retry the install after serializing installs of the same plugin (the operation is atomic-by-design and safe to repeat)","Exclude the plugins directory from real-time AV scanning","If PluginsDir is on SMB, move it to local disk for reliable rename semantics"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if _, err := store.InstallArchive(data, plugin, opts); err != nil {\n    if runtime.GOOS == \"windows\" && strings.Contains(err.Error(), \"install plugin file\") {\n        time.Sleep(2 * time.Second) // AV lock window\n        _, err = store.InstallArchive(data, plugin, opts) // safe: atomic + idempotent\n    }\n}","preventionTips":["Serialize installs of the same plugin (mutex per plugin id) in your orchestration","Exclude the plugins directory from real-time antivirus scanning","Prefer installing new versions over replacing loaded files on Windows"],"tags":["pluginstore","windows","file-locking","retry"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}