• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SpiNNakerManchester / JavaSpiNNaker / 13385392465

18 Feb 2025 07:28AM UTC coverage: 38.494% (+0.09%) from 38.406%
13385392465

push

github

rowleya
Use CSFR Token and make sure connection actually happens!

0 of 2 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

9189 of 23871 relevant lines covered (38.49%)

1.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.03
/SpiNNaker-comms/src/main/java/uk/ac/manchester/spinnaker/alloc/client/SpallocClientFactory.java
1
/*
2
 * Copyright (c) 2021 The University of Manchester
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     https://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package uk.ac.manchester.spinnaker.alloc.client;
17

18
import static com.fasterxml.jackson.databind.PropertyNamingStrategies.KEBAB_CASE;
19
import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS;
20
import static java.lang.String.format;
21
import static java.lang.Thread.sleep;
22
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
23
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
24
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
25
import static java.net.URLEncoder.encode;
26
import static java.nio.charset.StandardCharsets.UTF_8;
27
import static java.util.Collections.synchronizedMap;
28
import static java.util.Objects.isNull;
29
import static java.util.Objects.nonNull;
30
import static java.util.stream.Collectors.joining;
31
import static java.util.stream.Collectors.toList;
32
import static org.apache.commons.io.IOUtils.readLines;
33
import static org.slf4j.LoggerFactory.getLogger;
34
import static uk.ac.manchester.spinnaker.alloc.client.ClientUtils.asDir;
35

36
import java.io.BufferedReader;
37
import java.io.FileNotFoundException;
38
import java.io.IOException;
39
import java.io.InputStream;
40
import java.io.InputStreamReader;
41
import java.io.OutputStreamWriter;
42
import java.net.HttpURLConnection;
43
import java.net.URI;
44
import java.net.URISyntaxException;
45
import java.nio.ByteBuffer;
46
import java.nio.channels.Channels;
47
import java.util.ArrayList;
48
import java.util.Collection;
49
import java.util.HashMap;
50
import java.util.List;
51
import java.util.Map;
52
import java.util.stream.Stream;
53

54
import org.apache.commons.io.IOUtils;
55
import org.slf4j.Logger;
56

57
import com.fasterxml.jackson.databind.json.JsonMapper;
58
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
59
import com.google.errorprone.annotations.MustBeClosed;
60
import com.google.errorprone.annotations.concurrent.GuardedBy;
61

62
import uk.ac.manchester.spinnaker.alloc.client.SpallocClient.Job;
63
import uk.ac.manchester.spinnaker.alloc.client.SpallocClient.Machine;
64
import uk.ac.manchester.spinnaker.alloc.client.SpallocClient.SpallocException;
65
import uk.ac.manchester.spinnaker.machine.HasChipLocation;
66
import uk.ac.manchester.spinnaker.machine.MemoryLocation;
67
import uk.ac.manchester.spinnaker.machine.board.PhysicalCoords;
68
import uk.ac.manchester.spinnaker.machine.board.TriadCoords;
69
import uk.ac.manchester.spinnaker.messages.model.Version;
70
import uk.ac.manchester.spinnaker.spalloc.exceptions.SpallocServerException;
71
import uk.ac.manchester.spinnaker.storage.ProxyInformation;
72
import uk.ac.manchester.spinnaker.transceiver.SpinnmanException;
73
import uk.ac.manchester.spinnaker.transceiver.TransceiverInterface;
74
import uk.ac.manchester.spinnaker.utils.Daemon;
75

76
/**
77
 * A factory for clients to connect to the Spalloc service.
78
 * <p>
79
 * <strong>Implementation Note:</strong> Neither this class nor the client
80
 * classes it creates maintain state that needs to be closed explicitly
81
 * <em>except</em> for
82
 * {@linkplain SpallocClient.Job#getTransceiver() transceivers}, as transceivers
83
 * usually need to be closed.
84
 *
85
 * @author Donal Fellows
86
 */
