mirror of
https://github.com/ziglang/zig.git
synced 2024-11-21 11:32:24 +00:00
Merge 58cc61dbf2
into f845fa04a0
This commit is contained in:
commit
59c9b88e6f
@ -9383,6 +9383,7 @@ pub extern "c" fn setregid(rgid: gid_t, egid: gid_t) c_int;
|
||||
pub extern "c" fn setresuid(ruid: uid_t, euid: uid_t, suid: uid_t) c_int;
|
||||
pub extern "c" fn setresgid(rgid: gid_t, egid: gid_t, sgid: gid_t) c_int;
|
||||
pub extern "c" fn setpgid(pid: pid_t, pgid: pid_t) c_int;
|
||||
pub extern "c" fn setsid() pid_t;
|
||||
|
||||
pub extern "c" fn malloc(usize) ?*anyopaque;
|
||||
pub extern "c" fn realloc(?*anyopaque, usize) ?*anyopaque;
|
||||
@ -9566,6 +9567,7 @@ pub extern "c" fn setlogmask(maskpri: c_int) c_int;
|
||||
pub extern "c" fn if_nametoindex([*:0]const u8) c_int;
|
||||
|
||||
pub extern "c" fn getpid() pid_t;
|
||||
pub extern "c" fn getsid(pid: pid_t) pid_t;
|
||||
pub extern "c" fn getppid() pid_t;
|
||||
|
||||
/// These are implementation defined but share identical values in at least musl and glibc:
|
||||
|
@ -1694,6 +1694,10 @@ pub fn setsid() pid_t {
|
||||
return @bitCast(@as(u32, @truncate(syscall0(.setsid))));
|
||||
}
|
||||
|
||||
pub fn getsid(pid: pid_t) pid_t {
|
||||
return @bitCast(@as(u32, @truncate(syscall1(.getsid, @intCast(pid)))));
|
||||
}
|
||||
|
||||
pub fn getpid() pid_t {
|
||||
return @bitCast(@as(u32, @truncate(syscall0(.getpid))));
|
||||
}
|
||||
|
@ -3734,6 +3734,7 @@ pub const COORD = extern struct {
|
||||
Y: SHORT,
|
||||
};
|
||||
|
||||
pub const DETACHED_PROCESS = 8;
|
||||
pub const CREATE_UNICODE_ENVIRONMENT = 1024;
|
||||
|
||||
pub const TLS_OUT_OF_INDEXES = 4294967295;
|
||||
|
@ -3467,6 +3467,29 @@ pub fn setpgid(pid: pid_t, pgid: pid_t) SetPgidError!void {
|
||||
}
|
||||
}
|
||||
|
||||
pub const SetSidError = error{PermissionDenied} || UnexpectedError;
|
||||
|
||||
pub fn setsid() SetSidError!pid_t {
|
||||
const res = system.setsid();
|
||||
switch (errno(@as(isize, res))) {
|
||||
.SUCCESS => return res,
|
||||
.PERM => return error.PermissionDenied,
|
||||
else => |err| return unexpectedErrno(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub const GetSidError = error{ProcessNotFound} || SetSidError;
|
||||
|
||||
pub fn getsid(pid: pid_t) GetSidError!pid_t {
|
||||
const res = system.getsid(pid);
|
||||
switch (errno(@as(isize, res))) {
|
||||
.SUCCESS => return res,
|
||||
.PERM => return error.PermissionDenied,
|
||||
.SRCH => return error.ProcessNotFound,
|
||||
else => |err| return unexpectedErrno(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether a file descriptor refers to a terminal.
|
||||
pub fn isatty(handle: fd_t) bool {
|
||||
if (native_os == .windows) {
|
||||
|
@ -57,6 +57,9 @@ stdin_behavior: StdIo,
|
||||
stdout_behavior: StdIo,
|
||||
stderr_behavior: StdIo,
|
||||
|
||||
/// Set to spawn a detached process.
|
||||
detached: bool,
|
||||
|
||||
/// Set to change the user id when spawning the child process.
|
||||
uid: if (native_os == .windows or native_os == .wasi) void else ?posix.uid_t,
|
||||
|
||||
@ -172,6 +175,7 @@ pub const SpawnError = error{
|
||||
posix.ExecveError ||
|
||||
posix.SetIdError ||
|
||||
posix.SetPgidError ||
|
||||
posix.SetSidError ||
|
||||
posix.ChangeCurDirError ||
|
||||
windows.CreateProcessError ||
|
||||
windows.GetProcessMemoryInfoError ||
|
||||
@ -215,6 +219,7 @@ pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess {
|
||||
.term = null,
|
||||
.env_map = null,
|
||||
.cwd = null,
|
||||
.detached = false,
|
||||
.uid = if (native_os == .windows or native_os == .wasi) {} else null,
|
||||
.gid = if (native_os == .windows or native_os == .wasi) {} else null,
|
||||
.pgid = if (native_os == .windows or native_os == .wasi) {} else null,
|
||||
@ -228,13 +233,25 @@ pub fn init(argv: []const []const u8, allocator: mem.Allocator) ChildProcess {
|
||||
};
|
||||
}
|
||||
|
||||
/// Call this if you have no intention of calling `kill` or `wait` to properly
|
||||
/// dispose of any resources related to the child process.
|
||||
pub fn deinit(self: *ChildProcess) void {
|
||||
if (native_os == .windows) {
|
||||
posix.close(self.thread_handle);
|
||||
posix.close(self.id);
|
||||
}
|
||||
self.cleanupStreams();
|
||||
}
|
||||
|
||||
pub fn setUserName(self: *ChildProcess, name: []const u8) !void {
|
||||
const user_info = try process.getUserInfo(name);
|
||||
self.uid = user_info.uid;
|
||||
self.gid = user_info.gid;
|
||||
}
|
||||
|
||||
/// On success must call `kill` or `wait`.
|
||||
/// On success must call `kill` or `wait`. In the case of a detached process,
|
||||
/// consider using `deinit` instead if you have no intention of synchronizing
|
||||
/// with the child.
|
||||
/// After spawning the `id` is available.
|
||||
pub fn spawn(self: *ChildProcess) SpawnError!void {
|
||||
if (!process.can_spawn) {
|
||||
@ -656,6 +673,10 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void {
|
||||
const pid_result = try posix.fork();
|
||||
if (pid_result == 0) {
|
||||
// we are the child
|
||||
if (self.detached) {
|
||||
_ = posix.setsid() catch |err| forkChildErrReport(err_pipe[1], err);
|
||||
}
|
||||
|
||||
setUpChildIo(self.stdin_behavior, stdin_pipe[0], posix.STDIN_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err);
|
||||
setUpChildIo(self.stdout_behavior, stdout_pipe[1], posix.STDOUT_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err);
|
||||
setUpChildIo(self.stderr_behavior, stderr_pipe[1], posix.STDERR_FILENO, dev_null_fd) catch |err| forkChildErrReport(err_pipe[1], err);
|
||||
@ -682,6 +703,8 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void {
|
||||
posix.setpgid(0, pid) catch |err| forkChildErrReport(err_pipe[1], err);
|
||||
}
|
||||
|
||||
writeIntFd(err_pipe[1], maxInt(ErrInt)) catch {};
|
||||
|
||||
const err = switch (self.expand_arg0) {
|
||||
.expand => posix.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_buf.ptr, envp),
|
||||
.no_expand => posix.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_buf.ptr, envp),
|
||||
@ -690,6 +713,26 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void {
|
||||
}
|
||||
|
||||
// we are the parent
|
||||
|
||||
// We perform a blocking read on the err_pipe that gets us either an error
|
||||
// that occured between fork and exec or a maxInt(ErrInt) if there wasn't any.
|
||||
// Since we use eventfd on linux, it can happen that we get both
|
||||
// a maxInt(ErrInt) and an error code in the first read if the exec in child failed
|
||||
// and its error was written before this read (eventfd just sums the values up).
|
||||
const err_int = blk: {
|
||||
if (native_os == .linux) {
|
||||
const file = File{ .handle = err_pipe[0] };
|
||||
const err_int = file.reader().readInt(u64, .little) catch return error.SystemResources;
|
||||
break :blk err_int;
|
||||
} else {
|
||||
break :blk try readIntFd(err_pipe[0]);
|
||||
}
|
||||
};
|
||||
if (err_int != maxInt(ErrInt)) {
|
||||
const err = @errorFromInt(@as(ErrInt, @intCast(err_int % maxInt(ErrInt))));
|
||||
return @as(SpawnError, @errorCast(err));
|
||||
}
|
||||
|
||||
const pid: i32 = @intCast(pid_result);
|
||||
if (self.stdin_behavior == .Pipe) {
|
||||
self.stdin = .{ .handle = stdin_pipe[1] };
|
||||
@ -842,6 +885,10 @@ fn spawnWindows(self: *ChildProcess) SpawnError!void {
|
||||
.lpReserved2 = null,
|
||||
};
|
||||
var piProcInfo: windows.PROCESS_INFORMATION = undefined;
|
||||
var dwCreationFlags: windows.DWORD = windows.CREATE_UNICODE_ENVIRONMENT;
|
||||
if (self.detached) {
|
||||
dwCreationFlags |= windows.DETACHED_PROCESS;
|
||||
}
|
||||
|
||||
const cwd_w = if (self.cwd) |cwd| try unicode.wtf8ToWtf16LeAllocZ(self.allocator, cwd) else null;
|
||||
defer if (cwd_w) |cwd| self.allocator.free(cwd);
|
||||
@ -926,7 +973,7 @@ fn spawnWindows(self: *ChildProcess) SpawnError!void {
|
||||
dir_buf.shrinkRetainingCapacity(normalized_len);
|
||||
}
|
||||
|
||||
windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| {
|
||||
windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo, dwCreationFlags) catch |no_path_err| {
|
||||
const original_err = switch (no_path_err) {
|
||||
// argv[0] contains unsupported characters that will never resolve to a valid exe.
|
||||
error.InvalidArg0 => return error.FileNotFound,
|
||||
@ -954,7 +1001,7 @@ fn spawnWindows(self: *ChildProcess) SpawnError!void {
|
||||
const normalized_len = windows.normalizePath(u16, dir_buf.items) catch continue;
|
||||
dir_buf.shrinkRetainingCapacity(normalized_len);
|
||||
|
||||
if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) {
|
||||
if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, &cmd_line_cache, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo, dwCreationFlags)) {
|
||||
break :run;
|
||||
} else |err| switch (err) {
|
||||
// argv[0] contains unsupported characters that will never resolve to a valid exe.
|
||||
@ -1055,6 +1102,7 @@ fn windowsCreateProcessPathExt(
|
||||
cwd_ptr: ?[*:0]u16,
|
||||
lpStartupInfo: *windows.STARTUPINFOW,
|
||||
lpProcessInformation: *windows.PROCESS_INFORMATION,
|
||||
dwCreationFlags: windows.DWORD,
|
||||
) !void {
|
||||
const app_name_len = app_buf.items.len;
|
||||
const dir_path_len = dir_buf.items.len;
|
||||
@ -1203,7 +1251,7 @@ fn windowsCreateProcessPathExt(
|
||||
else
|
||||
full_app_name;
|
||||
|
||||
if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| {
|
||||
if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation, dwCreationFlags)) |_| {
|
||||
return;
|
||||
} else |err| switch (err) {
|
||||
error.FileNotFound,
|
||||
@ -1258,7 +1306,7 @@ fn windowsCreateProcessPathExt(
|
||||
else
|
||||
full_app_name;
|
||||
|
||||
if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| {
|
||||
if (windowsCreateProcess(app_name_w.ptr, cmd_line_w.ptr, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation, dwCreationFlags)) |_| {
|
||||
return;
|
||||
} else |err| switch (err) {
|
||||
error.FileNotFound => continue,
|
||||
@ -1286,6 +1334,7 @@ fn windowsCreateProcess(
|
||||
cwd_ptr: ?[*:0]u16,
|
||||
lpStartupInfo: *windows.STARTUPINFOW,
|
||||
lpProcessInformation: *windows.PROCESS_INFORMATION,
|
||||
dwCreationFlags: windows.DWORD,
|
||||
) !void {
|
||||
// TODO the docs for environment pointer say:
|
||||
// > A pointer to the environment block for the new process. If this parameter
|
||||
@ -1310,7 +1359,7 @@ fn windowsCreateProcess(
|
||||
null,
|
||||
null,
|
||||
windows.TRUE,
|
||||
windows.CREATE_UNICODE_ENVIRONMENT,
|
||||
dwCreationFlags,
|
||||
@as(?*anyopaque, @ptrCast(envp_ptr)),
|
||||
cwd_ptr,
|
||||
lpStartupInfo,
|
||||
|
@ -63,6 +63,12 @@
|
||||
.child_process = .{
|
||||
.path = "child_process",
|
||||
},
|
||||
.detached_child = .{
|
||||
.path = "detached_child",
|
||||
},
|
||||
.child_spawn_fail = .{
|
||||
.path = "child_spawn_fail",
|
||||
},
|
||||
.embed_generated_file = .{
|
||||
.path = "embed_generated_file",
|
||||
},
|
||||
|
32
test/standalone/child_spawn_fail/build.zig
Normal file
32
test/standalone/child_spawn_fail/build.zig
Normal file
@ -0,0 +1,32 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const test_step = b.step("test", "Test it");
|
||||
b.default_step = test_step;
|
||||
|
||||
const optimize: std.builtin.OptimizeMode = .Debug;
|
||||
const target = b.graph.host;
|
||||
|
||||
if (builtin.os.tag == .wasi) return;
|
||||
|
||||
const child = b.addExecutable(.{
|
||||
.name = "child",
|
||||
.root_source_file = b.path("child.zig"),
|
||||
.optimize = optimize,
|
||||
.target = target,
|
||||
});
|
||||
|
||||
const main = b.addExecutable(.{
|
||||
.name = "main",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.optimize = optimize,
|
||||
.target = target,
|
||||
});
|
||||
|
||||
const run = b.addRunArtifact(main);
|
||||
run.addArtifactArg(child);
|
||||
run.expectExitCode(0);
|
||||
|
||||
test_step.dependOn(&run.step);
|
||||
}
|
19
test/standalone/child_spawn_fail/child.zig
Normal file
19
test/standalone/child_spawn_fail/child.zig
Normal file
@ -0,0 +1,19 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
|
||||
defer if (gpa_state.deinit() == .leak) @panic("leaks were detected");
|
||||
const gpa = gpa_state.allocator();
|
||||
var args = try std.process.argsWithAllocator(gpa);
|
||||
defer args.deinit();
|
||||
_ = args.next() orelse unreachable; // skip executable name
|
||||
const sleep_seconds = try std.fmt.parseInt(u32, args.next() orelse unreachable, 0);
|
||||
|
||||
const stdout = std.io.getStdOut();
|
||||
_ = try stdout.write("started");
|
||||
|
||||
const end_time = std.time.timestamp() + sleep_seconds;
|
||||
while (std.time.timestamp() < end_time) {
|
||||
std.time.sleep(@max(end_time - std.time.timestamp(), 0) * 1_000_000_000);
|
||||
}
|
||||
}
|
36
test/standalone/child_spawn_fail/main.zig
Normal file
36
test/standalone/child_spawn_fail/main.zig
Normal file
@ -0,0 +1,36 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
|
||||
defer if (gpa_state.deinit() == .leak) @panic("memory leak detected");
|
||||
const gpa = gpa_state.allocator();
|
||||
|
||||
var args = try std.process.argsWithAllocator(gpa);
|
||||
defer args.deinit();
|
||||
_ = args.next() orelse unreachable; // skip executable name
|
||||
const child_path = args.next() orelse unreachable;
|
||||
|
||||
const argv = if (builtin.os.tag == .windows) &.{""} else &.{ child_path, "30" };
|
||||
var child = std.process.Child.init(argv, gpa);
|
||||
child.stdin_behavior = .Ignore;
|
||||
child.stderr_behavior = .Ignore;
|
||||
child.stdout_behavior = .Pipe;
|
||||
child.detached = true;
|
||||
child.pgid = if (builtin.os.tag == .windows) void{} else try std.posix.getsid(0);
|
||||
defer {
|
||||
_ = child.kill() catch {};
|
||||
}
|
||||
|
||||
if (child.spawn()) {
|
||||
return error.SpawnSilencedError;
|
||||
} else |_| {}
|
||||
|
||||
child = std.process.Child.init(&.{ child_path, "30" }, gpa);
|
||||
child.stdin_behavior = .Ignore;
|
||||
child.stdout_behavior = .Ignore;
|
||||
child.stderr_behavior = .Inherit;
|
||||
|
||||
// this spawn should succeed and return without an error
|
||||
try child.spawn();
|
||||
}
|
32
test/standalone/detached_child/build.zig
Normal file
32
test/standalone/detached_child/build.zig
Normal file
@ -0,0 +1,32 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const test_step = b.step("test", "Test it");
|
||||
b.default_step = test_step;
|
||||
|
||||
const optimize: std.builtin.OptimizeMode = .Debug;
|
||||
const target = b.graph.host;
|
||||
|
||||
if (builtin.os.tag == .wasi) return;
|
||||
|
||||
const child = b.addExecutable(.{
|
||||
.name = "child",
|
||||
.root_source_file = b.path("child.zig"),
|
||||
.optimize = optimize,
|
||||
.target = target,
|
||||
});
|
||||
|
||||
const main = b.addExecutable(.{
|
||||
.name = "main",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.optimize = optimize,
|
||||
.target = target,
|
||||
});
|
||||
|
||||
const run = b.addRunArtifact(main);
|
||||
run.addArtifactArg(child);
|
||||
run.expectExitCode(0);
|
||||
|
||||
test_step.dependOn(&run.step);
|
||||
}
|
19
test/standalone/detached_child/child.zig
Normal file
19
test/standalone/detached_child/child.zig
Normal file
@ -0,0 +1,19 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
|
||||
defer if (gpa_state.deinit() == .leak) @panic("leaks were detected");
|
||||
const gpa = gpa_state.allocator();
|
||||
var args = try std.process.argsWithAllocator(gpa);
|
||||
defer args.deinit();
|
||||
_ = args.next() orelse unreachable; // skip executable name
|
||||
const sleep_seconds = try std.fmt.parseInt(u32, args.next() orelse unreachable, 0);
|
||||
|
||||
const stdout = std.io.getStdOut();
|
||||
_ = try stdout.write("started");
|
||||
|
||||
const end_time = std.time.timestamp() + sleep_seconds;
|
||||
while (std.time.timestamp() < end_time) {
|
||||
std.time.sleep(@max(end_time - std.time.timestamp(), 0) * 1_000_000_000);
|
||||
}
|
||||
}
|
70
test/standalone/detached_child/main.zig
Normal file
70
test/standalone/detached_child/main.zig
Normal file
@ -0,0 +1,70 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const windows = std.os.windows;
|
||||
|
||||
extern "kernel32" fn GetProcessId(Process: windows.HANDLE) callconv(windows.WINAPI) windows.DWORD;
|
||||
extern "kernel32" fn GetConsoleProcessList(
|
||||
lpdwProcessList: [*]windows.DWORD,
|
||||
dwProcessCount: windows.DWORD,
|
||||
) callconv(windows.WINAPI) windows.DWORD;
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
|
||||
defer if (gpa_state.deinit() == .leak) @panic("memory leak detected");
|
||||
const gpa = gpa_state.allocator();
|
||||
|
||||
var args = try std.process.argsWithAllocator(gpa);
|
||||
defer args.deinit();
|
||||
_ = args.next() orelse unreachable; // skip executable name
|
||||
const child_path = args.next() orelse unreachable;
|
||||
|
||||
var child = std.process.Child.init(&.{ child_path, "30" }, gpa);
|
||||
child.stdin_behavior = .Ignore;
|
||||
child.stderr_behavior = .Inherit;
|
||||
child.stdout_behavior = .Pipe;
|
||||
child.detached = true;
|
||||
try child.spawn();
|
||||
defer {
|
||||
_ = child.kill() catch {};
|
||||
}
|
||||
|
||||
// Give the process some time to actually start doing something before
|
||||
// checking if it properly detached.
|
||||
var read_buffer: [1]u8 = undefined;
|
||||
if (try child.stdout.?.read(&read_buffer) != 1) {
|
||||
return error.OutputReadFailed;
|
||||
}
|
||||
|
||||
switch (builtin.os.tag) {
|
||||
.windows => {
|
||||
const child_pid = GetProcessId(child.id);
|
||||
if (child_pid == 0) return error.GetProcessIdFailed;
|
||||
|
||||
var proc_buffer: []windows.DWORD = undefined;
|
||||
var proc_count: windows.DWORD = 16;
|
||||
while (true) {
|
||||
proc_buffer = try gpa.alloc(windows.DWORD, proc_count);
|
||||
defer gpa.free(proc_buffer);
|
||||
|
||||
proc_count = GetConsoleProcessList(proc_buffer.ptr, @min(proc_buffer.len, std.math.maxInt(windows.DWORD)));
|
||||
if (proc_count == 0) return error.ConsoleProcessListFailed;
|
||||
|
||||
if (proc_count <= proc_buffer.len) {
|
||||
for (proc_buffer[0..proc_count]) |proc| {
|
||||
if (proc == child_pid) return error.ProcessAttachedToConsole;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {
|
||||
const posix = std.posix;
|
||||
const current_sid = try posix.getsid(0);
|
||||
const child_sid = try posix.getsid(child.id);
|
||||
|
||||
if (current_sid == child_sid) {
|
||||
return error.SameChildSession;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user