Проблема в период карантинной работы предприятия стала следующей: действительно нужно минимизировать количество посещений кабинетов специалистами, обслуживающими и консультирующими по прикладному ПО, да и сказать откровенно, пользователи частенько злоупотребляют помощью специалистов не желая вникать в сам вопрос, мол «придут — помогут — сделают, а я пока покурю/попью кофе и т.п.». Консультация по телефону при совместном доступе к серверу эффективнее, если просматривать удаленный экран.



Уже после «изобретения» нашего велосипеда подвернулась вменяемая информация на тему статьи: RDS Shadow – теневое подключение к RDP сессиям пользователей в Windows Server 2012 R2 или Режим shadow непривилегированного пользователя в windows server или Делегируем управление RDP-сеансами. Все они подразумевают применение консоли, даже с элементами простого диалога.

Вся изложенная ниже информация предназначена для тех, кто нормально переносит ненормальные извращения для получения нужного результата, изобретая ненужные способы.
Чтобы «не тянуть кота за хвост», начну с последнего: велосипед работает у обычного пользователя с помощью утилиты AdmiLink, за что ее автору и спасибо.

I. Консоль и shadow RDP.

Так как использование с админскими правами консоли Server Manager -> QuickSessionCollection -> щелкнув по сессии интересующего пользователя, выбрав в контекстном меню Shadow (Теневая копия) для персонала, инструктирующего по работе с ПО, — не вариант, был рассмотрен другой «деревянный» способ, а именно:

1. Узнаем RDP id сессии:

query user | findstr Administrator

или:

qwinsta | findstr Administrator 

Причем "| findstr Administrator" было удобно только когда ты знаешь, что именно Administrator тебе нужен, либо использовать только первую часть для лицезрения всех залогинившихся на сервере.



2. Подключаемся к этой сессии, при условии что в доменных групповых политиках параметр «Устанавливает правила удаленного управления для пользовательских сеансов служб удаленных рабочих столов» выбран параметр как минимум «Наблюдение за сеансом с разрешения пользователя» (подробнее):

mstsc /shadow:127

Прошу обратить внимание что в списке будут только логины пользователей.

Повторюсь что без админских прав вы получите следующее:



Но для предварительной отладки программы, о которой пойдет речь, я использовал учетку с правами администратора.

II. Программа

Итак постановка задачи: создание некого простого графического интерфейса для подключения к теневому сенсу пользователя с его разрешения, отправка сообщения пользователю. Среда программирования выбрана Lazarus.

1. Получаем полный доменный список пользователей «логин» — «полное имя» у админа, либо опять таки через консоль:

wmic useraccount get Name,FullName 

никто не запрещает даже так:

wmic useraccount get Name,FullName > c:\test\username.txt

Скажу сразу, что именно у Lazarus оказалось проблема с обработкой этого файла, так как по умолчанию его кодировка UCS-2, поэтому пришлось просто преобразовать вручную в обычный UTF-8. В структуре файла много табуляций, вернее множество пробелов, которые было решено все-таки программно обработать, рано или поздно задачка с кодировкой будет решена, и файл будет программно обновляться.

Итак, в задумке папка, доступная для пользователей программы, например c:\test, в которой будет 2 файла: первый с login и fullname, второй с id_rdp и login пользователей. Далее эти данные обрабатываем как можем:).

А пока для ассоциирования со списком сессий переносим это (login и fullname) содержимое в массив:

procedure Tf_rdp.UserF2Array;
var 
  F:TextFile;   i:integer;   f1, line1:String;   fL: TStringList;
begin //f_d глобальный путь к размещению файлов 
f1:=f_d+'user_name.txt';     //задача считать в массив содержимое файла
fL := TStringList.Create; // строку подвергнем метамарфозам с разделителями
fL.Delimiter := '|'; fL.StrictDelimiter := True;
AssignFile(F,f1); 
try // Открыть файл для чтения
  reset(F); ReadLn(F,line1);
  i:=0;
while not eof(F) do // Считываем строки, пока не закончится файл
begin
ReadLn(F,line1);
line1:= StringReplace(line1, '  ', '|',[]); //заменяем первый попавш.2пробела разделителем |
// удаляем все двойные пробелы
while pos('  ',line1)>0 do line1:= StringReplace(line1, '  ', ' ', [rfReplaceAll]);
begin
if (pos('|',line1)>0) then
begin //если разделитель существует заносим его в массив
fL.DelimitedText :=line1; // разбиваем на столбцы
if (fL[0]<>'') then //если учетка имеет имя
begin //вносим ее в массив
 inc(i); // избавляемся от возможных одиночных пробелов в логине
 fam[0,i]:=StringReplace(fL[1],' ','',[rfReplaceall, rfIgnoreCase]);
 fam[1,i]:=fL[0];
 end;end;end;end; // Готово. Закрываем файл.
 CloseFile(F);
 Fl.Free;
 except
 on E: EInOutError do  ShowMessage('Ошибка обработки файла. Детали: '+E.Message);
 end;end;

Прошу извинения за «много кода», следующие пункты будут лаконичнее.

2. Аналогично методом из предыдущего пункта считываем результат обработки списка в элемент StringGrid, при этом приведу «значимый» кусок кода:

2.1 Получаем актуальный список RDP сессий в файл:

f1:=f_d+'user.txt';
cmdline:='/c query user >'+ f1;
if ShellExecute(0,nil, PChar('cmd'),PChar(cmdline),nil,1)=0 then;
Sleep(500); // можно и подольше ждать пока файл для чтения создается

2.2 Обрабатываем файл (указан только значимые строки кода):

StringGrid1.Cells[0,i]:=fL[1]; StringGrid1.Cells[2,i]:=fL[3]; //кидаем в цикле в StringGrid1
login1:=StringReplace(fL[1],' ','',[rfReplaceall, rfIgnoreCase]); //убираем из логина пробелы
if (SearchArr(login1)>=0) then //ищем в массиве из п1. логин и записываем в таблицу ФИО
StringGrid1.Cells[1,i]:=fam[1,SearchArr(login1)]
else StringGrid1.Cells[1,i]:='+'; // либо записываем плюсик:)
.... //в зависимости от выбора пользователя сортируем и форматируем по данным
if (b_id.Checked=true) then SortGrid(0) else SortGrid(1);
StringGrid1.AutoSizeColumn(0);StringGrid1.AutoSizeColumn(1); StringGrid1.AutoSizeColumn(2);  

3. Непосредственно само подключение при клике на строку с пользователем и номером его сеанса:

  id:=(StringGrid1.Row);// узнаем номер строки  IntToStr(StringGrid1.Row)
  ids:=StringGrid1.Cells[2,id]; //получаем идентификатор rdp
  cmdline:='/c mstsc /shadow:'+ ids; //и подключаемся....
 if (b_rdp.Checked=True) then  if ShellExecute(0,nil, PChar('cmd'),PChar(cmdline),nil,1) =0 then;       

4. Сделано еще пару украшательств типа сортировки по клику на radiobutton, и сообщения пользователю, либо всем пользователям.



> Полный исходный код можно увидеть здесь

III. Применение AdminLink — что я увидел:

AdminLink действительно генерирует ярлык, в котором ссылается на расположение утилиты admilaunch.exe, и личной копией утилиты запуска AdmiRun.Exe которая находится в папке пользователя, например vasya, по типу C:\Users\vasya\WINDOWS\. В общем, не все так плохо: с правами доступа к файлу ярлыка и другими, для очищения собственной админской совести, можно поиграться.