87
public class SpallocClientFactory {
88
    private static final Logger log = getLogger(SpallocClientFactory.class);
3✔
89

90
    private static final String CONTENT_TYPE = "Content-Type";
91

92
    private static final String TEXT_PLAIN = "text/plain; charset=UTF-8";
93

94
    private static final String APPLICATION_JSON = "application/json";
95

96
    private static final String FORM_ENCODED =
97
            "application/x-www-form-urlencoded";
98

99
    private static final URI KEEPALIVE = URI.create("keepalive");
3✔
100

101
    private static final URI MACHINE = URI.create("machine");
3✔
102

103
    private static final URI POWER = URI.create("power");
3✔
104

105
    private static final URI WAIT_FLAG = URI.create("?wait=true");
3✔
106

107
    private static final URI MEMORY = URI.create("memory");
3✔
108

109
    // Amount to divide keepalive interval by to get actual keep alive delay
110
    private static final int KEEPALIVE_DIVIDER = 2;
111

112
    /** Used to convert to/from JSON. */
113
    static final JsonMapper JSON_MAPPER = JsonMapper.builder()
3✔
114
            .findAndAddModules().disable(WRITE_DATES_AS_TIMESTAMPS)
3✔
115
            .addModule(new JavaTimeModule())
3✔
116
            .propertyNamingStrategy(KEBAB_CASE).build();
3✔
117

118
    private final URI baseUrl;
119

120
    /**
121
     * Cache of machines, which don't expire.
122
     */
123
    private static final Map<String, Machine> MACHINE_MAP =
3✔
124
            synchronizedMap(new HashMap<>());
3✔
125

126
    /**
127
     * Create a factory that can talk to a given service.
128
     *
129
     * @param baseUrl
130
     *            Where the server is.
131
     */
132
    public SpallocClientFactory(URI baseUrl) {
×
133
        this.baseUrl = asDir(baseUrl);
×
134
    }
×
135

136
    /**
137
     * Get a handle to a job given its proxy access information (derived from a
138
     * database query).
139
     *
140
     * @param proxy
141
     *            The proxy information from the database. Handles {@code null}.
142
     * @return The job handle, or {@code null} if {@code proxy==null}.
143
     * @throws IOException
144
     *             If connecting to the job fails.
145
     */
146
    public static Job getJobFromProxyInfo(ProxyInformation proxy)
147
            throws IOException {
148
        if (proxy == null) {
×
149
            return null;
×
150
        }
151
        log.info("Using proxy {} for connections", proxy.spallocUrl);
×
152
        return new SpallocClientFactory(URI.create(proxy.spallocUrl))
×
153
                .getJob(proxy.jobUrl, proxy.headers, proxy.cookies);
×
154
    }
155

156
    /**
157
     * Read an object from a stream.
158
     *
159
     * @param <T>
160
     *            The type of the object to read.
161
     * @param is
162
     *            The stream
163
     * @param cls
164
     *            The class of object to read.
165
     * @return The object
166
     * @throws IOException
167
     *             If an I/O error happens or the content on the stream can't be
168
     *             made into an instance of the given class.
169
     */
170
    static <T> T readJson(InputStream is, Class<T> cls) throws IOException {
171
        BufferedReader streamReader = new BufferedReader(
×
172
                new InputStreamReader(is, "UTF-8"));
173
        StringBuilder responseStrBuilder = new StringBuilder();
×
174

175
        String inputStr;
176
        while ((inputStr = streamReader.readLine()) != null) {
×
177
            responseStrBuilder.append(inputStr);
×
178
        }
179
        String json = responseStrBuilder.toString();
×
180

181
        try {
182
            return JSON_MAPPER.readValue(json, cls);
×
183
        } catch (IOException e) {
×
184
            log.error("Error while reading json {}", json);
×
185
            throw e;
×
186
        }
187
    }
188

189
    /**
190
     * Outputs a form to a connection in
191
     * {@code application/x-www-form-urlencoded} format.
192
     *
193
     * @param connection
194
     *            The connection. Must have the right verb set.
195
     * @param map
196
     *            The contents of the form.
197
     * @throws IOException
198
     *             If I/O fails.
199
     */
200
    static void writeForm(HttpURLConnection connection, Map<String, String> map)
201
            throws IOException {
202
        var form = map.entrySet().stream()
×
203
                .map(e -> e.getKey() + "=" + encode(e.getValue(), UTF_8))
×
204
                .collect(joining("&"));
×
205

206
        connection.setDoOutput(true);
×
207
        connection.setRequestProperty(CONTENT_TYPE, FORM_ENCODED);
×
208
        try (var w =
×
209
                new OutputStreamWriter(connection.getOutputStream(), UTF_8)) {
×
210
            w.write(form);
×
211
        }
212
    }
×
213

214
    /**
215
     * Outputs an object to a connection in {@code application/json} format.
216
     *
217
     * @param connection
218
     *            The connection. Must have the right verb set.
219
     * @param object
220
     *            The object to write.
221
     * @throws IOException
222
     *             If I/O fails.
223
     */
224
    static void writeObject(HttpURLConnection connection, Object object)
225
            throws IOException {
226
        connection.setDoOutput(true);
×
227
        connection.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON);
×
228
        try (var out = connection.getOutputStream()) {
×
229
            JSON_MAPPER.writeValue(out, object);
×
230
        }
231
    }
