router-for-me/CLIProxyAPI · error

remove old plugin file: %w

Error message

remove old plugin file: %w

What it means

Windows-only branch of writeFileAtomic (install.go:558-563). On Windows, os.Rename onto an existing file can fail because the target is open or locked; the installer then tries os.Remove(targetPath) first and renames again. This error wraps that removal failing for a reason other than 'file did not exist' — typically the existing library is loaded into the process (locking the DLL) or held open by another process, including antivirus.

Source

Thrown at internal/pluginstore/install.go:561

	}()

	if errChmod := temp.Chmod(mode); errChmod != nil {
		return fmt.Errorf("chmod temp plugin file: %w", errChmod)
	}
	if _, errWrite := temp.Write(data); errWrite != nil {
		return fmt.Errorf("write temp plugin file: %w", errWrite)
	}
	if errSync := temp.Sync(); errSync != nil {
		return fmt.Errorf("sync temp plugin file: %w", errSync)
	}
	if errClose := temp.Close(); errClose != nil {
		return fmt.Errorf("close temp plugin file: %w", errClose)
	}
	closed = true
	if errRename := os.Rename(tempPath, targetPath); errRename != nil {
		if runtime.GOOS == "windows" {
			if errRemove := os.Remove(targetPath); errRemove != nil && !errors.Is(errRemove, os.ErrNotExist) {
				return fmt.Errorf("remove old plugin file: %w", errRemove)
			}
			if errRenameRetry := os.Rename(tempPath, targetPath); errRenameRetry == nil {
				removeTemp = false
				return nil
			} else {
				return fmt.Errorf("install plugin file: %w", errRenameRetry)
			}
		}
		return fmt.Errorf("install plugin file: %w", errRename)
	}
	removeTemp = false
	return nil
}

func loadedPluginInstallBlocked(options InstallOptions) bool {
	return options.PluginLoaded != nil && strings.EqualFold(options.GOOS, "windows") && options.PluginLoaded()
}

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Set InstallOptions.PluginLoaded to a callback reporting the real load state, and unload the plugin (or restart the server) so installs over a loaded DLL are blocked early with the clearer ErrLoadedPluginLocked instead
  2. Retry after stopping the process holding the DLL (or wait out the antivirus lock)
  3. Install a different version — the versioned filename means a new version writes a new path and does not contend with the old file

Example fix

// before: no callback, install races the loaded DLL
res, err := client.Install(ctx, plugin, pluginstore.InstallOptions{PluginsDir: dir, GOOS: "windows"})
// after: surface the lock deterministically
res, err := client.Install(ctx, plugin, pluginstore.InstallOptions{
    PluginsDir:   dir,
    GOOS:         "windows",
    PluginLoaded: func() bool { return host.IsPluginLoaded(plugin.ID) },
})
Defensive patterns

Strategy: validation

Validate before calling

// Pass PluginLoaded so the store refuses with ErrLoadedPluginLocked EARLY
// (install.go:296-297) instead of failing mid-rename:
opts := pluginstore.InstallOptions{
    PluginsDir:   dir,
    GOOS:         "windows",
    PluginLoaded: func() bool { return host.IsPluginLoaded(plugin.ID) },
}
if pluginstore.InstallBlockedOnWindows(plugin, opts) { // your own pre-check
    host.ScheduleRestartThenInstall(plugin)
}

Try / catch

if _, err := client.Install(ctx, plugin, opts); err != nil {
    if errors.Is(err, pluginstore.ErrLoadedPluginLocked) {
        // planned case: unload/restart, then re-run install
        return
    }
    if runtime.GOOS == "windows" && strings.Contains(err.Error(), "remove old plugin file") {
        // target locked by another process (AV?): wait and retry once
    }
}

Prevention

When it happens

Trigger: InstallArchive on windows where the old <id>-v<ver>.dll at the target path is memory-mapped by the running server (the plugin is loaded), or locked by a scanner/editor, so both rename and remove fail.

Common situations: Upgrading a plugin on a running Windows host without unloading it first; antivirus or indexing services briefly locking freshly-written DLLs.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/3d47982c33727a0d. Report an issue: GitHub.