Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d80fad6e48 | ||
|
|
a504adb22d | ||
|
|
c3fa2af348 |
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(KSwitcher VERSION 0.6.85 LANGUAGES CXX RC)
|
||||
project(KSwitcher VERSION 0.6.86 LANGUAGES CXX RC)
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||
endif()
|
||||
|
||||
@@ -29,6 +29,15 @@ Far Manager є зразком для оформлення файлового м
|
||||
Enter запускає введену команду або відкриває поточний елемент, якщо рядок
|
||||
порожній. Escape очищає рядок.
|
||||
Промпт і текст команди вирівняні по спільній базовій лінії.
|
||||
- У віддаленій панелі Enter запускає введену команду PowerShell на
|
||||
спареному ПК в поточному каталозі цієї панелі. Вивід і код завершення
|
||||
з’являються в терміналі Files. Кожна команда запускається окремо,
|
||||
тому змінні середовища й `cd` між командами не зберігаються. Escape
|
||||
в порожньому командному рядку скасовує поточну віддалену команду.
|
||||
- На ПК, який приймає команди, увімкніть **Options → Command line →
|
||||
Command history → Allow paired PCs to run PowerShell commands here**.
|
||||
Дозвіл за замовчуванням вимкнений і діє лише для спарених ПК. Команди
|
||||
запускаються без підвищених прав. Для цієї функції оновіть обидва ПК.
|
||||
|
||||
- Ctrl+F вставляє повний шлях елемента під курсором активної панелі в позицію
|
||||
курсора командного рядка або замінює виділення. Подвійні лапки додаються
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,11 @@
|
||||
# Latest Windows release
|
||||
|
||||
## v0.6.86 — 2026-11-15
|
||||
|
||||
The Files editor applies its palette before the text area appears. In a LAN pane, enter a PowerShell command to run it in that pane’s directory on the paired PC. The Files terminal displays output and the exit code. On the remote PC, enable **Allow paired PCs to run PowerShell commands here** in **Files → Options → Command line → Command history**. Both PCs need v0.6.86 or later.
|
||||
|
||||
**Українською.** Редактор Files застосовує палітру до появи текстового поля. У панелі LAN введена команда PowerShell виконується в каталозі цієї панелі на спареному ПК. Термінал Files показує вивід і код завершення. На віддаленому ПК увімкніть **Allow paired PCs to run PowerShell commands here** у **Files → Options → Command line → Command history**. На обох ПК потрібен v0.6.86 або новіший.
|
||||
|
||||
## v0.6.85 — 2026-11-15
|
||||
|
||||
Files viewer and editor windows now run independently with separate Windows taskbar buttons. Reopening a file offers to switch to its existing window. F3 and F4 work in LAN panes. The editor downloads files in the background and checks the remote file before replacing it on save. Update KSwitcher on the remote PC for writeback.
|
||||
|
||||
@@ -1 +1 @@
|
||||
658c9098a51408a680156534f9083491f9b9e53d330547a1003cefce23f34f49 KSwitcher-win-x64.zip
|
||||
70deb7c661dc7eb669ced086ebd85458a2c844125992bfa01e9ac7d745329b85 KSwitcher-win-x64.zip
|
||||
+30
-30
@@ -1,135 +1,135 @@
|
||||
{
|
||||
"version": "0.6.85",
|
||||
"schema": 1,
|
||||
"version": "0.6.86",
|
||||
"files": [
|
||||
{
|
||||
"sha256": "e0b471ca3aef6bf59c76b0554800cc7c0fde74f3aade3b82c0d5df906d7d8b0d",
|
||||
"path": "KSwitcher.exe",
|
||||
"size": 2454528
|
||||
"sha256": "119d6a114c79ebef8461eeaa8f19b30d4f87e5bbe0fdcdea30dbc30d7e33a570",
|
||||
"size": 2461696
|
||||
},
|
||||
{
|
||||
"sha256": "5cd5277464d37d8d4ea9141b5283010c3d480422d4c8fecea9b9e7346f9d72ad",
|
||||
"path": "KSwitcher.Config.exe",
|
||||
"size": 2431488
|
||||
"sha256": "d943fd063fa5f6e96d31c4300d701426e24798080a778586692117def3cdb4bd",
|
||||
"size": 2438656
|
||||
},
|
||||
{
|
||||
"sha256": "0a3db52490fb1e368ff859f01dd2fd7f49d254b78fd72cae0c79a197e345413d",
|
||||
"path": "KSwitcher.Screenshot.exe",
|
||||
"sha256": "d16b8a889225ed61e7f4b8331e4bf7a3354d2a9a8957c375c8bb0e4fd408b555",
|
||||
"size": 3050496
|
||||
},
|
||||
{
|
||||
"sha256": "53fa251679a827a000f3133fff27f26744c57336ab457b4db341221e523b45d5",
|
||||
"path": "KSwitcher.Files.exe",
|
||||
"size": 3635200
|
||||
"sha256": "f23160a531fbf800c512e76c9b171528126a1a8ed1f1f61b241e24ba66d0b8cb",
|
||||
"size": 3659264
|
||||
},
|
||||
{
|
||||
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6",
|
||||
"path": "WebView2Loader.dll",
|
||||
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6",
|
||||
"size": 194912
|
||||
},
|
||||
{
|
||||
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
|
||||
"path": "terminal/addon-fit-LICENSE.txt",
|
||||
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
|
||||
"size": 1103
|
||||
},
|
||||
{
|
||||
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527",
|
||||
"path": "terminal/addon-fit.js",
|
||||
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527",
|
||||
"size": 1521
|
||||
},
|
||||
{
|
||||
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef",
|
||||
"path": "terminal/index.html",
|
||||
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef",
|
||||
"size": 527
|
||||
},
|
||||
{
|
||||
"sha256": "d60e6ff8948b6616695297695b05ef6e74b497a8e32e6031fc2e4fe2cca39ca7",
|
||||
"path": "terminal/terminal.js",
|
||||
"sha256": "d60e6ff8948b6616695297695b05ef6e74b497a8e32e6031fc2e4fe2cca39ca7",
|
||||
"size": 4167
|
||||
},
|
||||
{
|
||||
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
|
||||
"path": "terminal/xterm-LICENSE.txt",
|
||||
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
|
||||
"size": 1261
|
||||
},
|
||||
{
|
||||
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6",
|
||||
"path": "terminal/xterm.css",
|
||||
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6",
|
||||
"size": 7112
|
||||
},
|
||||
{
|
||||
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972",
|
||||
"path": "terminal/xterm.js",
|
||||
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972",
|
||||
"size": 488663
|
||||
},
|
||||
{
|
||||
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
|
||||
"path": "LICENSE",
|
||||
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
|
||||
"size": 4401
|
||||
},
|
||||
{
|
||||
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
|
||||
"path": "json-LICENSE.txt",
|
||||
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
|
||||
"size": 1075
|
||||
},
|
||||
{
|
||||
"sha256": "185c5a37521316e940ead89cbf0b1ba5862d810c7c966e6c6a14a4a515b23625",
|
||||
"path": "files-text-license.txt",
|
||||
"sha256": "185c5a37521316e940ead89cbf0b1ba5862d810c7c966e6c6a14a4a515b23625",
|
||||
"size": 883
|
||||
},
|
||||
{
|
||||
"sha256": "7b34f2af7341f57d7bac1b5b7e55a40595a6bc20f461f98c0c6320336d0deba7",
|
||||
"path": "README.md",
|
||||
"sha256": "7b34f2af7341f57d7bac1b5b7e55a40595a6bc20f461f98c0c6320336d0deba7",
|
||||
"size": 94223
|
||||
},
|
||||
{
|
||||
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada",
|
||||
"path": "LAN-PREVIEW.md",
|
||||
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada",
|
||||
"size": 11418
|
||||
},
|
||||
{
|
||||
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
|
||||
"path": "dictionaries/en.txt",
|
||||
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
|
||||
"size": 682867
|
||||
},
|
||||
{
|
||||
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
|
||||
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
|
||||
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
|
||||
"size": 20138
|
||||
},
|
||||
{
|
||||
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
|
||||
"path": "dictionaries/README.md",
|
||||
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
|
||||
"size": 2265
|
||||
},
|
||||
{
|
||||
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
|
||||
"path": "dictionaries/ru.txt",
|
||||
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
|
||||
"size": 1060364
|
||||
},
|
||||
{
|
||||
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
|
||||
"path": "dictionaries/sources.json",
|
||||
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
|
||||
"size": 1280
|
||||
},
|
||||
{
|
||||
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
|
||||
"path": "dictionaries/uk.txt",
|
||||
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
|
||||
"size": 940799
|
||||
},
|
||||
{
|
||||
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
|
||||
"path": "tools/README.txt",
|
||||
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
|
||||
"size": 993
|
||||
},
|
||||
{
|
||||
"sha256": "5513e63e0a9c2a0d3bed65dd8add2c46588093e562d26c5fa2a24a421bd31fcb",
|
||||
"path": "tools/StayHere.exe",
|
||||
"sha256": "ddbd6664360c6538c215abddb8c85a3694ca7976af2ecba3d9974d33c5c55187",
|
||||
"size": 133120
|
||||
},
|
||||
{
|
||||
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
|
||||
"path": "tools/StayHere-README.md",
|
||||
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
|
||||
"size": 2796
|
||||
}
|
||||
]
|
||||
|
||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
||||
1 RT_MANIFEST "app.manifest"
|
||||
201 RCDATA "update.ps1"
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,6,85,0
|
||||
PRODUCTVERSION 0,6,85,0
|
||||
FILEVERSION 0,6,86,0
|
||||
PRODUCTVERSION 0,6,86,0
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
BEGIN
|
||||
@@ -20,7 +20,7 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||
VALUE "ProductName", "KSwitcher"
|
||||
VALUE "FileVersion", "0.6.85"
|
||||
VALUE "FileVersion", "0.6.86"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -114,6 +114,7 @@ BEGIN
|
||||
CONTROL "", 321, "msctls_hotkey32", WS_TABSTOP | WS_BORDER, 260, 27, 110, 15
|
||||
LTEXT "Open command history", 322, 131, 53, 120, 12
|
||||
CONTROL "", 323, "msctls_hotkey32", WS_TABSTOP | WS_BORDER, 260, 50, 110, 15
|
||||
AUTOCHECKBOX "Allow &paired PCs to run PowerShell commands here", 370, 131, 79, 250, 14
|
||||
PUSHBUTTON "Reset selected to defaults", 284, 131, 181, 125, 15
|
||||
COMBOBOX 285, 131, 183, 139, 95, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
|
||||
PUSHBUTTON "Reset property", 286, 278, 183, 108, 15
|
||||
|
||||
+102
-12
@@ -6,6 +6,7 @@
|
||||
#include <commdlg.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -279,6 +280,10 @@ struct Panel
|
||||
};
|
||||
Panel panels[2];
|
||||
FilesTerminal terminal;
|
||||
std::mutex remoteOutputMutex;
|
||||
std::string remoteOutput;
|
||||
std::atomic<bool> remoteCommandRunning{};
|
||||
std::jthread remoteCommandThread;
|
||||
std::ofstream terminalTrace;
|
||||
unsigned terminalTraceLines{};
|
||||
bool traceTerminalOutput{};
|
||||
@@ -808,7 +813,7 @@ void layout(HWND window)
|
||||
const int detailsRight=active?width:middle-2;
|
||||
place(status,detailsLeft+8,panelBottom+4,std::max(0,detailsRight-detailsLeft-16),textHeight);
|
||||
const auto ¤t=panels[active];
|
||||
const auto prompt=(current.peer?L"[Remote panel]":current.directory.wstring()+L">");
|
||||
const auto prompt=(current.peer?current.peer->name+L" · ":L"")+current.directory.wstring()+L">";
|
||||
SetWindowTextW(terminalPrompt,prompt.c_str());
|
||||
SIZE promptExtent{static_cast<LONG>(prompt.size())*cellWidth,0};
|
||||
if(const auto dc=GetDC(window))
|
||||
@@ -927,9 +932,8 @@ void insertCurrentPanelPath()
|
||||
reinterpret_cast<LPARAM>(quoted.empty()?path.c_str():quoted.c_str()));
|
||||
}
|
||||
|
||||
bool startTerminal(HWND owner)
|
||||
bool ensureTerminalView(HWND owner)
|
||||
{
|
||||
if(terminal.alive()) return true;
|
||||
if(!terminalViewRequested)
|
||||
{
|
||||
terminalViewRequested=terminalView.create(terminalOutput,[owner](const std::wstring &message)
|
||||
@@ -998,12 +1002,14 @@ bool startTerminal(HWND owner)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(panels[active].peer)
|
||||
{
|
||||
setStatus(L"Select a local panel to start the terminal.");
|
||||
return false;
|
||||
}
|
||||
if(terminal.start(panels[active].directory.c_str(),terminalSize))
|
||||
return true;
|
||||
}
|
||||
|
||||
bool startTerminal(HWND owner,const fs::path &directory)
|
||||
{
|
||||
if(terminal.alive()) return true;
|
||||
if(!ensureTerminalView(owner)) return false;
|
||||
if(terminal.start(directory.c_str(),terminalSize))
|
||||
{
|
||||
terminalCommandPanels={-1};
|
||||
terminalDirectory.clear();
|
||||
@@ -1013,16 +1019,90 @@ bool startTerminal(HWND owner)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool showCommandOutput(HWND owner,const Panel &panel)
|
||||
{
|
||||
return panel.peer?ensureTerminalView(owner):startTerminal(owner,panel.directory);
|
||||
}
|
||||
|
||||
void updateTerminalOutput()
|
||||
{
|
||||
if(!terminalView.initialized()) return;
|
||||
const auto bytes=terminal.takeOutput();
|
||||
if(!bytes.empty()) terminalView.send(bytes);
|
||||
std::string incoming;
|
||||
{
|
||||
const std::lock_guard lock(remoteOutputMutex);
|
||||
incoming.swap(remoteOutput);
|
||||
}
|
||||
if(!incoming.empty()) terminalView.send(incoming);
|
||||
}
|
||||
|
||||
void appendRemoteOutput(std::string_view output)
|
||||
{
|
||||
const std::lock_guard lock(remoteOutputMutex);
|
||||
for(const auto character:output)
|
||||
{
|
||||
const auto byte=static_cast<unsigned char>(character);
|
||||
if(byte>=32 || character=='\r' || character=='\n' || character=='\t')
|
||||
remoteOutput+=character;
|
||||
}
|
||||
constexpr size_t limit=1024*1024;
|
||||
if(remoteOutput.size()>limit) remoteOutput.erase(0,remoteOutput.size()-limit);
|
||||
}
|
||||
|
||||
bool submitTerminalCommand(const Panel &panel,const std::wstring &value)
|
||||
{
|
||||
if(!startTerminal(GetParent(panel.list))) return false;
|
||||
const auto owner=GetParent(panel.list);
|
||||
if(remoteCommandRunning)
|
||||
{
|
||||
setStatus(L"A remote command is running. Press Escape to cancel it.");
|
||||
return false;
|
||||
}
|
||||
if(panel.peer)
|
||||
{
|
||||
if(value.find_first_not_of(L" \t\r\n")==value.npos) return true;
|
||||
if(!ensureTerminalView(owner)) return false;
|
||||
terminalRows=std::max(terminalRows,5);
|
||||
layout(owner);
|
||||
const auto peer=*panel.peer;
|
||||
const auto directory=panel.directory;
|
||||
appendRemoteOutput(ks::utf8(L"\r\n["+peer.name+L" · "+directory.wstring()+L"]> "+value+L"\r\n"));
|
||||
remoteCommandRunning=true;
|
||||
try
|
||||
{
|
||||
remoteCommandThread=std::jthread([peer,directory,value](std::stop_token stop)
|
||||
{
|
||||
try
|
||||
{
|
||||
FilesConnection host(&peer);
|
||||
const auto code=ks::lan::runRemoteCommand(host,directory,value,appendRemoteOutput,stop);
|
||||
appendRemoteOutput("\r\n[Remote exit code "+std::to_string(code)+"]\r\n");
|
||||
}
|
||||
catch(const std::system_error &error)
|
||||
{
|
||||
const auto code=error.code().value();
|
||||
const auto reason=code==ERROR_ACCESS_DENIED
|
||||
? L"The remote PC denied this command. Check its Files command-line settings."
|
||||
: code==ERROR_INVALID_FUNCTION?L"Update KSwitcher on the remote PC."
|
||||
: code==ERROR_CANCELLED?L"Remote command canceled.":fileError(error);
|
||||
appendRemoteOutput(ks::utf8(L"\r\n"+std::wstring(reason)+L"\r\n"));
|
||||
}
|
||||
catch(const std::exception &)
|
||||
{
|
||||
appendRemoteOutput("\r\nThe remote command connection ended.\r\n");
|
||||
}
|
||||
remoteCommandRunning=false;
|
||||
});
|
||||
}
|
||||
catch(const std::system_error &error)
|
||||
{
|
||||
remoteCommandRunning=false;
|
||||
setStatus(fileError(error));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if(!startTerminal(owner,panel.directory)) return false;
|
||||
if(!terminal.write(value+L"\r"))
|
||||
{
|
||||
setStatus(L"Could not send PowerShell input.");
|
||||
@@ -1042,6 +1122,11 @@ LRESULT CALLBACK terminalInputKeys(HWND input,UINT message,WPARAM wp,LPARAM lp,U
|
||||
if(message==WM_CHAR && wp==VK_TAB && !terminalVisible) return 0;
|
||||
if(message==WM_CHAR && wp==VK_ESCAPE)
|
||||
{
|
||||
if(remoteCommandRunning && !GetWindowTextLengthW(input))
|
||||
{
|
||||
remoteCommandThread.request_stop();
|
||||
return 0;
|
||||
}
|
||||
recallCommand(terminalHistory.size());
|
||||
return 0;
|
||||
}
|
||||
@@ -4906,6 +4991,11 @@ LRESULT CALLBACK filesWindow(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
for(auto &panel:panels) panel.watch.reset();
|
||||
terminalView.close();
|
||||
terminal.close();
|
||||
if(remoteCommandThread.joinable())
|
||||
{
|
||||
remoteCommandThread.request_stop();
|
||||
remoteCommandThread.join();
|
||||
}
|
||||
ks::files::textWindowOwnerClosed();
|
||||
return 0;
|
||||
}
|
||||
@@ -5003,7 +5093,7 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
|
||||
const auto maximum=std::max(0,static_cast<int>(bounds.bottom)/textHeight-8);
|
||||
const int rows=std::clamp(terminalRows+(message.wParam==VK_UP?1:-1),0,maximum);
|
||||
traceTerminal(message.wParam==VK_UP?"key-grow":"key-shrink",terminalRows,rows);
|
||||
if(rows && !startTerminal(window)) continue;
|
||||
if(rows && !showCommandOutput(window,panels[active])) continue;
|
||||
terminalRows=rows;
|
||||
layout(window);
|
||||
continue;
|
||||
@@ -5013,7 +5103,7 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
|
||||
(message.hwnd==window || IsChild(window,message.hwnd)) &&
|
||||
!copyInProgress)
|
||||
{
|
||||
if(terminalVisible || startTerminal(window)) showTerminal(window,!terminalVisible);
|
||||
if(terminalVisible || showCommandOutput(window,panels[active])) showTerminal(window,!terminalVisible);
|
||||
continue;
|
||||
}
|
||||
const bool panelFocused=message.hwnd==panels[0].list || message.hwnd==panels[1].list;
|
||||
|
||||
+17
-6
@@ -1,4 +1,5 @@
|
||||
constexpr int optionsSearch=270,optionsTree=271,optionsTitle=272,optionsApply=283,optionsReset=284,optionsProperty=285,optionsResetProperty=286;
|
||||
constexpr int optionsSearch=270,optionsTree=271,optionsTitle=272,optionsApply=283,optionsReset=284,optionsProperty=285,optionsResetProperty=286,
|
||||
optionsRemoteTerminal=370;
|
||||
enum OptionsPage { PanelModes, PanelColors, Appearance, CommandLine, TextViewer, TextEditor, allOptions };
|
||||
constexpr const wchar_t *optionsTitles[]{L"File panel modes",L"Colors",L"Appearance",L"Command line",L"Viewer",L"Editor"};
|
||||
constexpr const wchar_t *colorNames[]{L"Panel background",L"Text",L"Frame",L"Popup background",
|
||||
@@ -78,6 +79,7 @@ struct OptionsBaseline
|
||||
std::array<COLORREF,ColorCount> palette=colors;
|
||||
std::vector<FileStyle> styles=fileStyles;
|
||||
std::array<WORD,2> keys=commandHistoryKeys;
|
||||
bool remoteTerminal=ks::lan::remoteTerminalAllowed();
|
||||
DWORD width=static_cast<DWORD>(sizeWidth), fields=statusFields;
|
||||
bool extensions=alignExtensions, folders=alignFolders, details=showDetails;
|
||||
std::array<bool,2> wide{panels[0].wide,panels[1].wide};
|
||||
@@ -151,7 +153,8 @@ void updateOptionsApply(HWND dialog,const OptionsDraft &draft)
|
||||
checked(202)!=base.extensions || checked(204)!=base.folders || checked(203)!=base.details ||
|
||||
fields!=base.fields || draft.palette!=base.palette || draft.styles!=base.styles ||
|
||||
draft.framePadding!=base.padding || optionsFontChanged(draft) ||
|
||||
optionHistoryKeys(dialog)!=base.keys || draft.text!=base.text;
|
||||
optionHistoryKeys(dialog)!=base.keys || checked(optionsRemoteTerminal)!=base.remoteTerminal ||
|
||||
draft.text!=base.text;
|
||||
EnableWindow(GetDlgItem(dialog,optionsApply),dirty);
|
||||
InvalidateRect(GetDlgItem(dialog,fileStylePreview),nullptr,FALSE);
|
||||
}
|
||||
@@ -420,7 +423,7 @@ void showOptionsPage(HWND dialog,OptionsDraft &draft,int page,int section)
|
||||
for(const auto &option:frameOptions)
|
||||
SendDlgItemMessageW(dialog,option.id,CB_SETCURSEL,draft.framePadding.*option.value,0);
|
||||
}
|
||||
constexpr int commandLineControls[]{320,321,322,323};
|
||||
constexpr int commandLineControls[]{320,321,322,323,optionsRemoteTerminal};
|
||||
show(commandLineControls,page==CommandLine && section==0);
|
||||
show(textControls,textOptionsPage(page));
|
||||
show(editingControls,page==TextEditor);
|
||||
@@ -515,7 +518,8 @@ void filterOptions(HWND dialog,OptionsDraft &draft)
|
||||
addSection(L"File type styles",fileStyleSection,extensionMatches || matches(L"System and hidden") ||
|
||||
matches(L"Underline") || matches(L"Strikethrough") || matches(L"Foreground"));
|
||||
}
|
||||
else if(page==CommandLine) addSection(L"Command history",0,matches(L"Previous command"));
|
||||
else if(page==CommandLine) addSection(L"Command history",0,
|
||||
matches(L"Previous command") || matches(L"Allow paired PCs to run PowerShell commands here"));
|
||||
else if(textOptionsPage(page))
|
||||
{
|
||||
const bool propertyMatches=matches(L"Tab width") ||
|
||||
@@ -579,6 +583,7 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
CheckDlgButton(dialog,274,panels[1].wide);
|
||||
for(int i=0;i<4;++i) CheckDlgButton(dialog,205+i,(statusFields>>i)&1u);
|
||||
setOptionHistoryKeys(dialog,commandHistoryKeys);
|
||||
CheckDlgButton(dialog,optionsRemoteTerminal,ks::lan::remoteTerminalAllowed());
|
||||
filterOptions(dialog,draft);
|
||||
updateOptionsApply(dialog,draft);
|
||||
return TRUE;
|
||||
@@ -692,7 +697,11 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
}
|
||||
return TRUE;
|
||||
case optionsReset:
|
||||
if(draft->page==allOptions || draft->page==CommandLine) setOptionHistoryKeys(dialog,defaultHistoryKeys);
|
||||
if(draft->page==allOptions || draft->page==CommandLine)
|
||||
{
|
||||
setOptionHistoryKeys(dialog,defaultHistoryKeys);
|
||||
CheckDlgButton(dialog,optionsRemoteTerminal,BST_UNCHECKED);
|
||||
}
|
||||
if(draft->page==allOptions || draft->page==PanelModes)
|
||||
for(const auto &property:panelProperties)
|
||||
if(draft->section<0 || property.section==draft->section) resetPanelProperty(dialog,property);
|
||||
@@ -731,7 +740,7 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
}
|
||||
case 201:
|
||||
case 202: case 203: case 204: case 205: case 206: case 207: case 208:
|
||||
case 273: case 274:
|
||||
case 273: case 274: case optionsRemoteTerminal:
|
||||
updateOptionsApply(dialog,*draft);
|
||||
return TRUE;
|
||||
case 321: case 323:
|
||||
@@ -831,6 +840,7 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
const auto inputKeys=optionHistoryKeys(dialog);
|
||||
saveChanged(L"PreviousCommandKey",inputKeys[0],base.keys[0]);
|
||||
saveChanged(L"CommandHistoryKey",inputKeys[1],base.keys[1]);
|
||||
saveChanged(ks::lan::remoteTerminalPreference,checked(optionsRemoteTerminal),base.remoteTerminal);
|
||||
saveChanged(L"SizeWidth",width,base.width);
|
||||
saveChanged(L"AlignExtensions",checked(202),base.extensions);
|
||||
saveChanged(L"AlignFolders",checked(204),base.folders);
|
||||
@@ -876,6 +886,7 @@ INT_PTR CALLBACK optionsDialog(HWND dialog,UINT message,WPARAM wp,LPARAM lp)
|
||||
CheckDlgButton(dialog,274,panels[1].wide);
|
||||
for(int i=0;i<4;++i) CheckDlgButton(dialog,205+i,(statusFields>>i)&1u);
|
||||
setOptionHistoryKeys(dialog,commandHistoryKeys);
|
||||
CheckDlgButton(dialog,optionsRemoteTerminal,ks::lan::remoteTerminalAllowed());
|
||||
if(draft->page==PanelColors && draft->section==fileStyleSection)
|
||||
updateFileStyleEditor(dialog,*draft,true);
|
||||
showOptionsPage(dialog,*draft,draft->page,draft->section);
|
||||
|
||||
@@ -1240,8 +1240,10 @@ void TextWindow::paint()
|
||||
RECT client{};
|
||||
GetClientRect(window, &client);
|
||||
const auto row = chromeHeight();
|
||||
SetDCBrushColor(dc, color(TextColor::chromebg));
|
||||
const auto brush = static_cast<HBRUSH>(GetStockObject(DC_BRUSH));
|
||||
SetDCBrushColor(dc, color(TextColor::background));
|
||||
FillRect(dc, &client, brush);
|
||||
SetDCBrushColor(dc, color(TextColor::chromebg));
|
||||
RECT menuBar{0, 0, client.right, row};
|
||||
RECT top{0, row, client.right, 2 * row};
|
||||
RECT bottom{0, client.bottom - row, client.right, client.bottom};
|
||||
@@ -1500,7 +1502,7 @@ LRESULT CALLBACK TextWindow::procedure(HWND window, UINT message, WPARAM wparam,
|
||||
identity.data(),static_cast<int>(identity.size()),TRUE)==CSTR_EQUAL;
|
||||
}
|
||||
case WM_CREATE:
|
||||
self->editor = CreateWindowExW(0, L"Scintilla", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP |
|
||||
self->editor = CreateWindowExW(0, L"Scintilla", L"", WS_CHILD | WS_TABSTOP |
|
||||
WS_VSCROLL | WS_HSCROLL, 0, 0, 0, 0, window, nullptr, GetModuleHandleW(nullptr), nullptr);
|
||||
if (!self->editor) throw std::runtime_error("Cannot create the text control.");
|
||||
self->send(SCI_SETCODEPAGE, SC_CP_UTF8);
|
||||
@@ -1519,6 +1521,7 @@ LRESULT CALLBACK TextWindow::procedure(HWND window, UINT message, WPARAM wparam,
|
||||
},1,reinterpret_cast<DWORD_PTR>(self.get()));
|
||||
if(self->remote)
|
||||
{
|
||||
self->applyPreferences();
|
||||
self->send(SCI_SETREADONLY,TRUE);
|
||||
if(!SetTimer(window,openTimer,75,nullptr))
|
||||
throw std::runtime_error("Cannot start the LAN file timer.");
|
||||
@@ -1529,6 +1532,7 @@ LRESULT CALLBACK TextWindow::procedure(HWND window, UINT message, WPARAM wparam,
|
||||
}
|
||||
else self->initializeDocument();
|
||||
self->layout();
|
||||
ShowWindow(self->editor,SW_SHOWNA);
|
||||
return 0;
|
||||
case WM_ERASEBKGND: return 1;
|
||||
case WM_PAINT: self->paint(); return 0;
|
||||
|
||||
+1
-1
@@ -295,7 +295,7 @@ struct LanService::Impl
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
return !stopping && trusted.contains(peer)?trusted.at(peer).at("key").get<std::string>():std::string{};
|
||||
});
|
||||
},lan::FilesPort,lan::remoteTerminalAllowed);
|
||||
hub.datagrams([&](const auto &target,const auto &packet){
|
||||
std::lock_guard lock(mutex);
|
||||
if(shareInput&&trusted.contains(target)&&peers.contains(target))send(socket,peers.at(target).address,packet);
|
||||
|
||||
+167
-5
@@ -5,6 +5,8 @@
|
||||
#include <winioctl.h>
|
||||
#include <atomic>
|
||||
#include <bcrypt.h>
|
||||
#include <wincrypt.h>
|
||||
#include <sddl.h>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
@@ -31,6 +33,13 @@ void checkFileReply(const Json &reply)
|
||||
if(error) throw std::system_error(static_cast<int>(error),std::system_category());
|
||||
}
|
||||
|
||||
bool remoteTerminalAllowed()
|
||||
{
|
||||
DWORD allowed{},size=sizeof(allowed);
|
||||
return RegGetValueW(HKEY_CURRENT_USER,L"Software\\KSwitcher\\Files",remoteTerminalPreference,
|
||||
RRF_RT_REG_DWORD,nullptr,&allowed,&size)==ERROR_SUCCESS && allowed==1;
|
||||
}
|
||||
|
||||
bool recycleFile(HWND owner,const fs::path &path)
|
||||
{
|
||||
const auto initialized=CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
||||
@@ -223,6 +232,122 @@ struct FileHandle
|
||||
~FileHandle(){close();}
|
||||
};
|
||||
|
||||
struct RemoteCommand
|
||||
{
|
||||
FileHandle job,process,output;
|
||||
|
||||
void stop()
|
||||
{
|
||||
job.close();
|
||||
process.close();
|
||||
output.close();
|
||||
}
|
||||
|
||||
DWORD start(const fs::path &directory,const std::wstring &command)
|
||||
{
|
||||
if(process.value!=INVALID_HANDLE_VALUE) return ERROR_BUSY;
|
||||
if(command.empty() || command.size()>12000 || command.find(L'\0')!=command.npos) return ERROR_INVALID_PARAMETER;
|
||||
const auto script=L"[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false); "
|
||||
L"$OutputEncoding=[Console]::OutputEncoding;\r\n"+command;
|
||||
const auto data=reinterpret_cast<const BYTE *>(script.data());
|
||||
const auto bytes=static_cast<DWORD>(script.size()*sizeof(wchar_t));
|
||||
constexpr DWORD encoding=CRYPT_STRING_BASE64|CRYPT_STRING_NOCRLF;
|
||||
DWORD length{};
|
||||
if(!CryptBinaryToStringW(data,bytes,encoding,nullptr,&length)) return GetLastError();
|
||||
std::wstring encoded(length,L'\0');
|
||||
if(!CryptBinaryToStringW(data,bytes,encoding,encoded.data(),&length)) return GetLastError();
|
||||
encoded.resize(length);
|
||||
auto arguments=L"powershell.exe -NoLogo -NoProfile -NonInteractive -OutputFormat Text -EncodedCommand "+encoded;
|
||||
wchar_t system[MAX_PATH]{};
|
||||
if(!GetSystemDirectoryW(system,MAX_PATH)) return GetLastError();
|
||||
const auto shell=std::wstring(system)+L"\\WindowsPowerShell\\v1.0\\powershell.exe";
|
||||
|
||||
FileHandle token,restricted,read,write,input,processJob;
|
||||
if(!OpenProcessToken(GetCurrentProcess(),TOKEN_QUERY|TOKEN_DUPLICATE|TOKEN_ASSIGN_PRIMARY|
|
||||
TOKEN_ADJUST_DEFAULT,&token.value) ||
|
||||
!CreateRestrictedToken(token.value,DISABLE_MAX_PRIVILEGE|LUA_TOKEN,0,nullptr,0,nullptr,0,nullptr,
|
||||
&restricted.value)) return GetLastError();
|
||||
PSID medium{};
|
||||
if(!ConvertStringSidToSidW(L"S-1-16-8192",&medium)) return GetLastError();
|
||||
TOKEN_MANDATORY_LABEL label{{medium,SE_GROUP_INTEGRITY}};
|
||||
const auto limited=SetTokenInformation(restricted.value,TokenIntegrityLevel,&label,
|
||||
sizeof(label)+GetLengthSid(medium));
|
||||
const auto tokenError=GetLastError();
|
||||
LocalFree(medium);
|
||||
if(!limited) return tokenError;
|
||||
|
||||
SECURITY_ATTRIBUTES inheritable{sizeof(inheritable),nullptr,TRUE};
|
||||
if(!CreatePipe(&read.value,&write.value,&inheritable,0) ||
|
||||
!SetHandleInformation(read.value,HANDLE_FLAG_INHERIT,0)) return GetLastError();
|
||||
input.value=CreateFileW(L"NUL",GENERIC_READ,FILE_SHARE_READ|FILE_SHARE_WRITE,&inheritable,
|
||||
OPEN_EXISTING,0,nullptr);
|
||||
if(input.value==INVALID_HANDLE_VALUE) return GetLastError();
|
||||
const auto createdJob=CreateJobObjectW(nullptr,nullptr);
|
||||
if(!createdJob) return GetLastError();
|
||||
processJob.value=createdJob;
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
|
||||
limits.BasicLimitInformation.LimitFlags=JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
if(!SetInformationJobObject(processJob.value,JobObjectExtendedLimitInformation,&limits,sizeof(limits)))
|
||||
return GetLastError();
|
||||
|
||||
SIZE_T attributeBytes{};
|
||||
InitializeProcThreadAttributeList(nullptr,1,0,&attributeBytes);
|
||||
std::vector<BYTE> storage(attributeBytes);
|
||||
const auto attributes=reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(storage.data());
|
||||
if(!InitializeProcThreadAttributeList(attributes,1,0,&attributeBytes)) return GetLastError();
|
||||
HANDLE inherited[]{input.value,write.value};
|
||||
STARTUPINFOEXW startup{};
|
||||
startup.StartupInfo.cb=sizeof(startup);
|
||||
startup.StartupInfo.dwFlags=STARTF_USESTDHANDLES;
|
||||
startup.StartupInfo.hStdInput=input.value;
|
||||
startup.StartupInfo.hStdOutput=startup.StartupInfo.hStdError=write.value;
|
||||
startup.lpAttributeList=attributes;
|
||||
PROCESS_INFORMATION child{};
|
||||
const auto started=UpdateProcThreadAttribute(attributes,0,PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||
inherited,sizeof(inherited),nullptr,nullptr) &&
|
||||
CreateProcessAsUserW(restricted.value,shell.c_str(),arguments.data(),nullptr,nullptr,TRUE,
|
||||
CREATE_SUSPENDED|CREATE_NO_WINDOW|EXTENDED_STARTUPINFO_PRESENT,nullptr,
|
||||
directory.c_str(),&startup.StartupInfo,&child);
|
||||
const auto processError=GetLastError();
|
||||
DeleteProcThreadAttributeList(attributes);
|
||||
if(!started) return processError;
|
||||
FileHandle thread{child.hThread},childProcess{child.hProcess};
|
||||
if(!AssignProcessToJobObject(processJob.value,childProcess.value) || ResumeThread(thread.value)==DWORD(-1))
|
||||
{
|
||||
const auto error=GetLastError();
|
||||
TerminateProcess(childProcess.value,error);
|
||||
return error;
|
||||
}
|
||||
job.value=std::exchange(processJob.value,INVALID_HANDLE_VALUE);
|
||||
process.value=std::exchange(childProcess.value,INVALID_HANDLE_VALUE);
|
||||
output.value=std::exchange(read.value,INVALID_HANDLE_VALUE);
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
Json poll()
|
||||
{
|
||||
if(process.value==INVALID_HANDLE_VALUE) return failure(ERROR_INVALID_HANDLE);
|
||||
const auto wait=WaitForSingleObject(process.value,0);
|
||||
if(wait==WAIT_FAILED) return failure();
|
||||
DWORD available{};
|
||||
if(!PeekNamedPipe(output.value,nullptr,0,nullptr,&available,nullptr) && GetLastError()!=ERROR_BROKEN_PIPE)
|
||||
return failure();
|
||||
Bytes bytes(std::min<DWORD>(available,FileChunk));
|
||||
DWORD read{};
|
||||
if(!bytes.empty() && !ReadFile(output.value,bytes.data(),static_cast<DWORD>(bytes.size()),&read,nullptr))
|
||||
return failure();
|
||||
bytes.resize(read);
|
||||
const bool done=wait==WAIT_OBJECT_0 && bytes.empty();
|
||||
DWORD exitCode{};
|
||||
if(done)
|
||||
{
|
||||
if(!GetExitCodeProcess(process.value,&exitCode)) return failure();
|
||||
stop();
|
||||
}
|
||||
return {{"error",0},{"data",Json::binary(std::move(bytes))},{"done",done},{"exitCode",exitCode}};
|
||||
}
|
||||
};
|
||||
|
||||
fs::path temporaryFile(const fs::path &target)
|
||||
{
|
||||
return target.parent_path()/wide(".kswitcher-"+randomHex(16)+".tmp");
|
||||
@@ -272,6 +397,8 @@ std::string hashPrefix(HANDLE file,uint64_t size,const FileProgress &progress,ui
|
||||
|
||||
class FileOperations
|
||||
{
|
||||
FilesListener::TerminalPermission terminalPermission;
|
||||
RemoteCommand terminal;
|
||||
bool remote;
|
||||
HANDLE search=INVALID_HANDLE_VALUE;
|
||||
WIN32_FIND_DATAW entry{};
|
||||
@@ -426,7 +553,8 @@ class FileOperations
|
||||
return reply;
|
||||
}
|
||||
public:
|
||||
explicit FileOperations(bool remote):remote(remote){}
|
||||
explicit FileOperations(bool remote,FilesListener::TerminalPermission permission={}):
|
||||
terminalPermission(std::move(permission)),remote(remote){}
|
||||
~FileOperations(){closeSearch();discard();}
|
||||
Json call(const Json &request,const FileProgress &progress)
|
||||
{
|
||||
@@ -440,6 +568,18 @@ public:
|
||||
Json perform(const Json &request,const FileProgress &progress)
|
||||
{
|
||||
const auto action=request.at("action").get<std::string>();
|
||||
if(action=="terminalRun" || action=="terminalPoll" || action=="terminalCancel")
|
||||
{
|
||||
if(!remote || !terminalPermission || !terminalPermission())
|
||||
{
|
||||
terminal.stop();
|
||||
return failure(ERROR_ACCESS_DENIED);
|
||||
}
|
||||
if(action=="terminalCancel") {terminal.stop();return ok();}
|
||||
if(action=="terminalPoll") return terminal.poll();
|
||||
const auto error=terminal.start(path(request.at("path")),wide(request.at("command").get<std::string>()));
|
||||
return error?failure(error):ok();
|
||||
}
|
||||
if(action=="drives")
|
||||
{
|
||||
auto reply=ok();
|
||||
@@ -805,6 +945,26 @@ void streamFile(FilesConnection &source,FilesConnection &target,const std::strin
|
||||
}
|
||||
}
|
||||
|
||||
DWORD runRemoteCommand(FilesConnection &host,const fs::path &directory,std::wstring_view command,
|
||||
const std::function<void(std::string_view)> &output,std::stop_token stop)
|
||||
{
|
||||
if(stop.stop_requested()) throw std::system_error(ERROR_CANCELLED,std::system_category());
|
||||
auto request=fileRequest("terminalRun",directory);
|
||||
request["command"]=utf8(std::wstring(command));
|
||||
checkFileReply(host.call(request));
|
||||
while(!stop.stop_requested())
|
||||
{
|
||||
const auto reply=host.call({{"action","terminalPoll"}});
|
||||
checkFileReply(reply);
|
||||
const auto &bytes=reply.at("data").get_binary();
|
||||
if(!bytes.empty()) output({reinterpret_cast<const char *>(bytes.data()),bytes.size()});
|
||||
if(reply.at("done").get<bool>()) return reply.at("exitCode").get<DWORD>();
|
||||
if(bytes.empty()) std::this_thread::sleep_for(std::chrono::milliseconds(60));
|
||||
}
|
||||
checkFileReply(host.call({{"action","terminalCancel"}}));
|
||||
throw std::system_error(ERROR_CANCELLED,std::system_category());
|
||||
}
|
||||
|
||||
struct FilesListener::Impl
|
||||
{
|
||||
struct Client
|
||||
@@ -820,10 +980,12 @@ struct FilesListener::Impl
|
||||
};
|
||||
std::string local;
|
||||
PairKey key;
|
||||
TerminalPermission terminalPermission;
|
||||
WinSocket listener;
|
||||
std::vector<std::unique_ptr<Client>> clients;
|
||||
uint16_t boundPort{};
|
||||
Impl(std::string id,PairKey lookup,uint16_t port):local(std::move(id)),key(std::move(lookup))
|
||||
Impl(std::string id,PairKey lookup,uint16_t port,TerminalPermission permission):
|
||||
local(std::move(id)),key(std::move(lookup)),terminalPermission(std::move(permission))
|
||||
{
|
||||
listener.value=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
|
||||
if(listener.value==INVALID_SOCKET) return;
|
||||
@@ -858,7 +1020,7 @@ struct FilesListener::Impl
|
||||
wire.authenticate(secret,peer,local,challenge,nonce,false);
|
||||
if(wire.read().at("action")!="authenticate") return;
|
||||
wire.write(ok());
|
||||
FileOperations operations(true);
|
||||
FileOperations operations(true,terminalPermission);
|
||||
for(;;)
|
||||
{
|
||||
const auto request=wire.read(true);
|
||||
@@ -899,8 +1061,8 @@ struct FilesListener::Impl
|
||||
}
|
||||
};
|
||||
|
||||
FilesListener::FilesListener(std::string local,PairKey key,uint16_t port):
|
||||
impl(std::make_unique<Impl>(std::move(local),std::move(key),port)){}
|
||||
FilesListener::FilesListener(std::string local,PairKey key,uint16_t port,TerminalPermission terminalPermission):
|
||||
impl(std::make_unique<Impl>(std::move(local),std::move(key),port,std::move(terminalPermission))){}
|
||||
FilesListener::~FilesListener()=default;
|
||||
uint16_t FilesListener::port()const{return impl->boundPort;}
|
||||
void FilesListener::poll(){impl->poll();}
|
||||
|
||||
+7
-1
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "lan_pairing.h"
|
||||
#include <functional>
|
||||
#include <stop_token>
|
||||
|
||||
namespace ks::lan
|
||||
{
|
||||
@@ -31,6 +32,8 @@ public:
|
||||
void streamFile(FilesConnection &source,FilesConnection &target,const std::string &sourceId,
|
||||
const std::filesystem::path &from,const std::filesystem::path &to,uint64_t size,uint64_t modified,
|
||||
bool replace,bool append,const FileProgress &progress,const FileRevision *expectedDestination=nullptr);
|
||||
DWORD runRemoteCommand(FilesConnection &host,const std::filesystem::path &directory,std::wstring_view command,
|
||||
const std::function<void(std::string_view)> &output,std::stop_token stop={});
|
||||
|
||||
class FilesListener
|
||||
{
|
||||
@@ -38,7 +41,8 @@ class FilesListener
|
||||
std::unique_ptr<Impl> impl;
|
||||
public:
|
||||
using PairKey = std::function<std::string(const std::string &)>;
|
||||
FilesListener(std::string local, PairKey key,uint16_t port=FilesPort);
|
||||
using TerminalPermission = std::function<bool()>;
|
||||
FilesListener(std::string local, PairKey key,uint16_t port=FilesPort,TerminalPermission terminalPermission={});
|
||||
uint16_t port() const;
|
||||
~FilesListener();
|
||||
void poll();
|
||||
@@ -48,4 +52,6 @@ Json fileRequest(std::string_view action, const std::filesystem::path &path);
|
||||
std::filesystem::path filePath(const Json &path);
|
||||
void checkFileReply(const Json &reply);
|
||||
bool recycleFile(HWND owner, const std::filesystem::path &path);
|
||||
inline constexpr wchar_t remoteTerminalPreference[]=L"AllowRemoteTerminal";
|
||||
bool remoteTerminalAllowed();
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.85";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.86";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
@@ -459,6 +459,11 @@ try {
|
||||
|
||||
Post $panel 0x100 0x73
|
||||
$editor=Wait-Until { Find-Window 'KSwitcher.Files.Text' $process.Id }
|
||||
$firstControl=[SearchArrowTest]::FindWindowEx($editor,0,'Scintilla',[System.Management.Automation.Language.NullString]::Value)
|
||||
if ($firstControl -eq [IntPtr]::Zero -or
|
||||
[SearchArrowTest]::Send($firstControl,2482,32,0).ToInt64() -eq 0xFFFFFF) {
|
||||
throw 'The editor is visible with the default white Scintilla background.'
|
||||
}
|
||||
$originalFocus=Wait-Until {
|
||||
$focus=[SearchArrowTest]::FocusedControl($editor)
|
||||
if ($focus -ne [IntPtr]::Zero) { $focus }
|
||||
|
||||
+28
-1
@@ -2,6 +2,7 @@
|
||||
#include <sddl.h>
|
||||
#include "lan_files.h"
|
||||
#include <bit>
|
||||
#include <atomic>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
@@ -113,7 +114,8 @@ int main()
|
||||
fs::create_directory(root);
|
||||
const auto client=randomHex(16),first=randomHex(16),second=randomHex(16),key=randomHex(32);
|
||||
auto lookup=[&](const std::string &id){return id==client?key:std::string{};};
|
||||
FilesListener a(first,lookup,0),b(second,lookup,0);
|
||||
std::atomic<bool> allowTerminal{};
|
||||
FilesListener a(first,lookup,0,[&]{return allowTerminal.load();}),b(second,lookup,0);
|
||||
require(a.port() && b.port() && a.port()!=b.port(),"test listeners unavailable");
|
||||
std::jthread polling([&](std::stop_token stop)
|
||||
{
|
||||
@@ -131,6 +133,31 @@ int main()
|
||||
}
|
||||
catch(const std::system_error &){}
|
||||
FilesConnection host(one,client,key);
|
||||
auto deniedCommand=fileRequest("terminalRun",root);
|
||||
deniedCommand["command"]="Write-Output denied";
|
||||
require(host.call(deniedCommand).at("error")==ERROR_ACCESS_DENIED,"remote command allowed without consent");
|
||||
allowTerminal=true;
|
||||
std::string console;
|
||||
const auto exit=runRemoteCommand(host,root,
|
||||
L"[Console]::Out.WriteLine((Get-Location).ProviderPath); "
|
||||
L"[Console]::Error.WriteLine('Тест stderr'); exit 7",
|
||||
[&](std::string_view part){console+=part;});
|
||||
require(exit==7 && console.find(utf8(root.native()))!=console.npos &&
|
||||
console.find(utf8(L"Тест stderr"))!=console.npos,"remote command output or exit code differs");
|
||||
std::stop_source cancel;
|
||||
try
|
||||
{
|
||||
runRemoteCommand(host,root,L"[Console]::Out.WriteLine('Ready'); Start-Sleep -Seconds 30",
|
||||
[&](std::string_view){cancel.request_stop();},cancel.get_token());
|
||||
require(false,"remote command cancellation did not stop execution");
|
||||
}
|
||||
catch(const std::system_error &error)
|
||||
{
|
||||
require(error.code().value()==ERROR_CANCELLED,"remote terminal returned the wrong cancellation error");
|
||||
}
|
||||
allowTerminal=false;
|
||||
require(host.call(deniedCommand).at("error")==ERROR_ACCESS_DENIED,"remote command allowed after revocation");
|
||||
std::cout<<"Verified remote PowerShell consent, working directory, streamed output, exit code and cancellation.\n";
|
||||
const auto edited=root/L"edited.txt", serverFile=root/L"server.txt", snapshot=root/L"snapshot.txt";
|
||||
save(serverFile,"original remote content");
|
||||
const auto initial=host.call(fileRequest("stat",serverFile));
|
||||
|
||||
Reference in new issue
Block a user