简体   繁体   English

如何使用 C# 从 Windows 服务运行 EXE 程序?

[英]How can I run an EXE program from a Windows Service using C#?

How can I run an EXE program from a Windows Service using C#?如何使用 C# 从 Windows 服务运行EXE程序?

This is my code:这是我的代码:

System.Diagnostics.Process.Start(@"E:\PROJECT XL\INI SQLLOADER\ConsoleApplication2\ConsoleApplication2\ConsoleApplication2\bin\Debug\ConsoleApplication2.exe");

When I run this service, the application is not starting.当我运行此服务时,应用程序没有启动。
What's wrong with my code?我的代码有什么问题?

This will never work , at least not under Windows Vista or later.这永远行不通,至少在 Windows Vista 或更高版本下不行。 The key problem is that you're trying to execute this from within a Windows Service, rather than a standard Windows application.关键问题是您试图从 Windows 服务而不是标准 Windows 应用程序中执行此操作。 The code you've shown will work perfectly in a Windows Forms, WPF, or Console application, but it won't work at all in a Windows Service.您展示的代码将在 Windows 窗体、WPF 或控制台应用程序中完美运行,但在 Windows 服务中根本无法运行。

Windows Services cannot start additional applications because they are not running in the context of any particular user. Windows 服务无法启动其他应用程序,因为它们不在任何特定用户的上下文中运行。 Unlike regular Windows applications, services are now run in an isolated session and are prohibited from interacting with a user or the desktop.与常规的 Windows 应用程序不同, 服务现在在隔离的会话中运行,并且被禁止与用户或桌面交互。 This leaves no place for the application to be run.这为应用程序留下了运行的空间。

More information is available in the answers to these related questions:这些相关问题的答案中提供了更多信息:

The best solution to your problem, as you've probably figured out by now, is to create a standard Windows application instead of a service.正如您现在可能已经想到的那样,解决您的问题的最佳方法是创建一个标准的 Windows 应用程序而不是一个服务。 These are designed to be run by a particular user and are associated with that user's desktop.它们旨在由特定用户运行并与该用户的桌面相关联。 This way, you can run additional applications whenever you want, using the code that you've already shown.这样,您可以随时使用已显示的代码运行其他应用程序。

Another possible solution, assuming that your Console application does not require an interface or output of any sort, is to instruct the process not to create a window.假设您的控制台应用程序不需要任何类型的接口或输出,另一种可能的解决方案是指示进程不要创建窗口。 This will prevent Windows from blocking the creation of your process, because it will no longer request that a Console window be created.这将防止 Windows 阻止创建您的进程,因为它将不再请求创建控制台窗口。 You can find the relevant code in this answer to a related question.您可以在相关问题的答案中找到相关代码。

i have tried this article Code Project , it is working fine for me.我已经尝试过这篇文章代码项目,它对我来说很好用。 I have used the code too.我也用过代码。 article is excellent in explanation with screenshot.文章很好地解释了截图。

I am adding necessary explanation to this scenario我正在为此场景添加必要的解释

You have just booted up your computer and are about to log on.您刚刚启动了计算机,即将登录。 When you log on, the system assigns you a unique Session ID.当您登录时,系统会为您分配一个唯一的会话 ID。 In Windows Vista, the first User to log on to the computer is assigned a Session ID of 1 by the OS.在 Windows Vista 中,操作系统为第一个登录计算机的用户分配会话 ID 1。 The next User to log on will be assigned a Session ID of 2. And so on and so forth.下一个要登录的用户将被分配一个会话 ID 2。依此类推。 You can view the Session ID assigned to each logged on User from the Users tab in Task Manager.您可以从任务管理器的“用户”选项卡中查看分配给每个登录用户的会话 ID。 在此处输入图片说明

But your windows service is brought under session ID of 0. This session is isolated from other sessions.但是您的 Windows 服务的会话 ID 为 0。此会话与其他会话隔离。 This ultimately prevent the windows service to invoke the application running under user session's like 1 or 2.这最终会阻止 Windows 服务调用在用户会话 1 或 2 下运行的应用程序。