×
232

233
    /**
234
     * Outputs a string to a connection in {@code text/plain} format.
235
     *
236
     * @param connection
237
     *            The connection. Must have the right verb set.
238
     * @param string
239
     *            The string to write.
240
     * @throws IOException
241
     *             If I/O fails.
242
     */
243
    static void writeString(HttpURLConnection connection, String string)
244
            throws IOException {
245
        connection.setDoOutput(true);
×
246
        connection.setRequestProperty(CONTENT_TYPE, TEXT_PLAIN);
×
247
        try (var w = new OutputStreamWriter(connection.getOutputStream(),
×
248
                UTF_8)) {
249
            w.write(string);
×
250
        }
251
    }
×
252

253
    /**
254
     * Checks for errors in the response.
255
     *
256
     * @param conn
257
     *            The HTTP connection
258
     * @param errorMessage
259
     *            The message to use on error (describes what did not work at a
260
     *            higher level)
261
     * @return The input stream so any non-error response content can be
262
     *         obtained.
263
     * @throws IOException
264
     *             If things go wrong with comms.
265
     * @throws FileNotFoundException
266
     *             on a {@link HttpURLConnection#HTTP_NOT_FOUND}
267
     * @throws SpallocException
268
     *             on other server errors
269
     */
270
    @MustBeClosed
271
    static InputStream checkForError(HttpURLConnection conn,
272
            String errorMessage) throws IOException {
273
        if (conn.getResponseCode() == HTTP_NOT_FOUND) {
×
274
            // Special case
275
            throw new FileNotFoundException(errorMessage);
×
276
        }
277
        if (conn.getResponseCode() >= HTTP_BAD_REQUEST) {
×
278
            throw new SpallocException(conn.getErrorStream(),
×
279
                    conn.getResponseCode());
×
280
        }
281
        return conn.getInputStream();
×
282
    }
283

284
    /**
285
     * Create a client and log in.
286
     *
287
     * @param username
288
     *            The username to log in with.
289
     * @param password
290
     *            The password to log in with.
291
     * @return The client API for the given server.
292
     * @throws IOException
293
     *             If the server doesn't respond or logging in fails.
294
     */
295
    public SpallocClient login(String username, String password)
296
            throws IOException {
297
        var s = new ClientSession(baseUrl, username, password);
×
298

299
        return new ClientImpl(s, s.discoverRoot());
×
300
    }
