diff --git a/base/client.jl b/base/client.jl index 31c8aa88ee423..e77b3de9aded6 100644 --- a/base/client.jl +++ b/base/client.jl @@ -217,8 +217,8 @@ function process_options(opts::JLOptions) startup && load_juliarc() # startup worker - if opts.worker != 0 - start_worker() # does not return + if opts.worker != C_NULL + start_worker(bytestring(opts.worker)) # does not return end # add processors if opts.nprocs > 0 diff --git a/base/docs/helpdb/Base.jl b/base/docs/helpdb/Base.jl index 67fd9a92cc28b..4eb3daca288c6 100644 --- a/base/docs/helpdb/Base.jl +++ b/base/docs/helpdb/Base.jl @@ -10485,3 +10485,10 @@ to. This is useful when writing custom `serialize` methods for a type, which opt data written out depending on the receiving process id. """ Base.worker_id_from_socket + +""" + Base.cluster_cookie([cookie]) -> cookie + +Returns the cluster cookie. If a cookie is passed, also sets it as the cluster cookie. +""" +Base.cluster_cookie diff --git a/base/initdefs.jl b/base/initdefs.jl index a64e134e0f852..2975b0d9802de 100644 --- a/base/initdefs.jl +++ b/base/initdefs.jl @@ -73,6 +73,7 @@ function init_parallel() global PGRP global LPROC LPROC.id = 1 + cluster_cookie(randstring()) assert(isempty(PGRP.workers)) register_worker(LPROC) end diff --git a/base/managers.jl b/base/managers.jl index 5ea93cb24138e..08b52fefe43f6 100644 --- a/base/managers.jl +++ b/base/managers.jl @@ -35,8 +35,9 @@ end function check_addprocs_args(kwargs) + valid_kw_names = collect(keys(default_addprocs_params())) for keyname in kwargs - !(keyname[1] in [:dir, :exename, :exeflags, :topology]) && throw(ArgumentError("Invalid keyword argument $(keyname[1])")) + !(keyname[1] in valid_kw_names) && throw(ArgumentError("Invalid keyword argument $(keyname[1])")) end end @@ -93,7 +94,7 @@ function launch_on_machine(manager::SSHManager, machine, cnt, params, launched, if length(machine_bind) > 1 exeflags = `--bind-to $(machine_bind[2]) $exeflags` end - exeflags = `$exeflags --worker` + exeflags = `$exeflags --worker $(cluster_cookie())` machine_def = split(machine_bind[1], ':') # if this machine def has a port number, add the port information to the ssh flags @@ -217,15 +218,15 @@ end # LocalManager - immutable LocalManager <: ClusterManager np::Integer + restrict::Bool # Restrict binding to 127.0.0.1 only end addprocs(; kwargs...) = addprocs(Sys.CPU_CORES; kwargs...) -function addprocs(np::Integer; kwargs...) +function addprocs(np::Integer; restrict=true, kwargs...) check_addprocs_args(kwargs) - addprocs(LocalManager(np); kwargs...) + addprocs(LocalManager(np, restrict); kwargs...) end show(io::IO, manager::LocalManager) = println(io, "LocalManager()") @@ -234,10 +235,11 @@ function launch(manager::LocalManager, params::Dict, launched::Array, c::Conditi dir = params[:dir] exename = params[:exename] exeflags = params[:exeflags] + bind_to = manager.restrict ? `127.0.0.1` : `$(LPROC.bind_addr)` for i in 1:manager.np io, pobj = open(pipeline(detach( - setenv(`$(julia_cmd(exename)) $exeflags --bind-to $(LPROC.bind_addr) --worker`, dir=dir)), + setenv(`$(julia_cmd(exename)) $exeflags --bind-to $bind_to --worker $(cluster_cookie())`, dir=dir)), stderr=STDERR), "r") wconfig = WorkerConfig() wconfig.process = pobj @@ -331,15 +333,7 @@ function connect_w2w(pid::Int, config::WorkerConfig) (rhost, rport) = get(config.connect_at) config.host = rhost config.port = rport - if get(get(config.environ), :self_is_local, false) && get(get(config.environ), :r_is_local, false) - # If on localhost, use the loopback address - this addresses - # the special case of system suspend wherein the local ip - # may be changed upon system awake. - (s, bind_addr) = connect_to_worker("127.0.0.1", rport) - else - (s, bind_addr)= connect_to_worker(rhost, rport) - end - + (s, bind_addr) = connect_to_worker(rhost, rport) (s,s) end @@ -375,24 +369,16 @@ function socket_reuse_port() end function connect_to_worker(host::AbstractString, port::Integer) - # Connect to the loopback port if requested host has the same ipaddress as self. s = socket_reuse_port() - if host == string(LPROC.bind_addr) - s = connect(s, "127.0.0.1", UInt16(port)) - else - s = connect(s, host, UInt16(port)) - end + connect(s, host, UInt16(port)) # Avoid calling getaddrinfo if possible - involves a DNS lookup # host may be a stringified ipv4 / ipv6 address or a dns name - if host == "localhost" - bind_addr = "127.0.0.1" - else - try - bind_addr = string(parse(IPAddr,host)) - catch - bind_addr = string(getaddrinfo(host)) - end + bind_addr = nothing + try + bind_addr = string(parse(IPAddr,host)) + catch + bind_addr = string(getaddrinfo(host)) end (s, bind_addr) end diff --git a/base/multi.jl b/base/multi.jl index 1822bd3b7bf3d..2a5d576038aa2 100644 --- a/base/multi.jl +++ b/base/multi.jl @@ -61,19 +61,24 @@ end # Worker initialization messages type IdentifySocketMsg <: AbstractMsg from_pid::Int + cookie::AbstractString +end +type IdentifySocketAckMsg <: AbstractMsg + cookie::AbstractString end type JoinPGRPMsg <: AbstractMsg self_pid::Int other_workers::Array - self_is_local::Bool notify_oid::RRID topology::Symbol worker_pool + cookie::AbstractString end type JoinCompleteMsg <: AbstractMsg notify_oid::RRID cpu_cores::Int ospid::Int + cookie::AbstractString end @@ -270,11 +275,15 @@ type LocalProcess id::Int bind_addr::AbstractString bind_port::UInt16 + cookie::AbstractString LocalProcess() = new(1) end const LPROC = LocalProcess() +cluster_cookie() = LPROC.cookie +cluster_cookie(cookie) = (LPROC.cookie = cookie; cookie) + const map_pid_wrkr = Dict{Int, Union{Worker, LocalProcess}}() const map_sock_wrkr = ObjectIdDict() const map_del_wrkr = Set{Int}() @@ -962,19 +971,25 @@ end process_messages(r_stream::IO, w_stream::IO) = @schedule message_handler_loop(r_stream, w_stream) function message_handler_loop(r_stream::IO, w_stream::IO) - global PGRP - global cluster_manager - try + # Check for a valid first message with a cookie. + msg = deserialize(r_stream) + if !any(x->isa(msg, x), [JoinPGRPMsg, JoinCompleteMsg, IdentifySocketMsg, IdentifySocketAckMsg]) || + (msg.cookie != cluster_cookie()) + + println(STDERR, "Unknown first message $(typeof(msg)) or cookie mismatch.") + error("Invalid connection credentials.") + end + while true + handle_msg(msg, r_stream, w_stream) msg = deserialize(r_stream) # println("got msg: ", msg) - handle_msg(msg, r_stream, w_stream) end catch e iderr = worker_id_from_socket(r_stream) if (iderr < 1) - print(STDERR, "Socket from unknown remote worker in worker ", myid()) + println(STDERR, "Socket from unknown remote worker in worker $(myid())") else werr = worker_from_id(iderr) oldstate = werr.state @@ -995,8 +1010,8 @@ function message_handler_loop(r_stream::IO, w_stream::IO) deregister_worker(iderr) end - if isopen(r_stream) close(r_stream) end - if isopen(w_stream) close(w_stream) end + isopen(r_stream) && close(r_stream) + isopen(w_stream) && close(w_stream) if (myid() == 1) && (iderr > 1) if oldstate != W_TERMINATING @@ -1028,7 +1043,12 @@ handle_msg(msg::RemoteDoMsg, r_stream, w_stream) = @schedule run_work_thunk(()-> handle_msg(msg::ResultMsg, r_stream, w_stream) = put!(lookup_ref(msg.response_oid), msg.value) -handle_msg(msg::IdentifySocketMsg, r_stream, w_stream) = Worker(msg.from_pid, r_stream, w_stream, cluster_manager) +function handle_msg(msg::IdentifySocketMsg, r_stream, w_stream) + # register a new peer worker connection + w=Worker(msg.from_pid, r_stream, w_stream, cluster_manager) + send_msg_now(w, IdentifySocketAckMsg(cluster_cookie())) +end +handle_msg(msg::IdentifySocketAckMsg, r_stream, w_stream) = nothing function handle_msg(msg::JoinPGRPMsg, r_stream, w_stream) LPROC.id = msg.self_pid @@ -1037,10 +1057,9 @@ function handle_msg(msg::JoinPGRPMsg, r_stream, w_stream) topology(msg.topology) wait_tasks = Task[] - for (connect_at, rpid, r_is_local) in msg.other_workers + for (connect_at, rpid) in msg.other_workers wconfig = WorkerConfig() wconfig.connect_at = connect_at - wconfig.environ = AnyDict(:self_is_local=>msg.self_is_local, :r_is_local=>r_is_local) let rpid=rpid, wconfig=wconfig t = @async connect_to_peer(cluster_manager, rpid, wconfig) @@ -1052,7 +1071,7 @@ function handle_msg(msg::JoinPGRPMsg, r_stream, w_stream) set_default_worker_pool(msg.worker_pool) - send_msg_now(controller, JoinCompleteMsg(msg.notify_oid, Sys.CPU_CORES, getpid())) + send_msg_now(controller, JoinCompleteMsg(msg.notify_oid, Sys.CPU_CORES, getpid(), cluster_cookie())) end function connect_to_peer(manager::ClusterManager, rpid::Int, wconfig::WorkerConfig) @@ -1060,8 +1079,9 @@ function connect_to_peer(manager::ClusterManager, rpid::Int, wconfig::WorkerConf (r_s, w_s) = connect(manager, rpid, wconfig) w = Worker(rpid, r_s, w_s, manager, wconfig) process_messages(w.r_stream, w.w_stream) - send_msg_now(w, IdentifySocketMsg(myid())) + send_msg_now(w, IdentifySocketMsg(myid(), cluster_cookie())) catch e + display_error(e, catch_backtrace()) println(STDERR, "Error [$e] on $(myid()) while connecting to peer $rpid. Exiting.") exit(1) end @@ -1069,7 +1089,6 @@ end function handle_msg(msg::JoinCompleteMsg, r_stream, w_stream) w = map_sock_wrkr[r_stream] - environ = get(w.config.environ, Dict()) environ[:cpu_cores] = msg.cpu_cores w.config.environ = environ @@ -1093,8 +1112,8 @@ worker_timeout() = parse(Float64, get(ENV, "JULIA_WORKER_TIMEOUT", "60.0")) # The entry point for julia worker processes. does not return. Used for TCP transport. # Cluster managers implementing their own transport will provide their own. # Argument is descriptor to write listening port # to. -start_worker() = start_worker(STDOUT) -function start_worker(out::IO) +start_worker(cookie::AbstractString) = start_worker(STDOUT, cookie) +function start_worker(out::IO, cookie::AbstractString) # we only explicitly monitor worker STDOUT on the console, so redirect # stderr to stdout so we can see the output. # at some point we might want some or all worker output to go to log @@ -1103,12 +1122,13 @@ function start_worker(out::IO) # exit when process 1 shut down. Don't yet know why. #redirect_stderr(STDOUT) - init_worker() + init_worker(cookie) + interface = IPv4(LPROC.bind_addr) if LPROC.bind_port == 0 - (actual_port,sock) = listenany(UInt16(9009)) + (actual_port,sock) = listenany(interface, UInt16(9009)) LPROC.bind_port = actual_port else - sock = listen(LPROC.bind_port) + sock = listen(interface, LPROC.bind_port) end @schedule while isopen(sock) client = accept(sock) @@ -1180,7 +1200,7 @@ function parse_connection_info(str) end end -function init_worker(manager::ClusterManager=DefaultClusterManager()) +function init_worker(cookie::AbstractString, manager::ClusterManager=DefaultClusterManager()) # On workers, the default cluster manager connects via TCP sockets. Custom # transports will need to call this function with their own manager. global cluster_manager @@ -1195,6 +1215,9 @@ function init_worker(manager::ClusterManager=DefaultClusterManager()) # System is started in head node mode, cleanup entries related to the same empty!(PGRP.workers) empty!(map_pid_wrkr) + + cluster_cookie(cookie) + nothing end @@ -1391,8 +1414,8 @@ function create_worker(manager, wconfig) end end - all_locs = map(x -> isa(x, Worker) ? (get(x.config.connect_at, ()), x.id, isa(x.manager, LocalManager)) : ((), x.id, true), join_list) - send_msg_now(w, JoinPGRPMsg(w.id, all_locs, isa(w.manager, LocalManager), ntfy_oid, PGRP.topology, default_worker_pool())) + all_locs = map(x -> isa(x, Worker) ? (get(x.config.connect_at, ()), x.id) : ((), x.id, true), join_list) + send_msg_now(w, JoinPGRPMsg(w.id, all_locs, ntfy_oid, PGRP.topology, default_worker_pool(), cluster_cookie())) @schedule manage(w.manager, w.id, w.config, :register) wait(rr_ntfy_join) diff --git a/base/options.jl b/base/options.jl index f48abdb314210..3c000138b1f30 100644 --- a/base/options.jl +++ b/base/options.jl @@ -25,7 +25,7 @@ immutable JLOptions depwarn::Int8 can_inline::Int8 fast_math::Int8 - worker::Int8 + worker::Ptr{UInt8} handle_signals::Int8 use_precompiled::Int8 use_compilecache::Int8 diff --git a/base/socket.jl b/base/socket.jl index 135d0dffb9ddc..9f8bc27c3b6a6 100644 --- a/base/socket.jl +++ b/base/socket.jl @@ -743,8 +743,8 @@ end ## Utility functions -function listenany(default_port) - addr = InetAddr(IPv4(UInt32(0)),default_port) +function listenany(host::IPAddr, default_port) + addr = InetAddr(host, default_port) while true sock = TCPServer() if bind(sock,addr) && _listen(sock) == 0 @@ -757,6 +757,7 @@ function listenany(default_port) end end end +listenany(default_port) = listenany(IPv4(UInt32(0)),default_port) function getsockname(sock::Union{TCPServer,TCPSocket}) rport = Ref{Cushort}(0) diff --git a/doc/manual/parallel-computing.rst b/doc/manual/parallel-computing.rst index 1e4a4a7120b55..62a3da007a5cd 100644 --- a/doc/manual/parallel-computing.rst +++ b/doc/manual/parallel-computing.rst @@ -808,14 +808,14 @@ signals that all requested workers have been launched. Hence the :func:`launch` as all the requested workers have been launched. Newly launched workers are connected to each other, and the master process, in a all-to-all manner. -Specifying command argument, ``--worker`` results in the launched processes initializing themselves +Specifying command argument, ``--worker `` results in the launched processes initializing themselves as workers and connections being setup via TCP/IP sockets. Optionally ``--bind-to bind_addr[:port]`` may also be specified to enable other workers to connect to it at the specified ``bind_addr`` and ``port``. This is useful for multi-homed hosts. For non-TCP/IP transports, for example, an implementation may choose to use MPI as the transport, -``--worker`` must NOT be specified. Instead newly launched workers should call ``init_worker()`` -before using any of the parallel constructs +``--worker`` must NOT be specified. Instead newly launched workers should call ``init_worker(cookie)`` +before using any of the parallel constructs. For every worker launched, the :func:`launch` method must add a :class:`WorkerConfig` object (with appropriate fields initialized) to ``launched`` :: @@ -918,7 +918,7 @@ When using custom transports: workers defaulting to the TCP/IP socket transport implementation - For every incoming logical connection with a worker, ``Base.process_messages(rd::AsyncStream, wr::AsyncStream)`` must be called. This launches a new task that handles reading and writing of messages from/to the worker represented by the ``AsyncStream`` objects -- ``init_worker(manager::FooManager)`` MUST be called as part of worker process initializaton +- ``init_worker(cookie, manager::FooManager)`` MUST be called as part of worker process initializaton - Field ``connect_at::Any`` in :class:`WorkerConfig` can be set by the cluster manager when ``launch`` is called. The value of this field is passed in in all ``connect`` callbacks. Typically, it carries information on *how to connect* to a worker. For example, the TCP/IP socket transport uses this field to specify the ``(host, port)`` tuple at which to connect to a worker @@ -929,6 +929,56 @@ implementation simply executes an ``exit()`` call on the specified remote worker ``examples/clustermanager/simple`` is an example that shows a simple implementation using unix domain sockets for cluster setup +Network requirements for LocalManager and SSHManager +---------------------------------------------------- +Julia clusters are designed to be executed on already secured environments on infrastructure ranging from local laptops, +to departmental clusters or even on the cloud. This section covers network security requirements for the inbuilt ``LocalManager`` +and ``SSHManager``: + +- The master process does not listen on any port. It only connects out to the workers. + +- Each worker binds to only one of the local interfaces and listens on the first free port starting from 9009. + +- ``LocalManager``, i.e. ``addprocs(N)``, by default binds only to the loopback interface. + This means that workers consequently started on remote hosts, or anyone with malicious intentions + is unable to connect to the cluster. A ``addprocs(4)`` followed by a ``addprocs(["remote_host"])`` + will fail. Some users may need to create a cluster comprised of their local system and a few remote systems. This can be done by + explicitly requesting ``LocalManager`` to bind to an external network interface via the ``restrict`` keyword + argument - ``addprocs(4; restrict=false)``. + +- ``SSHManager``, i.e. ``addprocs(list_of_remote_hosts)`` launches workers on remote hosts via SSH. + It is to be noted that by default SSH is only used to launch Julia workers. + Subsequent master-worker and worker-worker connections use plain, unencrypted TCP/IP sockets. The remote hosts + must have passwordless login enabled. Additional SSH flags or credentials may be specified via keyword + argument ``sshflags``. + +- ``addprocs(list_of_remote_hosts; tunnel=true, sshflags=)`` is useful when we wish to use + SSH connections for master-worker too. A typical scenario for this is a local laptop running the Julia REPL (i.e., the master) + with the rest of the cluster on the cloud, say on Amazon EC2. In this case only port 22 needs to be + opened at the remote cluster coupled with SSH client authenticated via PKI. + Authentication credentials can be supplied via ``sshflags``, for example ``sshflags=`-e ` ``. + + Note that worker-worker connections are still plain TCP and the local security policy on the remote cluster + must allow for free connections between worker nodes, at least for ports 9009 and above. + + Securing and encrypting all worker-worker traffic (via SSH), or encrypting individual messages can be done via + a custom ClusterManager. + +Cluster cookie +-------------- +All processes in a cluster share the same cookie which, by default, is a randomly generated string on the master process: + +- ``Base.cluster_cookie()`` returns the cookie, ``Base.cluster_cookie(cookie)`` sets it. +- All connections are authenticated on both sides to ensure that only workers started by the master are allowed + to connect to each other. +- The cookie must be passed to the workers at startup via argument ``--worker ``. + Custom ClusterManagers can retrieve the cookie on the master by calling + ``Base.cluster_cookie()``. Cluster managers not using the default TCP/IP transport (and hence not specifying ``--worker``) + must call ``init_worker(cookie, manager)`` with the same cookie as on the master. + +It is to be noted that environments requiring higher levels of security (for example, cookies can be pre-shared and hence not +specified as a startup arg) can implement this via a custom ClusterManager. + Specifying network topology (Experimental) ------------------------------------------- diff --git a/doc/stdlib/parallel.rst b/doc/stdlib/parallel.rst index 39c4b21660dd9..3aad04e6dc20a 100644 --- a/doc/stdlib/parallel.rst +++ b/doc/stdlib/parallel.rst @@ -535,6 +535,12 @@ General Parallel Computing Support A low-level API which given a ``IO`` connection, returns the pid of the worker it is connected to. This is useful when writing custom ``serialize`` methods for a type, which optimizes the data written out depending on the receiving process id. +.. function:: Base.cluster_cookie([cookie]) -> cookie + + .. Docstring generated from Julia source + + Returns the cluster cookie. If a cookie is passed, also sets it as the cluster cookie. + Shared Arrays ------------- diff --git a/examples/clustermanager/0mq/ZMQCM.jl b/examples/clustermanager/0mq/ZMQCM.jl index e360a9e1ce5c8..ac81fb7fa1834 100644 --- a/examples/clustermanager/0mq/ZMQCM.jl +++ b/examples/clustermanager/0mq/ZMQCM.jl @@ -197,7 +197,7 @@ end function launch(manager::ZMQCMan, params::Dict, launched::Array, c::Condition) #println("launch $(params[:np])") for i in 1:params[:np] - io, pobj = open(`julia worker.jl $i`, "r") + io, pobj = open(`julia worker.jl $i $(Base.cluster_cookie())`, "r") wconfig = WorkerConfig() wconfig.userdata = Dict(:zid=>i, :io=>io) @@ -228,9 +228,9 @@ function connect(manager::ZMQCMan, pid::Int, config::WorkerConfig) end # WORKER -function start_worker(zid) +function start_worker(zid, cookie) #println("start_worker") - Base.init_worker(ZMQCMan()) + Base.init_worker(cookie, ZMQCMan()) init_node(zid) while true diff --git a/examples/clustermanager/0mq/worker.jl b/examples/clustermanager/0mq/worker.jl index a11099e55d6a9..e541044b009f2 100644 --- a/examples/clustermanager/0mq/worker.jl +++ b/examples/clustermanager/0mq/worker.jl @@ -2,4 +2,4 @@ include("ZMQCM.jl") -start_worker(parse(Int,ARGS[1])) +start_worker(parse(Int,ARGS[1]), ARGS[2]) diff --git a/examples/clustermanager/simple/UnixDomainCM.jl b/examples/clustermanager/simple/UnixDomainCM.jl index 5151d92abd869..a67f68692f323 100644 --- a/examples/clustermanager/simple/UnixDomainCM.jl +++ b/examples/clustermanager/simple/UnixDomainCM.jl @@ -8,10 +8,11 @@ end function launch(manager::UnixDomainCM, params::Dict, launched::Array, c::Condition) # println("launch $(manager.np)") + cookie = Base.cluster_cookie() for i in 1:manager.np sockname = tempname() try - cmd = `$(params[:exename]) $(@__FILE__) udwrkr $sockname` + cmd = `$(params[:exename]) $(@__FILE__) udwrkr $sockname $cookie` io, pobj = open(cmd, "r") wconfig = WorkerConfig() @@ -58,8 +59,8 @@ function connect(manager::UnixDomainCM, pid::Int, config::WorkerConfig) end # WORKER -function start_worker(sockname) - Base.init_worker(UnixDomainCM(0)) +function start_worker(sockname, cookie) + Base.init_worker(cookie, UnixDomainCM(0)) srvr = listen(ascii(sockname)) while true @@ -87,5 +88,5 @@ end if (length(ARGS) > 0) && (ARGS[1] == "udwrkr") # script has been launched as a worker - start_worker(ARGS[2]) + start_worker(ARGS[2], ARGS[3]) end diff --git a/src/julia.h b/src/julia.h index d9a191706c100..d252b3f735922 100644 --- a/src/julia.h +++ b/src/julia.h @@ -1669,7 +1669,7 @@ typedef struct { int8_t depwarn; int8_t can_inline; int8_t fast_math; - int8_t worker; + const char *worker; int8_t handle_signals; int8_t use_precompiled; int8_t use_compilecache; diff --git a/test/topology.jl b/test/topology.jl index a72ab26771d47..c2b421090ce52 100644 --- a/test/topology.jl +++ b/test/topology.jl @@ -41,7 +41,7 @@ function Base.launch(manager::TopoTestManager, params::Dict, launched::Array, c: for i in 1:manager.np io, pobj = open(pipeline(detach( - setenv(`$(Base.julia_cmd(exename)) $exeflags --bind-to $(Base.LPROC.bind_addr) --worker`, dir=dir)); stderr=STDERR), "r") + setenv(`$(Base.julia_cmd(exename)) $exeflags --bind-to $(Base.LPROC.bind_addr) --worker $(Base.cluster_cookie())`, dir=dir)); stderr=STDERR), "r") wconfig = WorkerConfig() wconfig.process = pobj wconfig.io = io diff --git a/ui/repl.c b/ui/repl.c index 634588de10eff..0bfa632d59354 100644 --- a/ui/repl.c +++ b/ui/repl.c @@ -169,7 +169,7 @@ void parse_opts(int *argcp, char ***argvp) { "math-mode", required_argument, 0, opt_math_mode }, { "handle-signals", required_argument, 0, opt_handle_signals }, // hidden command line options - { "worker", no_argument, 0, opt_worker }, + { "worker", required_argument, 0, opt_worker }, { "bind-to", required_argument, 0, opt_bind_to }, { "lisp", no_argument, &lisp_prompt, 1 }, { 0, 0, 0, 0 } @@ -435,7 +435,7 @@ void parse_opts(int *argcp, char ***argvp) jl_errorf("julia: invalid argument to --math-mode (%s)", optarg); break; case opt_worker: - jl_options.worker = 1; + jl_options.worker = strdup(optarg); break; case opt_bind_to: jl_options.bindto = strdup(optarg);