In order to invoke the application from windows service you need to copy the control from winlogon.exe which acts as present logged user as shown in below screenshot.为了从 Windows 服务调用应用程序,您需要从 winlogon.exe 复制控件,该控件充当当前登录用户,如下面的屏幕截图所示。 在此处输入图片说明

Important codes重要代码

// obtain the process id of the winlogon process that 
// is running within the currently active session
Process[] processes = Process.GetProcessesByName("winlogon");
foreach (Process p in processes)
{
    if ((uint)p.SessionId == dwSessionId)
    {
        winlogonPid = (uint)p.Id;
    }
}

// obtain a handle to the winlogon process
hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);

// obtain a handle to the access token of the winlogon process
if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken))
{
    CloseHandle(hProcess);
    return false;
}

// Security attibute structure used in DuplicateTokenEx and   CreateProcessAsUser
// I would prefer to not have to use a security attribute variable and to just 
// simply pass null and inherit (by default) the security attributes
// of the existing token. However, in C# structures are value types and   therefore
// cannot be assigned the null value.
SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
sa.Length = Marshal.SizeOf(sa);

// copy the access token of the winlogon process; 
// the newly created token will be a primary token
if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa, 
    (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, 
    (int)TOKEN_TYPE.TokenPrimary, ref hUserTokenDup))
    {
      CloseHandle(hProcess);
      CloseHandle(hPToken);
      return false;
    }

 STARTUPINFO si = new STARTUPINFO();
 si.cb = (int)Marshal.SizeOf(si);

// interactive window station parameter; basically this indicates 
// that the process created can display a GUI on the desktop
si.lpDesktop = @"winsta0\default";

// flags that specify the priority and creation method of the process
int dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;

// create a new process in the current User's logon session
 bool result = CreateProcessAsUser(hUserTokenDup,  // client's access token
                            null,             // file to execute
                            applicationName,  // command line
                            ref sa,           // pointer to process    SECURITY_ATTRIBUTES
                            ref sa,           // pointer to thread SECURITY_ATTRIBUTES
                            false,            // handles are not inheritable
                            dwCreationFlags,  // creation flags
                            IntPtr.Zero,      // pointer to new environment block 
                            null,             // name of current directory 
                            ref si,           // pointer to STARTUPINFO structure
                            out procInfo      // receives information about new process
                            );

you can use from windows task scheduler for this purpose, there are many libraries like TaskScheduler that help you.为此,您可以从 Windows 任务调度程序中使用,有许多像TaskScheduler这样的库可以帮助您。

for example consider we want to scheduling a task that will executes once five seconds later:例如,考虑我们要调度一个将在五秒后执行一次的任务:

using (var ts = new TaskService())
        {

            var t = ts.Execute("notepad.exe")
                .Once()
                .Starting(DateTime.Now.AddSeconds(5))
                .AsTask("myTask");

        }

notepad.exe will be executed five seconds later. notepad.exe 将在五秒钟后执行。

for details and more information please go to wiki有关详细信息和更多信息,请访问wiki

if you know which class and method in that assembly you need, you can invoke it yourself like this:如果您知道您需要该程序集中的哪个类和方法,您可以像这样自己调用它:

        Assembly assembly = Assembly.LoadFrom("yourApp.exe");
        Type[] types = assembly.GetTypes();
        foreach (Type t in types)
        {
            if (t.Name == "YourClass")
            {
                MethodInfo method = t.GetMethod("YourMethod",
                BindingFlags.Public | BindingFlags.Instance);
                if (method != null)
                {
                    ParameterInfo[] parameters = method.GetParameters();
                    object classInstance = Activator.CreateInstance(t, null);

                    var result = method.Invoke(classInstance, parameters.Length == 0 ? null : parameters);
                    
                    break;
                }
            }
                         
        }
    