301

302
    /**
303
     * Get direct access to a Job.
304
     *
305
     * @param uri
306
     *            The URI of the job
307
     * @param headers
308
     *            The headers to read authentication from.
309
     * @param cookies
310
     *            The cookies to read authentication from.
311
     * @return A job.
312
     * @throws IOException
313
     *             If there is an error communicating with the server.
314
     */
315
    public Job getJob(String uri, Map<String, String> headers,
316
            Map<String, String> cookies) throws IOException {
317
        var u = URI.create(uri);
×
318
        var s = new ClientSession(baseUrl, headers, cookies);
×
319
        var c = new ClientImpl(s, s.discoverRoot());
×
320
        log.info("Connecting to job on {}", u);
×
321
        return c.job(u);
×
322
    }
323

324
    private abstract static class Common {
325
        private final SpallocClient client;
326

327
        final Session s;
328

329
        Common(SpallocClient client, Session s) {
×
330
            this.client = client != null ? client : (SpallocClient) this;
×
331
            this.s = s;
×
332
        }
×
333

334
        final Machine getMachine(String name) throws IOException {
335
            Machine m = MACHINE_MAP.get(name);
×
336
            if (m == null) {
×
337
                client.listMachines();
×
338
                m = MACHINE_MAP.get(name);
×
339
            }
340
            if (m == null) {
×
341
                throw new IOException("Machine " + name + " not found");
×
342
            }
343
            return m;
×
344
        }
345

346
        private WhereIs whereis(HttpURLConnection conn) throws IOException {
347
            try (var is = checkForError(conn,
×
348
                    "couldn't get board information")) {
349
                if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
350
                    throw new FileNotFoundException("machine not allocated");
×
351
                }
352
                return readJson(is, WhereIs.class);
×
353
            } finally {
354
                s.trackCookie(conn);
×
355
            }
356
        }
357

358
        final WhereIs whereis(URI uri) throws IOException {
359
            return s.withRenewal(() -> {
×
360
                var conn = s.connection(uri);
×
361
                var w = whereis(conn);
×
362
                w.setMachineHandle(getMachine(w.getMachineName()));
×
363
                w.clearMachineRef();
×
364
                return w;
×
365
            });
366
        }
367
    }
368

369
    private static final class ClientImpl extends Common
