* Use job object to manage polipo service's external processes. Signed-off-by: noisyfox <timemanager.rick@gmail.com> * Only kill those ss_privoxy.exe created by this ss instance. Signed-off-by: noisyfox <timemanager.rick@gmail.com> * Update README Signed-off-by: noisyfox <timemanager.rick@gmail.com>tags/3.3
@@ -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. | |||
@@ -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; | |||
@@ -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 | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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")); | |||
@@ -76,6 +76,7 @@ | |||
<Reference Include="System" /> | |||
<Reference Include="System.Data" /> | |||
<Reference Include="System.Drawing" /> | |||
<Reference Include="System.Management" /> | |||
<Reference Include="System.Net" /> | |||
<Reference Include="System.Windows.Forms" /> | |||
<Reference Include="System.Windows.Forms.DataVisualization" /> | |||
@@ -176,6 +177,8 @@ | |||
<Compile Include="Controller\Strategy\IStrategy.cs" /> | |||
<Compile Include="Proxy\Socks5Proxy.cs" /> | |||
<Compile Include="StringEx.cs" /> | |||
<Compile Include="Util\ProcessManagement\Job.cs" /> | |||
<Compile Include="Util\ProcessManagement\ThreadUtil.cs" /> | |||
<Compile Include="Util\SocketUtil.cs" /> | |||
<Compile Include="Util\Util.cs" /> | |||
<Compile Include="View\ConfigForm.cs"> | |||