Top answer with most upvotes isn't wrong but still the opposite of what I would post.大多数赞成的最佳答案并没有错,但仍然与我要发布的内容相反。 I say it will totally work to start an exe file and you can do this in the context of any user .我说启动 exe 文件完全可以工作,您可以在任何用户的上下文中执行此操作 Logically you just can't have any user interface or ask for user input...从逻辑上讲,您不能拥有任何用户界面或要求用户输入...

Here is my advice:这是我的建议:

  1. Create a simple Console Application that does what your service should do right on start without user interaction.创建一个简单的控制台应用程序,该应用程序在没有用户交互的情况下执行您的服务应在启动时正确执行的操作。 I really recommend not using the Windows Service project type especially because you (currently) can't using .NET Core.我真的不建议使用 Windows Service 项目类型,尤其是因为您(当前)不能使用 .NET Core。
  2. Add code to start your exe you want to call from service添加代码以启动要从服务调用的 exe

Example to start eg plink.exe.启动示例,例如 plink.exe。 You could even listen to the output:你甚至可以听输出:

var psi = new ProcessStartInfo()
{
    FileName = "./Client/plink.exe", //path to your *.exe
    Arguments = "-telnet -P 23 127.0.0.1 -l myUsername -raw", //arguments
    RedirectStandardError = true,
    RedirectStandardOutput = true,
    RedirectStandardInput = true,
    UseShellExecute = false,
    CreateNoWindow = true //no window, you can't show it anyway
};

var p = Process.Start(psi);
  1. Use NSSM (Non-Sucking Service Manager) to register that Console Application as service.使用 NSSM(Non-Sucking Service Manager)将该控制台应用程序注册为服务。 NSSM can be controlled via command line and can show an UI to configure the service or you configure it via command line. NSSM 可以通过命令行进行控制,并且可以显示 UI 来配置服务,或者您可以通过命令行进行配置。 You can run the service in the context of any user if you know the login data of that user.如果您知道该用户的登录数据,则可以在该用户的上下文中运行该服务。

I took LocalSystem account which is default and more than Local Service.我使用了默认的 LocalSystem 帐户,而不是本地服务。 It worked fine without having to enter login information of a specific user.它运行良好,无需输入特定用户的登录信息。 I didn't even tick the checkbox "Allow service to interact with desktop" which you could if you need higher permissions.如果您需要更高的权限,我什至没有勾选“允许服务与桌面交互”复选框。

允许服务与桌面交互的选项

Lastly I just want to say how funny it is that the top answer says quite the opposite of my answer and still both of us are right it's just how you interpret the question :-D.最后,我只想说最有趣的是,最重要的答案与我的答案完全相反,但我们俩仍然是对的,这只是您解释问题的方式:-D。 If you now say but you can't with the windows service project type - You CAN but I had this before and installation was sketchy and it was maybe kind of an unintentional hack until I found NSSM.如果你现在说但你不能使用 Windows 服务项目类型 - 你可以,但我之前有这个并且安装很粗略,在我找到 NSSM 之前这可能是一种无意的黑客攻击。

First, we are going to create a Windows Service that runs under the System account.首先,我们将创建一个在系统帐户下运行的 Windows 服务。 This service will be responsible for spawning an interactive process within the currently active User's Session.该服务将负责在当前活动的用户会话中生成一个交互进程。 This newly created process will display a UI and run with full admin rights.这个新创建的进程将显示一个 UI 并以完全管理员权限运行。 When the first User logs on to the computer, this service will be started and will be running in Session0;当第一个 User 登录到计算机时,该服务将启动并在 Session0 中运行; however the process that this service spawns will be running on the desktop of the currently logged on User.但是,此服务产生的进程将在当前登录用户的桌面上运行。 We will refer to this service as the LoaderService.我们将此服务称为 LoaderService。