370
            implements SpallocClient {
371
        private Version v;
372

373
        private URI jobs;
374

375
        private URI machines;
376

377
        private ClientImpl(Session s, RootInfo ri) throws IOException {
378
            super(null, s);
×
379
            this.v = ri.version;
×
380
            this.jobs = asDir(ri.jobsURI);
×
381
            this.machines = asDir(ri.machinesURI);
×
382
        }
×
383

384
        @Override
385
        public Version getVersion() {
386
            return v;
×
387
        }
388

389
        /**
390
         * Slightly convoluted class to fetch jobs. The complication means we
391
         * get the initial failure exception nice and early, while we're ready
392
         * for it. This code would be quite a lot simpler if we didn't want to
393
         * get the exception during construction.
394
         */
395
        private class JobLister extends ListFetchingIter<URI> {
396
            private URI next;
397

398
            private List<URI> first;
399

400
            JobLister(URI initial) throws IOException {
×
401
                var first = getJobList(s.connection(initial));
×
402
                next = first.next;
×
403
                this.first = first.jobs;
×
404
            }
×
405

406
            private Jobs getJobList(HttpURLConnection conn) throws IOException {
407
                try (var is = checkForError(conn, "couldn't list jobs")) {
×
408
                    return readJson(is, Jobs.class);
×
409
                } finally {
410
                    s.trackCookie(conn);
×
411
                }
412
            }
413

414
            @Override
415
            List<URI> fetchNext() throws IOException {
416
                if (nonNull(first)) {
×
417
                    try {
418
                        return first;
×
419
                    } finally {
420
                        first = null;
×
421
                    }
422
                }
423
                var j = getJobList(s.connection(next));
×
424
                next = j.next;
×
425
                return j.jobs;
×
426
            }
427

428
            @Override
429
            boolean canFetchMore() {
430
                if (nonNull(first)) {
×
431
                    return true;
×
432
                }
433
                return nonNull(next);
×
434
            }
435
        }
436

437
        private Stream<Job> listJobs(URI flags) throws IOException {
438
            var basicData = new JobLister(
×
439
                    nonNull(flags) ? jobs.resolve(flags) : jobs);
×
440
            return basicData.stream().flatMap(Collection::stream)
×
441
                    .map(this::job);
×
442
        }
443

444
        @Override
445
        public List<Job> listJobs(boolean wait) throws IOException {
446
            return s.withRenewal(() -> listJobs(WAIT_FLAG)).collect(toList());
×
447
        }
448

449
        @Override
450
        public Stream<Job> listJobsWithDeleted(boolean wait)
451
                throws IOException {
452
            var opts = new StringBuilder("?deleted=true");
×
453
            if (wait) {
×
454
                opts.append("&wait=true");
×
455
            }
456
            return s.withRenewal(() -> listJobs(URI.create(opts.toString())));
×
457
        }
458

459
        @Override
460
        public Job createJob(CreateJob createInstructions) throws IOException {
461
            var uri = s.withRenewal(() -> {
×
462
                var conn = s.connection(jobs, true);
×
463
                writeObject(conn, createInstructions);
×
464
                // Get the response entity... and discard it
465
                try (var is = checkForError(conn, "job create failed")) {
×
466
                    readLines(is, UTF_8);
×
467
                    // But we do want the Location header
468
                    return URI.create(conn.getHeaderField("Location"));
×
469
                } finally {
470
                    s.trackCookie(conn);
×
471
                }
472
            });
473
            var job = job(uri);
×
474
            job.startKeepalive(
×
475
                    createInstructions.getKeepaliveInterval().toMillis()
×
476
                    / KEEPALIVE_DIVIDER);
477
            return job;
×
478
        }
479

480
        JobImpl job(URI uri) {
481
            return new JobImpl(this, s, asDir(uri));
×
482
        }
483

484
        @Override
485
        public List<Machine> listMachines() throws IOException {
486
            return s.withRenewal(() -> {
×
487
                var conn = s.connection(machines);
×
488
                try (var is = checkForError(conn, "list machines failed")) {
×
489
                    var ms = readJson(is, Machines.class);
×
490
                    // Assume we can cache this
491
                    for (var bmd : ms.machines) {
×
492
                        log.debug("Machine {} found", bmd.name);
×
493
                        MACHINE_MAP.put(bmd.name,
×
494
                                new MachineImpl(this, s, bmd));
495
                    }
×
496
                    return new ArrayList<Machine>(MACHINE_MAP.values());
×
497
                } finally {
498
                    s.trackCookie(conn);
×
499
                }
500
            });
501
        }
502
    }
503

504
    private static final class JobImpl extends Common implements Job {
505
        private final URI uri;
506

507
        private volatile boolean dead;
508

509
        @GuardedBy("lock")
510
        private ProxyProtocolClient proxy;
511

512
        private final Object lock = new Object();
×
513

514
        JobImpl(SpallocClient client, Session session, URI uri) {
515
            super(client, session);
×
516
            this.uri = uri;
×
517
            this.dead = false;
×
518
        }
×
519

520
        @Override
521
        public JobDescription describe(boolean wait) throws IOException {
522
            return s.withRenewal(() -> {
×
523
                var conn = wait ? s.connection(uri, WAIT_FLAG)
×
524
                        : s.connection(uri);
×
525
                try (var is = checkForError(conn, "couldn't get job state")) {
×
526
                    return readJson(is, JobDescription.class);
×
527
                } finally {
528
                    s.trackCookie(conn);
×
529
                }
530
            });
531
        }
532

533
        @Override
534
        public void keepalive() throws IOException {
535
            s.withRenewal(() -> {
×
536
                var conn = s.connection(uri, KEEPALIVE, true);
×
537
                conn.setRequestMethod("PUT");
×
538
                writeString(conn, "alive");
×
539
                try (var is = checkForError(conn, "couldn't keep job alive")) {
×
540
                    return readLines(is, UTF_8);
×
541
                    // Ignore the output
542
                } finally {
543
                    s.trackCookie(conn);
×
544
                }
545
            });
546
        }
×
547

548
        public void startKeepalive(long delayMs) {
549
            if (dead) {
×
550
                throw new IllegalStateException("job is already deleted");
×
551
            }
552
            var t = new Daemon(() -> {
×
553
                try {
554
                    while (true) {
555
                        sleep(delayMs);
×
556
                        if (dead) {
×
557
                            break;
×
558
                        }
559
                        keepalive();
×
560
                    }
561
                } catch (IOException e) {
×
562
                    log.warn("failed to keep job alive for {}", this, e);
×
563
                } catch (InterruptedException e) {
×
564
                    // If interrupted, we're simply done
565
                }
×
566
            });
×
567
            t.setName("keepalive for " + uri);
×
568
            t.setUncaughtExceptionHandler((th, e) -> {
×
569
                log.warn("unexpected exception in {}", th, e);
×
570
            });
×
571
            t.start();
×
572
        }
×
573

574
        @Override
575
        public void delete(String reason) throws IOException {
576
            dead = true;
×
577
            s.withRenewal(() -> {
×
578
                var conn = s.connection(uri, "?reason=" + encode(reason, UTF_8),
×
579
                        true);
580
                conn.setRequestMethod("DELETE");
×
581
                try (var is = checkForError(conn, "couldn't delete job")) {
×
582
                    readLines(is, UTF_8);
×
583
                    // Ignore the output
584
                } finally {
585
                    s.trackCookie(conn);
×
586
                }
587
                return this;
×
588
            });
589
            synchronized (lock) {
×
590
                if (haveProxy()) {
×
591
                    proxy.close();
×
592
                    proxy = null;
×
593
                }
594
            }
×
595
        }
×
596

597
        @Override
598
        public AllocatedMachine machine() throws IOException {
599
            var am = s.withRenewal(() -> {
×
600
                var conn = s.connection(uri, MACHINE);
×
601
                try (var is = checkForError(conn,
×
602
                        "couldn't get allocation description")) {
603
                    if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
604
                        throw new IOException("machine not allocated");
×
605
                    }
606
                    return readJson(is, AllocatedMachine.class);
×
607
                } finally {
608
                    s.trackCookie(conn);
×
609
                }
610
            });
611
            am.setMachine(getMachine(am.getMachineName()));
×
612
            return am;
×
613
        }
614

615
        @Override
616
        public boolean getPower() throws IOException {
617
            return s.withRenewal(() -> {
×
618
                var conn = s.connection(uri, POWER);
×
619
                try (var is = checkForError(conn, "couldn't get power state")) {
×
620
                    if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
621
                        throw new IOException("machine not allocated");
×
622
                    }
623
                    return "ON".equals(readJson(is, Power.class).power);
×
624
                } finally {
625
                    s.trackCookie(conn);
×
626
                }
627
            });
628
        }
629

630
        @Override
