Как экспортировать вывод CMD в текстовый файл на C# после завершения печати текста в окне CMD?

В окне терминала напечатано много текста из моего кода C#, некоторый текст печатается другим приложением, которое я запускал через C# System.Diagnostics (без RedirectStandardOutput я не хочу использовать эту async вещь), и оно печатало свой собственный текст и некоторый текст печатается функцией C# Console.WriteLine.

Я хочу сохранить весь этот текст сверху вниз в текстовый файл. Я не хочу запускать какой-либо файл и сохранять его текст, поскольку все выполнение уже выполнено и весь текст уже напечатан. Я просто хочу сохранить весь этот текст в файл в конце программы.

ПРИМЕЧАНИЕ. Ниже приведен не мой настоящий код (очевидно), но он выглядит примерно так.

from rich.progress import track
import time, os

print("Python Test")

for i in track(range(20), description = "Processing..."):
    time.sleep(0.1)  # Simulate work being done

os.system("color 08")
using System.Diagnostics;


Console.WriteLine("Test");

// Create a new process instance
Process process = new Process();

// Configure the process using StartInfo
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = $"/c echo Hello world! & timeout /t 2 & python test.py";

// Start the process
process.Start();

// Wait for CMD to finish
process.WaitForExit();
Console.WriteLine("Test1");


/*
---- Save all of that above text that was printed here at the end of the code. ----
*/

Я не хочу использовать RedirectStandardOutput, потому что, насколько я знаю, если я сделаю это и распечатаю вещи из process, используя эти async вещи, во-первых, он не сможет печатать обновления в режиме реального времени, такие как в этой timeout /t 2 части, это также не сможет учесть изменение цвета окна color 08.

Мне нужна такая система, которая позволит коду выполняться нормально, но когда код достигает конца, он экспортирует текст в файл, очень похоже на то, как работает функция Export Text в терминале Windows.

Вот чего я хочу достичь:

Терминал должен работать нормально, никаких изменений в его работе.

После того, как все сделано в конце моего кода на C#, я хочу сохранить весь этот текст в текстовый файл.

Прежде всего, вам нужно четко понимать, выполняется ли этот вывод непосредственно в командной строке Windows, cmd.exe, или в окне/вкладке терминала Windows, wt.exe. Как только вы все поймете, Отредактируйте тело вопроса и теги соответствующим образом.

Compo 28.03.2024 21:19

@Compo Я только что привел пример терминала Windows. Пример: предположим, вы выполнили что-то в терминале Windows, а затем хотели сохранить весь полученный текст в файл, чтобы использовать его функцию Export Text. Аналогично, что есть в C#, я могу использовать для экспорта всего текста после завершения печати. Мой вопрос очень ясен.

Light-Lens 28.03.2024 21:23

Существует множество различных «терминалов». У каждого могут быть разные проблемы и/или решения. Мы ожидаем решения конкретной проблемы, а не общей. Если это только для cmd.exe, удалите тег [терминал ]. Если это не специально для cmd.exe, удалите тег [ cmd ] и предоставьте дополнительную информацию (вероятно, включая тег [ windows]).

Compo 28.03.2024 22:12

@Compo Это не общая проблема с терминалом. Это конкретная проблема C#. Я только что использовал функцию Export Text терминала Windows в качестве примера того, какого результата я хочу достичь на C#. Позвольте мне прояснить это для вас. Есть код, который печатает много чего. Некоторые из них напечатаны System.DiagnosticsProcess, некоторые — Console.WriteLine. В конце моей программы на C# я хочу сохранить весь этот напечатанный текст в текстовый файл без использования async или RedirectStandardOutput. Я все ясно сказал. Почему вас так беспокоит мой пример терминала?

Light-Lens 28.03.2024 22:18

Однако его необходимо распечатать для конкретной программы, а не для каждой, которую можно запустить в вашей целевой операционной системе! Если вам нужно получить доступ к буферу конкретной консоли, нам нужно знать, какая именно для конкретного вопроса.

Compo 28.03.2024 22:33

Командная строка @Compo. Моя целевая ОС — только Windows 10 и 11.

Light-Lens 28.03.2024 22:45

Почему вы вообще чувствуете, что вам нужен cmd? Почему бы просто не вызвать то, что вы хотите, напрямую? Вам не нужно cmd, чтобы получить стандартный результат.

Charlieface 28.03.2024 23:19
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
141
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Это то что ты хочешь?