Next, the winlogon.exe process is responsible for managing User login and logout procedures.接下来,winlogon.exe 进程负责管理用户登录和注销程序。 We know that every User who logs on to the computer will have a unique Session ID and a corresponding winlogon.exe process associated with their Session.我们知道,每个登录到计算机的用户都将拥有唯一的会话 ID 和与其会话关联的相应 winlogon.exe 进程。 Now, we mentioned above, the LoaderService runs under the System account.现在,我们上面提到过,LoaderService 在 System 帐户下运行。 We also confirmed that each winlogon.exe process on the computer runs under the System account.我们还确认计算机上的每个 winlogon.exe 进程都在系统帐户下运行。 Because the System account is the owner of both the LoaderService and the winlogon.exe processes, our LoaderService can copy the access token (and Session ID) of the winlogon.exe process and then call the Win32 API function CreateProcessAsUser to launch a process into the currently active Session of the logged on User.由于 System 帐户是 LoaderService 和 winlogon.exe 进程的所有者,我们的 LoaderService 可以复制 winlogon.exe 进程的访问令牌(和会话 ID),然后调用 Win32 API 函数 CreateProcessAsUser 将进程启动到登录用户的当前活动会话。 Since the Session ID located within the access token of the copied winlogon.exe process is greater than 0, we can launch an interactive process using that token.由于位于复制的 winlogon.exe 进程的访问令牌中的会话 ID 大于 0,我们可以使用该令牌启动一个交互式进程。

Try this one.试试这个。 Subverting Vista UAC in Both 32 and 64 bit Architectures 在 32 位和 64 位架构中颠覆 Vista UAC

You can execute an .exe from a Windows service very well in Windows XP.您可以在 Windows XP 中很好地从 Windows 服务执行 .exe。 I have done it myself in the past.过去我自己也做过。

You need to make sure you had checked the option "Allow to interact with the Desktop" in the Windows service properties.您需要确保已选中 Windows 服务属性中的“允许与桌面交互”选项。 If that is not done, it will not execute.如果不这样做,它将不会执行。

I need to check in Windows 7 or Vista as these versions requires additional security privileges so it may throw an error, but I am quite sure it can be achieved either directly or indirectly.我需要检查 Windows 7 或 Vista,因为这些版本需要额外的安全权限,因此可能会引发错误,但我很确定可以直接或间接实现。 For XP I am certain as I had done it myself.对于 XP,我很确定,因为我自己已经做到了。

You should check this MSDN article and download the .docx file and read it carefully , it was very helpful for me.您应该查看这篇 MSDN 文章并下载 .docx 文件并仔细阅读,它对我很有帮助。

