diff --git a/.github/workflows/shimgen.yaml b/.github/workflows/shimgen.yaml new file mode 100644 index 0000000..46b2b11 --- /dev/null +++ b/.github/workflows/shimgen.yaml @@ -0,0 +1,29 @@ +name: Shimgen CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build-shimgen: + + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest] + + steps: + - uses: actions/checkout@v2 + - uses: ilammy/msvc-dev-cmd@v1 + + - name: compile + run: cl /O1 scoop-better-shimexe/shim.c + + - uses: actions/upload-artifact@v2 + with: + name: shim.exe + path: shim.exe + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dabbb9c..fc59375 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -105,6 +105,10 @@ variables: - golden when: on_failure +# .test_ghcup_scoop: + # script: + # - cl /O1 scoop-better-shimexe/shim.c + .test_ghcup_version:linux: extends: - .test_ghcup_version @@ -184,6 +188,12 @@ variables: - set CABAL_DIR="$CI_PROJECT_DIR/cabal" - bash ./.gitlab/before_script/windows/install_deps.sh +# .test_ghcup_scoop:windows: + # extends: + # - .windows + # - .test_ghcup_scoop + # - .root_cleanup + .release_ghcup: script: - bash ./.gitlab/script/ghcup_release.sh @@ -338,6 +348,11 @@ test:windows:recommended: CABAL_VERSION: "3.4.0.0" needs: [] +# test:windows:scoop: + # stage: test + # extends: .test_ghcup_scoop:windows + # needs: [] + ######## linux release ######## release:linux:64bit: diff --git a/scoop-better-shimexe/LICENSE-MIT b/scoop-better-shimexe/LICENSE-MIT new file mode 100644 index 0000000..0338066 --- /dev/null +++ b/scoop-better-shimexe/LICENSE-MIT @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2019 Grégoire Geis + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/scoop-better-shimexe/LICENSE-UNLICENSE b/scoop-better-shimexe/LICENSE-UNLICENSE new file mode 100644 index 0000000..bfa9230 --- /dev/null +++ b/scoop-better-shimexe/LICENSE-UNLICENSE @@ -0,0 +1,10 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/scoop-better-shimexe/README.md b/scoop-better-shimexe/README.md new file mode 100644 index 0000000..e0e9543 --- /dev/null +++ b/scoop-better-shimexe/README.md @@ -0,0 +1,71 @@ +# `shim.c` + +[`shim.c`](./shim.c) is a simple Windows program that, when started: +1. Looks for a file with the exact same name as the running program, but with + the extension `shim` (e.g. `C:\bin\foo.exe` will read the file `C:\bin\foo.shim`). +2. Reads and [parses](#shim-format) the files into a + [Scoop](https://github.com/lukesampson/scoop) shim format. +3. Executes the target executable with the given arguments. + +`shim.c` was originally made to replace [Scoop](https://github.com/lukesampson/scoop)'s +[`shim.cs`](https://github.com/lukesampson/scoop/blob/96de9c14bb483f9278e4b0a9e22b1923ee752901/supporting/shimexe/shim.cs) +since it had several important flaws: +1. [It was made in C#](https://github.com/lukesampson/scoop/tree/96de9c14bb483f9278e4b0a9e22b1923ee752901/supporting/shimexe), + and thus required an instantiation of a .NET command line app everytime it was started, + which can make a command run much slower than if it had been ran directly; +2. [It](https://github.com/lukesampson/scoop/issues/2339) [did](https://github.com/lukesampson/scoop/issues/1896) + [not](https://github.com/felixse/FluentTerminal/issues/221) handle Ctrl+C and other + signals correctly, which could be quite infuriating (and essentially killing REPLs and long-running apps). + +[`shim.c`](./shim.c) is: +- **Faster**, because it does not use the .NET Framework, and parses the `.shim` file in a simpler way. +- **More efficient**, because by the time the target of the shim is started, all allocated memory will have been freed. +- And more importantly, it **works better**: + - Signals originating from pressing `Ctrl+C` are ignored, and therefore handled directly by the spawned child. + Your processes and REPLs will no longer close when pressing `Ctrl+C`. + - Children are automatically killed when the shim process is killed. No more orphaned processes and weird behaviors. + +> **Note**: This project is not affiliated with [Scoop](https://github.com/lukesampson/scoop). + + +## Installation for Scoop + +- In a Visual Studio command prompt, run `cl /O1 shim.c`. +- Replace any `.exe` in `scoop\shims` by `shim.exe`. + +An additional script, `repshims.bat`, is provided. It will replace all `.exe`s in the user's Scoop directory +by `shim.exe`. + + +## Example + +Given the following shim `gs.shim`: +``` +path = C:\Program Files\Git\git.exe +args = status -u +``` + +In this directory, where `gs.exe` is the compiled `shim.c`: +``` +C:\Bin\ + gs.exe + gs.shim +``` + +Then calling `gs -s` will run the program `C:\Program Files\Git\git.exe status -u -s`. + + +## Shim format + +Shims follow the same format as Scoop's shims: line-separated `key = value` pairs. +``` +path = C:\Program Files\Git\git.exe +args = status -uno +``` + +`path` is a required value, but `args` can be omitted. Also, do note that lines **must** end with a line feed. + + +## License + +`SPDX-License-Identifier: MIT OR Unlicense` diff --git a/scoop-better-shimexe/repshims.bat b/scoop-better-shimexe/repshims.bat new file mode 100644 index 0000000..e9bc392 --- /dev/null +++ b/scoop-better-shimexe/repshims.bat @@ -0,0 +1,15 @@ +@echo off + +if not defined SCOOP set SCOOP=%USERPROFILE%\scoop + +for %%x in ("%SCOOP%\shims\*.exe") do ( + echo Replacing %%x by new shim. + copy /B /Y shim.exe "%%~x" >NUL +) + +if not defined SCOOP_GLOBAL set SCOOP_GLOBAL=%ProgramData%\scoop + +for %%x in ("%SCOOP_GLOBAL%\shims\*.exe") do ( + echo Replacing %%x by new shim. + copy /B /Y shim.exe "%%~x" >NUL +) diff --git a/scoop-better-shimexe/shim.c b/scoop-better-shimexe/shim.c new file mode 100644 index 0000000..2415738 --- /dev/null +++ b/scoop-better-shimexe/shim.c @@ -0,0 +1,256 @@ +#pragma comment(lib, "SHELL32.LIB") + +#include +#include +#include +#include + +#ifndef ERROR_ELEVATION_REQUIRED +# define ERROR_ELEVATION_REQUIRED 740 +#endif + +#define MAX_FILENAME_SIZE 512 + +BOOL WINAPI ctrlhandler(DWORD fdwCtrlType) +{ + switch (fdwCtrlType) { + // Ignore all events, and let the child process + // handle them. + case CTRL_C_EVENT: + case CTRL_CLOSE_EVENT: + case CTRL_LOGOFF_EVENT: + case CTRL_BREAK_EVENT: + case CTRL_SHUTDOWN_EVENT: + return TRUE; + + default: + return FALSE; + } +} + +int compute_program_length(const wchar_t* commandline) +{ + int i = 0; + + if (commandline[0] == L'"') { + // Wait till end of string + i++; + + for (;;) { + wchar_t c = commandline[i++]; + + if (c == 0) + return i - 1; + else if (c == L'\\') + i++; + else if (c == L'"') + return i; + } + } else { + for (;;) { + wchar_t c = commandline[i++]; + + if (c == 0) + return i - 1; + else if (c == L'\\') + i++; + else if (c == L' ') + return i; + } + } +} + +int main() +{ + DWORD exit_code = 0; + + wchar_t* path = NULL; + wchar_t* args = NULL; + wchar_t* cmd = NULL; + + // Find filename of current executable. + wchar_t filename[MAX_FILENAME_SIZE + 2]; + const unsigned int filename_size = GetModuleFileNameW(NULL, filename, MAX_FILENAME_SIZE); + + if (filename_size >= MAX_FILENAME_SIZE) { + fprintf(stderr, "The filename of the program is too long to handle.\n"); + + exit_code = 1; + goto cleanup; + } + + // Use filename of current executable to find .shim + filename[filename_size - 3] = L's'; + filename[filename_size - 2] = L'h'; + filename[filename_size - 1] = L'i'; + filename[filename_size - 0] = L'm'; + filename[filename_size + 1] = 0 ; + + FILE* shim_file; + + if ((shim_file = _wfsopen(filename, L"r,ccs=UTF-8", _SH_DENYNO)) == NULL) { + fprintf(stderr, "Cannot open shim file for read.\n"); + + exit_code = 1; + goto cleanup; + } + + size_t command_length = 256; + size_t path_length; + size_t args_length; + + // Read shim + wchar_t linebuf[8192]; + + for (;;) { + const wchar_t* line = fgetws(linebuf, 8192, shim_file); + + if (line == NULL) + break; + + if (line[4] != L' ' || line[5] != L'=' || line[6] != L' ') + continue; + + const int linelen = wcslen(line); + const int len = linelen - 8 + (line[linelen - 1] != '\n'); + + if (line[0] == L'p' && line[1] == L'a' && line[2] == L't' && line[3] == L'h') { + // Reading path + path = calloc(len + 1, sizeof(wchar_t)); + wmemcpy(path, line + 7, len); + + command_length += len; + path_length = len; + + continue; + } + + if (line[0] == L'a' && line[1] == L'r' && line[2] == L'g' && line[3] == L's') { + // Reading args + args = calloc(len + 1, sizeof(wchar_t)); + wmemcpy(args, line + 7, len); + + command_length += len + 1; + args_length = len; + + continue; + } + + continue; + } + + fclose(shim_file); + + if (path == NULL) { + fprintf(stderr, "Could not read shim file.\n"); + + exit_code = 1; + goto cleanup; + } + + // Find length of command to run + wchar_t* given_cmd = GetCommandLineW(); + const int program_length = compute_program_length(given_cmd); + + given_cmd += program_length; + + const int given_length = wcslen(given_cmd); + + command_length += given_length; + + // Start building command to run, using '[path] [args]', as given by shim. + cmd = calloc(command_length, sizeof(wchar_t)); + int cmd_i = 0; + + wmemcpy(cmd, path, path_length); + cmd[path_length] = ' '; + cmd_i += path_length + 1; + + if (args != NULL) { + wmemcpy(cmd + path_length + 1, args, args_length); + cmd[path_length + args_length + 1] = ' '; + cmd_i += args_length + 1; + } + + // Copy all given arguments to command + wmemcpy(cmd + cmd_i, given_cmd, given_length); + + // Find out if the target program is a console app + SHFILEINFOW sfi = {0}; + const BOOL is_windows_app = HIWORD(SHGetFileInfoW(path, -1, &sfi, sizeof(sfi), SHGFI_EXETYPE)); + + if (is_windows_app) + // Unfortunately, this technique will still show a window for a fraction of time, + // but there's just no workaround. + FreeConsole(); + + // Create job object, which can be attached to child processes + // to make sure they terminate when the parent terminates as well. + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {0}; + HANDLE jobHandle = CreateJobObject(NULL, NULL); + + jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK; + SetInformationJobObject(jobHandle, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli)); + + // Start subprocess + STARTUPINFOW si = {0}; + PROCESS_INFORMATION pi = {0}; + + if (CreateProcessW(NULL, cmd, NULL, NULL, TRUE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) { + AssignProcessToJobObject(jobHandle, pi.hProcess); + ResumeThread(pi.hThread); + } else { + if (GetLastError() == ERROR_ELEVATION_REQUIRED) { + // We must elevate the process, which is (basically) impossible with + // CreateProcess, and therefore we fallback to ShellExecuteEx, + // which CAN create elevated processes, at the cost of opening a new separate + // window. + // Theorically, this could be fixed (or rather, worked around) using pipes + // and IPC, but... this is a question for another day. + SHELLEXECUTEINFOW sei = {0}; + + sei.cbSize = sizeof(SHELLEXECUTEINFOW); + sei.fMask = SEE_MASK_NOCLOSEPROCESS; + sei.lpFile = path; + sei.lpParameters = cmd + path_length + 1; + sei.nShow = SW_SHOW; + + if (!ShellExecuteExW(&sei)) { + fprintf(stderr, "Unable to create elevated process: error %li.", GetLastError()); + + exit_code = 1; + goto cleanup; + } + + pi.hProcess = sei.hProcess; + } else { + fprintf(stderr, "Could not create process with command '%ls'.\n", cmd); + + exit_code = 1; + goto cleanup; + } + } + + // Ignore Ctrl-C and other signals + if (!SetConsoleCtrlHandler(ctrlhandler, TRUE)) + fprintf(stderr, "Could not set control handler; Ctrl-C behavior may be invalid.\n"); + + // Wait till end of process + WaitForSingleObject(pi.hProcess, INFINITE); + + GetExitCodeProcess(pi.hProcess, &exit_code); + + // Dispose of everything + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + CloseHandle(jobHandle); + +cleanup: + + // Free obsolete buffers + free(path); + free(args); + free(cmd); + + return (int)exit_code; +}