Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > Общие вопросы по .NET и C# > AppDomain. Ограничение памяти и времени выполнения


Автор: kolaspirit 16.4.2014, 03:44
Добрый вечер. Постараюсь подробно описать проблему.
    Необходимо было реализовать "песочницу" (Sandbox) для автоматической проверки олимпиадных решений по программированию через домены приложений (AppDomain). То есть участник соревнования присылает исходник с решением задачи, моя программа компилирует его и запускает exe-файл с помощью AppDomain.ExecuteAssembly(<имя_сборки>) в мною созданном AppDomain, в котором уже выставлены разрешения (Permissions), ограничивающие сборку в правах. Например, чтобы нельзя было работать с реестром, файловой системой, создавать новые процессы, новые домены приложений и т.д. 
    Но я не смог найти ни на форумах, ни в MSDN информацию о том, как ограничить по времени выполнения и по выделению оперативной памяти запускаемую в домене сборку. Сейчас использую связку: AppDomain с его Permissions и  Process с возможностью ограничивать время выполнения. Но этот вариант не подходит, так как последовательно сначала сборка исполняется в AppDomain без учета времени и памяти (в этот момент может произойти переполнение или же программа может "вечно" работать), а потом уже, по завершении, запускается в Process. Нужно чтобы все ограничения были в рамках AppDomain. Ниже приведен код. Подскажите, пожалуйста, как решить этот вопрос? Или же подскажите другие варианты, сочетающиеся с AppDomain (от доменов приложений отказываться не вижу смысла, ибо в них есть большая часть необходимых мне ограничений). Не исключаю, что решение может быть простым. Заранее прошу прощения, если его не разглядел.

Сам класс "песочницы" (убраны не относящиеся к теме методы и свойства):
Код

public class Sandbox
{
    private AppDomain SandboxAppDomain { get; set; }
    private PermissionSet SandboxPermissionSet { get; set; }

    public Sandbox(string workingDirectory, PermissionSet permissionSet)
    {
        SandboxPermissionSet = permissionSet;
        AppDomainSetup sandboxAppDomainSetup = new AppDomainSetup();
        sandboxAppDomainSetup.ApplicationBase = workindDirectory;
        SandboxAppDomain = AppDomain.CreateDomain("Sandbox", new Evidence(), sandboxAppDomainSetup, SandboxPermissionSet);
    }

    public void ExecuteAssembly(string assemblyFile)
    {
        SandboxAppDomain.ExecuteAssembly(assemblyFile);
    } 
}


Вызов ExecuteAssembly из кода:
Код

private string InputFileName { get; set; }
private string OutputFileName { get; set; }
private string ExecutableName { get; set; }

private bool RunInSandbox(string workingDirectory, int timeLimit)
{
    string fileNameArg = System.IO.Path.Combine(workingDirectory, ExecutableName + ".exe");

    var tempInput = System.IO.Path.Combine(workingDirectory, InputFileName);
    var tempOutput = System.IO.Path.Combine(workingDirectory, OutputFileName);

    // изначально запрещаем все AppDomain'у
    PermissionSet permissionSet = new PermissionSet(PermissionState.None);

    // добавляем необходимые разрешения
    permissionSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
    permissionSet.AddPermission(new UIPermission(PermissionState.Unrestricted));
    permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.PathDiscovery, workingDirectory));
    permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read, tempInput));
    permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Write, tempOutput));
    permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Append, tempOutput));
    permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read, fileNameArg));

    // создаем новый домен приложений с выставленными разрешениями
    Sandbox sandbox = new Sandbox(workingDirectory, permissionSet);

    // загружаем сборку в домен и исполняем
    sandbox.ExecuteAssembly(fileNameArg);

    // выгружаем домен
    sandbox.Unload();

    // а теперь проверяем сборку на ограничение по времени (после того, как она завершит исполняться в AppDomain)
    var startInfo = new ProcessStartInfo
    {
            FileName = fileNameArg,
            CreateNoWindow = true,
            UseShellExecute = false,
            WindowStyle = ProcessWindowStyle.Hidden,
            WorkingDirectory = workingDirectory,
            ErrorDialog = false,
            LoadUserProfile = false,
    };

    var process = new Process();
    process.StartInfo = startInfo;

    if (process.Start())
    {
        if (process.WaitForExit(timeLimit * 1000))
        {
            return true;
        }
        else
        {   
            process.Kill();
            if (!process.HasExited)
            {
                process.WaitForExit();
            }
        }
    }
    return false;
}

Автор: jonie 16.4.2014, 10:33
http://www.c-sharpcorner.com/UploadFile/hasansheik/diagnostic-and-performance-monitoring-in-dotnet-4-0/

а сам "чужой" код вы можете запускать в потоке, который по таймауту убивать...

но по-мне так проще делать суррогатный процесс, имхо так проще чистить ресурсы..

Автор: kolaspirit 15.5.2014, 15:37
Спасибо. Проблема решена.

Автор: jonie 15.5.2014, 20:47
Цитата(kolaspirit @ 15.5.2014,  16:37)
Спасибо. Проблема решена.