However this is a class which works fine for my case :然而,这是一个适用于我的案例的类:

    [StructLayout(LayoutKind.Sequential)]
    internal struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public uint dwProcessId;
        public uint dwThreadId;
    }
    
    [StructLayout(LayoutKind.Sequential)]
    internal struct SECURITY_ATTRIBUTES
    {
        public uint nLength;
        public IntPtr lpSecurityDescriptor;
        public bool bInheritHandle;
    }


    [StructLayout(LayoutKind.Sequential)]
    public struct STARTUPINFO
    {
        public uint cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        public uint dwX;
        public uint dwY;
        public uint dwXSize;
        public uint dwYSize;
        public uint dwXCountChars;
        public uint dwYCountChars;
        public uint dwFillAttribute;
        public uint dwFlags;
        public short wShowWindow;
        public short cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;

    }

    internal enum SECURITY_IMPERSONATION_LEVEL
    {
        SecurityAnonymous,
        SecurityIdentification,
        SecurityImpersonation,
        SecurityDelegation
    }

    internal enum TOKEN_TYPE
    {
        TokenPrimary = 1,
        TokenImpersonation
    }

    public static class ProcessAsUser
    {

        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool CreateProcessAsUser(
            IntPtr hToken,
            string lpApplicationName,
            string lpCommandLine,
            ref SECURITY_ATTRIBUTES lpProcessAttributes,
            ref SECURITY_ATTRIBUTES lpThreadAttributes,
            bool bInheritHandles,
            uint dwCreationFlags,
            IntPtr lpEnvironment,
            string lpCurrentDirectory,
            ref STARTUPINFO lpStartupInfo,
            out PROCESS_INFORMATION lpProcessInformation);


        [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx", SetLastError = true)]
        private static extern bool DuplicateTokenEx(
            IntPtr hExistingToken,
            uint dwDesiredAccess,
            ref SECURITY_ATTRIBUTES lpThreadAttributes,
            Int32 ImpersonationLevel,
            Int32 dwTokenType,
            ref IntPtr phNewToken);


        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool OpenProcessToken(
            IntPtr ProcessHandle,
            UInt32 DesiredAccess,
            ref IntPtr TokenHandle);

        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool CreateEnvironmentBlock(
                ref IntPtr lpEnvironment,
                IntPtr hToken,
                bool bInherit);


        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool DestroyEnvironmentBlock(
                IntPtr lpEnvironment);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(
            IntPtr hObject);

        private const short SW_SHOW = 5;
        private const uint TOKEN_QUERY = 0x0008;
        private const uint TOKEN_DUPLICATE = 0x0002;
        private const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
        private const int GENERIC_ALL_ACCESS = 0x10000000;
        private const int STARTF_USESHOWWINDOW = 0x00000001;
        private const int STARTF_FORCEONFEEDBACK = 0x00000040;
        private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;


        private static bool LaunchProcessAsUser(string cmdLine, IntPtr token, IntPtr envBlock)
        {
            bool result = false;


            PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
            SECURITY_ATTRIBUTES saProcess = new SECURITY_ATTRIBUTES();
            SECURITY_ATTRIBUTES saThread = new SECURITY_ATTRIBUTES();
            saProcess.nLength = (uint)Marshal.SizeOf(saProcess);
            saThread.nLength = (uint)Marshal.SizeOf(saThread);

            STARTUPINFO si = new STARTUPINFO();
            si.cb = (uint)Marshal.SizeOf(si);


            //if this member is NULL, the new process inherits the desktop
            //and window station of its parent process. If this member is
            //an empty string, the process does not inherit the desktop and
            //window station of its parent process; instead, the system
            //determines if a new desktop and window station need to be created.
            //If the impersonated user already has a desktop, the system uses the
            //existing desktop.

            si.lpDesktop = @"WinSta0\Default"; //Modify as needed
            si.dwFlags = STARTF_USESHOWWINDOW | STARTF_FORCEONFEEDBACK;
            si.wShowWindow = SW_SHOW;
            //Set other si properties as required.

            result = CreateProcessAsUser(
                token,
                null,
                cmdLine,
                ref saProcess,
                ref saThread,
                false,
                CREATE_UNICODE_ENVIRONMENT,
                envBlock,
                null,
                ref si,
                out pi);


            if (result == false)
            {
                int error = Marshal.GetLastWin32Error();
                string message = String.Format("CreateProcessAsUser Error: {0}", error);
                FilesUtilities.WriteLog(message,FilesUtilities.ErrorType.Info);

            }

            return result;
        }


        private static IntPtr GetPrimaryToken(int processId)
        {
            IntPtr token = IntPtr.Zero;
            IntPtr primaryToken = IntPtr.Zero;
            bool retVal = false;
            Process p = null;

            try
            {
                p = Process.GetProcessById(processId);
            }

            catch (ArgumentException)
            {

                string details = String.Format("ProcessID {0} Not Available", processId);
                FilesUtilities.WriteLog(details, FilesUtilities.ErrorType.Info);
                throw;
            }


            //Gets impersonation token
            retVal = OpenProcessToken(p.Handle, TOKEN_DUPLICATE, ref token);
            if (retVal == true)
            {

                SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
                sa.nLength = (uint)Marshal.SizeOf(sa);

                //Convert the impersonation token into Primary token
                retVal = DuplicateTokenEx(
                    token,
                    TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY,
                    ref sa,
                    (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
                    (int)TOKEN_TYPE.TokenPrimary,
                    ref primaryToken);

                //Close the Token that was previously opened.
                CloseHandle(token);
                if (retVal == false)
                {
                    string message = String.Format("DuplicateTokenEx Error: {0}", Marshal.GetLastWin32Error());
                    FilesUtilities.WriteLog(message, FilesUtilities.ErrorType.Info);

                }

            }

            else
            {

                string message = String.Format("OpenProcessToken Error: {0}", Marshal.GetLastWin32Error());
                FilesUtilities.WriteLog(message, FilesUtilities.ErrorType.Info);

            }

            //We'll Close this token after it is used.
            return primaryToken;

        }

        private static IntPtr GetEnvironmentBlock(IntPtr token)
        {

            IntPtr envBlock = IntPtr.Zero;
            bool retVal = CreateEnvironmentBlock(ref envBlock, token, false);
            if (retVal == false)
            {

                //Environment Block, things like common paths to My Documents etc.
                //Will not be created if "false"
                //It should not adversley affect CreateProcessAsUser.

                string message = String.Format("CreateEnvironmentBlock Error: {0}", Marshal.GetLastWin32Error());
                FilesUtilities.WriteLog(message, FilesUtilities.ErrorType.Info);

            }
            return envBlock;
        }

        public static bool Launch(string appCmdLine /*,int processId*/)
        {

            bool ret = false;

            //Either specify the processID explicitly
            //Or try to get it from a process owned by the user.
            //In this case assuming there is only one explorer.exe

            Process[] ps = Process.GetProcessesByName("explorer");
            int processId = -1;//=processId
            if (ps.Length > 0)
            {
                processId = ps[0].Id;
            }

            if (processId > 1)
            {
                IntPtr token = GetPrimaryToken(processId);

                if (token != IntPtr.Zero)
                {

                    IntPtr envBlock = GetEnvironmentBlock(token);
                    ret = LaunchProcessAsUser(appCmdLine, token, envBlock);
                    if (envBlock != IntPtr.Zero)
                        DestroyEnvironmentBlock(envBlock);

                    CloseHandle(token);
                }

            }
            return ret;
        }

    }

And to execute , simply call like this :要执行,只需像这样调用:

string szCmdline = "AbsolutePathToYourExe\\ExeNameWithoutExtension";
ProcessAsUser.Launch(szCmdline);

I think You are copying the .exe to different location.我认为您正在将 .exe 复制到不同的位置。 This might be the problem I guess.这可能是我猜的问题。 When you copy the exe, you are not copying its dependencies.当您复制 exe 时,您不是在复制其依赖项。

So, what you can do is, put all dependent dlls in GAC so that any .net exe can access it因此,您可以做的是,将所有依赖的 dll 放在 GAC 中,以便任何 .net exe 都可以访问它

Else, do not copy the exe to new location.否则,请勿将 exe 复制到新位置。 Just create a environment variable and call the exe in your c#.只需创建一个环境变量并在您的 c# 中调用 exe。 Since the path is defined in environment variables, the exe is can be accessed by your c# program.由于路径是在环境变量中定义的,因此您的 c# 程序可以访问该 exe。

Update:更新:

previously I had some kind of same issue in my c#.net 3.5 project in which I was trying to run a .exe file from c#.net code and that exe was nothing but the another project exe(where i added few supporting dlls for my functionality) and those dlls methods I was using in my exe application.以前我在我的 c#.net 3.5 项目中遇到了一些相同的问题,我试图从 c#.net 代码运行一个 .exe 文件,而那个 exe 只不过是另一个项目 exe(我为我的功能)以及我在 exe 应用程序中使用的那些 dll 方法。 At last I resolved this by creating that application as a separate project to the same solution and i added that project output to my deployment project.最后,我通过将该应用程序创建为同一解决方案的单独项目来解决此问题,并将该项目输出添加到我的部署项目中。 According to this scenario I answered, If its not what he wants then I am extremely sorry.根据这个场景我回答,如果不是他想要的那我非常抱歉。

System.Diagnostics.Process.Start("Exe Name");

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM