diff --git a/README.md b/README.md index b4952dd8..b2758e94 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,6 @@ If you want to manage multiple servers using other tools like SwitchyOmega, you can start multiple Shadowsocks instances. To avoid configuration conflicts, copy Shadowsocks to a new directory and choose a different local port. -Also, make sure to use `SOCKS5` proxy in SwitchyOmega, since we have only -one HTTP proxy instance. - #### Server Configuration Please visit [Servers] for more information. diff --git a/shadowsocks-csharp/Controller/Service/PolipoRunner.cs b/shadowsocks-csharp/Controller/Service/PolipoRunner.cs index a8b1e2d2..cc4d1b35 100644 --- a/shadowsocks-csharp/Controller/Service/PolipoRunner.cs +++ b/shadowsocks-csharp/Controller/Service/PolipoRunner.cs @@ -1,20 +1,26 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; +using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Runtime.InteropServices; using System.Text; - +using System.Windows.Forms; using Shadowsocks.Model; using Shadowsocks.Properties; using Shadowsocks.Util; +using Shadowsocks.Util.ProcessManagement; namespace Shadowsocks.Controller { class PolipoRunner { + private static int Uid; + private static string UniqueConfigFile; + private static Job PolipoJob; private Process _process; private int _runningPort; @@ -22,6 +28,10 @@ namespace Shadowsocks.Controller { try { + Uid = Application.StartupPath.GetHashCode(); // Currently we use ss's StartupPath to identify different polipo instance. + UniqueConfigFile = $"privoxy_{Uid}.conf"; + PolipoJob = new Job(); + FileManager.UncompressFile(Utils.GetTempPath("ss_privoxy.exe"), Resources.privoxy_exe); FileManager.UncompressFile(Utils.GetTempPath("mgwz.dll"), Resources.mgwz_dll); } @@ -45,7 +55,7 @@ namespace Shadowsocks.Controller if (_process == null) { Process[] existingPolipo = Process.GetProcessesByName("ss_privoxy"); - foreach (Process p in existingPolipo) + foreach (Process p in existingPolipo.Where(IsChildProcess)) { KillProcess(p); } @@ -54,17 +64,23 @@ namespace Shadowsocks.Controller polipoConfig = polipoConfig.Replace("__SOCKS_PORT__", configuration.localPort.ToString()); polipoConfig = polipoConfig.Replace("__POLIPO_BIND_PORT__", _runningPort.ToString()); polipoConfig = polipoConfig.Replace("__POLIPO_BIND_IP__", configuration.shareOverLan ? "0.0.0.0" : "127.0.0.1"); - FileManager.ByteArrayToFile(Utils.GetTempPath("privoxy.conf"), Encoding.UTF8.GetBytes(polipoConfig)); + FileManager.ByteArrayToFile(Utils.GetTempPath(UniqueConfigFile), Encoding.UTF8.GetBytes(polipoConfig)); _process = new Process(); // Configure the process using the StartInfo properties. _process.StartInfo.FileName = "ss_privoxy.exe"; - _process.StartInfo.Arguments = "privoxy.conf"; + _process.StartInfo.Arguments = UniqueConfigFile; _process.StartInfo.WorkingDirectory = Utils.GetTempPath(); _process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; _process.StartInfo.UseShellExecute = true; _process.StartInfo.CreateNoWindow = true; _process.Start(); + + /* + * Add this process to job obj associated with this ss process, so that + * when ss exit unexpectedly, this process will be forced killed by system. + */ + PolipoJob.AddProcess(_process.Handle); } RefreshTrayArea(); } @@ -97,6 +113,45 @@ namespace Shadowsocks.Controller } } + /* + * We won't like to kill other ss instances' ss_privoxy.exe. + * This function will check whether the given process is created + * by this process by checking the module path or command line. + * + * Since it's required to put ss in different dirs to run muti instances, + * different instance will create their unique "privoxy_UID.conf" where + * UID is hash of ss's location. + */ + private static bool IsChildProcess(Process process) + { + if (Utils.IsPortableMode()) + { + /* + * Under PortableMode, we could identify it by the path of ss_privoxy.exe. + */ + string path = process.MainModule.FileName; + return Utils.GetTempPath("ss_privoxy.exe").Equals(path); + } + else + { + try + { + var cmd = process.GetCommandLine(); + + return cmd.Contains(UniqueConfigFile); + } + catch (Win32Exception ex) + { + if ((uint) ex.ErrorCode != 0x80004005) + { + throw; + } + } + + return false; + } + } + private int GetFreePort() { int defaultPort = 8123; diff --git a/shadowsocks-csharp/Util/ProcessManagement/Job.cs b/shadowsocks-csharp/Util/ProcessManagement/Job.cs new file mode 100644 index 00000000..b79444d6 --- /dev/null +++ b/shadowsocks-csharp/Util/ProcessManagement/Job.cs @@ -0,0 +1,154 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Shadowsocks.Controller; + +namespace Shadowsocks.Util.ProcessManagement +{ + /* + * See: + * http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net + */ + public class Job : IDisposable + { + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + static extern IntPtr CreateJobObject(IntPtr a, string lpName); + + [DllImport("kernel32.dll")] + static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, UInt32 cbJobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool CloseHandle(IntPtr hObject); + + private IntPtr handle; + private bool disposed; + + public Job() + { + handle = CreateJobObject(IntPtr.Zero, null); + + var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION + { + LimitFlags = 0x2000 + }; + + var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + BasicLimitInformation = info + }; + + int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length); + Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); + + if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length)) + throw new Exception(string.Format("Unable to set information. Error: {0}", Marshal.GetLastWin32Error())); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposed) + return; + + if (disposing) { } + + Close(); + disposed = true; + } + + public void Close() + { + CloseHandle(handle); + handle = IntPtr.Zero; + } + + public bool AddProcess(IntPtr processHandle) + { + var succ = AssignProcessToJobObject(handle, processHandle); + + if (!succ) + { + var err = Marshal.GetLastWin32Error(); + Logging.Error("Failed to call AssignProcessToJobObject! GetLastError=" + err); + } + + return succ; + } + + public bool AddProcess(int processId) + { + return AddProcess(Process.GetProcessById(processId).Handle); + } + + } + + #region Helper classes + + [StructLayout(LayoutKind.Sequential)] + struct IO_COUNTERS + { + public UInt64 ReadOperationCount; + public UInt64 WriteOperationCount; + public UInt64 OtherOperationCount; + public UInt64 ReadTransferCount; + public UInt64 WriteTransferCount; + public UInt64 OtherTransferCount; + } + + + [StructLayout(LayoutKind.Sequential)] + struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + public Int64 PerProcessUserTimeLimit; + public Int64 PerJobUserTimeLimit; + public UInt32 LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public UInt32 ActiveProcessLimit; + public UIntPtr Affinity; + public UInt32 PriorityClass; + public UInt32 SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public UInt32 nLength; + public IntPtr lpSecurityDescriptor; + public Int32 bInheritHandle; + } + + [StructLayout(LayoutKind.Sequential)] + struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } + + public enum JobObjectInfoType + { + AssociateCompletionPortInformation = 7, + BasicLimitInformation = 2, + BasicUIRestrictions = 4, + EndOfJobTimeInformation = 6, + ExtendedLimitInformation = 9, + SecurityLimitInformation = 5, + GroupInformation = 11 + } + + #endregion +} diff --git a/shadowsocks-csharp/Util/ProcessManagement/ThreadUtil.cs b/shadowsocks-csharp/Util/ProcessManagement/ThreadUtil.cs new file mode 100644 index 00000000..9421638f --- /dev/null +++ b/shadowsocks-csharp/Util/ProcessManagement/ThreadUtil.cs @@ -0,0 +1,31 @@ +using System.Diagnostics; +using System.Management; +using System.Text; + +namespace Shadowsocks.Util.ProcessManagement +{ + static class ThreadUtil + { + + /* + * See: + * http://stackoverflow.com/questions/2633628/can-i-get-command-line-arguments-of-other-processes-from-net-c + */ + public static string GetCommandLine(this Process process) + { + var commandLine = new StringBuilder(process.MainModule.FileName); + + commandLine.Append(" "); + using (var searcher = new ManagementObjectSearcher("SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + process.Id)) + { + foreach (var @object in searcher.Get()) + { + commandLine.Append(@object["CommandLine"]); + commandLine.Append(" "); + } + } + + return commandLine.ToString(); + } + } +} diff --git a/shadowsocks-csharp/Util/Util.cs b/shadowsocks-csharp/Util/Util.cs index fdb8482e..872147b6 100755 --- a/shadowsocks-csharp/Util/Util.cs +++ b/shadowsocks-csharp/Util/Util.cs @@ -11,14 +11,25 @@ namespace Shadowsocks.Util { public class Utils { + private static bool? _portableMode; private static string TempPath = null; + public static bool IsPortableMode() + { + if (!_portableMode.HasValue) + { + _portableMode = File.Exists(Path.Combine(Application.StartupPath, "shadowsocks_portable_mode.txt")); + } + + return _portableMode.Value; + } + // return path to store temporary files public static string GetTempPath() { if (TempPath == null) { - if (File.Exists(Path.Combine(Application.StartupPath, "shadowsocks_portable_mode.txt"))) + if (IsPortableMode()) try { Directory.CreateDirectory(Path.Combine(Application.StartupPath, "temp")); diff --git a/shadowsocks-csharp/shadowsocks-csharp.csproj b/shadowsocks-csharp/shadowsocks-csharp.csproj index 10e2c7d2..a9d6cbc3 100644 --- a/shadowsocks-csharp/shadowsocks-csharp.csproj +++ b/shadowsocks-csharp/shadowsocks-csharp.csproj @@ -76,6 +76,7 @@ + @@ -176,6 +177,8 @@ + +