void Main()
{
    var sb = new StringBuilder();
    var startInfo = new ProcessStartInfo
    {
        FileName = "cmd.exe ",
        Arguments = $"/c echo Hello world! & timeout /t 2 & color 08",
        UseShellExecute = false,
        RedirectStandardOutput = true,
        RedirectStandardError = true,
    };
    
    using (var process = new Process{StartInfo = startInfo})
    {
        process.OutputDataReceived += (sender, args) => sb.AppendLine(args.Data);
        process.ErrorDataReceived += (sender, args) => sb.AppendLine(args.Data);
        process.Start();
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();
        process.WaitForExit();
    }
    Console.WriteLine("start log");
    Console.WriteLine(sb.ToString()); //can tace output and save to file
    Console.WriteLine("end log");
}

выход:

start log
Hello world!
Waiting for 2 seconds, press a key to continue ...081080

end log

Я уже пробовал, и это не решение моей проблемы. Однако спасибо за ваше решение.

Light-Lens 29.03.2024 06:31
Ответ принят как подходящий

Определение STDIN, STDOUT и STDERR


Ядро ОС всех операционных систем использует эти три основных потока ввода-вывода (ввода-вывода): STDIN, STDOUT и STDERR. STDIN — это поток ввода-вывода, обрабатывающий информацию, связанную с вводом, STDOUT — это поток ввода-вывода, обрабатывающий информацию, связанную с выводом, а STDERR — это поток ввода-вывода, обрабатывающий информацию, связанную с ошибками. Все службы, приложения и компоненты ядра ОС используют эти потоки для управления информацией ввода-вывода в любой ОС.



Решение с использованием перенаправления STDOUT

В предоставленном вами коде проблема заключается в том, что вы хотите обработать информацию, предоставляемую работой пакетного сценария, без перенаправления потока STDOUT. Приложение C# может читать выходные данные пакетного сценария с помощью файлов (например, файлов конфигурации JSON), сокетов, каналов и потока STDOUT. Вышеупомянутые методы являются методами межпроцессного взаимодействия (вы можете проверить эту ссылку для получения дополнительной информации: https://stackoverflow.com/a/76196178/16587692). Эти методы необходимо использовать, поскольку между приложением C# и пакетным сценарием нет прямого канала связи. В этой ситуации наиболее жизнеспособным методом является использование потока STDOUT. Вы можете сохранить цвета консоли, изменив их с помощью приложения C#, а не изменяя их путем передачи аргументов пакетному сценарию. Вы также можете записать вывод в реальном времени в нужный файл журнала, но в качестве предостережения не читайте из файла журнала, пока приложение C# записывает в этот файл, поскольку это вызовет состояние гонки, и это может повредить вашу Ячейки HDD/SSD, в которых находится файл.

using System.Text;
using System.Diagnostics;


namespace Test
{
    class Program
    {

        static void Main(string[] args)
        {
            Operation().Wait();
            Console.ReadLine();
        }


        private static void SetPermissions(string file_name)
        {
            #pragma warning disable CA1416 // Validate platform compatibility

            // CHECK IF THE CURRENT OS IS WINDOWS
            if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) == true)
            {
                // GET THE SPECIFIED FILE INFORMATION OF THE SELECTED FILE
                FileInfo settings_file_info = new FileInfo(file_name);

                // GET THE ACCESS CONTROL INFORMATION OF THE SELECTED FILE AND STORE THEM IN A 'FileSecurity' OBJECT
                System.Security.AccessControl.FileSecurity settings_file_security = settings_file_info.GetAccessControl();

                // ADD THE ACCESS RULES THAT ALLOW READ, WRITE, AND DELETE PERMISSIONS ON THE SELECTED FILE FOR THE CURRENT USER
                settings_file_security.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(System.Security.Principal.WindowsIdentity.GetCurrent().Name, System.Security.AccessControl.FileSystemRights.Write, System.Security.AccessControl.AccessControlType.Allow));
                settings_file_security.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(System.Security.Principal.WindowsIdentity.GetCurrent().Name, System.Security.AccessControl.FileSystemRights.Read, System.Security.AccessControl.AccessControlType.Allow));
                settings_file_security.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(System.Security.Principal.WindowsIdentity.GetCurrent().Name, System.Security.AccessControl.FileSystemRights.Delete, System.Security.AccessControl.AccessControlType.Allow));
                
                // UPDATE THE ACCESS CONTROL SETTINGS OF THE FILE BY SETTING THE 
                // MODIFIED ACCESS CONTROL SETTINGS AS THE CURRENT SETTINGS
                settings_file_info.SetAccessControl(settings_file_security);
            }
            else
            {
                // IF THE OS IS A UNIX BASED OS, SET THE FILE PERMISSIONS FOR READ AND WRITE OPERATIONS
                // WITH THE 'UnixFileMode.UserRead | UnixFileMode.UserWrite' BITWISE 'OR' OPERATION
                File.SetUnixFileMode(file_name, UnixFileMode.UserRead | UnixFileMode.UserWrite);
                
            }
            #pragma warning restore CA1416 // Validate platform compatibility
        }



        private static async Task<bool> Operation()
        {
            // Process object
            System.Diagnostics.Process proc = new System.Diagnostics.Process();

            string file_name = String.Empty;
            string arguments = String.Empty;

            // CHECK IF THE CURRENT OS IS WINDOWS AND SET THE FILE PATH AND ARGUMETS ACCORDINGLY
            if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) == true)
            {
                file_name = @"C:\Users\teodo\PycharmProjects\Test\.venv\Scripts\python.exe";
                arguments = @"C:\Users\teodo\PycharmProjects\Test\main.py";
            }
            else
            {
                file_name = @"python3";
                arguments = @"/mnt/c/Users/teodo/PycharmProjects/Test/main.py";
            }

            // Path where the python executable is located
            proc.StartInfo.FileName = file_name;

            // Path where python executable is located
            proc.StartInfo.Arguments = arguments;

            // Start the process
            proc.Start();


            // Named pipe server object with an "In" direction. This means that this pipe can only read messages. On Windows it creates a pipe at the 
            // '\\.\pipe\pipe-sub-directory\pipe-name' virtual directory, on Linux it creates a Unix Named Socket in the '/tmp' directory 
            System.IO.Pipes.NamedPipeServerStream fifo_pipe_connection = new System.IO.Pipes.NamedPipeServerStream("/tmp/fifo_pipe", System.IO.Pipes.PipeDirection.In);

            // Create a backlog text file if none is existent, set its permissions as Read/Write,
            // and create a stream that allows direct read and write operations to the file
            FileStream fs = File.Open("backlog.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite);
            SetPermissions("backlog.txt");

            try
            {
                // Wait for a client to connect synchronously
                fifo_pipe_connection.WaitForConnection();

                while (true)
                {
                    // Initiate a binary buffer byte array with a size of 1 Kb 
                    byte[] buffer = new byte[1024];

                    // Read the received bytes into the buffer byte array and also 
                    // store the number of bytes read into an integer named 'read'
                    int read = await fifo_pipe_connection.ReadAsync(buffer, 0, buffer.Length);

                    // Write the received bytes into the 'backlog.txt' file
                    await fs.WriteAsync(buffer, 0, read);

                    // Flush the bytes within the stream's buffer into the file
                    await fs.FlushAsync();

                    // If the number of bytes read is equal to '0' there are no bytes
                    // left to read on the Pipe's stream and the read loop is closed
                    if (read == 0)
                    {
                        break;
                    }
                }

            }
            catch
            {
            }
            finally
            {
                fs?.DisposeAsync();
                fifo_pipe_connection?.DisposeAsync();
            }
            return true;
        }
    }
}

Вывод метода перенаправления STDOUT

Определение труб FIFO

Каналы FIFO, также известные как именованные каналы, представляют собой тип сокета, который использует файловую систему операционной системы для облегчения обмена информацией между приложениями. Трубы FIFO подразделяются на 3 категории: входящие трубы, внешние трубы и двусторонние трубы. Входящие каналы — это каналы, по которым сервер каналов может получать информацию только от клиентов каналов, исходящие каналы — это каналы, по которым сервер каналов может отправлять информацию только клиентам каналов, а двусторонние каналы — это каналы, по которым сервер каналов может как отправлять и получать информацию от Клиентов Pipe.

Кроссплатформенное решение с использованием каналов FIFO.

Если необходимо сохранить целостность вывода, каналы FIFO являются лучшим вариантом для метода межпроцессного взаимодействия. В этом сценарии лучшим решением будет внутренний канал, поскольку приложение C# должно получать информацию от приложения Python. Чтобы иметь кросс-платформенные возможности, приложения C# и Python используют условные операторы, чтобы проверить, на какой платформе ОС работают приложения.


Приложение С#

using System.Text;
using System.Diagnostics;


namespace Test
{
    class Program
    {

        static void Main(string[] args)
        {
            Operation().Wait();
            Console.ReadLine();
        }


        private static void SetPermissions(string file_name)
        {
            #pragma warning disable CA1416 // Validate platform compatibility

            // CHECK IF THE CURRENT OS IS WINDOWS
            if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) == true)
            {
                // GET THE SPECIFIED FILE INFORMATION OF THE SELECTED FILE
                FileInfo settings_file_info = new FileInfo(file_name);

                // GET THE ACCESS CONTROL INFORMATION OF THE SELECTED FILE AND STORE THEM IN A 'FileSecurity' OBJECT
                System.Security.AccessControl.FileSecurity settings_file_security = settings_file_info.GetAccessControl();

                // ADD THE ACCESS RULES THAT ALLOW READ, WRITE, AND DELETE PERMISSIONS ON THE SELECTED FILE FOR THE CURRENT USER
                settings_file_security.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(System.Security.Principal.WindowsIdentity.GetCurrent().Name, System.Security.AccessControl.FileSystemRights.Write, System.Security.AccessControl.AccessControlType.Allow));
                settings_file_security.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(System.Security.Principal.WindowsIdentity.GetCurrent().Name, System.Security.AccessControl.FileSystemRights.Read, System.Security.AccessControl.AccessControlType.Allow));
                settings_file_security.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(System.Security.Principal.WindowsIdentity.GetCurrent().Name, System.Security.AccessControl.FileSystemRights.Delete, System.Security.AccessControl.AccessControlType.Allow));
                
                // UPDATE THE ACCESS CONTROL SETTINGS OF THE FILE BY SETTING THE 
                // MODIFIED ACCESS CONTROL SETTINGS AS THE CURRENT SETTINGS
                settings_file_info.SetAccessControl(settings_file_security);
            }
            else
            {
                // IF THE OS IS A UNIX BASED OS, SET THE FILE PERMISSIONS FOR READ AND WRITE OPERATIONS
                // WITH THE 'UnixFileMode.UserRead | UnixFileMode.UserWrite' BITWISE 'OR' OPERATION
                File.SetUnixFileMode(file_name, UnixFileMode.UserRead | UnixFileMode.UserWrite);
                
            }
            #pragma warning restore CA1416 // Validate platform compatibility
        }



        private static async Task<bool> Operation()
        {
            // Process object
            System.Diagnostics.Process proc = new System.Diagnostics.Process();

            string file_name = String.Empty;
            string arguments = String.Empty;

            // CHECK IF THE CURRENT OS IS WINDOWS AND SET THE FILE PATH AND ARGUMETS ACCORDINGLY
            if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) == true)
            {
                file_name = @"C:\Users\teodo\PycharmProjects\Test\.venv\Scripts\python.exe";
                arguments = @"C:\Users\teodo\PycharmProjects\Test\main.py";
            }
            else
            {
                file_name = @"python3";
                arguments = @"/mnt/c/Users/teodo/PycharmProjects/Test/main.py";
            }

            // Path where the python executable is located
            proc.StartInfo.FileName = file_name;

            // Path where python executable is located
            proc.StartInfo.Arguments = arguments;

            // Start the process
            proc.Start();


            // Named pipe server object with an "In" direction. This means that this pipe can only read messages. On Windows it creates a pipe at the 
            // '\\.\pipe\pipe-sub-directory\pipe-name' virtual directory, on Linux it creates a Unix Named Socket in the '/tmp' directory 
            System.IO.Pipes.NamedPipeServerStream fifo_pipe_connection = new System.IO.Pipes.NamedPipeServerStream("/tmp/fifo_pipe", System.IO.Pipes.PipeDirection.In);

            // Create a backlog text file if none is existent, set its permissions as Read/Write,
            // and create a stream that allows direct read and write operations to the file
            FileStream fs = File.Open("backlog.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite);
            SetPermissions("backlog.txt");

            try
            {
                // Wait for a client to connect synchronously
                fifo_pipe_connection.WaitForConnection();

                while (true)
                {
                    // Initiate a binary buffer byte array with a size of 1 Kb 
                    byte[] buffer = new byte[1024];

                    // Read the received bytes into the buffer byte array and also 
                    // store the number of bytes read into an integer named 'read'
                    int read = await fifo_pipe_connection.ReadAsync(buffer, 0, buffer.Length);


                    // Print the bytes sent by the Python application on the pipe
                    Console.WriteLine(Encoding.UTF8.GetString(buffer));

                    // Write the received bytes into the 'backlog.txt' file
                    await fs.WriteAsync(buffer, 0, read);

                    // Flush the bytes within the stream's buffer into the file
                    await fs.FlushAsync();

                    // If the number of bytes read is equal to '0' there are no bytes
                    // left to read on the Pipe's stream and the read loop is closed
                    if (read == 0)
                    {
                        break;
                    }
                }

            }
            catch
            {
            }
            finally
            {
                fs?.DisposeAsync();
                fifo_pipe_connection?.DisposeAsync();
            }
            return true;
        }
    }
}


Приложение Python

import os
import sys
import time
from rich.progress import track
import platform
import socket

fifo_write = None
unix_named_pipe = None


def operation():
    try:
        write("!!! Python Test !!!\n\n")

        # Simulate work being done
        set_range = range(20)
        for i in track(set_range, description = "Processing..."):
            time.sleep(0.1)

            # SEND THE CURRENT PROGRESS AS A PERCENTAGE OVER THE PIPE
            write("Processing..." + str((100 / set_range.stop) * (i + 1)) + "%\n")

        # CHANGE THE TERMINAL COLOR TO GREY
        if platform.system() == "Windows":
            os.system("color 08")
        else:
            print("\n\n")
            os.system(r"echo '\e[91m!!! COLOR !!!'")

        # PAUSE THE CURRENT THREAD FOR 2 SECONDS
        time.sleep(2)

        # CHANGE THE TERMINAL COLOR TO WHITE
        if platform.system() == "Windows":
            os.system("color F")
        else:
            os.system(r"echo '\e[00m!!! COLOR !!!'")
            print("\n\n")
        write("[ Finished ]")
    except KeyboardInterrupt:
        sys.exit(0)


def write(msg):
    if platform.system() == "Windows":
        fifo_pipe_write(msg)
    else:
        unix_named_socket_write(msg)


def fifo_pipe_write(msg):
    try:
        global fifo_write
        if fifo_write is not None:
            # WRITE THE STRING PASSED TO THE FUNCTION'S AS AN ARGUMENT
            # IN THE FIFO PIPE FILE USING THE GLOBAL STREAM
            fifo_write.write(msg)
    except KeyboardInterrupt:
        sys.exit(0)


def unix_named_socket_write(msg):
    try:
        global unix_named_pipe
        if unix_named_pipe is not None:
            unix_named_pipe.send(str(msg).encode(encoding = "utf-8"))
    except KeyboardInterrupt:
        pass


def stream_finder() -> bool:
    is_found = False

    try:
        # INITIATE A PIPE SEARCH SEQUENCE FOR 10 SECONDS
        for t in range(0, 10):
            try:
                try:
                    # IF THE OS IS WINDOWS SEARCH FOR A PIPE FILE
                    if platform.system() == "Windows":
                        # IF PIPE IS FOUND RETURN TRUE AND STORE THE OPENED PIPE FILE STREAM GLOBALLY
                        global fifo_write
                        fifo_write = open(r"\\.\pipe\tmp\fifo_pipe", "w")
                        is_found = True
                        break
                    # ELSE, SEARCH FOR A NAMED UNIX SOCKET
                    else:
                        # IF SOCKET IS FOUND RETURN TRUE AND STORE THE OPENED SOCKET GLOBALLY
                        global unix_named_pipe
                        unix_named_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
                        unix_named_pipe.connect("/tmp/fifo_pipe")
                        is_found = True
                        break
                except FileNotFoundError:
                    # IF PIPE IS NOT FOUND
                    print("\n\n[ Searching pipe ]")
            except OSError:
                # IF PIPE IS NOT FOUND
                print("\n\n[ Searching pipe ]")

            # MAKE THE LOOP WAIT 1 SECOND FOR EACH ITERATION
            time.sleep(1)
    except KeyboardInterrupt:
        sys.exit(0)

    return is_found


if __name__ == "__main__":
    try:
        # INITIATE THE PIPE SEARCHING OPERATION
        found = stream_finder()
        # IF PIPE SEARCHING OPERATION IS SUCCESSFUL
        if found is True:
            print("\n\n[ Pipe found ]\n\n")
            # INITIATE THE MAIN OPERATION
            operation()
    except KeyboardInterrupt:
        sys.exit(0)


Выходные окна FIFO Pipes

Вывод FIFO Pipes Linux

Подробности об ОС Linux

Вывод ОС Linux

Да, это работает, но дело в том, что какой-то цветной текст отображается каким-то другим приложением. Например, в своем вопросе я привел этот пример color 08, потому что в моем реальном коде есть скрипт Python, который использует модуль rich для панели загрузки (он похож на обновление цвета и в реальном времени, вы знаете). В этом случае ваш код не сможет отображать изменения цвета, которые выполняются другими приложениями и скриптами. Хотя ваше решение очень близко к тому, что я на самом деле хочу.

Light-Lens 29.03.2024 06:35

Я обновил свой вопрос скриншотами, поэтому, пожалуйста, проверьте его.

Light-Lens 29.03.2024 08:02

Другие вопросы по теме