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

SpiNNakerManchester / JavaSpiNNaker / 13430425590

20 Feb 2025 07:52AM UTC coverage: 38.603% (+0.1%) from 38.494%
13430425590

push

github

rowleya
Handle bytebuffer correctly

0 of 4 new or added lines in 2 files covered. (0.0%)

708 existing lines in 3 files now uncovered.

9189 of 23804 relevant lines covered (38.6%)

1.16 hits per line

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

3.99
/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.Integer.toUnsignedString;
21
import static java.lang.String.format;
22
import static java.lang.Thread.sleep;
23
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
24
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
25
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
26
import static java.net.URLEncoder.encode;
27
import static java.nio.charset.StandardCharsets.UTF_8;
28
import static java.nio.ByteOrder.LITTLE_ENDIAN;
29
import static java.util.Collections.synchronizedMap;
30
import static java.util.Objects.isNull;
31
import static java.util.Objects.nonNull;
32
import static java.util.stream.Collectors.joining;
33
import static java.util.stream.Collectors.toList;
34
import static org.apache.commons.io.IOUtils.readLines;
35
import static org.slf4j.LoggerFactory.getLogger;
36
import static uk.ac.manchester.spinnaker.alloc.client.ClientUtils.asDir;
37

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

56
import org.apache.commons.io.IOUtils;
57
import org.slf4j.Logger;
58

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

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

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

92
    private static final String CONTENT_TYPE = "Content-Type";
93

94
    private static final String TEXT_PLAIN = "text/plain; charset=UTF-8";
95

96
    private static final String APPLICATION_JSON = "application/json";
97

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

101
    private static final URI KEEPALIVE = URI.create("keepalive");
3✔
102

103
    private static final URI MACHINE = URI.create("machine");
3✔
104

105
    private static final URI POWER = URI.create("power");
3✔
106

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

109
    private static final URI MEMORY = URI.create("memory");
3✔
110

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

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

120
    private final URI baseUrl;
121

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
301
        return new ClientImpl(s, s.discoverRoot());
×
302
    }
303

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

326
    private abstract static class Common {
327
        private final SpallocClient client;
328

329
        final Session s;
330

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

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

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

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

371
    private static final class ClientImpl extends Common
372
            implements SpallocClient {
373
        private Version v;
374

375
        private URI jobs;
376

377
        private URI machines;
378

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

386
        @Override
387
        public Version getVersion() {
UNCOV
388
            return v;
×
389
        }
390

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

400
            private List<URI> first;
401

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

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

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

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

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

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

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

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

482
        JobImpl job(URI uri) {
UNCOV
483
            return new JobImpl(this, s, asDir(uri));
×
484
        }
485

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

506
    private static final class JobImpl extends Common implements Job {
507
        private final URI uri;
508

509
        private volatile boolean dead;
510

511
        @GuardedBy("lock")
512
        private ProxyProtocolClient proxy;
513

UNCOV
514
        private final Object lock = new Object();
×
515

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

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

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

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

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

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

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

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

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

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

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

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

706
        @Override
707
        public String toString() {
UNCOV
708
            return "Job(" + uri + ")";
×
709
        }
710

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

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

770
    private static final class MachineImpl extends Common implements Machine {
771
        private static final int TRIAD = 3;
772

773
        private final BriefMachineDescription bmd;
774

775
        private List<BoardCoords> deadBoards;
776

777
        private List<DeadLink> deadLinks;
778

779
        MachineImpl(SpallocClient client, Session session,
780
                BriefMachineDescription bmd) {
781
            super(client, session);
×
UNCOV
782
            this.bmd = bmd;
×
UNCOV
783
            this.deadBoards = List.copyOf(bmd.deadBoards);
×
UNCOV
784
            this.deadLinks = List.copyOf(bmd.deadLinks);
×
785
        }
×
786

787
        @Override
788
        public String getName() {
UNCOV
789
            return bmd.name;
×
790
        }
791

792
        @Override
793
        public List<String> getTags() {
UNCOV
794
            return bmd.tags;
×
795
        }
796

797
        @Override
798
        public int getWidth() {
UNCOV
799
            return bmd.width;
×
800
        }
801

802
        @Override
803
        public int getHeight() {
UNCOV
804
            return bmd.height;
×
805
        }
806

807
        @Override
808
        public int getLiveBoardCount() {
UNCOV
809
            return bmd.width * bmd.height * TRIAD - bmd.deadBoards.size();
×
810
        }
811

812
        @Override
813
        public List<BoardCoords> getDeadBoards() {
UNCOV
814
            return deadBoards;
×
815
        }
816

817
        @Override
818
        public List<DeadLink> getDeadLinks() {
UNCOV
819
            return deadLinks;
×
820
        }
821

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

837
        @Override
838
        public WhereIs getBoard(TriadCoords coords) throws IOException {
UNCOV
839
            return whereis(
×
UNCOV
840
                    bmd.uri.resolve(format("logical-board?x=%d&y=%d&z=%d",
×
UNCOV
841
                            coords.x, coords.y, coords.z)));
×
842
        }
843

844
        @Override
845
        public WhereIs getBoard(PhysicalCoords coords) throws IOException {
UNCOV
846
            return whereis(bmd.uri.resolve(
×
UNCOV
847
                    format("physical-board?cabinet=%d&frame=%d&board=%d",
×
UNCOV
848
                            coords.c, coords.f, coords.b)));
×
849
        }
850

851
        @Override
852
        public WhereIs getBoard(HasChipLocation chip) throws IOException {
UNCOV
853
            return whereis(bmd.uri.resolve(
×
UNCOV
854
                    format("chip?x=%d&y=%d", chip.getX(), chip.getY())));
×
855
        }
856

857
        @Override
858
        public WhereIs getBoard(String address) throws IOException {
UNCOV
859
            return whereis(bmd.uri.resolve(
×
UNCOV
860
                    format("board-ip?address=%s", encode(address, UTF_8))));
×
861
        }
862
    }
863
}
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