и как решили?

Автор: kolaspirit 15.5.2014, 21:11
Следующим образом:

Код

private string InputFileName { get; set; }
private string OutputFileName { get; set; }
private string ExecutableName { get; set; }
// ограничение по времени (в секундах)
private int TimeLimit {get ; set; }
// ограничение по памяти (в байтах)
private int MaxMemorySize { get; set; }

private bool RunInSandbox(string workingDirectory, int timeLimit)
{
    string fileNameArg = System.IO.Path.Combine(workingDirectory, ExecutableName + ".exe");

    var tempInput = System.IO.Path.Combine(workingDirectory, InputFileName);
    var tempOutput = System.IO.Path.Combine(workingDirectory, OutputFileName);

    // изначально запрещаем все AppDomain'у
    PermissionSet permissionSet = new PermissionSet(PermissionState.None);

    // добавляем необходимые разрешения
    permissionSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
    permissionSet.AddPermission(new UIPermission(PermissionState.Unrestricted));
    permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.PathDiscovery, workingDirectory));
    permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read, tempInput));
    permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Write, tempOutput));
    permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Append, tempOutput));
    permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read, fileNameArg));

    // включаем мониторинг ЦП и памяти доменов приложений для данного процесса
    AppDomain.MonitoringIsEnabled = true;
    
    // создаем новый домен приложений с выставленными разрешениями
    Sandbox sandbox = new Sandbox(workingDirectory, permissionSet);

    // заводим переменную Exception для хранения в основном потоке тех исключений, которые возникают во вторичном потоке
    Exception threadException = null;
    var newThread = new Thread(() =>
    {
        try
        {
             // запускаем сборку с "чужим" кодом на исполнение
             sandbox.ExecuteAssembly(fileNameArg);
        }
        catch (Exception ex)
        {
             // если во вторичном потоке возникло исключение, то запоминаем его
             threadException = ex;
        }
    });
     
    newThread.Start();

    // задаем тайм-аут для созданного потока. Если время превышено, выдаем исключение
    if (!newThread.Join(timeLimit * 1000))
    {
         sandbox.Unload();
         throw new TimeoutException("Время выполнения программы было превышено.");
    }
         
    // если во вторичном потоке возникло исключение, то threadException не будет пуст    
    if (threadException != null)
    {
        sandbox.Unload();
        throw new Exception(threadException.Message);
    }
    
    // проверяем на превышение предела выделенной памяти. Если превышен, - выдаем исключение       
    if (sandbox.GetTotalAllocatedMemorySize() > MaxMemorySize)
    {
        sandbox.Unload();
        throw new Exception("Превышен допустимый предел выделяемой приложением памяти.");
    }
    
    sandbox.Unload();
    return true;
}


При этом в класс Sandbox был добавлен один метод:

Код

public long GetTotalAllocatedMemorySize()
{
    return SandboxAppDomain.MonitoringTotalAllocatedMemorySize;
}


Остается только один вопрос: как мониторить кол-во выделяемой сборкой памяти, пока эта сборка исполняется? Сейчас "проверка на память" происходит после завершения работы потока, что не есть хорошо. 

Автор: jonie 15.5.2014, 21:40
Цитата(kolaspirit @  15.5.2014,  22:11 Найти цитируемый пост)

Остается только один вопрос: как мониторить кол-во выделяемой сборкой памяти, пока эта сборка исполняется? Сейчас "проверка на память" происходит после завершения работы потока, что не есть хорошо. 

поток завести (например в новом домене) и while(true) { смотреть на память } ?)

Автор: kolaspirit 15.5.2014, 21:54
Цитата

поток завести (например в новом домене) и while(true) { смотреть на память } ?)


    Благодарю, уже думал об этом) Но это даже не проблема, решается парой строчек кода. Сейчас думаю над тем, как организовать очередь на проверку решений. Дело в том, что каждый участник, присылая решение на проверку, вызывает свой экземпляр dll-библиотеки, в которой содержатся все эти классы (для проверки). Мне необходимо сделать общую для всех очередь, например, чтобы одновременно могло проверяться не более 5 присланных решений, остальные должны дожидаться, находясь в этой очереди.
    Пока не копал глубоко, скорей всего решение в скором времени найдется. Однако, если есть здравые соображения по этому поводу, буду весьма признателен.

   Если в скором времени решение не найду, скорей всего, вынесу в отдельную тему. Вопрос сам по себе интересный.

Автор: jonie 16.5.2014, 09:29
Цитата(kolaspirit @  15.5.2014,  22:54 Найти цитируемый пост)
Мне необходимо сделать общую для всех очередь, например, чтобы одновременно могло проверяться не более 5 присланных решений, остальные должны дожидаться, находясь в этой очереди.

ну заведите очередь "задач на проверку" синхронизированную  семафором

Автор: kolaspirit 16.5.2014, 10:27
Цитата

ну заведите очередь "задач на проверку" синхронизированную  семафором

   Спасибо за совет, буду разбираться. До этого с семафорами только вскользь был знаком)

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)