631
        public boolean setPower(boolean switchOn) throws IOException {
632
            var power = new Power();
×
633
            power.power = (switchOn ? "ON" : "OFF");
×
634
            boolean powered = s.withRenewal(() -> {
×
635
                var conn = s.connection(uri, POWER, true);
×
636
                conn.setRequestMethod("PUT");
×
637
                writeObject(conn, power);
×
638
                try (var is = checkForError(conn, "couldn't set power state")) {
×
639
                    if (conn.getResponseCode() == HTTP_NO_CONTENT) {
×
640
                        throw new IOException("machine not allocated");
×
641
                    }
642
                    return "ON".equals(readJson(is, Power.class).power);
×
643
                } finally {
644
                    s.trackCookie(conn);
×
645
                }
646
            });
647
            if (!powered) {
×
648
                // If someone turns the power off, close the proxy
649
                synchronized (lock) {
×
650
                    if (haveProxy()) {
×
651
                        proxy.close();
×
652
                        proxy = null;
×
653
                    }
654
                }
×
655
            }
656
            return powered;
×
657
        }
658

659
        @Override
660
        public WhereIs whereIs(HasChipLocation chip) throws IOException {
661
            return whereis(uri.resolve(
×
662
                    format("chip?x=%d&y=%d", chip.getX(), chip.getY())));
×
663
        }
664

665
        @GuardedBy("lock")
666
        private boolean haveProxy() {
667
            return nonNull(proxy) && proxy.isOpen();
×
668
        }
669

670
        /**
671
         * @return The websocket-based proxy.
672
         * @throws IOException
673
         *             if we can't connect
674
         * @throws InterruptedException
675
         *             if we're interrupted while connecting
676
         */
677
        private ProxyProtocolClient getProxy()
678
                throws IOException, InterruptedException {
679
            synchronized (lock) {
×
680
                if (haveProxy()) {
×
681
                    return proxy;
×
682
                }
683
            }
×
684
            var wssAddr = describe(false).getProxyAddress();
×
685
            if (isNull(wssAddr)) {
×
686
                throw new IOException("machine not allocated");
×
687
            }
688
            synchronized (lock) {
×
689
                if (!haveProxy()) {
×
690
                    proxy = s.withRenewal(() -> s.websocket(wssAddr));
×
691
                }
692
                return proxy;
×
693
            }
694
        }
695

696
        @MustBeClosed
697
        @Override
698
        public TransceiverInterface getTransceiver()
699
                throws IOException, InterruptedException, SpinnmanException {
700
            var ws = getProxy();
×
701
            return new ProxiedTransceiver(this, ws);
×
702
        }
703

704
        @Override
705
        public String toString() {
706
            return "Job(" + uri + ")";
×
707
        }
708

709
        @Override
710
        public void writeMemory(HasChipLocation chip,
711
                MemoryLocation baseAddress, ByteBuffer data)
712
                throws IOException, SpallocServerException,
713
                InterruptedException {
714
            try {
715
                s.withRenewal(() -> {
×
716
                    var conn = s.connection(uri,
×
717
                            new URI(MEMORY + "?x=" + chip.getX()
×
718
                                    + "&y=" + chip.getY()
×
719
                                    + "&address=" + baseAddress.address),
720
                            true);
721
                    conn.setDoOutput(true);
×
722
                    conn.setRequestMethod("POST");
×
723
                    conn.setRequestProperty(
×
724
                            "Content-Type", "application/octet-stream");
725
                    try (var os = conn.getOutputStream();
×
726
                         var channel = Channels.newChannel(os)) {
×
727
                        channel.write(data);
×
728
                    }
NEW
729
                    try (var is = checkForError(conn, "couldn't write memory")) {
×
730
                        // Do Nothing
NEW
731
                    }
×
UNCOV
732
                    return null;
×
733
                });
734
            } catch (URISyntaxException e) {
×
735
                throw new IOException(e);
×
736
            }
×
737
        }
×
738

739
        @Override
740
        public ByteBuffer readMemory(HasChipLocation chip,
741
                MemoryLocation baseAddress, int length)
742
                throws IOException, SpallocServerException,
