using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Management;
|
|
using System.Security.AccessControl;
|
|
using System.ServiceProcess;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using TrustedUninstaller.Shared.Exceptions;
|
|
using TrustedUninstaller.Shared.Tasks;
|
|
using YamlDotNet.Serialization;
|
|
|
|
namespace TrustedUninstaller.Shared.Actions
|
|
{
|
|
public class FileAction : ITaskAction
|
|
{
|
|
[YamlMember(typeof(string), Alias = "path")]
|
|
public string RawPath { get; set; }
|
|
|
|
[YamlMember(typeof(string), Alias = "prioritizeExe")]
|
|
public bool ExeFirst { get; set; } = false;
|
|
|
|
[YamlMember(typeof(string), Alias = "weight")]
|
|
public int ProgressWeight { get; set; } = 2;
|
|
|
|
[YamlMember(typeof(string), Alias = "useNSudoTI")]
|
|
public bool TrustedInstaller { get; set; } = false;
|
|
|
|
public int GetProgressWeight() => ProgressWeight;
|
|
private bool InProgress { get; set; }
|
|
public void ResetProgress() => InProgress = false;
|
|
|
|
|
|
public string ErrorString() => $"FileAction failed to remove file or directory '{Environment.ExpandEnvironmentVariables(RawPath)}'.";
|
|
|
|
private string GetRealPath()
|
|
{
|
|
return Environment.ExpandEnvironmentVariables(RawPath);
|
|
}
|
|
|
|
private string GetRealPath(string path)
|
|
{
|
|
return Environment.ExpandEnvironmentVariables(path);
|
|
}
|
|
|
|
public UninstallTaskStatus GetStatus()
|
|
{
|
|
if (InProgress) return UninstallTaskStatus.InProgress; var realPath = GetRealPath();
|
|
|
|
if (realPath.Contains("*"))
|
|
{
|
|
var lastToken = realPath.LastIndexOf("\\");
|
|
var parentPath = realPath.Remove(lastToken).TrimEnd('\\');
|
|
|
|
// This is to prevent it from re-iterating with an incorrect argument
|
|
if (parentPath.Contains("*")) return UninstallTaskStatus.Completed;
|
|
var filter = realPath.Substring(lastToken + 1);
|
|
|
|
if (Directory.Exists(parentPath) && (Directory.GetFiles(parentPath, filter).Any() || Directory.GetDirectories(parentPath, filter).Any()))
|
|
{
|
|
return UninstallTaskStatus.ToDo;
|
|
}
|
|
else return UninstallTaskStatus.Completed;
|
|
}
|
|
|
|
var isFile = File.Exists(realPath);
|
|
var isDirectory = Directory.Exists(realPath);
|
|
|
|
return isFile || isDirectory ? UninstallTaskStatus.ToDo : UninstallTaskStatus.Completed;
|
|
}
|
|
|
|
private async Task DeleteFile(string file, bool log = false)
|
|
{
|
|
if (!TrustedInstaller)
|
|
{
|
|
try {await Task.Run(() => File.Delete(file));} catch {}
|
|
|
|
if (File.Exists(file))
|
|
{
|
|
CmdAction delAction = new CmdAction()
|
|
{
|
|
Command = $"del /q /f {file}"
|
|
};
|
|
await delAction.RunTask();
|
|
}
|
|
}
|
|
else if (File.Exists("NSudoLC.exe"))
|
|
{
|
|
RunAction tiDelAction = new RunAction()
|
|
{
|
|
Exe = "NSudoLC.exe",
|
|
Arguments = $"-U:T -P:E -M:S -Priority:RealTime -UseCurrentConsole -Wait cmd /c \"del /q /f \"{file}\"\"",
|
|
BaseDir = true,
|
|
CreateWindow = false
|
|
};
|
|
|
|
await tiDelAction.RunTask();
|
|
if (tiDelAction.Output != null)
|
|
{
|
|
if (log) ErrorLogger.WriteToErrorLog(tiDelAction.Output, Environment.StackTrace,
|
|
$"FileAction Error", file);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ErrorLogger.WriteToErrorLog($"NSudo was invoked with no supplied NSudo executable.", Environment.StackTrace,
|
|
$"FileAction Error", file);
|
|
}
|
|
}
|
|
private async Task RemoveDirectory(string dir, bool log = false)
|
|
{
|
|
if (!TrustedInstaller)
|
|
{
|
|
try { Directory.Delete(dir, true); } catch { }
|
|
|
|
if (Directory.Exists(dir))
|
|
{
|
|
Console.WriteLine("Directory still exists.. trying second method.");
|
|
var deleteDirCmd = new CmdAction()
|
|
{
|
|
Command = $"rmdir /Q /S \"{dir}\""
|
|
};
|
|
await deleteDirCmd.RunTask();
|
|
|
|
if (deleteDirCmd.StandardError != null)
|
|
{
|
|
Console.WriteLine($"Error Output: {deleteDirCmd.StandardError}");
|
|
}
|
|
if (deleteDirCmd.StandardOutput != null)
|
|
{
|
|
Console.WriteLine($"Standard Output: {deleteDirCmd.StandardOutput}");
|
|
}
|
|
}
|
|
}
|
|
else if (File.Exists("NSudoLC.exe"))
|
|
{
|
|
RunAction tiDelAction = new RunAction()
|
|
{
|
|
Exe = "NSudoLC.exe",
|
|
Arguments = $"-U:T -P:E -M:S -Priority:RealTime -UseCurrentConsole -Wait cmd /c \"rmdir /q /s \"{dir}\"\"",
|
|
BaseDir = true,
|
|
CreateWindow = false
|
|
};
|
|
|
|
await tiDelAction.RunTask();
|
|
|
|
if (tiDelAction.Output != null)
|
|
{
|
|
if (log) ErrorLogger.WriteToErrorLog(tiDelAction.Output, Environment.StackTrace,
|
|
$"FileAction Error", dir);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ErrorLogger.WriteToErrorLog($"NSudo was invoked with no supplied NSudo executable.", Environment.StackTrace,
|
|
$"FileAction Error", dir);
|
|
}
|
|
}
|
|
private async Task DeleteItemsInDirectory(string dir, string filter = "*")
|
|
{
|
|
var realPath = GetRealPath(dir);
|
|
|
|
var files = Directory.EnumerateFiles(realPath, filter);
|
|
var directories = Directory.EnumerateDirectories(realPath, filter);
|
|
|
|
if (ExeFirst) files = files.ToList().OrderByDescending(x => x.EndsWith(".exe"));
|
|
|
|
var lockedFilesList = new List<string> { "MpOAV.dll", "MsMpLics.dll", "EppManifest.dll", "MpAsDesc.dll", "MpClient.dll", "MsMpEng.exe" };
|
|
foreach (var file in files)
|
|
{
|
|
Console.WriteLine($"Deleting {file}...");
|
|
|
|
System.GC.Collect();
|
|
System.GC.WaitForPendingFinalizers();
|
|
await DeleteFile(file);
|
|
|
|
if (File.Exists(file))
|
|
{
|
|
TaskKillAction taskKillAction = new TaskKillAction();
|
|
|
|
if (file.EndsWith(".sys"))
|
|
{
|
|
var driverService = Path.GetFileNameWithoutExtension(file);
|
|
try
|
|
{
|
|
//ServiceAction won't work here due to it not being able to detect driver services.
|
|
var cmdAction = new CmdAction();
|
|
Console.WriteLine($"Removing driver service {driverService}...");
|
|
|
|
cmdAction.Command = Environment.Is64BitOperatingSystem ?
|
|
$"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction stop" :
|
|
$"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction stop";
|
|
await cmdAction.RunTask();
|
|
|
|
cmdAction.Command = Environment.Is64BitOperatingSystem ?
|
|
$"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction delete" :
|
|
$"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction delete";
|
|
await cmdAction.RunTask();
|
|
}
|
|
catch (Exception servException)
|
|
{
|
|
ErrorLogger.WriteToErrorLog(servException.Message, servException.StackTrace,
|
|
$"FileAction Error: Error while trying to delete driver service {driverService}.", file);
|
|
}
|
|
}
|
|
if (lockedFilesList.Contains(Path.GetFileName(file)))
|
|
{
|
|
TaskKillAction killAction = new TaskKillAction()
|
|
{
|
|
ProcessName = "MsMpEng"
|
|
};
|
|
|
|
await killAction.RunTask();
|
|
|
|
killAction.ProcessName = "NisSrv";
|
|
await killAction.RunTask();
|
|
|
|
killAction.ProcessName = "SecurityHealthService";
|
|
await killAction.RunTask();
|
|
|
|
killAction.ProcessName = "smartscreen";
|
|
await killAction.RunTask();
|
|
|
|
}
|
|
|
|
var processes = new List<Process>();
|
|
try
|
|
{
|
|
processes = WinUtil.WhoIsLocking(file);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
|
|
$"FileAction Error", file);
|
|
}
|
|
|
|
var delay = 0;
|
|
|
|
int svcCount = 0;
|
|
foreach (var svchost in processes.Where(x => x.ProcessName.Equals("svchost")))
|
|
{
|
|
try
|
|
{
|
|
using var search = new ManagementObjectSearcher($"select * from Win32_Service where ProcessId = '{svchost.Id}'");
|
|
|
|
foreach (ManagementObject queryObj in search.Get())
|
|
{
|
|
var serviceName = (string)queryObj["Name"]; // Access service name
|
|
|
|
var serv = ServiceController.GetServices().FirstOrDefault(x => x.ServiceName.Equals(serviceName));
|
|
|
|
if (serv == null) svcCount++;
|
|
else svcCount += serv.DependentServices.Length + 1;
|
|
}
|
|
} catch (Exception e)
|
|
{
|
|
Console.WriteLine($"\r\nError: Could not get amount of services locking file.\r\nException: " + e.Message);
|
|
}
|
|
}
|
|
if (svcCount > 8) Console.WriteLine("Amount of locking services exceeds 8, skipping...");
|
|
|
|
while (processes.Any() && delay <= 800 && svcCount <= 8)
|
|
{
|
|
Console.WriteLine("Processes locking the file:");
|
|
foreach (var process in processes)
|
|
{
|
|
Console.WriteLine(process.ProcessName);
|
|
}
|
|
|
|
foreach (var process in processes)
|
|
{
|
|
try
|
|
{
|
|
if (process.ProcessName.Equals("TrustedUninstaller.CLI"))
|
|
{
|
|
Console.WriteLine("Skipping TU.CLI...");
|
|
continue;
|
|
}
|
|
if (Regex.Match(process.ProcessName, "ame.?wizard", RegexOptions.IgnoreCase).Success)
|
|
{
|
|
Console.WriteLine("Skipping AME Wizard...");
|
|
continue;
|
|
}
|
|
|
|
taskKillAction.ProcessName = process.ProcessName;
|
|
taskKillAction.ProcessID = process.Id;
|
|
|
|
Console.WriteLine($"Killing locking process {process.ProcessName} with PID {process.Id}...");
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Calling ProcessName on a process object that has exited will thrown this exception causing the
|
|
// entire loop to abort. Since killing a process takes a bit of time, another process in the loop
|
|
// could exit during that time. This accounts for that.
|
|
continue;
|
|
}
|
|
|
|
await taskKillAction.RunTask();
|
|
}
|
|
|
|
// This gives any obstinant processes some time to unlock the file on their own.
|
|
//
|
|
// This could be done above but it's likely to cause HasExited errors if delays are
|
|
// introduced after WhoIsLocking.
|
|
System.Threading.Thread.Sleep(delay);
|
|
|
|
try
|
|
{
|
|
processes = WinUtil.WhoIsLocking(file);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
|
|
$"FileAction Error", file);
|
|
}
|
|
|
|
delay += 100;
|
|
}
|
|
if (delay >= 800)
|
|
ErrorLogger.WriteToErrorLog($"Could not kill locking processes for file '{file}'. Process termination loop exceeded max cycles (8).",
|
|
Environment.StackTrace, "FileAction Error");
|
|
|
|
await DeleteFile(file, true);
|
|
|
|
using (var writer = new StreamWriter("Logs\\FileChecklist.txt", true))
|
|
{
|
|
writer.WriteLine($"File Path: {file}\r\nDeleted: {!File.Exists(file)}\r\n" +
|
|
$"======================");
|
|
}
|
|
}
|
|
}
|
|
//Loop through any subdirectories
|
|
foreach (var directory in directories)
|
|
{
|
|
//Deletes the content of the directory
|
|
await DeleteItemsInDirectory(directory);
|
|
|
|
System.GC.Collect();
|
|
System.GC.WaitForPendingFinalizers();
|
|
await RemoveDirectory(directory, true);
|
|
|
|
if (Directory.Exists(directory))
|
|
ErrorLogger.WriteToErrorLog($"Could not remove directory '{directory}'.",
|
|
Environment.StackTrace, $"FileAction Error");
|
|
}
|
|
}
|
|
|
|
public async Task<bool> RunTask()
|
|
{
|
|
if (InProgress) throw new TaskInProgressException("Another File action was called while one was in progress.");
|
|
InProgress = true;
|
|
|
|
var realPath = GetRealPath();
|
|
|
|
Console.WriteLine($"Removing file or directory '{realPath}'...");
|
|
|
|
if (realPath.Contains("*"))
|
|
{
|
|
var lastToken = realPath.LastIndexOf("\\");
|
|
var parentPath = realPath.Remove(lastToken).TrimEnd('\\');
|
|
|
|
if (parentPath.Contains("*")) throw new ArgumentException("Parent directories to a given file filter cannot contain wildcards.");
|
|
var filter = realPath.Substring(lastToken + 1);
|
|
|
|
await DeleteItemsInDirectory(parentPath, filter);
|
|
|
|
InProgress = false;
|
|
return true;
|
|
}
|
|
|
|
var isFile = File.Exists(realPath);
|
|
var isDirectory = Directory.Exists(realPath);
|
|
|
|
if (isDirectory)
|
|
{
|
|
System.GC.Collect();
|
|
System.GC.WaitForPendingFinalizers();
|
|
await RemoveDirectory(realPath);
|
|
|
|
if (Directory.Exists(realPath))
|
|
{
|
|
CmdAction permAction = new CmdAction()
|
|
{
|
|
Command = $"takeown /f \"{realPath}\" /r /d Y>NUL & icacls \"{realPath}\" /t /grant Administrators:F /c > NUL",
|
|
Timeout = 5000
|
|
};
|
|
try
|
|
{
|
|
await permAction.RunTask();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, "FileAction Error", realPath);
|
|
}
|
|
|
|
try
|
|
{
|
|
if (realPath.Contains("Defender"))
|
|
{
|
|
TaskKillAction killAction = new TaskKillAction()
|
|
{
|
|
ProcessName = "MsMpEng"
|
|
};
|
|
|
|
await killAction.RunTask();
|
|
|
|
killAction.ProcessName = "NisSrv";
|
|
await killAction.RunTask();
|
|
|
|
killAction.ProcessName = "SecurityHealthService";
|
|
await killAction.RunTask();
|
|
|
|
killAction.ProcessName = "smartscreen";
|
|
await killAction.RunTask();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
|
|
$"FileAction Error", realPath);
|
|
}
|
|
|
|
await RemoveDirectory(realPath, true);
|
|
|
|
if (Directory.Exists(realPath))
|
|
{
|
|
//Delete the files in the initial directory. DOES delete directories.
|
|
await DeleteItemsInDirectory(realPath);
|
|
|
|
System.GC.Collect();
|
|
System.GC.WaitForPendingFinalizers();
|
|
await RemoveDirectory(realPath, true);
|
|
}
|
|
}
|
|
}
|
|
else if (isFile)
|
|
{
|
|
try
|
|
{
|
|
var lockedFilesList = new List<string> { "MpOAV.dll", "MsMpLics.dll", "EppManifest.dll", "MpAsDesc.dll", "MpClient.dll", "MsMpEng.exe" };
|
|
var fileName = realPath.Split('\\').LastOrDefault();
|
|
|
|
System.GC.Collect();
|
|
System.GC.WaitForPendingFinalizers();
|
|
await DeleteFile(realPath);
|
|
|
|
if (File.Exists(realPath))
|
|
{
|
|
CmdAction permAction = new CmdAction()
|
|
{
|
|
Command = $"takeown /f \"{realPath}\" /r /d Y>NUL & icacls \"{realPath}\" /t /grant Administrators:F /c > NUL",
|
|
Timeout = 5000
|
|
};
|
|
try
|
|
{
|
|
await permAction.RunTask();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, "FileAction Error", realPath);
|
|
}
|
|
|
|
TaskKillAction taskKillAction = new TaskKillAction();
|
|
|
|
if (realPath.EndsWith(".sys"))
|
|
{
|
|
var driverService = Path.GetFileNameWithoutExtension(realPath);
|
|
try
|
|
{
|
|
//ServiceAction won't work here due to it not being able to detect driver services.
|
|
var cmdAction = new CmdAction();
|
|
Console.WriteLine($"Removing driver service {driverService}...");
|
|
|
|
cmdAction.Command = Environment.Is64BitOperatingSystem ?
|
|
$"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction stop" :
|
|
$"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction stop";
|
|
await cmdAction.RunTask();
|
|
|
|
cmdAction.Command = Environment.Is64BitOperatingSystem ?
|
|
$"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction delete" :
|
|
$"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction delete";
|
|
await cmdAction.RunTask();
|
|
}
|
|
catch (Exception servException)
|
|
{
|
|
ErrorLogger.WriteToErrorLog(servException.Message, servException.StackTrace,
|
|
$"FileAction Error: Error trying to delete driver service {driverService}.", realPath);
|
|
}
|
|
}
|
|
|
|
if (lockedFilesList.Contains(fileName))
|
|
{
|
|
TaskKillAction killAction = new TaskKillAction()
|
|
{
|
|
ProcessName = "MsMpEng"
|
|
};
|
|
|
|
await killAction.RunTask();
|
|
|
|
killAction.ProcessName = "NisSrv";
|
|
await killAction.RunTask();
|
|
|
|
killAction.ProcessName = "SecurityHealthService";
|
|
await killAction.RunTask();
|
|
|
|
killAction.ProcessName = "smartscreen";
|
|
await killAction.RunTask();
|
|
|
|
}
|
|
|
|
var processes = new List<Process>();
|
|
try
|
|
{
|
|
processes = WinUtil.WhoIsLocking(realPath);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
|
|
$"FileAction Error", realPath);
|
|
}
|
|
var delay = 0;
|
|
|
|
int svcCount = 0;
|
|
foreach (var svchost in processes.Where(x => x.ProcessName.Equals("svchost")))
|
|
{
|
|
try
|
|
{
|
|
using var search = new ManagementObjectSearcher($"select * from Win32_Service where ProcessId = '{svchost.Id}'");
|
|
|
|
foreach (ManagementObject queryObj in search.Get())
|
|
{
|
|
var serviceName = (string)queryObj["Name"]; // Access service name
|
|
|
|
var serv = ServiceController.GetServices().FirstOrDefault(x => x.ServiceName.Equals(serviceName));
|
|
|
|
if (serv == null) svcCount++;
|
|
else svcCount += serv.DependentServices.Length + 1;
|
|
}
|
|
} catch (Exception e)
|
|
{
|
|
Console.WriteLine($"\r\nError: Could not get amount of services locking file.\r\nException: " + e.Message);
|
|
}
|
|
}
|
|
if (svcCount > 8) Console.WriteLine("Amount of locking services exceeds 8, skipping...");
|
|
|
|
while (processes.Any() && delay <= 800 && svcCount <= 8)
|
|
{
|
|
Console.WriteLine("Processes locking the file:");
|
|
foreach (var process in processes)
|
|
{
|
|
Console.WriteLine(process.ProcessName);
|
|
}
|
|
|
|
foreach (var process in processes)
|
|
{
|
|
try
|
|
{
|
|
if (process.ProcessName.Equals("TrustedUninstaller.CLI"))
|
|
{
|
|
Console.WriteLine("Skipping TU.CLI...");
|
|
continue;
|
|
}
|
|
if (Regex.Match(process.ProcessName, "ame.?wizard", RegexOptions.IgnoreCase).Success)
|
|
{
|
|
Console.WriteLine("Skipping AME Wizard...");
|
|
continue;
|
|
}
|
|
|
|
taskKillAction.ProcessName = process.ProcessName;
|
|
taskKillAction.ProcessID = process.Id;
|
|
|
|
Console.WriteLine($"Killing {process.ProcessName} with PID {process.Id}... it is locking {realPath}");
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Calling ProcessName on a process object that has exited will thrown this exception causing the
|
|
// entire loop to abort. Since killing a process takes a bit of time, another process in the loop
|
|
// could exit during that time. This accounts for that.
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
await taskKillAction.RunTask();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
|
|
$"FileAction Error: Could not kill process {process.ProcessName}.");
|
|
}
|
|
}
|
|
|
|
// This gives any obstinant processes some time to unlock the file on their own.
|
|
//
|
|
// This could be done above but it's likely to cause HasExited errors if delays are
|
|
// introduced after WhoIsLocking.
|
|
System.Threading.Thread.Sleep(delay);
|
|
|
|
try
|
|
{
|
|
processes = WinUtil.WhoIsLocking(realPath);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
|
|
$"FileAction Error", realPath);
|
|
}
|
|
|
|
delay += 100;
|
|
}
|
|
if (delay >= 800)
|
|
ErrorLogger.WriteToErrorLog($"Could not kill locking processes for file '{realPath}'. Process termination loop exceeded max cycles (8).",
|
|
Environment.StackTrace, "FileAction Error");
|
|
|
|
await DeleteFile(realPath, true);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
|
|
$"FileAction Error: Error while trying to delete {realPath}.");
|
|
}
|
|
using (var writer = new StreamWriter("Logs\\FileChecklist.txt", true))
|
|
{
|
|
writer.WriteLine($"File Path: {realPath}\r\nDeleted: {!File.Exists(realPath)}\r\n" +
|
|
$"======================");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"File or directory '{realPath}' not found.");
|
|
}
|
|
|
|
InProgress = false;
|
|
return true;
|
|
}
|
|
}
|
|
}
|