743
                InterruptedException {
744
            try {
745
                return s.withRenewal(() -> {
×
746
                    var conn = s.connection(uri,
×
747
                            new URI(MEMORY + "?x=" + chip.getX()
×
748
                                    + "&y=" + chip.getY()
×
749
                                    + "&address=" + baseAddress.address
750
                                    + "&size=" + length));
751
                    conn.setRequestMethod("GET");
×
752
                    conn.setRequestProperty(
×
753
                            "Accept", "application/octet-stream");
754
                    var buffer = ByteBuffer.allocate(length);
×
755
                    var channel = Channels.newChannel(conn.getInputStream());
×
756
                    IOUtils.readFully(channel, buffer);
×
757
                    return buffer;
×
758
                });
759
            } catch (URISyntaxException e) {
×
760
                throw new IOException(e);
×
761
            }
762
        }
763
    }
764

765
    private static final class MachineImpl extends Common implements Machine {
766
        private static final int TRIAD = 3;
767

768
        private final BriefMachineDescription bmd;
769

770
        private List<BoardCoords> deadBoards;
771

772
        private List<DeadLink> deadLinks;
773

774
        MachineImpl(SpallocClient client, Session session,
775
                BriefMachineDescription bmd) {
776
            super(client, session);
×
777
            this.bmd = bmd;
×
778
            this.deadBoards = List.copyOf(bmd.deadBoards);
×
779
            this.deadLinks = List.copyOf(bmd.deadLinks);
×
780
        }
×
781

782
        @Override
783
        public String getName() {
784
            return bmd.name;
×
785
        }
786

787
        @Override
788
        public List<String> getTags() {
789
            return bmd.tags;
×
790
        }
791

792
        @Override
793
        public int getWidth() {
794
            return bmd.width;
×
795
        }
796

797
        @Override
798
        public int getHeight() {
799
            return bmd.height;
×
800
        }
801

802
        @Override
803
        public int getLiveBoardCount() {
804
            return bmd.width * bmd.height * TRIAD - bmd.deadBoards.size();
×
805
        }
806

807
        @Override
808
        public List<BoardCoords> getDeadBoards() {
809
            return deadBoards;
×
810
        }
811

812
        @Override
813
        public List<DeadLink> getDeadLinks() {
814
            return deadLinks;
×
815
        }
816

817
        @Override
818
        public void waitForChange() throws IOException {
819
            var nbmd = s.withRenewal(() -> {
×
820
                var conn = s.connection(bmd.uri, WAIT_FLAG);
×
821
                try (var is = checkForError(conn,
×
822
                        "couldn't wait for state change")) {
823
                    return readJson(is, BriefMachineDescription.class);
×
824
                } finally {
825
                    s.trackCookie(conn);
×
826
                }
827
            });
828
            this.deadBoards = List.copyOf(nbmd.deadBoards);
×
829
            this.deadLinks = List.copyOf(nbmd.deadLinks);
×
830
        }
×
831

832
        @Override
833
        public WhereIs getBoard(TriadCoords coords) throws IOException {
834
            return whereis(
×
835
                    bmd.uri.resolve(format("logical-board?x=%d&y=%d&z=%d",
×
836
                            coords.x, coords.y, coords.z)));
×
837
        }
838

839
        @Override
840
        public WhereIs getBoard(PhysicalCoords coords) throws IOException {
841
            return whereis(bmd.uri.resolve(
×
842
                    format("physical-board?cabinet=%d&frame=%d&board=%d",
×
843
                            coords.c, coords.f, coords.b)));
×
844
        }
845

846
        @Override
847
        public WhereIs getBoard(HasChipLocation chip) throws IOException {
848
            return whereis(bmd.uri.resolve(
×
849
                    format("chip?x=%d&y=%d", chip.getX(), chip.getY())));
×
850
        }
851

852
        @Override
853
        public WhereIs getBoard(String address) throws IOException {
854
            return whereis(bmd.uri.resolve(
×
855
                    format("board-ip?address=%s", encode(address, UTF_8))));
×
856
        }
857
    }
858
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc